current commit
260826.19 · crypto: optimize key slot caching
'use strict';

// KeyObject instances must survive structured cloning with their
// native backing data and hidden JS slot semantics preserved.

const common = require('../common');
if (!common.hasCrypto)
  common.skip('missing crypto');

const assert = require('node:assert');
const { once } = require('node:events');
const {
  createHmac,
  createSecretKey,
  generateKeyPairSync,
  sign,
  verify,
} = require('node:crypto');
const { hasFIPS } = require('../common/crypto');
const { MessageChannel, Worker } = require('node:worker_threads');
const { types: { isKeyObject } } = require('node:util');

function assertNoOwnKeys(key) {
  assert.deepStrictEqual(Object.getOwnPropertySymbols(key), []);
  assert.deepStrictEqual(Object.getOwnPropertyNames(key), []);
  assert.deepStrictEqual(Reflect.ownKeys(key), []);
}

function assertSameKeyObject(original, clone) {
  assert.notStrictEqual(original, clone);
  assert.strictEqual(isKeyObject(clone), true);
  assert.strictEqual(original.type, clone.type);
  assert.strictEqual(original.equals(clone), true);
  assert.deepStrictEqual(original, clone);
  if (clone.type === 'secret') {
    assert.strictEqual(original.symmetricKeySize, clone.symmetricKeySize);
  } else {
    assert.strictEqual(original.asymmetricKeyType, clone.asymmetricKeyType);
    assert.deepStrictEqual(
      original.asymmetricKeyDetails,
      clone.asymmetricKeyDetails);
  }
  assertNoOwnKeys(original);
  assertNoOwnKeys(clone);
}

async function roundTripViaMessageChannel(key) {
  const { port1, port2 } = new MessageChannel();
  port1.postMessage(key);
  const [received] = await once(port2, 'message');
  port1.close();
  port2.close();
  return received;
}

function workerMain() {
  const { parentPort } = require('node:worker_threads');
  const { types: { isKeyObject: workerIsKeyObject } } = require('node:util');

  parentPort.once('message', ({ key, expectedType }) => {
    try {
      if (!workerIsKeyObject(key) || key.type !== expectedType) {
        throw new Error('KeyObject slot mismatch in worker');
      }
      parentPort.postMessage({ key });
    } catch (err) {
      parentPort.postMessage({ error: err.stack || err.message });
    }
  });
}

async function roundTripViaWorker(key) {
  const worker = new Worker(
    `'use strict';(${workerMain.toString()})()`, { eval: true });

  worker.postMessage({ key, expectedType: key.type });
  const [msg] = await once(worker, 'message');
  await worker.terminate();

  assert.strictEqual(msg.error, undefined, msg.error);
  return msg.key;
}

function hmacDigest(key) {
  return createHmac('sha256', key).update('payload').digest('hex');
}

(async () => {
  const secret = createSecretKey(Buffer.alloc(16));
  const { publicKey, privateKey } = generateKeyPairSync('rsa', {
    modulusLength: hasFIPS(3) ? 2048 : 1024,
  });

  for (const key of [secret, publicKey, privateKey]) {
    const cloned = structuredClone(key);
    assertSameKeyObject(key, cloned);

    const viaPort = await roundTripViaMessageChannel(key);
    assertSameKeyObject(key, viaPort);

    const clonedAgain = structuredClone(viaPort);
    assertSameKeyObject(key, clonedAgain);

    const viaWorker = await roundTripViaWorker(key);
    assertSameKeyObject(key, viaWorker);
  }

  const secretClones = [
    secret,
    structuredClone(secret),
    await roundTripViaMessageChannel(secret),
    await roundTripViaWorker(secret),
  ];
  const digest = hmacDigest(secret);
  for (const key of secretClones) {
    assert.strictEqual(hmacDigest(key), digest);
  }

  const data = Buffer.from('payload');
  const publicClones = [
    publicKey,
    structuredClone(publicKey),
    await roundTripViaMessageChannel(publicKey),
    await roundTripViaWorker(publicKey),
  ];
  const privateClones = [
    privateKey,
    structuredClone(privateKey),
    await roundTripViaMessageChannel(privateKey),
    await roundTripViaWorker(privateKey),
  ];

  const signature = sign('sha256', data, privateKey);
  for (const key of publicClones) {
    assert.strictEqual(verify('sha256', data, key, signature), true);
  }
  for (const key of privateClones) {
    const clonedSignature = sign('sha256', data, key);
    assert.strictEqual(verify('sha256', data, publicKey, clonedSignature), true);
  }
})().then(common.mustCall());