Add a few passing tests for node:crypto (#17987)

This commit is contained in:
Dylan Conway
2025-03-07 20:53:06 -08:00
committed by GitHub
parent bf0253df1d
commit 1a68ce05dc
17 changed files with 607 additions and 56 deletions

View File

@@ -1010,8 +1010,11 @@ JSC::EncodedJSValue CRYPTO_INVALID_CURVE(JSC::ThrowScope& throwScope, JSC::JSGlo
JSC::EncodedJSValue CRYPTO_JWK_UNSUPPORTED_CURVE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& curve)
{
auto message = makeString("Unsupported JWK EC curve: "_s, curve);
throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_JWK_UNSUPPORTED_CURVE, message));
WTF::StringBuilder builder;
builder.append("Unsupported JWK EC curve: "_s);
builder.append(curve);
builder.append('.');
throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_JWK_UNSUPPORTED_CURVE, builder.toString()));
return {};
}
@@ -1072,6 +1075,13 @@ JSC::EncodedJSValue CRYPTO_HASH_UPDATE_FAILED(JSC::ThrowScope& throwScope, JSC::
return {};
}
JSC::EncodedJSValue CRYPTO_TIMING_SAFE_EQUAL_LENGTH(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject)
{
auto message = "Input buffers must have the same byte length"_s;
scope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH, message));
return {};
}
JSC::EncodedJSValue MISSING_PASSPHRASE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral message)
{
throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_MISSING_PASSPHRASE, message));

View File

@@ -98,6 +98,7 @@ JSC::EncodedJSValue CRYPTO_INVALID_DIGEST(JSC::ThrowScope& throwScope, JSC::JSGl
JSC::EncodedJSValue CRYPTO_HASH_FINALIZED(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject);
JSC::EncodedJSValue CRYPTO_HASH_UPDATE_FAILED(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject);
JSC::EncodedJSValue MISSING_PASSPHRASE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral message);
JSC::EncodedJSValue CRYPTO_TIMING_SAFE_EQUAL_LENGTH(JSC::ThrowScope&, JSC::JSGlobalObject*);
// URL

View File

@@ -35,6 +35,7 @@ const errors: ErrorCodeMapping = [
["ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", Error],
["ERR_CRYPTO_HASH_FINALIZED", Error],
["ERR_CRYPTO_HASH_UPDATE_FAILED", Error],
["ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH", RangeError],
["ERR_MISSING_PASSPHRASE", TypeError],
["ERR_DLOPEN_FAILED", Error],
["ERR_ENCODING_INVALID_ENCODED_DATA", TypeError],

View File

@@ -102,13 +102,34 @@ fn pbkdf2Sync(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JS
return out_arraybuffer;
}
pub fn createNodeCryptoBindingZig(global: *JSC.JSGlobalObject) JSC.JSValue {
const crypto = JSC.JSValue.createEmptyObject(global, 4);
pub fn timingSafeEqual(global: *JSGlobalObject, callFrame: *JSC.CallFrame) JSError!JSValue {
const l_value, const r_value = callFrame.argumentsAsArray(2);
crypto.put(global, bun.String.init("pbkdf2"), JSC.JSFunction.create(global, "pbkdf2", pbkdf2, 5, .{}));
crypto.put(global, bun.String.init("pbkdf2Sync"), JSC.JSFunction.create(global, "pbkdf2Sync", pbkdf2Sync, 5, .{}));
crypto.put(global, bun.String.init("randomInt"), JSC.JSFunction.create(global, "randomInt", randomInt, 2, .{}));
const l_buf = l_value.asArrayBuffer(global) orelse {
return global.ERR_INVALID_ARG_TYPE("The \"buf1\" argument must be an instance of ArrayBuffer, Buffer, TypedArray, or DataView.", .{}).throw();
};
const l = l_buf.byteSlice();
const r_buf = r_value.asArrayBuffer(global) orelse {
return global.ERR_INVALID_ARG_TYPE("The \"buf2\" argument must be an instance of ArrayBuffer, Buffer, TypedArray, or DataView.", .{}).throw();
};
const r = r_buf.byteSlice();
if (l.len != r.len) {
return global.ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH("Input buffers must have the same byte length", .{}).throw();
}
return JSC.jsBoolean(BoringSSL.CRYPTO_memcmp(l.ptr, r.ptr, l.len) == 0);
}
pub fn createNodeCryptoBindingZig(global: *JSC.JSGlobalObject) JSC.JSValue {
const crypto = JSC.JSValue.createEmptyObject(global, 5);
crypto.put(global, String.init("pbkdf2"), JSC.JSFunction.create(global, "pbkdf2", pbkdf2, 5, .{}));
crypto.put(global, String.init("pbkdf2Sync"), JSC.JSFunction.create(global, "pbkdf2Sync", pbkdf2Sync, 5, .{}));
crypto.put(global, String.init("randomInt"), JSC.JSFunction.create(global, "randomInt", randomInt, 2, .{}));
crypto.put(global, String.init("randomUUID"), JSC.JSFunction.create(global, "randomUUID", randomUUID, 1, .{}));
crypto.put(global, String.init("timingSafeEqual"), JSC.JSFunction.create(global, "timingSafeEqual", timingSafeEqual, 2, .{}));
return crypto;
}

View File

@@ -539,28 +539,8 @@ pub const Crypto = struct {
return globalThis.ERR_CRYPTO_INVALID_SCRYPT_PARAMS(message, fmt).throw();
}
pub fn timingSafeEqual(_: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
const arguments = callframe.arguments_old(2).slice();
if (arguments.len < 2) {
return globalThis.throwInvalidArguments("Expected 2 typed arrays but got nothing", .{});
}
const array_buffer_a = arguments[0].asArrayBuffer(globalThis) orelse {
return globalThis.throwInvalidArguments("Expected typed array but got {s}", .{@tagName(arguments[0].jsType())});
};
const a = array_buffer_a.byteSlice();
const array_buffer_b = arguments[1].asArrayBuffer(globalThis) orelse {
return globalThis.throwInvalidArguments("Expected typed array but got {s}", .{@tagName(arguments[1].jsType())});
};
const b = array_buffer_b.byteSlice();
const len = a.len;
if (b.len != len) {
return globalThis.throw("Input buffers must have the same byte length", .{});
}
return JSC.jsBoolean(len == 0 or bun.BoringSSL.c.CRYPTO_memcmp(a.ptr, b.ptr, len) == 0);
pub fn timingSafeEqual(_: *@This(), global: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
return JSC.Node.Crypto.timingSafeEqual(global, callframe);
}
pub fn timingSafeEqualWithoutTypeChecks(
@@ -574,10 +554,10 @@ pub const Crypto = struct {
const len = a.len;
if (b.len != len) {
return globalThis.throw("Input buffers must have the same byte length", .{});
return globalThis.ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH("Input buffers must have the same byte length", .{}).throw();
}
return JSC.jsBoolean(len == 0 or bun.BoringSSL.c.CRYPTO_memcmp(a.ptr, b.ptr, len) == 0);
return JSC.jsBoolean(bun.BoringSSL.c.CRYPTO_memcmp(a.ptr, b.ptr, len) == 0);
}
pub fn getRandomValues(

View File

@@ -47,9 +47,10 @@ const { POINT_CONVERSION_COMPRESSED, POINT_CONVERSION_HYBRID, POINT_CONVERSION_U
const {
randomInt: _randomInt,
pbkdf2: _pbkdf2,
pbkdf2Sync: _pbkdf2Sync,
timingSafeEqual: _timingSafeEqual,
randomUUID: _randomUUID,
pbkdf2: pbkdf2_,
pbkdf2Sync: pbkdf2Sync_,
} = $zig("node_crypto_binding.zig", "createNodeCryptoBindingZig");
const { validateObject, validateString, validateInt32 } = require("internal/validators");
@@ -493,7 +494,7 @@ function pbkdf2(password, salt, iterations, keylen, digest, callback) {
digest = undefined;
}
const promise = pbkdf2_(password, salt, iterations, keylen, digest, callback);
const promise = _pbkdf2(password, salt, iterations, keylen, digest, callback);
if (callback) {
promise.then(
result => callback(null, result),
@@ -506,7 +507,7 @@ function pbkdf2(password, salt, iterations, keylen, digest, callback) {
}
function pbkdf2Sync(password, salt, iterations, keylen, digest) {
return pbkdf2Sync_(password, salt, iterations, keylen, digest);
return _pbkdf2Sync(password, salt, iterations, keylen, digest);
}
// node_modules/des.js/lib/des/utils.js
@@ -4380,8 +4381,9 @@ var require_browser7 = __commonJS({
exports.DiffieHellmanGroup = exports.createDiffieHellmanGroup = exports.getDiffieHellman = getDiffieHellman;
exports.createDiffieHellman = exports.DiffieHellman = createDiffieHellman;
// TODO: move entire function out of js in diffie-hellman pr
exports.diffieHellman = function diffieHellman(options) {
validateObject(options);
validateObject(options, "options");
const { privateKey, publicKey } = options;
@@ -10194,17 +10196,6 @@ var crypto_exports = require_crypto_browserify2();
var getRandomValues = array => crypto.getRandomValues(array),
randomUUID = () => crypto.randomUUID(),
timingSafeEqual =
"timingSafeEqual" in crypto
? (a, b) => {
let { byteLength: byteLengthA } = a,
{ byteLength: byteLengthB } = b;
if (typeof byteLengthA != "number" || typeof byteLengthB != "number")
throw new TypeError("Input must be an array buffer view");
if (byteLengthA !== byteLengthB) throw new RangeError("Input buffers must have the same length");
return crypto.timingSafeEqual(a, b);
}
: void 0,
scryptSync =
"scryptSync" in crypto
? (password, salt, keylen, options) => {
@@ -10229,16 +10220,14 @@ var getRandomValues = array => crypto.getRandomValues(array),
}
}
: void 0;
timingSafeEqual &&
(Object.defineProperty(timingSafeEqual, "name", {
value: "::bunternal::",
}),
scrypt &&
Object.defineProperty(scrypt, "name", {
value: "::bunternal::",
}),
Object.defineProperty(scryptSync, "name", {
value: "::bunternal::",
}));
scryptSync &&
Object.defineProperty(scryptSync, "name", {
value: "::bunternal::",
});
class KeyObject {
// we use $bunNativePtr so that util.types.isKeyObject can detect it
@@ -10576,7 +10565,7 @@ crypto_exports.getCurves = getCurves;
crypto_exports.getCipherInfo = getCipherInfo;
crypto_exports.scrypt = scrypt;
crypto_exports.scryptSync = scryptSync;
crypto_exports.timingSafeEqual = timingSafeEqual;
crypto_exports.timingSafeEqual = _timingSafeEqual;
crypto_exports.webcrypto = webcrypto;
crypto_exports.subtle = _subtle;
crypto_exports.X509Certificate = X509Certificate;

View File

@@ -69,6 +69,7 @@ pub const Node = struct {
pub const Util = struct {
pub const parseArgs = @import("./bun.js/node/util/parse_args.zig").parseArgs;
};
pub const Crypto = @import("./bun.js/node/node_crypto_binding.zig");
};
const std = @import("std");

View File

@@ -0,0 +1,67 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const {
generateKeyPair,
} = require('crypto');
const {
assertApproximateSize,
testEncryptDecrypt,
testSignVerify,
} = require('../common/crypto');
// Test async RSA key generation with an encrypted private key, but encoded as DER.
{
generateKeyPair('rsa', {
publicExponent: 0x10001,
modulusLength: 512,
publicKeyEncoding: {
type: 'pkcs1',
format: 'der'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'der',
cipher: 'aes-256-cbc',
passphrase: 'secret'
}
}, common.mustSucceed((publicKeyDER, privateKeyDER) => {
assert(Buffer.isBuffer(publicKeyDER));
assertApproximateSize(publicKeyDER, 74);
assert(Buffer.isBuffer(privateKeyDER));
// Since the private key is encrypted, signing shouldn't work anymore.
const publicKey = {
key: publicKeyDER,
type: 'pkcs1',
format: 'der',
};
assert.throws(() => {
testSignVerify(publicKey, {
key: privateKeyDER,
format: 'der',
type: 'pkcs8'
});
}, {
name: 'TypeError',
code: 'ERR_MISSING_PASSPHRASE',
message: 'Passphrase required for encrypted key'
});
// Signing should work with the correct password.
const privateKey = {
key: privateKeyDER,
format: 'der',
type: 'pkcs8',
passphrase: 'secret'
};
testEncryptDecrypt(publicKey, privateKey);
testSignVerify(publicKey, privateKey);
}));
}

View File

@@ -0,0 +1,57 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const {
generateKeyPair,
} = require('crypto');
const {
testSignVerify,
spkiExp,
pkcs8EncExp,
} = require('../common/crypto');
const { hasOpenSSL3 } = require('../common/crypto');
// Test async elliptic curve key generation, e.g. for ECDSA, with an encrypted
// private key with paramEncoding explicit.
{
generateKeyPair('ec', {
namedCurve: 'P-256',
paramEncoding: 'explicit',
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: 'aes-128-cbc',
passphrase: 'top secret'
}
}, common.mustSucceed((publicKey, privateKey) => {
assert.strictEqual(typeof publicKey, 'string');
assert.match(publicKey, spkiExp);
assert.strictEqual(typeof privateKey, 'string');
assert.match(privateKey, pkcs8EncExp);
// Since the private key is encrypted, signing shouldn't work anymore.
assert.throws(() => testSignVerify(publicKey, privateKey),
hasOpenSSL3 ? {
message: 'error:07880109:common libcrypto ' +
'routines::interrupted or cancelled'
} : {
name: 'TypeError',
code: 'ERR_MISSING_PASSPHRASE',
message: 'Passphrase required for encrypted key'
});
testSignVerify(publicKey, {
key: privateKey,
passphrase: 'top secret'
});
}));
}

View File

@@ -0,0 +1,53 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const {
generateKeyPair,
} = require('crypto');
const {
testSignVerify,
spkiExp,
sec1EncExp,
hasOpenSSL3,
} = require('../common/crypto');
{
// Test async explicit elliptic curve key generation with an encrypted
// private key.
generateKeyPair('ec', {
namedCurve: 'prime256v1',
paramEncoding: 'explicit',
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'sec1',
format: 'pem',
cipher: 'aes-128-cbc',
passphrase: 'secret'
}
}, common.mustSucceed((publicKey, privateKey) => {
assert.strictEqual(typeof publicKey, 'string');
assert.match(publicKey, spkiExp);
assert.strictEqual(typeof privateKey, 'string');
assert.match(privateKey, sec1EncExp('AES-128-CBC'));
// Since the private key is encrypted, signing shouldn't work anymore.
assert.throws(() => testSignVerify(publicKey, privateKey),
hasOpenSSL3 ? {
message: 'error:07880109:common libcrypto ' +
'routines::interrupted or cancelled'
} : {
name: 'TypeError',
code: 'ERR_MISSING_PASSPHRASE',
message: 'Passphrase required for encrypted key'
});
testSignVerify(publicKey, { key: privateKey, passphrase: 'secret' });
}));
}

View File

@@ -0,0 +1,56 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const {
generateKeyPair,
} = require('crypto');
const {
testSignVerify,
spkiExp,
pkcs8EncExp,
hasOpenSSL3,
} = require('../common/crypto');
// Test async elliptic curve key generation, e.g. for ECDSA, with an encrypted
// private key.
{
generateKeyPair('ec', {
namedCurve: 'P-256',
paramEncoding: 'named',
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: 'aes-128-cbc',
passphrase: 'top secret'
}
}, common.mustSucceed((publicKey, privateKey) => {
assert.strictEqual(typeof publicKey, 'string');
assert.match(publicKey, spkiExp);
assert.strictEqual(typeof privateKey, 'string');
assert.match(privateKey, pkcs8EncExp);
// Since the private key is encrypted, signing shouldn't work anymore.
assert.throws(() => testSignVerify(publicKey, privateKey),
hasOpenSSL3 ? {
message: 'error:07880109:common libcrypto ' +
'routines::interrupted or cancelled'
} : {
name: 'TypeError',
code: 'ERR_MISSING_PASSPHRASE',
message: 'Passphrase required for encrypted key'
});
testSignVerify(publicKey, {
key: privateKey,
passphrase: 'top secret'
});
}));
}

View File

@@ -0,0 +1,53 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const {
generateKeyPair,
} = require('crypto');
const {
testSignVerify,
spkiExp,
sec1EncExp,
hasOpenSSL3,
} = require('../common/crypto');
{
// Test async named elliptic curve key generation with an encrypted
// private key.
generateKeyPair('ec', {
namedCurve: 'prime256v1',
paramEncoding: 'named',
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'sec1',
format: 'pem',
cipher: 'aes-128-cbc',
passphrase: 'secret'
}
}, common.mustSucceed((publicKey, privateKey) => {
assert.strictEqual(typeof publicKey, 'string');
assert.match(publicKey, spkiExp);
assert.strictEqual(typeof privateKey, 'string');
assert.match(privateKey, sec1EncExp('AES-128-CBC'));
// Since the private key is encrypted, signing shouldn't work anymore.
assert.throws(() => testSignVerify(publicKey, privateKey),
hasOpenSSL3 ? {
message: 'error:07880109:common libcrypto ' +
'routines::interrupted or cancelled'
} : {
name: 'TypeError',
code: 'ERR_MISSING_PASSPHRASE',
message: 'Passphrase required for encrypted key'
});
testSignVerify(publicKey, { key: privateKey, passphrase: 'secret' });
}));
}

View File

@@ -0,0 +1,62 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const {
generateKeyPair,
} = require('crypto');
const {
assertApproximateSize,
testEncryptDecrypt,
testSignVerify,
pkcs1EncExp,
hasOpenSSL3,
} = require('../common/crypto');
// Test async RSA key generation with an encrypted private key.
{
generateKeyPair('rsa', {
publicExponent: 0x10001,
modulusLength: 512,
publicKeyEncoding: {
type: 'pkcs1',
format: 'der'
},
privateKeyEncoding: {
type: 'pkcs1',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: 'secret'
}
}, common.mustSucceed((publicKeyDER, privateKey) => {
assert(Buffer.isBuffer(publicKeyDER));
assertApproximateSize(publicKeyDER, 74);
assert.strictEqual(typeof privateKey, 'string');
assert.match(privateKey, pkcs1EncExp('AES-256-CBC'));
// Since the private key is encrypted, signing shouldn't work anymore.
const publicKey = {
key: publicKeyDER,
type: 'pkcs1',
format: 'der',
};
const expectedError = hasOpenSSL3 ? {
name: 'Error',
message: 'error:07880109:common libcrypto routines::interrupted or ' +
'cancelled'
} : {
name: 'TypeError',
code: 'ERR_MISSING_PASSPHRASE',
message: 'Passphrase required for encrypted key'
};
assert.throws(() => testSignVerify(publicKey, privateKey), expectedError);
const key = { key: privateKey, passphrase: 'secret' };
testEncryptDecrypt(publicKey, key);
testSignVerify(publicKey, key);
}));
}

View File

@@ -0,0 +1,54 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const {
createPrivateKey,
generateKeyPair,
} = require('crypto');
const {
testSignVerify,
hasOpenSSL3,
} = require('../common/crypto');
// Passing an empty passphrase string should not cause OpenSSL's default
// passphrase prompt in the terminal.
// See https://github.com/nodejs/node/issues/35898.
for (const type of ['pkcs1', 'pkcs8']) {
generateKeyPair('rsa', {
modulusLength: 1024,
privateKeyEncoding: {
type,
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: ''
}
}, common.mustSucceed((publicKey, privateKey) => {
assert.strictEqual(publicKey.type, 'public');
for (const passphrase of ['', Buffer.alloc(0)]) {
const privateKeyObject = createPrivateKey({
passphrase,
key: privateKey
});
assert.strictEqual(privateKeyObject.asymmetricKeyType, 'rsa');
}
// Encrypting with an empty passphrase is not the same as not encrypting
// the key, and not specifying a passphrase should fail when decoding it.
assert.throws(() => {
return testSignVerify(publicKey, privateKey);
}, hasOpenSSL3 ? {
name: 'Error',
code: 'ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED',
message: 'error:07880109:common libcrypto routines::interrupted or cancelled'
} : {
name: 'TypeError',
code: 'ERR_MISSING_PASSPHRASE',
message: 'Passphrase required for encrypted key'
});
}));
}

View File

@@ -0,0 +1,27 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const {
generateKeyPairSync,
} = require('crypto');
{
assert.throws(() => generateKeyPairSync('ec', {
namedCurve: 'secp224r1',
publicKeyEncoding: {
format: 'jwk'
},
privateKeyEncoding: {
format: 'jwk'
}
}), {
name: 'Error',
code: 'ERR_CRYPTO_JWK_UNSUPPORTED_CURVE',
message: 'Unsupported JWK EC curve: secp224r1.'
});
}

View File

@@ -0,0 +1,119 @@
// Flags: --expose-internals --no-warnings --allow-natives-syntax
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const crypto = require('crypto');
// 'should consider equal strings to be equal'
assert.strictEqual(
crypto.timingSafeEqual(Buffer.from('foo'), Buffer.from('foo')),
true
);
// 'should consider unequal strings to be unequal'
assert.strictEqual(
crypto.timingSafeEqual(Buffer.from('foo'), Buffer.from('bar')),
false
);
{
// Test TypedArrays with different lengths but equal byteLengths.
const buf = crypto.randomBytes(16).buffer;
const a1 = new Uint8Array(buf);
const a2 = new Uint16Array(buf);
const a3 = new Uint32Array(buf);
for (const left of [a1, a2, a3]) {
for (const right of [a1, a2, a3]) {
assert.strictEqual(crypto.timingSafeEqual(left, right), true);
}
}
}
{
// When the inputs are floating-point numbers, timingSafeEqual neither has
// equality nor SameValue semantics. It just compares the underlying bytes,
// ignoring the TypedArray type completely.
const cmp = (fn) => (a, b) => a.every((x, i) => fn(x, b[i]));
const eq = cmp((a, b) => a === b);
const is = cmp(Object.is);
function test(a, b, { equal, sameValue, timingSafeEqual }) {
assert.strictEqual(eq(a, b), equal);
assert.strictEqual(is(a, b), sameValue);
assert.strictEqual(crypto.timingSafeEqual(a, b), timingSafeEqual);
}
test(new Float32Array([NaN]), new Float32Array([NaN]), {
equal: false,
sameValue: true,
timingSafeEqual: true
});
test(new Float64Array([0]), new Float64Array([-0]), {
equal: true,
sameValue: false,
timingSafeEqual: false
});
const x = new BigInt64Array([0x7ff0000000000001n, 0xfff0000000000001n]);
test(new Float64Array(x.buffer), new Float64Array([NaN, NaN]), {
equal: false,
sameValue: true,
timingSafeEqual: false
});
}
assert.throws(
() => crypto.timingSafeEqual(Buffer.from([1, 2, 3]), Buffer.from([1, 2])),
{
code: 'ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH',
name: 'RangeError',
message: 'Input buffers must have the same byte length'
}
);
assert.throws(
() => crypto.timingSafeEqual('not a buffer', Buffer.from([1, 2])),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
}
);
assert.throws(
() => crypto.timingSafeEqual(Buffer.from([1, 2]), 'not a buffer'),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
}
);
if (typeof Bun === 'undefined') {
// V8 Fast API
const foo = Buffer.from('foo');
const bar = Buffer.from('bar');
const longer = Buffer.from('longer');
function testFastPath(buf1, buf2) {
return crypto.timingSafeEqual(buf1, buf2);
}
eval('%PrepareFunctionForOptimization(testFastPath)');
assert.strictEqual(testFastPath(foo, bar), false);
eval('%OptimizeFunctionOnNextCall(testFastPath)');
assert.strictEqual(testFastPath(foo, bar), false);
assert.strictEqual(testFastPath(foo, foo), true);
assert.throws(() => testFastPath(foo, longer), {
code: 'ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH',
});
if (common.isDebug) {
const { internalBinding } = require('internal/test/binding');
const { getV8FastApiCallCount } = internalBinding('debug');
assert.strictEqual(getV8FastApiCallCount('crypto.timingSafeEqual.ok'), 2);
assert.strictEqual(getV8FastApiCallCount('crypto.timingSafeEqual.error'), 1);
}
}

View File

@@ -163,7 +163,7 @@ it("crypto.timingSafeEqual", () => {
crypto.timingSafeEqual(uuid, uuid.slice(0, uuid.length - 2));
expect.unreachable();
} catch (e) {
expect(e.message).toBe("Input buffers must have the same length");
expect(e.message).toBe("Input buffers must have the same byte length");
}
try {