Skip to content

Commit 7531311

Browse files
proggeramlugRalph Küpper
andauthored
fix(crypto): route createSecretKey/createPrivateKey/createPublicKey through value-dispatch (#6680)
The dynamic crypto value-dispatch (`js_crypto_native_dispatch`) — reached when a key-material constructor is called as a VALUE rather than a direct member call: a named import, a computed property access, or a bundled `require('crypto').createPrivateKey` (turbopack/webpack) — had no arms for `createSecretKey`, `createPrivateKey`, or `createPublicKey`, so they fell to `_ => undefined`. That silently broke jsonwebtoken's HS* signing path. `sign.js` normalizes a string secret with: try { secretOrPrivateKey = createPrivateKey(secret) } catch { secretOrPrivateKey = createSecretKey(Buffer.from(secret)) } ... if (alg.startsWith('HS') && secretOrPrivateKey.type !== 'secret') { ... } Node's `createPrivateKey('<plain string>')` THROWS on non-key material, so the `catch` runs and produces a `secret` KeyObject. Under Perry the value-dispatch returned `undefined` without throwing, the `catch` never ran, `secretOrPrivateKey` became `undefined`, and reading `.type` crashed with "Cannot read properties of undefined (reading 'type')". Add the three arms, routing to the same runtime helpers the codegen fast-path uses (`js_crypto_create_secret_key` / `js_crypto_create_private_key_value` / `js_crypto_create_public_key_value`). Those helpers already throw on invalid material, so the value-form now matches the direct member-call form and Node. Fixes #6675. Co-authored-by: Ralph Küpper <ralph@skelpo.com>
1 parent a3b3a2e commit 7531311

2 files changed

Lines changed: 102 additions & 0 deletions

File tree

crates/perry-stdlib/src/crypto/random.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,32 @@ pub unsafe extern "C" fn js_crypto_native_dispatch(
346346
"generateKeySync" => {
347347
pointer_value(js_crypto_generate_key_sync(str_ptr(0), arg(1)) as *mut u8)
348348
}
349+
// #6675: the key-material constructors reached as a VALUE — a named
350+
// import (`import { createPrivateKey } from "crypto"`), a computed
351+
// property access, or a bundled `require('crypto').createPrivateKey`
352+
// (turbopack/webpack) — route here instead of through the codegen
353+
// fast-path. Without these arms they fell to `_ => undefined`, so
354+
// `createPrivateKey(secret)` returned `undefined` rather than THROWING
355+
// on non-key material. jsonwebtoken's sign()/verify() depend on the
356+
// throw: `try { createPrivateKey(secret) } catch { createSecretKey(...) }`.
357+
// The missing throw left `secretOrPrivateKey` undefined and crashed at
358+
// `secretOrPrivateKey.type` ("Cannot read properties of undefined").
359+
// The runtime helpers throw on invalid material, matching Node.
360+
"createSecretKey" => {
361+
let encoding = if args_len >= 2 && JSValue::from_bits(arg(1).to_bits()).is_any_string()
362+
{
363+
str_ptr(1)
364+
} else {
365+
0
366+
};
367+
pointer_value(js_crypto_create_secret_key(bytes_ptr(0), encoding) as *mut u8)
368+
}
369+
"createPrivateKey" => {
370+
f64::from_bits(JSValue::string_ptr(js_crypto_create_private_key_value(arg(0))).bits())
371+
}
372+
"createPublicKey" => {
373+
f64::from_bits(JSValue::string_ptr(js_crypto_create_public_key_value(arg(0))).bits())
374+
}
349375
"generatePrime" if args_len >= 3 => js_crypto_generate_prime_async(arg(0), arg(1), arg(2)),
350376
"generatePrime" | "generatePrimeSync" => js_crypto_generate_prime_sync(arg(0), arg(1)),
351377
"checkPrime" if args_len >= 3 => js_crypto_check_prime_async(arg(0), arg(1), arg(2)),
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Issue #6675: jsonwebtoken jwt.sign(payload, "<string secret>", {algorithm:"HS256"})
2+
// threw "Cannot read properties of undefined (reading 'type')" under Perry.
3+
//
4+
// jsonwebtoken's sign.js normalizes a string secret with:
5+
// if (secretOrPrivateKey != null && !(secretOrPrivateKey instanceof KeyObject)) {
6+
// try { secretOrPrivateKey = createPrivateKey(secretOrPrivateKey) }
7+
// catch (_) { try { secretOrPrivateKey = createSecretKey(Buffer.from(secretOrPrivateKey)) } catch (_) {...} }
8+
// }
9+
// if (header.alg.startsWith('HS') && secretOrPrivateKey.type !== 'secret') {...}
10+
//
11+
// In Node, createPrivateKey("<plain string>") THROWS (invalid key material), so
12+
// the catch runs createSecretKey and produces a KeyObject with .type === "secret".
13+
//
14+
// The bundled jsonwebtoken reaches these constructors as VALUES (via
15+
// require('crypto').createPrivateKey) rather than direct member calls, so it hits
16+
// the runtime value-dispatch. That dispatch had no arms for the key constructors
17+
// and returned undefined without throwing, so the catch never ran,
18+
// secretOrPrivateKey became undefined, and reading `.type` crashed.
19+
//
20+
// This test mirrors that by obtaining the constructors through a value the
21+
// compiler can't statically resolve to crypto.<method> — exercising the runtime
22+
// value-dispatch path exactly like the bundled package does.
23+
import * as cryptoNs from "crypto";
24+
25+
function assert(condition: boolean, message: string) {
26+
if (!condition) {
27+
throw new Error(message);
28+
}
29+
}
30+
31+
const crypto = cryptoNs as any;
32+
const createPrivateKey = crypto["createPrivateKey"];
33+
const createSecretKey = crypto["createSecretKey"];
34+
const KeyObject = crypto["KeyObject"];
35+
36+
function normalizeSecret(secretOrPrivateKey: any): any {
37+
if (secretOrPrivateKey != null && !(secretOrPrivateKey instanceof KeyObject)) {
38+
try {
39+
secretOrPrivateKey = createPrivateKey(secretOrPrivateKey);
40+
} catch (_) {
41+
try {
42+
secretOrPrivateKey = createSecretKey(
43+
typeof secretOrPrivateKey === "string"
44+
? Buffer.from(secretOrPrivateKey)
45+
: secretOrPrivateKey,
46+
);
47+
} catch (_) {
48+
throw new Error("secretOrPrivateKey is not valid key material");
49+
}
50+
}
51+
}
52+
return secretOrPrivateKey;
53+
}
54+
55+
const key = normalizeSecret("secret-value-here");
56+
// The console.log lines are the byte-for-byte parity oracle (diffed against
57+
// `node --experimental-strip-types`); the asserts make the test fail loudly if
58+
// it is ever run standalone with an incorrect result.
59+
console.log("type:", key.type);
60+
console.log("isSecret:", key.type === "secret");
61+
assert(
62+
key.type === "secret",
63+
"normalizeSecret must fall back to a secret KeyObject",
64+
);
65+
66+
// createPublicKey must also THROW on a plain string (the verify() path relies on
67+
// this to fall through to createSecretKey for HS* tokens).
68+
const createPublicKey = crypto["createPublicKey"];
69+
let pubThrew = false;
70+
try {
71+
createPublicKey("secret-value-here");
72+
} catch (_) {
73+
pubThrew = true;
74+
}
75+
console.log("createPublicKey throws on plain string:", pubThrew);
76+
assert(pubThrew, "createPublicKey must throw on a plain (non-key) string");

0 commit comments

Comments
 (0)