Skip to content

Commit f9f70b3

Browse files
committed
test: add WebCrypto Promise.prototype.then pollution regression tests
1 parent a90c252 commit f9f70b3

File tree

1 file changed

+131
-0
lines changed

1 file changed

+131
-0
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import * as common from '../common/index.mjs';
2+
3+
if (!common.hasCrypto) common.skip('missing crypto');
4+
5+
// WebCrypto subtle methods must not leak intermediate values
6+
// through Promise.prototype.then pollution.
7+
// Regression test for https://github.com/nodejs/node/pull/61492
8+
// and https://github.com/nodejs/node/issues/59699.
9+
10+
import * as assert from 'node:assert/strict';
11+
import { hasOpenSSL } from '../common/crypto.js';
12+
13+
const { subtle } = globalThis.crypto;
14+
15+
const originalThen = Promise.prototype.then;
16+
const intercepted = [];
17+
18+
// Pollute Promise.prototype.then to record all resolved values.
19+
Promise.prototype.then = function(onFulfilled, ...rest) {
20+
return originalThen.call(this, function(value) {
21+
intercepted.push(value);
22+
return typeof onFulfilled === 'function' ? onFulfilled(value) : value;
23+
}, ...rest);
24+
};
25+
26+
async function test(label, fn) {
27+
const result = await fn();
28+
assert.strictEqual(
29+
intercepted.length, 0,
30+
`Promise.prototype.then was called during ${label}`
31+
);
32+
return result;
33+
}
34+
35+
await test('digest', () =>
36+
subtle.digest('SHA-256', new Uint8Array([1, 2, 3])));
37+
38+
await test('generateKey (AES-CBC)', () =>
39+
subtle.generateKey({ name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']));
40+
41+
await test('generateKey (ECDSA)', () =>
42+
subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']));
43+
44+
const rawKey = globalThis.crypto.getRandomValues(new Uint8Array(32));
45+
46+
const importedKey = await test('importKey', () =>
47+
subtle.importKey('raw', rawKey, { name: 'AES-CBC', length: 256 }, false, ['encrypt', 'decrypt']));
48+
49+
const exportableKey = await test('importKey (extractable)', () =>
50+
subtle.importKey('raw', rawKey, { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']));
51+
52+
await test('exportKey', () =>
53+
subtle.exportKey('raw', exportableKey));
54+
55+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(16));
56+
const plaintext = new TextEncoder().encode('Hello, world!');
57+
58+
const ciphertext = await test('encrypt', () =>
59+
subtle.encrypt({ name: 'AES-CBC', iv }, importedKey, plaintext));
60+
61+
await test('decrypt', () =>
62+
subtle.decrypt({ name: 'AES-CBC', iv }, importedKey, ciphertext));
63+
64+
const signingKey = await test('generateKey (HMAC)', () =>
65+
subtle.generateKey({ name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']));
66+
67+
const data = new TextEncoder().encode('test data');
68+
69+
const signature = await test('sign', () =>
70+
subtle.sign('HMAC', signingKey, data));
71+
72+
await test('verify', () =>
73+
subtle.verify('HMAC', signingKey, signature, data));
74+
75+
const pbkdf2Key = await test('importKey (PBKDF2)', () =>
76+
subtle.importKey('raw', rawKey, 'PBKDF2', false, ['deriveBits', 'deriveKey']));
77+
78+
await test('deriveBits', () =>
79+
subtle.deriveBits(
80+
{ name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' },
81+
pbkdf2Key, 256));
82+
83+
// deriveKey — this was the primary leak reported in the issue
84+
await test('deriveKey', () =>
85+
subtle.deriveKey(
86+
{ name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' },
87+
pbkdf2Key,
88+
{ name: 'AES-CBC', length: 256 },
89+
true,
90+
['encrypt', 'decrypt']));
91+
92+
const wrappingKey = await test('generateKey (AES-KW)', () =>
93+
subtle.generateKey({ name: 'AES-KW', length: 256 }, true, ['wrapKey', 'unwrapKey']));
94+
95+
const keyToWrap = await test('generateKey (AES-CBC for wrap)', () =>
96+
subtle.generateKey({ name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']));
97+
98+
const wrapped = await test('wrapKey', () =>
99+
subtle.wrapKey('raw', keyToWrap, wrappingKey, 'AES-KW'));
100+
101+
await test('unwrapKey', () =>
102+
subtle.unwrapKey(
103+
'raw', wrapped, wrappingKey, 'AES-KW',
104+
{ name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']));
105+
106+
const { privateKey } = await test('generateKey (ECDSA for getPublicKey)', () =>
107+
subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']));
108+
109+
await test('getPublicKey', () =>
110+
subtle.getPublicKey(privateKey, ['verify']));
111+
112+
if (hasOpenSSL(3, 5)) {
113+
const kemPair = await test('generateKey (ML-KEM-768)', () =>
114+
subtle.generateKey(
115+
{ name: 'ML-KEM-768' }, false,
116+
['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits']));
117+
118+
const { ciphertext: ct1 } = await test('encapsulateKey', () =>
119+
subtle.encapsulateKey(
120+
{ name: 'ML-KEM-768' }, kemPair.publicKey, 'HKDF', false, ['deriveBits']));
121+
122+
await test('decapsulateKey', () =>
123+
subtle.decapsulateKey(
124+
{ name: 'ML-KEM-768' }, kemPair.privateKey, ct1, 'HKDF', false, ['deriveBits']));
125+
126+
const { ciphertext: ct2 } = await test('encapsulateBits', () =>
127+
subtle.encapsulateBits({ name: 'ML-KEM-768' }, kemPair.publicKey));
128+
129+
await test('decapsulateBits', () =>
130+
subtle.decapsulateBits({ name: 'ML-KEM-768' }, kemPair.privateKey, ct2));
131+
}

0 commit comments

Comments
 (0)