SpringBoot集成ECDH密鑰交換的方法
簡介
對稱加解密算法都需要一把秘鑰,但是很多情況下,互聯(lián)網(wǎng)環(huán)境不適合傳輸這把對稱密碼,有密鑰泄露的風(fēng)險,為了解決這個問題ECDH密鑰交換應(yīng)運而生
EC:Elliptic Curve——橢圓曲線,生成密鑰的方法
DH:Diffie-Hellman Key Exchange——交換密鑰的方法
設(shè)計
數(shù)據(jù)傳輸?shù)膬煞椒?wù)端(Server)和客戶端(Client)
服務(wù)端生成密鑰對Server-Public和Servier-Private
客戶端生成密鑰對Client-Public和Client-Private
客戶端獲取服務(wù)端的公鑰和客戶端的私鑰進(jìn)行計算CaculateKey(Server-Public,Client-Private)出共享密鑰ShareKey1
服務(wù)端獲取客戶端的公鑰和服務(wù)端的私鑰進(jìn)行計算CaculateKey(Client-Public,Server-Private)出共享密鑰ShareKey2
ShareKey1和ShareKey2必定一致,ShareKey就是雙方傳輸數(shù)據(jù)進(jìn)行AES加密時的密鑰
實現(xiàn)
生成密鑰對
后端
public static ECDHKeyInfo generateKeyInfo(){
ECDHKeyInfo keyInfo = new ECDHKeyInfo();
try{
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
keyPairGenerator.initialize(ecSpec, new SecureRandom());
KeyPair kp = keyPairGenerator.generateKeyPair();
ECPublicKey ecPublicKey = (ECPublicKey) kp.getPublic();
ECPrivateKey ecPrivateKey = (ECPrivateKey) kp.getPrivate();
// 獲取公鑰點的x和y坐標(biāo)
BigInteger x = ecPublicKey.getW().getAffineX();
BigInteger y = ecPublicKey.getW().getAffineY();
// 將x和y坐標(biāo)轉(zhuǎn)換為十六進(jìn)制字符串
String xHex = x.toString(16);
String yHex = y.toString(16);
String publicKey = xHex + "|" + yHex;
String privateKey = Base64.getEncoder().encodeToString(ecPrivateKey.getEncoded());
keyInfo.setPublicKey(publicKey);
keyInfo.setPrivateKey(privateKey);
}catch (Exception e){
e.printStackTrace();
}
return keyInfo;
}
public static class ECDHKeyInfo{
private String publicKey;
private String privateKey;
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public String getPrivateKey() {
return privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
}前端
引入elliptic.js(https://cdn.bootcdn.net/ajax/libs/elliptic/6.5.6/elliptic.js)
const EC = elliptic.ec;
const ec = new EC('p256'); // P-256曲線
// 生成密鑰對
const keyPair = ec.genKeyPair();
const publicKey = keyPair.getPublic().getX().toString('hex') + "|" + keyPair.getPublic().getY().toString('hex');共享密鑰計算
后端
public static String caculateShareKey(String serverPrivateKey,String receivePublicKey){
String shareKey = "";
try{
// 1. 后端私鑰 Base64 字符串
// 2. 從 Base64 恢復(fù)后端私鑰
ECPrivateKey privKey = loadPrivateKeyFromBase64(serverPrivateKey);
// 3. 前端傳遞的公鑰坐標(biāo) (x 和 y 坐標(biāo),假設(shè)為十六進(jìn)制字符串)
// 假設(shè)這是從前端接收到的公鑰的 x 和 y 坐標(biāo)
String xHex = receivePublicKey.split("\\|")[0]; // 用前端傳遞的 x 坐標(biāo)替換
String yHex = receivePublicKey.split("\\|")[1]; // 用前端傳遞的 y 坐標(biāo)替換
// 4. 將 x 和 y 轉(zhuǎn)換為 BigInteger
BigInteger x = new BigInteger(xHex, 16);
BigInteger y = new BigInteger(yHex, 16);
// 5. 創(chuàng)建 ECPoint 對象 (公鑰坐標(biāo))
ECPoint ecPoint = new ECPoint(x, y);
// 6. 獲取 EC 參數(shù)(例如 secp256r1)
ECParameterSpec ecSpec = getECParameterSpec();
// 7. 恢復(fù)公鑰
ECPublicKey pubKey = recoverPublicKey(ecPoint, ecSpec);
// 8. 使用 ECDH 計算共享密鑰
byte[] sharedSecret = calculateSharedSecret(privKey, pubKey);
// 9. 打印共享密鑰
shareKey = bytesToHex(sharedSecret);
}catch (Exception e){
e.printStackTrace();
}
return shareKey;
}
// 從 Base64 加載 ECPrivateKey
private static ECPrivateKey loadPrivateKeyFromBase64(String privateKeyBase64) throws Exception {
byte[] decodedKey = Base64.getDecoder().decode(privateKeyBase64);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
return (ECPrivateKey) keyFactory.generatePrivate(keySpec);
}
// 獲取 EC 參數(shù)(例如 secp256r1)
private static ECParameterSpec getECParameterSpec() throws Exception {
// 手動指定 EC 曲線(例如 secp256r1)
AlgorithmParameters params = AlgorithmParameters.getInstance("EC");
params.init(new ECGenParameterSpec("secp256r1")); // 使用標(biāo)準(zhǔn)的 P-256 曲線
return params.getParameterSpec(ECParameterSpec.class);
}
// 恢復(fù)公鑰
private static ECPublicKey recoverPublicKey(ECPoint ecPoint, ECParameterSpec ecSpec) throws Exception {
ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(ecPoint, ecSpec);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
return (ECPublicKey) keyFactory.generatePublic(pubKeySpec);
}
// 使用 ECDH 計算共享密鑰
private static byte[] calculateSharedSecret(ECPrivateKey privKey, ECPublicKey pubKey) throws Exception {
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
keyAgreement.init(privKey);
keyAgreement.doPhase(pubKey, true);
return keyAgreement.generateSecret();
}
// 將字節(jié)數(shù)組轉(zhuǎn)換為十六進(jìn)制字符串
private static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
hexString.append(String.format("%02x", b));
}
return hexString.toString();
} 前端
var keyArray = serverPublicPointKey.split("|")
const otherKey = ec.keyFromPublic({ x: keyArray[0], y: keyArray[1] }, 'hex');
const sharedSecret = keyPair.derive(otherKey.getPublic());AES加密
后端
public static String encryptData(String data,String shareKey){
String result = "";
try{
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] aesKey = digest.digest(shareKey.getBytes()); // 獲取 256 位密鑰
SecretKey key = new SecretKeySpec(aesKey, "AES");
byte[] resultData = encrypt(data,key);
result = Base64.getEncoder().encodeToString(resultData);
}catch (Exception e){
e.printStackTrace();
}
return result;
}
public static String decryptData(String data,String shareKey){
String result = "";
try{
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] aesKey = digest.digest(shareKey.getBytes()); // 獲取 256 位密鑰
SecretKey key = new SecretKeySpec(aesKey, "AES");
byte[] resultData = decrypt(Base64.getDecoder().decode(data),key);
result = new String(resultData);
}catch (Exception e){
e.printStackTrace();
}
return result;
}
private static final String KEY_ALGORITHM = "AES";
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final String IV = "0102030405060708"; // 16 bytes key
// 使用AES密鑰加密數(shù)據(jù)
private static byte[] encrypt(String plaintext, SecretKey aesKey) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(aesKey.getEncoded(), KEY_ALGORITHM);
IvParameterSpec iv = new IvParameterSpec(IV.getBytes());
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
byte[] encrypted = cipher.doFinal(plaintext.getBytes());
return encrypted;
}
// 使用AES密鑰解密數(shù)據(jù)
private static byte[] decrypt(byte[] encryptedData, SecretKey aesKey) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(aesKey.getEncoded(), KEY_ALGORITHM);
IvParameterSpec iv = new IvParameterSpec(IV.getBytes());
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
byte[] original = cipher.doFinal(encryptedData);
return original;
} 前端
引入crypto-js.min.js(https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js)
function encryptByECDH(message, shareKey) {
const aesKey = CryptoJS.SHA256(shareKey);
const key = CryptoJS.enc.Base64.parse(aesKey.toString(CryptoJS.enc.Base64));
return encryptByAES(message,key)
}
function decryptByECDH(message, shareKey) {
const aesKey = CryptoJS.SHA256(shareKey);
const key = CryptoJS.enc.Base64.parse(aesKey.toString(CryptoJS.enc.Base64));
return decryptByAES(message,key)
}
function encryptByAES(message, key) {
const iv = CryptoJS.enc.Utf8.parse("0102030405060708");
const encrypted = CryptoJS.AES.encrypt(message, key, { iv: iv , mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
return encrypted.toString();
}
function decryptByAES(message, key) {
const iv = CryptoJS.enc.Utf8.parse("0102030405060708");
const bytes = CryptoJS.AES.decrypt(message, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
const originalText = bytes.toString(CryptoJS.enc.Utf8);
return originalText;
}注意
- 前端生成的密鑰對和后端生成的密鑰對形式不一致,需要將前端的公鑰拆解成坐標(biāo)點到后端進(jìn)行公鑰還原
- 同理后端的公鑰也要拆分成坐標(biāo)點傳輸?shù)角岸诉M(jìn)行計算
- 生成的ShareKey共享密鑰為了滿足AES的密鑰長度要求需要進(jìn)行Share256計算
- 前后端AES互通需要保證IV向量為同一值
到此這篇關(guān)于SpringBoot集成ECDH密鑰交換的文章就介紹到這了,更多相關(guān)SpringBoot集成ECDH密鑰交換內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中Lambda表達(dá)式的進(jìn)化之路詳解
本文通過示例大家給大家介紹了Java中Lambda表達(dá)式的進(jìn)化之路,感興趣的的朋友一起看看吧,希望能夠給你帶來幫助2021-11-11
Java畢業(yè)設(shè)計實戰(zhàn)之藥店信息管理系統(tǒng)的實現(xiàn)
這是一個使用了java+SSM+JSP+layui+maven+mysql開發(fā)的藥店信息管理系統(tǒng),是一個畢業(yè)設(shè)計的實戰(zhàn)練習(xí),具有藥店信息管理該有的所有功能,感興趣的朋友快來看看吧2022-01-01
使用.NET Core3.0創(chuàng)建一個Windows服務(wù)的方法
這篇文章主要介紹了使用.NET Core3.0創(chuàng)建一個Windows服務(wù)的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-04-04
SpringBoot中多個PostConstruct注解執(zhí)行順序控制
本文介紹了SpringBoot中使用多個@PostConstruct注解的方法執(zhí)行順序,以解決ClassA依賴ClassB初始化結(jié)果的問題,具有一定的參考價值,感興趣的可以了解一下2025-08-08
elasticsearch+logstash并使用java代碼實現(xiàn)日志檢索
這篇文章主要介紹了elasticsearch+logstash并使用java代碼實現(xiàn)日志檢索,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02
SpringBoot項目打成war和jar的區(qū)別說明
這篇文章主要介紹了SpringBoot項目打成war和jar的區(qū)別說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06

