current commit
261019.4 · crypto: do not leak scrypt param check errors
'use strict';
const common = require('../common');
if (!common.hasCrypto)
  common.skip('missing crypto');

const assert = require('assert');
const crypto = require('crypto');
const fixtures = require('../common/fixtures');
const { hasFIPS, isBoringSSL } = require('../common/crypto');
const isFips = hasFIPS(3);

if (typeof crypto.scrypt !== 'function' || typeof crypto.scryptSync !== 'function')
  common.skip('no scrypt support');

const good = [
  // Zero-length key is legal, functions as a parameter validation check.
  {
    pass: '',
    salt: '',
    keylen: 0,
    N: 16,
    p: 1,
    r: 1,
    expected: '',
  },
  // Test vectors from https://tools.ietf.org/html/rfc7914#page-13 that
  // should pass.  Note that the test vector with N=1048576 is omitted
  // because it takes too long to complete and uses over 1 GiB of memory.
  {
    pass: '',
    salt: '',
    keylen: 64,
    N: 16,
    p: 1,
    r: 1,
    expected:
        '77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442' +
        'fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906',
  },
  {
    pass: 'password',
    salt: 'NaCl',
    keylen: 64,
    N: 1024,
    p: 16,
    r: 8,
    testAsync: true,
    expected:
        'fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b373162' +
        '2eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640',
  },
  {
    pass: 'pleaseletmein',
    salt: 'SodiumChloride',
    keylen: 64,
    N: 16384,
    p: 1,
    r: 8,
    expected:
        '7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2' +
        'd5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887',
  },
  {
    pass: '',
    salt: '',
    keylen: 64,
    cost: 16,
    parallelization: 1,
    blockSize: 1,
    testAsync: true,
    expected:
        '77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442' +
        'fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906',
  },
];

// Test vectors that contain invalid parameters.
const bad = [
  { N: 1, p: 1, r: 1 },         // N < 2
  { N: 3, p: 1, r: 1 },         // Not power of 2.
];

// Test vectors that contain incompatible options.
const incompatibleOptions = [
  { N: 1, cost: 1 },             // Both N and cost
  { p: 1, parallelization: 1 },  // Both p and parallelization
  { r: 1, blockSize: 1 },        // Both r and blocksize
];

// Test vectors where 128*N*r exceeds maxmem.
const toobig = [
  { N: 2 ** 16, p: 1, r: 1 },   // N >= 2**(r*16)
  { N: 2, p: 2 ** 30, r: 1 },   // p > (2**30-1)/r
  { N: 2 ** 20, p: 1, r: 8 },
  { N: 2 ** 10, p: 1, r: 8, maxmem: 2 ** 20 },
];

const badargs = [
  {
    args: [],
    expected: { code: 'ERR_INVALID_ARG_TYPE', message: /"password"/ },
  },
  {
    args: [null],
    expected: { code: 'ERR_INVALID_ARG_TYPE', message: /"password"/ },
  },
  {
    args: [''],
    expected: { code: 'ERR_INVALID_ARG_TYPE', message: /"salt"/ },
  },
  {
    args: ['', null],
    expected: { code: 'ERR_INVALID_ARG_TYPE', message: /"salt"/ },
  },
  {
    args: ['', ''],
    expected: { code: 'ERR_INVALID_ARG_TYPE', message: /"keylen"/ },
  },
  {
    args: ['', '', null],
    expected: { code: 'ERR_INVALID_ARG_TYPE', message: /"keylen"/ },
  },
  {
    args: ['', '', .42],
    expected: { code: 'ERR_OUT_OF_RANGE', message: /"keylen"/ },
  },
  {
    args: ['', '', -42],
    expected: { code: 'ERR_OUT_OF_RANGE', message: /"keylen"/ },
  },
  {
    args: ['', '', 2 ** 31],
    expected: { code: 'ERR_OUT_OF_RANGE', message: /"keylen"/ },
  },
  {
    args: ['', '', 2147485780],
    expected: { code: 'ERR_OUT_OF_RANGE', message: /"keylen"/ },
  },
  {
    args: ['', '', 2 ** 32],
    expected: { code: 'ERR_OUT_OF_RANGE', message: /"keylen"/ },
  },
];

if (isFips) {
  const expected = { code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS' };
  assert.throws(() => crypto.scryptSync('pass', 'salt', 1), expected);
  assert.throws(
    () => crypto.scrypt('pass', 'salt', 1, () => {}), expected);
} else {
  for (const options of good) {
    const { pass, salt, keylen, expected } = options;
    const actual = crypto.scryptSync(pass, salt, keylen, options);
    assert.strictEqual(actual.toString('hex'), expected);
    if (options.testAsync) {
      crypto.scrypt(pass, salt, keylen, options, common.mustSucceed((actual) => {
        assert.strictEqual(actual.toString('hex'), expected);
      }));
    }
  }
}

for (const options of bad) {
  const expected = {
    message: /Invalid scrypt param/,
  };
  assert.throws(() => crypto.scrypt('pass', 'salt', 1, options, () => {}),
                expected);
  assert.throws(() => crypto.scryptSync('pass', 'salt', 1, options),
                expected);
}

for (const options of incompatibleOptions) {
  const [short, long] = Object.keys(options).sort((a, b) => a.length - b.length);
  const expected = {
    message: `Option "${short}" cannot be used in combination with option "${long}"`,
    code: 'ERR_INCOMPATIBLE_OPTION_PAIR',
  };
  assert.throws(() => crypto.scrypt('pass', 'salt', 1, options, () => {}),
                expected);
  assert.throws(() => crypto.scryptSync('pass', 'salt', 1, options),
                expected);
}

for (const options of toobig) {
  const expected = isFips ? {
    code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS',
  } : {
    message: isBoringSSL ?
      /Invalid scrypt params:.*(INVALID_PARAMETERS|MEMORY_LIMIT_EXCEEDED)/ :
      /Invalid scrypt params:.*memory limit exceeded/,
    code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS',
  };
  assert.throws(() => crypto.scrypt('pass', 'salt', 1, options, () => {}),
                expected);
  assert.throws(() => crypto.scryptSync('pass', 'salt', 1, options),
                expected);
}

// A failed parameter check must not leave an error on the OpenSSL error queue
// for the next unrelated crypto operation to pick up.
{
  const key = fixtures.readKey('ec_p256_private.pem');
  const options = { N: 2 ** 10, p: 1, r: 8, maxmem: 2 ** 20 };
  const expected = { code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS' };
  assert.throws(() => crypto.scryptSync('pass', 'salt', 1, options), expected);
  crypto.createPrivateKey(key);
  assert.throws(() => crypto.scrypt('pass', 'salt', 1, options, () => {}),
                expected);
  crypto.createPrivateKey(key);
}

if (!isFips) {
  const expected = '4cac4540';
  const actual = crypto.scryptSync('pass', 'salt', 4);
  assert.strictEqual(actual.toString('hex'), expected);
  crypto.scrypt('pass', 'salt', 4, common.mustSucceed((actual) => {
    assert.strictEqual(actual.toString('hex'), expected);
  }));
}

for (const { args, expected } of badargs) {
  assert.throws(() => crypto.scrypt(...args), expected);
  assert.throws(() => crypto.scryptSync(...args), expected);
}

{
  const expected = { code: 'ERR_INVALID_ARG_TYPE' };
  assert.throws(() => crypto.scrypt('', '', 42, null), expected);
  assert.throws(() => crypto.scrypt('', '', 42, {}, null), expected);
  assert.throws(() => crypto.scrypt('', '', 42, {}), expected);
  assert.throws(() => crypto.scrypt('', '', 42, {}, {}), expected);
}

{
  // Values for maxmem that do not fit in 32 bits but that are still safe
  // integers should be allowed.
  if (!isFips) {
    crypto.scrypt('', '', 4, { N: 16, maxmem: 2 ** 52 },
                  common.mustSucceed((actual) => {
                    assert.strictEqual(actual.toString('hex'), 'e2b18837');
                  }));
  }

  // Values that exceed Number.isSafeInteger should not be allowed.
  assert.throws(() => crypto.scryptSync('', '', 0, { maxmem: 2 ** 53 }), {
    code: 'ERR_OUT_OF_RANGE'
  });
}

if (!isFips) {
  // Regression test for https://github.com/nodejs/node/issues/28836.

  function testParameter(name, value) {
    let accessCount = 0;
    // This regression checks getter access, so the derivation can be cheap.
    const options = name === 'cost' ? { cost: 16 } : { N: 16 };

    // Find out how often the value is accessed.
    crypto.scryptSync('', '', 1, {
      ...options,
      get [name]() {
        accessCount++;
        return value;
      }
    });

    // Try to crash the process on the last access.
    assert.throws(() => {
      crypto.scryptSync('', '', 1, {
        ...options,
        get [name]() {
          if (--accessCount === 0)
            return '';
          return value;
        }
      });
    }, {
      code: 'ERR_INVALID_ARG_TYPE'
    });
  }

  [
    ['N', 16], ['cost', 16],
    ['r', 8], ['blockSize', 8],
    ['p', 1], ['parallelization', 1],
  ].forEach((arg) => testParameter(...arg));
}

// `-0` keylen must not abort the process via the native binding's
// IsInt32() assertion. Assert that `-0` produces the same outcome as
// `+0` (which differs by OpenSSL build).
{
  const options = { N: 16 };
  let posError;
  let posResult;
  try {
    posResult = crypto.scryptSync('', '', 0, options);
  } catch (err) {
    posError = err;
  }
  let negError;
  let negResult;
  try {
    negResult = crypto.scryptSync('', '', -0, options);
  } catch (err) {
    negError = err;
  }
  if (posError !== undefined) {
    assert.strictEqual(negError?.message, posError.message);
  } else {
    assert.deepStrictEqual(negResult, posResult);
  }

  if (isFips) {
    assert.throws(
      () => crypto.scrypt('', '', -0, options, () => {}),
      { code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS' });
  } else {
    crypto.scrypt('', '', -0, options, common.mustCall());
  }
}