ECDH java后端和javaScript前后端互通实现
问题背景
web前端和后端之间的通信不一定可靠,如果不方便使用预共享密钥的方式完成身份的确认,那么可以使用ECDH密钥协商算法在前端和后端进行密钥间的协商。
ECDH算法
Elliptic-curve Diffie-Hellman(ECDH)是一种通过两方达成密钥协商的密钥生成算法,每一方都有一个椭圆双曲线的公私钥对,双方在不安全的通信通道间传递信息达成密钥生成协议。生成的密钥会被直接作为一个密钥或者是生成其他密钥的种子。最终生成的密钥将被对称密码学算法例如AESGCM作为密钥使用。
接下来的例子阐述了共享密钥是如何产生的:
Alice想和Bob建立一个共享密钥,但是唯一能够传递消息的通道被第三方监听。在初始时刻,Alice和Bob共享了些域的参数(p a,b,G,n,h),Alice在整数范围[t,n-1]中随机选择一个私钥d,并通过计算(Q=d·G)具体计算方式,得到了公钥Q,公私钥对为(
d
A
d_A
dA,
Q
A
Q_A
QA)。Bob用同样的计算方式得到了(
d
B
d_B
dB,
Q
B
Q_B
QB)。双方将自己的公钥发送给对方,分别计算:
(
x
k
x_k
xk,
y
k
y_k
yk)=
d
A
d_A
dA·
Q
B
Q_B
QB
(
x
k
x_k
xk,
y
k
y_k
yk)=
d
B
d_B
dB·
Q
A
Q_A
QA
(
x
k
x_k
xk,
y
k
y_k
yk)即为所得的私钥。
Alice仅仅公开了她的公钥信息,任何参与方想要计算出Alice的公钥需要解决椭圆曲线离散对数问题(这很难被计算出来)。因此我们认为Alice和Bob通过这样的方法协商密钥是安全的。除了Alice和Bob,没有谁能计算出他们协商的密钥。
在众多ECDH算法的计算标准中,Curve25519是使用最为广泛的密钥生成算法。
Java 实现
public class EcdhKeyPair{
private String privatekey;
private String publicKey;
public String getprivatekey(){
return privatekey;
}
public String getPublicKey(){
return publickey;
}
public class Ecdhservice{
private static final int ECDH_KEY_LEN= 32;
private static final int SHARED_KEY_LEN= 16;
private byte[] privatekey;
private byte[] publicKey;
private BigInteger pInteger;
private EcdhKeyPair ecdhKeyPair;
private static final SecureRandom RANDOM_GENERATOR;
static {
try {
RANDOM_GENERATOR = SecureRandom.getInstancestrong();
} catch(NoSuchAlgorithmException e){
log.error("SecureRandom init failed. ECDH encryption would be unavailable.");
EcdhService.");
throw new InnerException(INTERNAL_ERROR, "Failed to init ECDHService.");
}
}
private void generatetckeyPair() {
privatekey = new byte[ECDH KEY_LEN];
publicKey= new byte [ECDH KEY LEN];
X25519.generatePrivateKey(RANDOM_GENERATOR, privateKey);
X25519.generatePublickey(privatekey, 0, publicKey, 0);
}
private byte[] establisSharedKey(byte[] clientPublicKey){
if (clientPublicKey.length != 32 ) {
throw new InnerExecption(INVALID_REQUEST, "Client ECDH public key should be 256 bits/32 bytes");
}
BigInteger cInteger = new Biginteger(clientpublickey);
if (cInteger.equals(BigInteger ZERO) || cInteger.equals(BigInteger.ONE) || cInteger.equals(pInteger)|| cInteger.equals(pInteger.subtract(BigInteges.ONE))){
throw new InnerException(INVALID_REQUEST, "Client ECDH public key Cannot be specific value.");
}
try {
byte[] agreement = new byte[ECDH_KEY_LEN];
try {
X25519.calculateAgreement(getprivatekey(), 0,clientPublickey,0, agreement, 0);
}catch (RuntimetException e) {
throw new InnerException(INVALID_REQUEST,
"Cannot calculate the agreement with client key, please check the request parameters.");
}
MessageDigest digester = MessageDigest.getInstance("SHA-256");
return Arrays.copy0fRange(digester.digest(agreement),0, SHARED_KEY_LEN);
} catch (NoSuchAIgorithmException e) {
throw new InnerException(INTERNAL_ERROR,
"Failed to calculate shared key. " + e.getLocalizedMessage());
}
}
前端JavaScript 实现
注意: 此处JavaScript代码没有将生成的sharedKey进行SHA256处理。
import { sharedkey, generateKeypair } from "curve25519-js"
* @description 生成公钥秘钥
* @param {Uint8Array} backPublickeyBase64 后端base64加密的的公钥
* @return 共享秘key:Uint&Arnay privatekey 私钥
*
export function generatekey(backPublicKeyBase64: string){
// 获取后端的公钥进行base64转成Uint8Array
const backPublickey=base64ToUint8Array(backPublicKeyBase64)
//符合密码学要求的安全的随机值 Uint8Array 32字节 8位无符号整型数组
const randomBytes = window.crypto.getRandomValues(new Uint8Array(32))
// 前端使用generatekeyPair得到自己的公钥和私钥,用自己前端的公钥去交换后端的公钥
const keyPair = generateKeyPair(randomBytes)
//将生成的公钥通过base64 加密发给后台
const publicKey =uint8arrayToBase64(keyPair.public)
// 根据后端的公钥用前端的松铜和后端的公钥生成前端的sharekey
const sharekey = sharedkey(keypair.private, backPublicKey)
return {
publicKey,
shareKey
}