Skip to content

Commit 9b2493a

Browse files
committed
crypto: add SubtleCrypto.supports feature detection in Web Crypto API
1 parent 6fdd4e6 commit 9b2493a

File tree

16 files changed

+573
-169
lines changed

16 files changed

+573
-169
lines changed

doc/api/webcrypto.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,74 @@ async function digest(data, algorithm = 'SHA-512') {
351351
}
352352
```
353353

354+
### Checking for runtime algorithm support
355+
356+
> Stability: 1.0 - Early development. SubleCrypto.supports is an experimental
357+
> implementation based on [Modern Algorithms in the Web Cryptography API][] as
358+
> of 8 January 2025
359+
360+
This example derives a key from a password using Argon2, if available,
361+
or PBKDF2, otherwise; and then encrypts and decrypts some text with it
362+
using AES-OCB, if available, and AES-GCM, otherwise.
363+
364+
```mjs
365+
const password = 'correct horse battery staple';
366+
const derivationAlg =
367+
SubtleCrypto.supports?.('importKey', 'Argon2id') ?
368+
'Argon2id' :
369+
'PBKDF2';
370+
const encryptionAlg =
371+
SubtleCrypto.supports?.('importKey', 'AES-OCB') ?
372+
'AES-OCB' :
373+
'AES-GCM';
374+
const passwordKey = await crypto.subtle.importKey(
375+
'raw',
376+
new TextEncoder().encode(password),
377+
derivationAlg,
378+
false,
379+
['deriveKey'],
380+
);
381+
const nonce = crypto.getRandomValues(new Uint8Array(16));
382+
const derivationParams =
383+
derivationAlg === 'Argon2id' ?
384+
{
385+
nonce,
386+
parallelism: 4,
387+
memory: 2 ** 21,
388+
passes: 1,
389+
} :
390+
{
391+
salt: nonce,
392+
iterations: 100_000,
393+
hash: 'SHA-256',
394+
};
395+
const key = await crypto.subtle.deriveKey(
396+
{
397+
name: derivationAlg,
398+
...derivationParams,
399+
},
400+
passwordKey,
401+
{
402+
name: encryptionAlg,
403+
length: 256,
404+
},
405+
false,
406+
['encrypt', 'decrypt'],
407+
);
408+
const plaintext = 'Hello, world!';
409+
const iv = crypto.getRandomValues(new Uint8Array(16));
410+
const encrypted = await crypto.subtle.encrypt(
411+
{ name: encryptionAlg, iv },
412+
key,
413+
new TextEncoder().encode(plaintext),
414+
);
415+
const decrypted = new TextDecoder().decode(await crypto.subtle.decrypt(
416+
{ name: encryptionAlg, iv },
417+
key,
418+
encrypted,
419+
));
420+
```
421+
354422
## Algorithm matrix
355423
356424
The table details the algorithms supported by the Node.js Web Crypto API
@@ -549,6 +617,28 @@ added: v15.0.0
549617
added: v15.0.0
550618
-->
551619
620+
### Static method: `SubtleCrypto.supports(operation, algorithm[, lengthOrAdditionalAlgorithm])`
621+
622+
> Stability: 1.0 - Early development. An experimental implementation of SubtleCrypto.supports from
623+
> [Modern Algorithms in the Web Cryptography API][] as of 8 January 2025
624+
625+
<!-- YAML
626+
added: REPLACEME
627+
-->
628+
629+
<!--lint disable maximum-line-length remark-lint-->
630+
631+
* `operation`: {string} "encrypt", "decrypt", "sign", "verify", "digest", "generateKey", "deriveKey", "deriveBits", "importKey", "exportKey", "wrapKey" or "unwrapKey"
632+
* `algorithm`: {AesCbcParams|AesCtrParams|AesGcmParams|AesKeyGenParams|AlgorithmIdentifier|EcdhKeyDeriveParams|EcdsaParams|EcKeyGenParams|EcKeyImportParams|Ed448Params|HkdfParams|HmacImportParams|HmacKeyGenParams|Pbkdf2Params|RsaHashedImportParams|RsaHashedKeyGenParams|RsaOaepParams|RsaPssParams|string}
633+
* `lengthOrAdditionalAlgorithm`: Depending on the operation this is either ignored, the value of the SubtleCrypto method's length argument, the algorithm of key to be derived when operation is "deriveKey", the algorithm of key to be exported before wrapping when operation is "wrapKey", and the algorithm of key to be imported after unwrapping when operation is "unwrapKey"
634+
* Returns: {boolean} Indicating whether the implementation supports the given operation
635+
636+
<!--lint enable maximum-line-length remark-lint-->
637+
638+
Allows feature detection in Web Crypto API, which can be used to detect whether
639+
a given algorithm identifier (including any of its parameters) is supported for
640+
the given operation.
641+
552642
### `subtle.decrypt(algorithm, key, data)`
553643
554644
<!-- YAML
@@ -1653,6 +1743,7 @@ The length (in bytes) of the random salt to use.
16531743
16541744
[JSON Web Key]: https://tools.ietf.org/html/rfc7517
16551745
[Key usages]: #cryptokeyusages
1746+
[Modern Algorithms in the Web Cryptography API]: https://twiss.github.io/webcrypto-modern-algos/
16561747
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
16571748
[RFC 4122]: https://www.rfc-editor.org/rfc/rfc4122.txt
16581749
[Secure Curves in the Web Cryptography API]: https://wicg.github.io/webcrypto-secure-curves/

lib/internal/crypto/aes.js

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const {
44
ArrayBufferIsView,
55
ArrayBufferPrototypeSlice,
66
ArrayFrom,
7-
ArrayPrototypeIncludes,
87
ArrayPrototypePush,
98
MathFloor,
109
PromiseReject,
@@ -35,10 +34,7 @@ const {
3534
const {
3635
hasAnyNotIn,
3736
jobPromise,
38-
validateByteLength,
3937
validateKeyOps,
40-
validateMaxBufferLength,
41-
kAesKeyLengths,
4238
kHandle,
4339
kKeyObject,
4440
} = require('internal/crypto/util');
@@ -58,7 +54,6 @@ const {
5854
generateKey: _generateKey,
5955
} = require('internal/crypto/keygen');
6056

61-
const kTagLengths = [32, 64, 96, 104, 112, 120, 128];
6257
const generateKey = promisify(_generateKey);
6358

6459
function getAlgorithmName(name, length) {
@@ -108,20 +103,7 @@ function getVariant(name, length) {
108103
}
109104
}
110105

111-
function validateAesCtrAlgorithm(algorithm) {
112-
validateByteLength(algorithm.counter, 'algorithm.counter', 16);
113-
// The length must specify an integer between 1 and 128. While
114-
// there is no default, this should typically be 64.
115-
if (algorithm.length === 0 || algorithm.length > 128) {
116-
throw lazyDOMException(
117-
'AES-CTR algorithm.length must be between 1 and 128',
118-
'OperationError');
119-
}
120-
}
121-
122106
function asyncAesCtrCipher(mode, key, data, algorithm) {
123-
validateAesCtrAlgorithm(algorithm);
124-
125107
return jobPromise(() => new AESCipherJob(
126108
kCryptoJobAsync,
127109
mode,
@@ -132,12 +114,7 @@ function asyncAesCtrCipher(mode, key, data, algorithm) {
132114
algorithm.length));
133115
}
134116

135-
function validateAesCbcAlgorithm(algorithm) {
136-
validateByteLength(algorithm.iv, 'algorithm.iv', 16);
137-
}
138-
139117
function asyncAesCbcCipher(mode, key, data, algorithm) {
140-
validateAesCbcAlgorithm(algorithm);
141118
return jobPromise(() => new AESCipherJob(
142119
kCryptoJobAsync,
143120
mode,
@@ -156,23 +133,8 @@ function asyncAesKwCipher(mode, key, data) {
156133
getVariant('AES-KW', key.algorithm.length)));
157134
}
158135

159-
function validateAesGcmAlgorithm(algorithm) {
160-
if (!ArrayPrototypeIncludes(kTagLengths, algorithm.tagLength)) {
161-
throw lazyDOMException(
162-
`${algorithm.tagLength} is not a valid AES-GCM tag length`,
163-
'OperationError');
164-
}
165-
166-
validateMaxBufferLength(algorithm.iv, 'algorithm.iv');
167-
168-
if (algorithm.additionalData !== undefined) {
169-
validateMaxBufferLength(algorithm.additionalData, 'algorithm.additionalData');
170-
}
171-
}
172-
173136
function asyncAesGcmCipher(mode, key, data, algorithm) {
174137
algorithm.tagLength ??= 128;
175-
validateAesGcmAlgorithm(algorithm);
176138

177139
const tagByteLength = MathFloor(algorithm.tagLength / 8);
178140
let tag;
@@ -220,16 +182,7 @@ function aesCipher(mode, key, data, algorithm) {
220182
}
221183
}
222184

223-
function validateAesGenerateKeyAlgorithm(algorithm) {
224-
if (!ArrayPrototypeIncludes(kAesKeyLengths, algorithm.length)) {
225-
throw lazyDOMException(
226-
'AES key length must be 128, 192, or 256 bits',
227-
'OperationError');
228-
}
229-
}
230-
231185
async function aesGenerateKey(algorithm, extractable, keyUsages) {
232-
validateAesGenerateKeyAlgorithm(algorithm);
233186
const { name, length } = algorithm;
234187

235188
const checkUsages = ['wrapKey', 'unwrapKey'];

lib/internal/crypto/cfrg.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -329,15 +329,7 @@ function cfrgImportKey(
329329
extractable);
330330
}
331331

332-
function validateEdDSASignVerifyAlgorithm(algorithm) {
333-
if (algorithm.name === 'Ed448' && algorithm.context?.byteLength) {
334-
throw lazyDOMException(
335-
'Non zero-length context is not yet supported.', 'NotSupportedError');
336-
}
337-
}
338-
339332
function eddsaSignVerify(key, data, algorithm, signature) {
340-
validateEdDSASignVerifyAlgorithm(algorithm);
341333
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
342334
const type = mode === kSignJobModeSign ? 'private' : 'public';
343335

lib/internal/crypto/diffiehellman.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -297,22 +297,9 @@ function diffieHellman(options) {
297297
}
298298

299299
let masks;
300-
301-
function validateEcdhDeriveBitsAlgorithmAndLength(algorithm, length) {
302-
if (algorithm.public.type !== 'public') {
303-
throw lazyDOMException(
304-
'algorithm.public must be a public key', 'InvalidAccessError');
305-
}
306-
307-
if (algorithm.name !== algorithm.public.algorithm.name) {
308-
throw lazyDOMException(`algorithm.public must be an ${algorithm.name} key`, 'InvalidAccessError');
309-
}
310-
}
311-
312300
// The ecdhDeriveBits function is part of the Web Crypto API and serves both
313301
// deriveKeys and deriveBits functions.
314302
async function ecdhDeriveBits(algorithm, baseKey, length) {
315-
validateEcdhDeriveBitsAlgorithmAndLength(algorithm, length);
316303
const { 'public': key } = algorithm;
317304

318305
if (baseKey.type !== 'private') {

lib/internal/crypto/ec.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict';
22

33
const {
4-
ObjectPrototypeHasOwnProperty,
54
SafeSet,
65
} = primordials;
76

@@ -76,16 +75,7 @@ function createECPublicKeyRaw(namedCurve, keyData) {
7675
return new PublicKeyObject(handle);
7776
}
7877

79-
function validateEcKeyAlgorithm(algorithm) {
80-
if (!ObjectPrototypeHasOwnProperty(kNamedCurveAliases, algorithm.namedCurve)) {
81-
throw lazyDOMException(
82-
'Unrecognized namedCurve',
83-
'NotSupportedError');
84-
}
85-
}
86-
8778
async function ecGenerateKey(algorithm, extractable, keyUsages) {
88-
validateEcKeyAlgorithm(algorithm);
8979
const { name, namedCurve } = algorithm;
9080

9181
const usageSet = new SafeSet(keyUsages);
@@ -158,7 +148,6 @@ function ecImportKey(
158148
extractable,
159149
keyUsages,
160150
) {
161-
validateEcKeyAlgorithm(algorithm);
162151
const { name, namedCurve } = algorithm;
163152

164153
let keyObject;

lib/internal/crypto/hkdf.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ function hkdfSync(hash, key, salt, info, length) {
138138
}
139139

140140
const hkdfPromise = promisify(hkdf);
141-
function validateHkdfDeriveBitsAlgorithmAndLength(algorithm, length) {
141+
function validateHkdfDeriveBitsLength(length) {
142142
if (length === null)
143143
throw lazyDOMException('length cannot be null', 'OperationError');
144144
if (length % 8) {
@@ -149,7 +149,7 @@ function validateHkdfDeriveBitsAlgorithmAndLength(algorithm, length) {
149149
}
150150

151151
async function hkdfDeriveBits(algorithm, baseKey, length) {
152-
validateHkdfDeriveBitsAlgorithmAndLength(algorithm, length);
152+
validateHkdfDeriveBitsLength(length);
153153
const { hash, salt, info } = algorithm;
154154

155155
if (length === 0)
@@ -170,4 +170,5 @@ module.exports = {
170170
hkdf,
171171
hkdfSync,
172172
hkdfDeriveBits,
173+
validateHkdfDeriveBitsLength,
173174
};

lib/internal/crypto/keygen.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@ const {
3333
parsePrivateKeyEncoding,
3434
} = require('internal/crypto/keys');
3535

36-
const {
37-
kAesKeyLengths,
38-
} = require('internal/crypto/util');
39-
4036
const {
4137
customPromisifyArgs,
4238
kEmptyObject,
@@ -355,7 +351,7 @@ function generateKeyJob(mode, keyType, options) {
355351
validateInteger(length, 'options.length', 8, 2 ** 31 - 1);
356352
break;
357353
case 'aes':
358-
validateOneOf(length, 'options.length', kAesKeyLengths);
354+
validateOneOf(length, 'options.length', [128, 192, 256]);
359355
break;
360356
default:
361357
throw new ERR_INVALID_ARG_VALUE(

lib/internal/crypto/mac.js

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -40,24 +40,7 @@ const {
4040

4141
const generateKey = promisify(_generateKey);
4242

43-
function validateHmacGenerateKeyAlgorithm(algorithm) {
44-
if (algorithm.length !== undefined) {
45-
if (algorithm.length === 0)
46-
throw lazyDOMException(
47-
'Zero-length key is not supported',
48-
'OperationError');
49-
50-
// The Web Crypto spec allows for key lengths that are not multiples of 8. We don't.
51-
if (algorithm.length % 8) {
52-
throw lazyDOMException(
53-
'Unsupported algorithm.length',
54-
'NotSupportedError');
55-
}
56-
}
57-
}
58-
5943
async function hmacGenerateKey(algorithm, extractable, keyUsages) {
60-
validateHmacGenerateKeyAlgorithm(algorithm);
6144
const { hash, name } = algorithm;
6245
let { length } = algorithm;
6346

@@ -96,27 +79,13 @@ function getAlgorithmName(hash) {
9679
}
9780
}
9881

99-
function validateHmacImportKeyAlgorithm(algorithm) {
100-
if (algorithm.length !== undefined) {
101-
if (algorithm.length === 0) {
102-
throw lazyDOMException('Zero-length key is not supported', 'DataError');
103-
}
104-
105-
// The Web Crypto spec allows for key lengths that are not multiples of 8. We don't.
106-
if (algorithm.length % 8) {
107-
throw lazyDOMException('Unsupported algorithm.length', 'NotSupportedError');
108-
}
109-
}
110-
}
111-
11282
function hmacImportKey(
11383
format,
11484
keyData,
11585
algorithm,
11686
extractable,
11787
keyUsages,
11888
) {
119-
validateHmacImportKeyAlgorithm(algorithm);
12089
const usagesSet = new SafeSet(keyUsages);
12190
if (hasAnyNotIn(usagesSet, ['sign', 'verify'])) {
12291
throw lazyDOMException(

0 commit comments

Comments
 (0)