共享 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.