current commit
261025.12 · test,benchmark: drop OpenSSL 1.x-only coverage
'use strict';
const common = require('../common');
if (!common.hasCrypto)
  common.skip('missing crypto');

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

const fips4 = hasFIPS(4);
const validPassword = fips4 ? 'password' : 'pass';
const validSalt = fips4 ? '0123456789abcdef' : 'salt';
const validIterations = fips4 ? 1000 : 8;
const validKeyLength = fips4 ? 16 : 8;

function runPBKDF2(password, salt, iterations, keylen, hash, testAsync = true) {
  const syncResult =
    crypto.pbkdf2Sync(password, salt, iterations, keylen, hash);

  if (testAsync) {
    crypto.pbkdf2(password, salt, iterations, keylen, hash,
                  common.mustSucceed((asyncResult) => {
                    assert.deepStrictEqual(asyncResult, syncResult);
                  }));
  }

  return syncResult;
}

function assertPBKDF2Fails(password, salt, iterations, keylen, hash) {
  const expected = { message: 'PBKDF2 derivation failed' };
  assert.throws(
    () => crypto.pbkdf2Sync(password, salt, iterations, keylen, hash),
    expected,
  );
  crypto.pbkdf2(
    password,
    salt,
    iterations,
    keylen,
    hash,
    common.mustCall((err, result) => {
      assert.strictEqual(err?.message, expected.message);
      assert.strictEqual(result, undefined);
    }),
  );
}

function testPBKDF2(password, salt, iterations, keylen, expected, encoding,
                    testAsync = false) {
  const actual = runPBKDF2(password, salt, iterations, keylen, 'sha256', testAsync);
  assert.strictEqual(actual.toString(encoding || 'latin1'), expected);
}

//
// Test PBKDF2 with RFC 6070 test vectors (except #4). Run one known answer
// through the callback API; the rest only need synchronous derivation.
//

if (fips4) {
  testPBKDF2(validPassword, validSalt, validIterations, 32,
             '8514638175a45bc45eb1f22f04ff7d27' +
             'f4f8be480498c455ff4b494ce8d1e7d2',
             'hex', true);

  for (const args of [
    ['short', validSalt, validIterations],
    [validPassword, 'short', validIterations],
    [validPassword, validSalt, 999],
  ]) {
    assertPBKDF2Fails(...args, 32, 'sha256');
  }
  assertPBKDF2Fails(
    validPassword, validSalt, validIterations, 8, 'sha256');
} else {
  testPBKDF2('password', 'salt', 1, 20,
             '\x12\x0f\xb6\xcf\xfc\xf8\xb3\x2c\x43\xe7\x22\x52' +
             '\x56\xc4\xf8\x37\xa8\x65\x48\xc9');

  testPBKDF2('password', 'salt', 2, 20,
             '\xae\x4d\x0c\x95\xaf\x6b\x46\xd3\x2d\x0a\xdf\xf9' +
             '\x28\xf0\x6d\xd0\x2a\x30\x3f\x8e');

  testPBKDF2('password', 'salt', 4096, 20,
             '\xc5\xe4\x78\xd5\x92\x88\xc8\x41\xaa\x53\x0d\xb6' +
             '\x84\x5c\x4c\x8d\x96\x28\x93\xa0', 'latin1', true);

  testPBKDF2('passwordPASSWORDpassword',
             'saltSALTsaltSALTsaltSALTsaltSALTsalt',
             4096,
             25,
             '\x34\x8c\x89\xdb\xcb\xd3\x2b\x2f\x32\xd8\x14\xb8\x11' +
             '\x6e\x84\xcf\x2b\x17\x34\x7e\xbc\x18\x00\x18\x1c');

  testPBKDF2('pass\0word', 'sa\0lt', 4096, 16,
             '\x89\xb6\x9d\x05\x16\xf8\x29\x89\x3c\x69\x62\x26\x65' +
             '\x0a\x86\x87');

  testPBKDF2('password', 'salt', 32, 32,
             '64c486c55d30d4c5a079b8823b7d7cb3' +
             '7ff0556f537da8410233bcec330ed956',
             'hex');
}

// Error path should not leak memory (check with valgrind).
assert.throws(
  () => crypto.pbkdf2(
    validPassword, validSalt, validIterations, 20, 'sha1'),
  {
    code: 'ERR_INVALID_ARG_TYPE',
    name: 'TypeError'
  }
);

for (const iterations of [-1, 0, 2147483648]) {
  assert.throws(
    () => crypto.pbkdf2Sync(
      validPassword, validSalt, iterations, 20, 'sha1'),
    {
      code: 'ERR_OUT_OF_RANGE',
      name: 'RangeError',
    }
  );
}

['str', null, undefined, [], {}].forEach((notNumber) => {
  assert.throws(
    () => {
      crypto.pbkdf2Sync(
        validPassword, validSalt, validIterations, notNumber, 'sha256');
    }, {
      code: 'ERR_INVALID_ARG_TYPE',
      name: 'TypeError',
      message: 'The "keylen" argument must be of type number.' +
               `${common.invalidArgTypeHelper(notNumber)}`
    });
});

[Infinity, -Infinity, NaN].forEach((input) => {
  assert.throws(
    () => {
      crypto.pbkdf2(validPassword, validSalt, validIterations, input, 'sha256',
                    common.mustNotCall());
    }, {
      code: 'ERR_OUT_OF_RANGE',
      name: 'RangeError',
      message: 'The value of "keylen" is out of range. It ' +
               `must be an integer. Received ${input}`
    });
});

[-1, 2147483648, 4294967296].forEach((input) => {
  assert.throws(
    () => {
      crypto.pbkdf2(validPassword, validSalt, validIterations, input, 'sha256',
                    common.mustNotCall());
    }, {
      code: 'ERR_OUT_OF_RANGE',
      name: 'RangeError',
    });
});

// `-0` keylen must not abort the process via the native binding's
// IsInt32() assertion. Behavior of `keylen=0` itself varies by OpenSSL
// build (bundled returns an empty buffer; some shared OpenSSL builds
// throw); the requirement here is only that `-0` produces the same
// outcome as `+0`.
{
  let posError;
  let posResult;
  try {
    posResult = crypto.pbkdf2Sync(
      validPassword, validSalt, validIterations, 0, 'sha256');
  } catch (err) {
    posError = err;
  }
  let negError;
  let negResult;
  try {
    negResult = crypto.pbkdf2Sync(
      validPassword, validSalt, validIterations, -0, 'sha256');
  } catch (err) {
    negError = err;
  }
  if (posError !== undefined) {
    assert.strictEqual(negError?.message, posError.message);
  } else {
    assert.deepStrictEqual(negResult, posResult);
  }

  crypto.pbkdf2(
    validPassword, validSalt, validIterations, -0, 'sha256',
    common.mustCall());
}

// Should not get FATAL ERROR with empty password and salt
// https://github.com/nodejs/node/issues/8571
if (fips4) {
  assertPBKDF2Fails('', '', 1, 32, 'sha256');
} else {
  crypto.pbkdf2('', '', 1, 32, 'sha256', common.mustSucceed());
}

assert.throws(
  () => crypto.pbkdf2(
    validPassword, validSalt, validIterations, 8, common.mustNotCall()),
  {
    code: 'ERR_INVALID_ARG_TYPE',
    name: 'TypeError',
    message: 'The "digest" argument must be of type string. ' +
             'Received undefined'
  });

assert.throws(
  () => crypto.pbkdf2Sync(validPassword, validSalt, validIterations, 8),
  {
    code: 'ERR_INVALID_ARG_TYPE',
    name: 'TypeError',
    message: 'The "digest" argument must be of type string. ' +
             'Received undefined'
  });

assert.throws(
  () => crypto.pbkdf2Sync(validPassword, validSalt, validIterations, 8, null),
  {
    code: 'ERR_INVALID_ARG_TYPE',
    name: 'TypeError',
    message: 'The "digest" argument must be of type string. ' +
             'Received null'
  });
[1, {}, [], true, undefined, null].forEach((input) => {
  assert.throws(
    () => crypto.pbkdf2(
      input, validSalt, validIterations, 8, 'sha256', common.mustNotCall()),
    {
      code: 'ERR_INVALID_ARG_TYPE',
      name: 'TypeError',
    }
  );

  assert.throws(
    () => crypto.pbkdf2(
      validPassword, input, validIterations, 8, 'sha256',
      common.mustNotCall()),
    {
      code: 'ERR_INVALID_ARG_TYPE',
      name: 'TypeError',
    }
  );

  assert.throws(
    () => crypto.pbkdf2Sync(
      input, validSalt, validIterations, 8, 'sha256'),
    {
      code: 'ERR_INVALID_ARG_TYPE',
      name: 'TypeError',
    }
  );

  assert.throws(
    () => crypto.pbkdf2Sync(
      validPassword, input, validIterations, 8, 'sha256'),
    {
      code: 'ERR_INVALID_ARG_TYPE',
      name: 'TypeError',
    }
  );
});

['test', {}, [], true, undefined, null].forEach((i) => {
  const received = common.invalidArgTypeHelper(i);
  assert.throws(
    () => crypto.pbkdf2(
      validPassword, validSalt, i, 8, 'sha256', common.mustNotCall()),
    {
      code: 'ERR_INVALID_ARG_TYPE',
      name: 'TypeError',
      message: `The "iterations" argument must be of type number.${received}`
    }
  );

  assert.throws(
    () => crypto.pbkdf2Sync(validPassword, validSalt, i, 8, 'sha256'),
    {
      code: 'ERR_INVALID_ARG_TYPE',
      name: 'TypeError',
      message: `The "iterations" argument must be of type number.${received}`
    }
  );
});

if (fips4) {
  assertPBKDF2Fails(
    new Uint8Array(1), validSalt, validIterations, validKeyLength, 'sha256');
  assertPBKDF2Fails(
    validPassword, new Uint8Array(1), validIterations, validKeyLength,
    'sha256');
}

// Any TypedArray should work for password and salt.
for (const SomeArray of [Uint8Array, Uint16Array, Uint32Array, Float32Array,
                         Float64Array, ArrayBuffer, SharedArrayBuffer]) {
  const length = fips4 ? 16 : 10;
  const input = new SomeArray(length);
  const bytes = ArrayBuffer.isView(input) ?
    new Uint8Array(input.buffer, input.byteOffset, input.byteLength) :
    new Uint8Array(input);
  for (let index = 0; index < bytes.length; index++)
    bytes[index] = index + 1;
  runPBKDF2(input, validSalt, validIterations, validKeyLength, 'sha256');
  runPBKDF2(validPassword, input, validIterations, validKeyLength, 'sha256');
}

assert.throws(
  () => crypto.pbkdf2(
    validPassword, validSalt, validIterations, 8, 'md55',
    common.mustNotCall()),
  {
    code: 'ERR_CRYPTO_INVALID_DIGEST',
    name: 'TypeError',
    message: 'Invalid digest: md55'
  }
);

assert.throws(
  () => crypto.pbkdf2Sync(
    validPassword, validSalt, validIterations, 8, 'md55'),
  {
    code: 'ERR_CRYPTO_INVALID_DIGEST',
    name: 'TypeError',
    message: 'Invalid digest: md55'
  }
);

if (isBoringSSL) {
  const kNotPBKDF2Supported = ['shake128', 'shake256'];
  crypto.getHashes()
    .filter((hash) => !kNotPBKDF2Supported.includes(hash))
    .forEach((hash) => {
      runPBKDF2(new Uint8Array(10), 'salt', 8, 8, hash);
    });
}

{
  // This should not crash.
  assert.throws(
    () => crypto.pbkdf2Sync(
      validPassword, validSalt, validIterations, 1, '%'),
    {
      code: 'ERR_CRYPTO_INVALID_DIGEST',
      name: 'TypeError',
      message: 'Invalid digest: %'
    }
  );
}