Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/jsc/bindings/node/crypto/CryptoGenKeyPair.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ KeyEncodingConfig parseKeyEncodingConfig(JSGlobalObject* globalObject, ThrowScop
// defaults and output key object
publicKeyEncoding.output_key_object = true;
} else if (JSObject* publicKeyEncodingObj = publicKeyEncodingValue.getObject()) {
parsePublicKeyEncoding(globalObject, scope, publicKeyEncodingObj, keyTypeValue, "publicKeyEncoding"_s, publicKeyEncoding);
parsePublicKeyEncoding(globalObject, scope, publicKeyEncodingObj, keyTypeValue, "options.publicKeyEncoding"_s, publicKeyEncoding);
RETURN_IF_EXCEPTION(scope, {});
} else {
ERR::INVALID_ARG_VALUE(scope, globalObject, "options.publicKeyEncoding"_s, publicKeyEncodingValue);
Expand All @@ -89,7 +89,7 @@ KeyEncodingConfig parseKeyEncodingConfig(JSGlobalObject* globalObject, ThrowScop
// defaults and output key object
privateKeyEncoding.output_key_object = true;
} else if (JSObject* privateKeyEncodingObj = privateKeyEncodingValue.getObject()) {
parsePrivateKeyEncoding(globalObject, scope, privateKeyEncodingObj, keyTypeValue, "privateKeyEncoding"_s, privateKeyEncoding);
parsePrivateKeyEncoding(globalObject, scope, privateKeyEncodingObj, keyTypeValue, "options.privateKeyEncoding"_s, privateKeyEncoding);
RETURN_IF_EXCEPTION(scope, {});
} else {
ERR::INVALID_ARG_VALUE(scope, globalObject, "options.privateKeyEncoding"_s, privateKeyEncodingValue);
Expand Down
7 changes: 5 additions & 2 deletions src/jsc/bindings/node/crypto/CryptoUtil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -797,9 +797,12 @@ bool isStringOrBuffer(JSValue value)
return false;
}

String makeOptionString(WTF::StringView objName, const ASCIILiteral& optionName)
// Same as `option()` in node's lib/internal/crypto/keys.js: a null prefix means the encoding object is
// the `options` argument itself (KeyObject#export); otherwise prefix is its full path ("key",
// "options.privateKeyEncoding").
String makeOptionString(WTF::StringView prefix, const ASCIILiteral& optionName)
{
return objName.isNull() ? makeString("options."_s, optionName) : makeString("options."_s, objName, '.', optionName);
return prefix.isNull() ? makeString("options."_s, optionName) : makeString(prefix, '.', optionName);
}

ncrypto::EVPKeyPointer::PKFormatType parseKeyFormat(JSGlobalObject* globalObject, ThrowScope& scope, JSValue formatValue, std::optional<EVPKeyPointer::PKFormatType> defaultFormat, WTF::String optionName)
Expand Down
6 changes: 3 additions & 3 deletions src/jsc/bindings/node/crypto/KeyObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2005,7 +2005,7 @@ KeyObject::PrepareAsymmetricKeyResult KeyObject::prepareAsymmetricKey(JSC::JSGlo
RETURN_IF_EXCEPTION(scope, {});
if (auto* decodedView = dynamicDowncast<JSArrayBufferView>(decoded)) {
EVPKeyPointer::PrivateKeyEncodingConfig config;
parseKeyEncoding(globalObject, scope, keyObj, jsUndefined(), isPublic, WTF::nullStringView(), config);
parseKeyEncoding(globalObject, scope, keyObj, jsUndefined(), isPublic, "key"_s, config);
RETURN_IF_EXCEPTION(scope, {});

return {
Expand All @@ -2020,7 +2020,7 @@ KeyObject::PrepareAsymmetricKeyResult KeyObject::prepareAsymmetricKey(JSC::JSGlo

if (auto* view = dynamicDowncast<JSArrayBufferView>(dataValue)) {
EVPKeyPointer::PrivateKeyEncodingConfig config;
parseKeyEncoding(globalObject, scope, keyObj, jsUndefined(), isPublic, WTF::nullStringView(), config);
parseKeyEncoding(globalObject, scope, keyObj, jsUndefined(), isPublic, "key"_s, config);
RETURN_IF_EXCEPTION(scope, {});

return {
Expand All @@ -2034,7 +2034,7 @@ KeyObject::PrepareAsymmetricKeyResult KeyObject::prepareAsymmetricKey(JSC::JSGlo

if (auto* arrayBuffer = dynamicDowncast<JSArrayBuffer>(dataValue)) {
EVPKeyPointer::PrivateKeyEncodingConfig config;
parseKeyEncoding(globalObject, scope, keyObj, jsUndefined(), isPublic, WTF::nullStringView(), config);
parseKeyEncoding(globalObject, scope, keyObj, jsUndefined(), isPublic, "key"_s, config);
RETURN_IF_EXCEPTION(scope, {});

auto* buffer = arrayBuffer->impl();
Expand Down
73 changes: 72 additions & 1 deletion test/js/node/crypto/crypto.key-objects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ function assertApproximateSize(key: any, expectedSize: number) {
expect(key.length).toBeGreaterThanOrEqual(min);
expect(key.length).toBeLessThanOrEqual(max);
}
// The error node's ERR_INVALID_ARG_VALUE(name, value) produces; `received` is util.inspect(value).
function invalidArgValue(name: string, received: string) {
return expect.objectContaining({
code: "ERR_INVALID_ARG_VALUE",
message: `The property '${name}' is invalid. Received ${received}`,
});
}

// Tests that a key pair can be used for encryption / decryption.
function testEncryptDecrypt(publicKey: any, privateKey: any) {
const message = "Hello Node.js world!";
Expand Down Expand Up @@ -328,7 +336,70 @@ describe("crypto.KeyObjects", () => {
expect(() => createPrivateKey({ key: "" })).toThrow();
});
test("This should not abort either: https://github.com/nodejs/node/issues/29904", async () => {
expect(() => createPrivateKey({ key: Buffer.alloc(0), format: "der", type: "spki" })).toThrow();
expect(() => createPrivateKey({ key: Buffer.alloc(0), format: "der", type: "spki" })).toThrow(
invalidArgValue("key.type", "'spki'"),
);
});

// Node names a rejected encoding option after the object it was read from (nodejs/node#62527, v26.1.0):
// `key.<option>` for a `{ key, format, type, ... }` argument, `options.<option>` for KeyObject#export and
// `options.<public|private>KeyEncoding.<option>` for generateKeyPair.
describe("encoding option names in ERR_INVALID_ARG_VALUE messages", () => {
const privateDer = createPrivateKey(privatePem).export({ format: "der", type: "pkcs8" }) as Buffer;
const keyMaterial = {
string: () => privatePem,
Buffer: () => privateDer,
ArrayBuffer: () => privateDer.buffer.slice(privateDer.byteOffset, privateDer.byteOffset + privateDer.byteLength),
};
const data = Buffer.from("hello");
const consumers = {
createPrivateKey: (key: any) => createPrivateKey(key),
createPublicKey: (key: any) => createPublicKey(key),
sign: (key: any) => sign("sha256", data, key),
verify: (key: any) => verify("sha256", data, key, data),
"Verify#verify": (key: any) => createVerify("sha256").update(data).verify(key, data),
publicEncrypt: (key: any) => publicEncrypt(key, data),
publicDecrypt: (key: any) => publicDecrypt(key, data),
};

describe.each(Object.entries(consumers))("%s", (_, consume) => {
test.each(Object.entries(keyMaterial))("names the options key.<option> (%s key material)", (_, material) => {
const key = material();
expect(() => consume({ key, format: "bogus" })).toThrow(invalidArgValue("key.format", "'bogus'"));
expect(() => consume({ key, format: "der", type: "bogus" })).toThrow(invalidArgValue("key.type", "'bogus'"));
expect(() => consume({ key, format: "der" })).toThrow(invalidArgValue("key.type", "undefined"));
expect(() => consume({ key, passphrase: 123 })).toThrow(invalidArgValue("key.passphrase", "123"));
});
});

test("KeyObject#export names the options options.<option>", () => {
const privateKey = createPrivateKey(privatePem);
const exportWith = (options: any) => () => privateKey.export(options);
expect(exportWith({ format: "bogus" })).toThrow(invalidArgValue("options.format", "'bogus'"));
expect(exportWith({ format: "der", type: "bogus" })).toThrow(invalidArgValue("options.type", "'bogus'"));
expect(exportWith({ format: "pem", type: "pkcs8", cipher: 5, passphrase: "secret" })).toThrow(
invalidArgValue("options.cipher", "5"),
);
expect(exportWith({ format: "pem", type: "pkcs8", cipher: "aes-128-cbc", passphrase: 5 })).toThrow(
invalidArgValue("options.passphrase", "5"),
);
});

test("generateKeyPairSync names the options options.<public|private>KeyEncoding.<option>", () => {
const generateWith = (options: any) => () => generateKeyPairSync("ed25519", options);
expect(generateWith({ publicKeyEncoding: { format: "bogus", type: "spki" } })).toThrow(
invalidArgValue("options.publicKeyEncoding.format", "'bogus'"),
);
expect(generateWith({ privateKeyEncoding: { format: "pem", type: "bogus" } })).toThrow(
invalidArgValue("options.privateKeyEncoding.type", "'bogus'"),
);
expect(
generateWith({ privateKeyEncoding: { format: "pem", type: "pkcs8", cipher: 5, passphrase: "secret" } }),
).toThrow(invalidArgValue("options.privateKeyEncoding.cipher", "5"));
expect(
generateWith({ privateKeyEncoding: { format: "pem", type: "pkcs8", cipher: "aes-128-cbc", passphrase: 5 } }),
).toThrow(invalidArgValue("options.privateKeyEncoding.passphrase", "5"));
});
});

test("BoringSSL will not parse PKCS#1", async () => {
Expand Down
2 changes: 1 addition & 1 deletion test/js/node/test/parallel/test-crypto-key-objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem',
createPrivateKey({ key: Buffer.alloc(0), format: 'der', type: 'spki' });
}, {
code: 'ERR_INVALID_ARG_VALUE',
message: "The property 'options.type' is invalid. Received 'spki'"
message: "The property 'key.type' is invalid. Received 'spki'"
});

// Unlike SPKI, PKCS#1 is a valid encoding for private keys (and public keys),
Expand Down
Loading