Shared ECDH Secret, Browser + NodeJS

Viewed 483

I'm trying to use Elliptical Curve Diffie-Hellman keys to create a shared secret between a Browser and NodeJS. If I export the browser public key as raw, everything works, but I'm required to export the key as spki and then NodeJS gets mad about it.

In the Browser I do this:

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();

So, now I've exported the Public Key and converted it to Base64. I then send it to the NodeJS server, and I try to create a shared secret:

In NodeJS, I do this:

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 };
}

And I get an error saying "Public key is not valid for specified curve."

However, if in the Browser code I export the key as raw (instead of spki) it works....

How can I export the public key as spki in the browser, and then use it to generate a shared secret in NodeJS? Or, how can I convert a Base64 SPKI public key into a raw key in Node?

EDIT It has been discovered that the Browser Crypto APIs are indeed supported in Node v15.0.0+, meaning my browser JS can be simply copied and run in a Node context. Rather than access window.crypto.subtle as I would in a browser, in a Node application I can import the subtle module like so:

const { subtle } = require("crypto").webcrypto;

HOWEVER... as @Topaco pointed out, as of Node v16.2.0, this API is still experimental and subject to change. See @Topaco's answer for additional information and documentation links.

1 Answers

As far as I know, the NodeJS crypto module does not support the X.509/SPKI format for the public key in ECDH context, but only the raw key. However, it is possible to derive the raw key from the X.509/SPKI key.

The X.509/SPKI key generated with the WebCrypto code encapsulates the raw (more precisely uncompressed) key, 0x04 + <x> + <y>, which is localized at the end. For P-256 aka prime256v1 the last 65 bytes correspond to the raw key. The front part is identical for different P-256 keys.

This way, in the NodeJS code, the raw key for P-256 can be determined as the last 65 bytes from the X.509/SPKI key.
Similarly, the front part of the X.509/SPKI key can be concatenated with the raw key generated with the NodeJS code, thus converting the raw key to the X.509/SPKI format.

The NodeJS code for this is:

// 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

where generateDHKeys() is the function posted in the question.

Edit: As noted in the comment from the OP, the WebCrypto API is now part of NodeJS, so X.509/SPKI keys are also supported in the context of ECDH via the WebCrypto API within NodeJS. However, it should be mentioned that the WebCrypto API in the current NodeJS version 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) does not include the WebCrypto API.

Related