共享 ECDH 秘密,浏览器 + NodeJS
Shared ECDH Secret, Browser + NodeJS
我正在尝试使用椭圆曲线 Diffie-Hellman 密钥在浏览器和 NodeJS 之间创建共享密钥。如果我将浏览器 public 密钥导出为 raw
,一切正常,但我需要将密钥导出为 spki
,然后 NodeJS 对此很生气。
我在浏览器中这样做:
async function generateDHKeys() {
const key_ECDH = await window.crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
true,
['deriveKey'],
);
const publicKeyData = await window.crypto.subtle.exportKey(
'spki',
key_ECDH.publicKey,
);
const publicKeyBytes = new Uint8Array(publicKeyData);
publicKeyB64 = btoa(String.fromCharCode.apply(null, publicKeyBytes));
const privateKeyData = await window.crypto.subtle.exportKey(
'pkcs8',
key_ECDH.privateKey,
);
const privateKeyBytes = new Uint8Array(privateKeyData);
privateKeyB64 = btoa(String.fromCharCode.apply(null, privateKeyBytes));
privateKeyBytes.fill(0);
return { publicKeyB64, privateKeyB64 };
}
const {publicKeyB64} = await generateDHKeys();
所以,现在我已经导出 Public 密钥并将其转换为 Base64。然后我将它发送到 NodeJS 服务器,并尝试创建一个共享密钥:
在 NodeJS 中,我这样做:
export function generateDHKeys(foreignPublicKeyB64) {
const ecdh = crypto.createECDH("prime256v1");
ecdh.generateKeys();
const publicKeyB64 = ecdh.getPublicKey("base64");
const privateKeyB64 = ecdh.getPrivateKey("base64");
const sharedSecretB64 = ecdh.computeSecret(foreignPublicKeyB64, "base64", "base64");
const sharedSecretHashB64 = crypto
.createHash("sha256")
.update(sharedSecretB64, "base64")
.digest("base64");
return { publicKeyB64, privateKeyB64, sharedSecretB64, sharedSecretHashB64 };
}
我收到一条错误消息“Public 密钥对指定曲线无效。”
但是,如果在浏览器代码中我将密钥导出为 raw
(而不是 spki
),它就可以工作....
如何在浏览器中将 public 密钥导出为 spki
,然后使用它在 NodeJS 中生成共享密钥?或者,如何将 Base64 SPKI public 密钥转换为 Node 中的原始密钥?
编辑
已经发现 Node v15.0.0+ 确实支持 Browser Crypto API,这意味着我的浏览器 JS 可以简单地复制并 运行 在 Node 上下文中。在 Node 应用程序中,我可以像这样导入微妙的模块,而不是像在浏览器中那样访问 window.crypto.subtle
:
const { subtle } = require("crypto").webcrypto;
然而...正如@Topaco 指出的那样,从 Node v16.2.0 开始,这个 API 仍处于试验阶段,可能会发生变化。有关其他信息和文档链接,请参阅@Topaco 的回答。
据我所知,NodeJS 加密模块不支持 ECDH 上下文中 public 密钥的 X.509/SPKI 格式,但仅支持原始密钥。但是,可以从 X.509/SPKI 密钥派生原始密钥。
使用 WebCrypto 代码生成的 X.509/SPKI 密钥封装了原始(更准确地说是未压缩)密钥 0x04 + + ,它在末尾进行了本地化。对于 P-256 aka prime256v1,最后 65 个字节对应于原始密钥。不同的 P-256 键的前部相同。
这样,在 NodeJS 代码中,P-256 的原始密钥可以确定为 X.509/SPKI 密钥的最后 65 个字节。
同样,X.509/SPKI密钥的前半部分可以与NodeJS代码生成的原始密钥拼接,从而将原始密钥转换为X.509/SPKI格式。
NodeJS 代码是:
// Convert the SPKI key of the WebCrypto side into the raw format
var webcryptoSpkiB64 = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPF2r2yyMp/PykPZEt6v8WFAvnrf5FsI3UnpEYsbKo7UKVKB8k2hfxxhjKw8p9nulNaRo472hTcEqsSbsGcr5Dg==';
var webcryptoRawB64 = Buffer.from(webcryptoSpkiB64, 'base64').slice(-65).toString('base64'); // the last 65 bytes
// Calculate the shared secret for the NodeJS side
var { publicKeyB64, privateKeyB64, sharedSecretB64, sharedSecretHashB64 } = generateDHKeys(webcryptoRawB64);
// Convert the raw key of the NodeJS side into the SPKI format
var nodejsSpkiB64 = Buffer.concat([
Buffer.from(webcryptoSpkiB64, 'base64').slice(0, -65), // all bytes except the last 65
Buffer.from(publicKeyB64, 'base64')]
).toString('base64');
console.log("Shared secret:", sharedSecretB64);
console.log("SPKI:", nodejsSpkiB64); // will be sent to the WebCrypto side and used there to calculate the shared secret
其中 generateDHKeys()
是问题中发布的函数。
编辑: 如 OP 的评论所述,WebCrypto API 现在是 NodeJS 的一部分,因此 X.509/SPKI 密钥也受支持通过 NodeJS 中的 WebCrypto API 的 ECDH 上下文。但是,应该提到的是,当前NodeJS版本v16.0.2 has stability 1 level (Experimental). This means that non-backward compatible changes or removals are possible. Also, the current LTS version (v14.17.0中的WebCrypto API不包括WebCrypto API.
我正在尝试使用椭圆曲线 Diffie-Hellman 密钥在浏览器和 NodeJS 之间创建共享密钥。如果我将浏览器 public 密钥导出为 raw
,一切正常,但我需要将密钥导出为 spki
,然后 NodeJS 对此很生气。
我在浏览器中这样做:
async function generateDHKeys() {
const key_ECDH = await window.crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
true,
['deriveKey'],
);
const publicKeyData = await window.crypto.subtle.exportKey(
'spki',
key_ECDH.publicKey,
);
const publicKeyBytes = new Uint8Array(publicKeyData);
publicKeyB64 = btoa(String.fromCharCode.apply(null, publicKeyBytes));
const privateKeyData = await window.crypto.subtle.exportKey(
'pkcs8',
key_ECDH.privateKey,
);
const privateKeyBytes = new Uint8Array(privateKeyData);
privateKeyB64 = btoa(String.fromCharCode.apply(null, privateKeyBytes));
privateKeyBytes.fill(0);
return { publicKeyB64, privateKeyB64 };
}
const {publicKeyB64} = await generateDHKeys();
所以,现在我已经导出 Public 密钥并将其转换为 Base64。然后我将它发送到 NodeJS 服务器,并尝试创建一个共享密钥:
在 NodeJS 中,我这样做:
export function generateDHKeys(foreignPublicKeyB64) {
const ecdh = crypto.createECDH("prime256v1");
ecdh.generateKeys();
const publicKeyB64 = ecdh.getPublicKey("base64");
const privateKeyB64 = ecdh.getPrivateKey("base64");
const sharedSecretB64 = ecdh.computeSecret(foreignPublicKeyB64, "base64", "base64");
const sharedSecretHashB64 = crypto
.createHash("sha256")
.update(sharedSecretB64, "base64")
.digest("base64");
return { publicKeyB64, privateKeyB64, sharedSecretB64, sharedSecretHashB64 };
}
我收到一条错误消息“Public 密钥对指定曲线无效。”
但是,如果在浏览器代码中我将密钥导出为 raw
(而不是 spki
),它就可以工作....
如何在浏览器中将 public 密钥导出为 spki
,然后使用它在 NodeJS 中生成共享密钥?或者,如何将 Base64 SPKI public 密钥转换为 Node 中的原始密钥?
编辑
已经发现 Node v15.0.0+ 确实支持 Browser Crypto API,这意味着我的浏览器 JS 可以简单地复制并 运行 在 Node 上下文中。在 Node 应用程序中,我可以像这样导入微妙的模块,而不是像在浏览器中那样访问 window.crypto.subtle
:
const { subtle } = require("crypto").webcrypto;
然而...正如@Topaco 指出的那样,从 Node v16.2.0 开始,这个 API 仍处于试验阶段,可能会发生变化。有关其他信息和文档链接,请参阅@Topaco 的回答。
据我所知,NodeJS 加密模块不支持 ECDH 上下文中 public 密钥的 X.509/SPKI 格式,但仅支持原始密钥。但是,可以从 X.509/SPKI 密钥派生原始密钥。
使用 WebCrypto 代码生成的 X.509/SPKI 密钥封装了原始(更准确地说是未压缩)密钥 0x04 +
这样,在 NodeJS 代码中,P-256 的原始密钥可以确定为 X.509/SPKI 密钥的最后 65 个字节。
同样,X.509/SPKI密钥的前半部分可以与NodeJS代码生成的原始密钥拼接,从而将原始密钥转换为X.509/SPKI格式。
NodeJS 代码是:
// Convert the SPKI key of the WebCrypto side into the raw format
var webcryptoSpkiB64 = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPF2r2yyMp/PykPZEt6v8WFAvnrf5FsI3UnpEYsbKo7UKVKB8k2hfxxhjKw8p9nulNaRo472hTcEqsSbsGcr5Dg==';
var webcryptoRawB64 = Buffer.from(webcryptoSpkiB64, 'base64').slice(-65).toString('base64'); // the last 65 bytes
// Calculate the shared secret for the NodeJS side
var { publicKeyB64, privateKeyB64, sharedSecretB64, sharedSecretHashB64 } = generateDHKeys(webcryptoRawB64);
// Convert the raw key of the NodeJS side into the SPKI format
var nodejsSpkiB64 = Buffer.concat([
Buffer.from(webcryptoSpkiB64, 'base64').slice(0, -65), // all bytes except the last 65
Buffer.from(publicKeyB64, 'base64')]
).toString('base64');
console.log("Shared secret:", sharedSecretB64);
console.log("SPKI:", nodejsSpkiB64); // will be sent to the WebCrypto side and used there to calculate the shared secret
其中 generateDHKeys()
是问题中发布的函数。
编辑: 如 OP 的评论所述,WebCrypto API 现在是 NodeJS 的一部分,因此 X.509/SPKI 密钥也受支持通过 NodeJS 中的 WebCrypto API 的 ECDH 上下文。但是,应该提到的是,当前NodeJS版本v16.0.2 has stability 1 level (Experimental). This means that non-backward compatible changes or removals are possible. Also, the current LTS version (v14.17.0中的WebCrypto API不包括WebCrypto API.