Skip to content

Commit 698ab5b

Browse files
authored
fix(subtle): harden normalizeAlgorithm WebIDL validation (#1042)
1 parent dbf4fb5 commit 698ab5b

5 files changed

Lines changed: 912 additions & 59 deletions

File tree

example/src/tests/subtle/digest_turboshake.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,11 +353,12 @@ test(SUITE, 'KangarooTwelve rejects missing outputLength', async () => {
353353
let threw = false;
354354
try {
355355
// outputLength deliberately omitted — required by WICG WebCrypto Modern
356-
// Algos draft and Node webidl.js:880-897.
356+
// Algos draft. Caught by strict WebIDL normalization (#1025) as
357+
// TypeError before reaching the runtime length validator.
357358
await subtle.digest({ name: 'KT128' }, new Uint8Array(0));
358359
} catch (err) {
359360
threw = true;
360-
expect((err as Error).name).to.equal('OperationError');
361+
expect((err as Error).name).to.equal('TypeError');
361362
}
362363
expect(threw).to.equal(true);
363364
});

example/src/tests/subtle/generateKey.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,14 +288,17 @@ async function testECKeyGen(
288288
'NotSupportedError',
289289
);
290290
});
291+
// Strict WebIDL normalization (#1025): an explicit `undefined` is
292+
// indistinguishable from omission, so EcKeyGenParams.namedCurve is
293+
// reported as missing rather than as an unrecognized curve.
291294
await assertThrowsAsync(
292295
async () =>
293296
subtle.generateKey(
294297
{ name, namedCurve: undefined },
295298
true,
296299
privateUsages,
297300
),
298-
"Unrecognized namedCurve 'undefined'",
301+
"'namedCurve' is required",
299302
);
300303
},
301304
);

example/src/tests/subtle/supports.ts

Lines changed: 164 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,56 @@
11
import { expect } from 'chai';
22
import { Subtle } from 'react-native-quick-crypto';
3+
import type { SubtleAlgorithm } from 'react-native-quick-crypto';
34
import { test } from '../util';
45

56
const SUITE = 'subtle.supports';
67

78
// --- Encrypt ---
8-
test(SUITE, 'encrypt: AES-GCM is supported', () => {
9-
expect(Subtle.supports('encrypt', 'AES-GCM')).to.equal(true);
9+
// Strict WebIDL normalization (#1025): AeadParams.iv is required, so passing
10+
// just the string returns false. Full params are needed to assert support.
11+
test(SUITE, 'encrypt: AES-GCM with iv is supported', () => {
12+
expect(
13+
Subtle.supports('encrypt', {
14+
name: 'AES-GCM',
15+
iv: new Uint8Array(12),
16+
}),
17+
).to.equal(true);
18+
});
19+
20+
test(SUITE, 'encrypt: AES-GCM without iv is not supported', () => {
21+
expect(Subtle.supports('encrypt', 'AES-GCM')).to.equal(false);
22+
});
23+
24+
test(SUITE, 'encrypt: AES-CBC with invalid iv length is not supported', () => {
25+
expect(
26+
Subtle.supports('encrypt', {
27+
name: 'AES-CBC',
28+
iv: new Uint8Array(12),
29+
}),
30+
).to.equal(false);
31+
});
32+
33+
test(SUITE, 'encrypt: AES-GCM with invalid tagLength is not supported', () => {
34+
expect(
35+
Subtle.supports('encrypt', {
36+
name: 'AES-GCM',
37+
iv: new Uint8Array(12),
38+
tagLength: 24,
39+
}),
40+
).to.equal(false);
1041
});
1142

1243
test(SUITE, 'encrypt: RSA-OAEP is supported', () => {
1344
expect(Subtle.supports('encrypt', 'RSA-OAEP')).to.equal(true);
1445
});
1546

16-
test(SUITE, 'encrypt: ChaCha20-Poly1305 is supported', () => {
17-
expect(Subtle.supports('encrypt', 'ChaCha20-Poly1305')).to.equal(true);
47+
test(SUITE, 'encrypt: ChaCha20-Poly1305 with iv is supported', () => {
48+
expect(
49+
Subtle.supports('encrypt', {
50+
name: 'ChaCha20-Poly1305',
51+
iv: new Uint8Array(12),
52+
}),
53+
).to.equal(true);
1854
});
1955

2056
test(SUITE, 'encrypt: HMAC is not supported', () => {
@@ -30,8 +66,21 @@ test(SUITE, 'sign: Ed25519 is supported', () => {
3066
expect(Subtle.supports('sign', 'Ed25519')).to.equal(true);
3167
});
3268

33-
test(SUITE, 'sign: ECDSA is supported', () => {
34-
expect(Subtle.supports('sign', 'ECDSA')).to.equal(true);
69+
// EcdsaParams.hash is required under strict normalization (#1025).
70+
test(SUITE, 'sign: ECDSA with hash is supported', () => {
71+
expect(Subtle.supports('sign', { name: 'ECDSA', hash: 'SHA-256' })).to.equal(
72+
true,
73+
);
74+
});
75+
76+
test(SUITE, 'sign: ECDSA without hash is not supported', () => {
77+
expect(Subtle.supports('sign', 'ECDSA')).to.equal(false);
78+
});
79+
80+
test(SUITE, 'sign: ECDSA with non-SHA hash is not supported', () => {
81+
expect(Subtle.supports('sign', { name: 'ECDSA', hash: 'MD5' })).to.equal(
82+
false,
83+
);
3584
});
3685

3786
test(SUITE, 'sign: HMAC is supported', () => {
@@ -55,6 +104,33 @@ test(SUITE, 'digest: SHA-512 is supported', () => {
55104
expect(Subtle.supports('digest', 'SHA-512')).to.equal(true);
56105
});
57106

107+
test(
108+
SUITE,
109+
'digest: TurboSHAKE128 invalid outputLength is not supported',
110+
() => {
111+
expect(
112+
Subtle.supports('digest', {
113+
name: 'TurboSHAKE128',
114+
outputLength: 0,
115+
}),
116+
).to.equal(false);
117+
},
118+
);
119+
120+
test(
121+
SUITE,
122+
'digest: TurboSHAKE128 invalid domainSeparation is not supported',
123+
() => {
124+
expect(
125+
Subtle.supports('digest', {
126+
name: 'TurboSHAKE128',
127+
outputLength: 256,
128+
domainSeparation: 0x80,
129+
}),
130+
).to.equal(false);
131+
},
132+
);
133+
58134
// --- GenerateKey ---
59135
test(SUITE, 'generateKey: Ed25519 is supported', () => {
60136
expect(Subtle.supports('generateKey', 'Ed25519')).to.equal(true);
@@ -70,20 +146,54 @@ test(SUITE, 'generateKey: HKDF is not supported', () => {
70146

71147
// --- DeriveBits ---
72148
// HKDF/PBKDF2/Argon2 require an explicit length per Node webcrypto.js:1689-1714.
73-
test(SUITE, 'deriveBits: HKDF with length is supported', () => {
74-
expect(Subtle.supports('deriveBits', 'HKDF', 256)).to.equal(true);
149+
// Under strict normalization (#1025) the dictionary members are also required.
150+
const HKDF_FULL: SubtleAlgorithm = {
151+
name: 'HKDF',
152+
hash: 'SHA-256',
153+
salt: new Uint8Array(0),
154+
info: new Uint8Array(0),
155+
};
156+
const PBKDF2_FULL: SubtleAlgorithm = {
157+
name: 'PBKDF2',
158+
hash: 'SHA-256',
159+
salt: new Uint8Array(8),
160+
iterations: 1000,
161+
};
162+
163+
test(SUITE, 'deriveBits: HKDF with full params + length is supported', () => {
164+
expect(Subtle.supports('deriveBits', HKDF_FULL, 256)).to.equal(true);
75165
});
76166

77-
test(SUITE, 'deriveBits: PBKDF2 with length is supported', () => {
78-
expect(Subtle.supports('deriveBits', 'PBKDF2', 256)).to.equal(true);
167+
test(SUITE, 'deriveBits: PBKDF2 with full params + length is supported', () => {
168+
expect(Subtle.supports('deriveBits', PBKDF2_FULL, 256)).to.equal(true);
169+
});
170+
171+
test(SUITE, 'deriveBits: PBKDF2 with zero iterations is not supported', () => {
172+
expect(
173+
Subtle.supports(
174+
'deriveBits',
175+
{
176+
...PBKDF2_FULL,
177+
iterations: 0,
178+
},
179+
256,
180+
),
181+
).to.equal(false);
182+
});
183+
184+
test(SUITE, 'deriveBits: HKDF missing salt/info is not supported', () => {
185+
expect(Subtle.supports('deriveBits', 'HKDF', 256)).to.equal(false);
79186
});
80187

81188
test(SUITE, 'deriveBits: HKDF without length is not supported', () => {
82-
expect(Subtle.supports('deriveBits', 'HKDF')).to.equal(false);
189+
expect(Subtle.supports('deriveBits', HKDF_FULL)).to.equal(false);
83190
});
84191

85-
test(SUITE, 'deriveBits: X25519 is supported', () => {
86-
expect(Subtle.supports('deriveBits', 'X25519')).to.equal(true);
192+
// EcdhKeyDeriveParams.public (a CryptoKey) is required, so calling
193+
// supports('deriveBits', 'X25519') without it returns false under strict
194+
// normalization — mirrors Node's behavior.
195+
test(SUITE, 'deriveBits: X25519 without public key is not supported', () => {
196+
expect(Subtle.supports('deriveBits', 'X25519')).to.equal(false);
87197
});
88198

89199
test(SUITE, 'deriveBits: AES-GCM is not supported', () => {
@@ -93,20 +203,23 @@ test(SUITE, 'deriveBits: AES-GCM is not supported', () => {
93203
// --- DeriveKey ---
94204
test(SUITE, 'deriveKey: HKDF + AES-GCM with length 256 is supported', () => {
95205
expect(
96-
Subtle.supports('deriveKey', 'HKDF', { name: 'AES-GCM', length: 256 }),
206+
Subtle.supports('deriveKey', HKDF_FULL, {
207+
name: 'AES-GCM',
208+
length: 256,
209+
}),
97210
).to.equal(true);
98211
});
99212

100213
// AES key length is required for getKeyLength — Node webcrypto.js:269-279.
101214
test(SUITE, 'deriveKey: HKDF + AES-GCM without length is not supported', () => {
102-
expect(Subtle.supports('deriveKey', 'HKDF', 'AES-GCM')).to.equal(false);
215+
expect(Subtle.supports('deriveKey', HKDF_FULL, 'AES-GCM')).to.equal(false);
103216
});
104217

105218
test(
106219
SUITE,
107220
'deriveKey: HKDF without additional algorithm returns false',
108221
() => {
109-
expect(Subtle.supports('deriveKey', 'HKDF')).to.equal(false);
222+
expect(Subtle.supports('deriveKey', HKDF_FULL)).to.equal(false);
110223
},
111224
);
112225

@@ -176,24 +289,30 @@ test(SUITE, 'encapsulateKey: ML-KEM-768 + Ed25519 is not supported', () => {
176289
);
177290
});
178291

292+
// Under strict normalization (#1025), HmacImportParams.hash is required.
179293
test(
180294
SUITE,
181-
'encapsulateKey: ML-KEM-768 + HMAC default length supported',
295+
'encapsulateKey: ML-KEM-768 + HMAC without hash is not supported',
182296
() => {
183297
expect(Subtle.supports('encapsulateKey', 'ML-KEM-768', 'HMAC')).to.equal(
184-
true,
298+
false,
185299
);
186300
},
187301
);
188302

189-
test(SUITE, 'encapsulateKey: ML-KEM-768 + HMAC length 256 supported', () => {
190-
expect(
191-
Subtle.supports('encapsulateKey', 'ML-KEM-768', {
192-
name: 'HMAC',
193-
length: 256,
194-
}),
195-
).to.equal(true);
196-
});
303+
test(
304+
SUITE,
305+
'encapsulateKey: ML-KEM-768 + HMAC with hash + length 256 supported',
306+
() => {
307+
expect(
308+
Subtle.supports('encapsulateKey', 'ML-KEM-768', {
309+
name: 'HMAC',
310+
hash: 'SHA-256',
311+
length: 256,
312+
}),
313+
).to.equal(true);
314+
},
315+
);
197316

198317
test(
199318
SUITE,
@@ -202,6 +321,7 @@ test(
202321
expect(
203322
Subtle.supports('encapsulateKey', 'ML-KEM-768', {
204323
name: 'HMAC',
324+
hash: 'SHA-256',
205325
length: 512,
206326
}),
207327
).to.equal(false);
@@ -210,19 +330,33 @@ test(
210330

211331
// --- DeriveBits per-algorithm length validators ---
212332
test(SUITE, 'deriveBits: HKDF with non-multiple-of-8 length rejected', () => {
213-
expect(Subtle.supports('deriveBits', 'HKDF', 257)).to.equal(false);
333+
expect(Subtle.supports('deriveBits', HKDF_FULL, 257)).to.equal(false);
214334
});
215335

216336
test(SUITE, 'deriveBits: PBKDF2 with non-multiple-of-8 length rejected', () => {
217-
expect(Subtle.supports('deriveBits', 'PBKDF2', 257)).to.equal(false);
337+
expect(Subtle.supports('deriveBits', PBKDF2_FULL, 257)).to.equal(false);
218338
});
219339

340+
const ARGON2_FULL: SubtleAlgorithm = {
341+
name: 'Argon2id',
342+
nonce: new Uint8Array(16),
343+
parallelism: 1,
344+
memory: 8,
345+
passes: 2,
346+
};
347+
220348
test(SUITE, 'deriveBits: Argon2id length below 32 rejected', () => {
221-
expect(Subtle.supports('deriveBits', 'Argon2id', 16)).to.equal(false);
349+
expect(Subtle.supports('deriveBits', ARGON2_FULL, 16)).to.equal(false);
222350
});
223351

224352
test(SUITE, 'deriveBits: Argon2id length 32 supported', () => {
225-
expect(Subtle.supports('deriveBits', 'Argon2id', 32)).to.equal(true);
353+
expect(Subtle.supports('deriveBits', ARGON2_FULL, 32)).to.equal(true);
354+
});
355+
356+
// New: regression for #1025 — strict normalization rejects missing required
357+
// dictionary members during deriveBits length validation.
358+
test(SUITE, 'deriveBits: Argon2id missing required members rejected', () => {
359+
expect(Subtle.supports('deriveBits', 'Argon2id', 32)).to.equal(false);
226360
});
227361

228362
// --- Invalid operation ---

0 commit comments

Comments
 (0)