Skip to content

Commit 28acafe

Browse files
panvaaduh95
authored andcommitted
lib: avoid repeat internal receiver checks
Let internal helpers access the private cache directly to avoid repeating receiver checks. Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #65910 Refs: #65846 Reviewed-By: Xuguang Mei <meixuguang@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 81eba46 commit 28acafe

3 files changed

Lines changed: 78 additions & 57 deletions

File tree

lib/internal/crypto/keys.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,9 @@ const {
11441144
if (depth < 0)
11451145
return this;
11461146

1147+
if (!isCryptoKey(this))
1148+
throw new ERR_INVALID_THIS('CryptoKey');
1149+
11471150
const opts = {
11481151
...options,
11491152
depth: options.depth == null ? null : options.depth - 1,
@@ -1158,14 +1161,20 @@ const {
11581161
}
11591162

11601163
get type() {
1164+
if (!isCryptoKey(this))
1165+
throw new ERR_INVALID_THIS('CryptoKey');
11611166
return getCryptoKeyType(this);
11621167
}
11631168

11641169
get extractable() {
1170+
if (!isCryptoKey(this))
1171+
throw new ERR_INVALID_THIS('CryptoKey');
11651172
return getCryptoKeyExtractable(this);
11661173
}
11671174

11681175
get algorithm() {
1176+
if (!isCryptoKey(this))
1177+
throw new ERR_INVALID_THIS('CryptoKey');
11691178
const slots = getSlots(this);
11701179
let cached = slots[kSlotClonedAlgorithm];
11711180
if (cached === undefined) {
@@ -1176,6 +1185,8 @@ const {
11761185
}
11771186

11781187
get usages() {
1188+
if (!isCryptoKey(this))
1189+
throw new ERR_INVALID_THIS('CryptoKey');
11791190
const slots = getSlots(this);
11801191
let cached = slots[kSlotClonedUsages];
11811192
if (cached === undefined) {
@@ -1210,12 +1221,8 @@ const {
12101221
return #slots in key || isNativeCryptoKey(key);
12111222
};
12121223
getSlots = (key) => {
1213-
if (!key || typeof key !== 'object')
1214-
throw new ERR_INVALID_THIS('CryptoKey');
1215-
if (#slots in key) {
1216-
const cached = key.#slots;
1217-
if (cached !== undefined) return cached;
1218-
}
1224+
const cached = key.#slots;
1225+
if (cached !== undefined) return cached;
12191226
const slots = nativeGetCryptoKeySlots(key);
12201227
slots[kSlotAlgorithm] = cloneInternalAlgorithm(slots[kSlotAlgorithm]);
12211228
key.#slots = slots;
Lines changed: 53 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
'use strict';
22

3-
// The four CryptoKey prototype getters (`type`, `extractable`,
4-
// `algorithm`, `usages`) are user-configurable per Web IDL, so they
5-
// can be invoked with an arbitrary `this`. The native callbacks that
6-
// implement them must brand-check their receiver and throw cleanly
7-
// (ERR_INVALID_THIS) rather than crashing the process or returning
8-
// garbage. This test exercises four progressively more hostile
9-
// receiver shapes, including subverting `instanceof` via
10-
// `Symbol.hasInstance`, to make sure the C++ brand check holds.
3+
// CryptoKey prototype getters and methods can be invoked with an
4+
// arbitrary `this`. They must brand-check their receiver and throw
5+
// cleanly (ERR_INVALID_THIS) rather than crashing the process or
6+
// returning garbage. This test exercises invalid receiver shapes,
7+
// including subverting `instanceof` via `Symbol.hasInstance`.
118
//
129
// It also verifies that `util.types.isCryptoKey()` cannot be fooled
1310
// by prototype spoofing.
@@ -17,7 +14,7 @@ if (!common.hasCrypto)
1714
common.skip('missing crypto');
1815

1916
const assert = require('node:assert');
20-
const { types: { isCryptoKey } } = require('node:util');
17+
const { inspect, types: { isCryptoKey } } = require('node:util');
2118
const { subtle } = globalThis.crypto;
2219

2320
(async () => {
@@ -29,22 +26,16 @@ const { subtle } = globalThis.crypto;
2926

3027
const CryptoKey = key.constructor;
3128

32-
// Capture the underlying prototype getters once, so that subsequent
29+
// Capture the underlying prototype members once, so that subsequent
3330
// tampering with `CryptoKey.prototype` cannot affect what we call.
34-
const getters = {
35-
type: Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'type').get,
36-
extractable:
37-
Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'extractable').get,
38-
algorithm:
39-
Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'algorithm').get,
40-
usages:
41-
Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'usages').get,
42-
};
31+
const descriptors = Object.getOwnPropertyDescriptors(CryptoKey.prototype);
4332

4433
// Sanity: each getter works on a real CryptoKey.
45-
Object.entries(getters).forEach(([name, getter]) => {
46-
assert.notStrictEqual(getter.call(key), undefined, `baseline ${name}`);
47-
});
34+
for (const name of Reflect.ownKeys(descriptors)) {
35+
const { get } = descriptors[name];
36+
if (get !== undefined)
37+
Reflect.apply(get, key, []);
38+
}
4839
assert.strictEqual(isCryptoKey(key), true);
4940
assert.strictEqual(Object.hasOwn(CryptoKey, 'getSlots'), false);
5041
const internalProto = Object.getPrototypeOf(key);
@@ -56,36 +47,51 @@ const { subtle } = globalThis.crypto;
5647
const invalidThis = { code: 'ERR_INVALID_THIS', name: 'TypeError' };
5748
const invalidArgType = { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' };
5849

50+
async function assertInvalidReceiver(receiver) {
51+
for (const name of Reflect.ownKeys(descriptors)) {
52+
if (name === 'constructor') continue;
53+
const descriptor = descriptors[name];
54+
const args = name === inspect.custom ? [0, {}] : [];
55+
for (const kind of ['get', 'set', 'value']) {
56+
const member = descriptor[kind];
57+
if (typeof member !== 'function') continue;
58+
await assert.rejects(
59+
async () => Reflect.apply(member, receiver, args),
60+
invalidThis,
61+
`CryptoKey.${String(name)} (${kind})`,
62+
);
63+
}
64+
}
65+
}
66+
5967
// Plain object receiver.
60-
Object.entries(getters).forEach(([, getter]) => {
61-
assert.throws(() => getter.call({}), invalidThis);
62-
});
68+
await assertInvalidReceiver({});
6369

6470
// Null-prototype object receiver.
65-
Object.entries(getters).forEach(([, getter]) => {
66-
assert.throws(() => getter.call({ __proto__: null }), invalidThis);
67-
});
71+
await assertInvalidReceiver({ __proto__: null });
6872

6973
// Primitive receiver.
70-
Object.entries(getters).forEach(([, getter]) => {
71-
assert.throws(() => getter.call(1), invalidThis);
72-
});
74+
await assertInvalidReceiver(1);
7375

7476
// Null.
75-
Object.entries(getters).forEach(([, getter]) => {
76-
// eslint-disable-next-line no-useless-call
77-
assert.throws(() => getter.call(null), invalidThis);
78-
});
77+
await assertInvalidReceiver(null);
7978

8079
// Undefined.
81-
Object.entries(getters).forEach(([, getter]) => {
82-
assert.throws(() => getter.call(), invalidThis);
83-
});
80+
await assertInvalidReceiver(undefined);
8481

8582
// Function
86-
Object.entries(getters).forEach(([, getter]) => {
87-
assert.throws(() => getter.call(function() {}), invalidThis);
88-
});
83+
await assertInvalidReceiver(function() {});
84+
85+
const revoked = Proxy.revocable(key, {});
86+
revoked.revoke();
87+
for (const receiver of [
88+
{ __proto__: CryptoKey.prototype },
89+
{ __proto__: key },
90+
new Proxy(key, {}),
91+
revoked.proxy,
92+
]) {
93+
await assertInvalidReceiver(receiver);
94+
}
8995

9096
// Prototype spoofing with InternalCryptoKey.prototype must not pass
9197
// util.types.isCryptoKey().
@@ -111,23 +117,19 @@ const { subtle } = globalThis.crypto;
111117
const fake = { foo: 'bar' };
112118
assert.strictEqual(fake instanceof CryptoKey, true);
113119
assert.strictEqual(isCryptoKey(fake), false);
114-
Object.entries(getters).forEach(([, getter]) => {
115-
assert.throws(() => getter.call(fake), invalidThis);
116-
});
120+
await assertInvalidReceiver(fake);
117121

118122
// Subverted `instanceof` plus a real BaseObject of a different
119123
// kind (a Buffer) as the receiver. Without the C++ tag check
120124
// this would type-confuse `Unwrap<NativeCryptoKey>`.
121125
const buf = Buffer.alloc(16);
122126
assert.strictEqual(buf instanceof CryptoKey, true);
123127
assert.strictEqual(isCryptoKey(buf), false);
124-
Object.entries(getters).forEach(([, getter]) => {
125-
assert.throws(() => getter.call(buf), invalidThis);
126-
});
128+
await assertInvalidReceiver(buf);
127129

128130
// The real CryptoKey continues to work after all of the above.
129-
assert.strictEqual(getters.type.call(key), 'secret');
130-
assert.strictEqual(getters.extractable.call(key), true);
131-
assert.strictEqual(getters.algorithm.call(key).name, 'HMAC');
132-
assert.deepStrictEqual(getters.usages.call(key), ['sign']);
131+
assert.strictEqual(descriptors.type.get.call(key), 'secret');
132+
assert.strictEqual(descriptors.extractable.get.call(key), true);
133+
assert.strictEqual(descriptors.algorithm.get.call(key).name, 'HMAC');
134+
assert.deepStrictEqual(descriptors.usages.get.call(key), ['sign']);
133135
})().then(common.mustCall());

test/parallel/test-webcrypto-cryptokey-clone-transfer.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ if (!common.hasCrypto)
1717
common.skip('missing crypto');
1818

1919
const assert = require('node:assert');
20+
const { KeyObject } = require('node:crypto');
2021
const { inspect } = require('node:util');
2122
const { once } = require('node:events');
2223
const { Worker, MessageChannel } = require('node:worker_threads');
@@ -320,6 +321,17 @@ async function checkRsaPssTransferToWorker({ publicKey, privateKey }) {
320321
{ name: 'AES-GCM', iv }, k, ciphertext);
321322
assert.deepStrictEqual(Buffer.from(decrypted), plaintext);
322323
}
324+
325+
const bytes = new Uint8Array(await subtle.exportKey('raw', key));
326+
const nullPrototypeClone = structuredClone(key);
327+
Object.setPrototypeOf(nullPrototypeClone, null);
328+
assert.deepStrictEqual(
329+
new Uint8Array(await subtle.exportKey('raw', nullPrototypeClone)), bytes);
330+
const typeGetter = Object.getOwnPropertyDescriptor(key.constructor.prototype, 'type').get;
331+
const customInspect = key[inspect.custom];
332+
assert.strictEqual(typeGetter.call(nullPrototypeClone), 'secret');
333+
assert.strictEqual(typeof customInspect.call(nullPrototypeClone, 0, {}), 'string');
334+
assert.deepStrictEqual(KeyObject.from(structuredClone(key)).export(), Buffer.from(bytes));
323335
}
324336

325337
// ECDSA keypair (public extractable, private non-extractable)

0 commit comments

Comments
 (0)