Skip to content

Commit f74d019

Browse files
committed
crypto: add SubtleCrypto.supports feature detection in Web Crypto API
1 parent 8ccbfb6 commit f74d019

File tree

6 files changed

+519
-0
lines changed

6 files changed

+519
-0
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`: {string|AlgorithmIdentifier|AesCbcParams|AesCtrParams|AesGcmParams|AesKeyGenParams|EcdhKeyDeriveParams|EcdsaParams|EcKeyGenParams|EcKeyImportParams|Ed448Params|HkdfParams|HmacImportParams|HmacKeyGenParams|Pbkdf2Params|RsaHashedImportParams|RsaHashedKeyGenParams|RsaOaepParams|RsaPssParams}
633+
* `lengthOrAdditionalAlgorithm`: {null|number|string|AlgorithmIdentifier|AesCbcParams|AesCtrParams|AesDerivedKeyParams|AesGcmParams|AesKeyGenParams|EcdhKeyDeriveParams|EcdsaParams|EcKeyGenParams|EcKeyImportParams|Ed448Params|HkdfParams|HmacImportParams|HmacKeyGenParams|Pbkdf2Params|RsaHashedImportParams|RsaHashedKeyGenParams|RsaOaepParams|RsaPssParams} Depending on the operation this is either ignored, the value of the length argument when operation is "deriveBits", the algorithm of key to be derived when operation is "deriveKey", the algorithm of key to be exported before wrapping when operation is "wrapKey", or the algorithm of key to be imported after unwrapping when operation is "unwrapKey". **Default:** `null` when operation is "deriveBits", `undefined` otherwise.
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
@@ -1673,6 +1763,7 @@ The length (in bytes) of the random salt to use.
16731763
16741764
[JSON Web Key]: https://tools.ietf.org/html/rfc7517
16751765
[Key usages]: #cryptokeyusages
1766+
[Modern Algorithms in the Web Cryptography API]: https://twiss.github.io/webcrypto-modern-algos/
16761767
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
16771768
[RFC 4122]: https://www.rfc-editor.org/rfc/rfc4122.txt
16781769
[Secure Curves in the Web Cryptography API]: https://wicg.github.io/webcrypto-secure-curves/

lib/internal/crypto/hkdf.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,5 @@ module.exports = {
170170
hkdf,
171171
hkdfSync,
172172
hkdfDeriveBits,
173+
validateHkdfDeriveBitsLength,
173174
};

lib/internal/crypto/pbkdf2.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,5 @@ module.exports = {
128128
pbkdf2,
129129
pbkdf2Sync,
130130
pbkdf2DeriveBits,
131+
validatePbkdf2DeriveBitsLength,
131132
};

lib/internal/crypto/util.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,20 @@ const kSupportedAlgorithms = {
175175
'SHA-384': null,
176176
'SHA-512': null,
177177
},
178+
'exportKey': {
179+
'RSASSA-PKCS1-v1_5': null,
180+
'RSA-PSS': null,
181+
'RSA-OAEP': null,
182+
'ECDSA': null,
183+
'ECDH': null,
184+
'HMAC': null,
185+
'AES-CTR': null,
186+
'AES-CBC': null,
187+
'AES-GCM': null,
188+
'AES-KW': null,
189+
'Ed25519': null,
190+
'X25519': null,
191+
},
178192
'generateKey': {
179193
'RSASSA-PKCS1-v1_5': 'RsaHashedKeyGenParams',
180194
'RSA-PSS': 'RsaHashedKeyGenParams',
@@ -259,12 +273,14 @@ const experimentalAlgorithms = ObjectEntries({
259273
generateKey: null,
260274
importKey: null,
261275
deriveBits: 'EcdhKeyDeriveParams',
276+
exportKey: null,
262277
},
263278
'Ed448': {
264279
generateKey: null,
265280
sign: 'Ed448Params',
266281
verify: 'Ed448Params',
267282
importKey: null,
283+
exportKey: null,
268284
},
269285
});
270286

lib/internal/crypto/webcrypto.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const {
4747
} = require('internal/crypto/util');
4848

4949
const {
50+
emitExperimentalWarning,
5051
kEnumerableProperty,
5152
lazyDOMException,
5253
} = require('internal/util');
@@ -923,7 +924,153 @@ class SubtleCrypto {
923924
constructor() {
924925
throw new ERR_ILLEGAL_CONSTRUCTOR();
925926
}
927+
928+
static supports(operation, algorithm, lengthOrAdditionalAlgorithm = null) {
929+
emitExperimentalWarning('The supports Web Crypto API method');
930+
if (this !== SubtleCrypto) throw new ERR_INVALID_THIS('SubtleCrypto constructor');
931+
webidl ??= require('internal/crypto/webidl');
932+
const prefix = "Failed to execute 'supports' on 'SubtleCrypto'";
933+
webidl.requiredArguments(arguments.length, 2, { prefix });
934+
935+
operation = webidl.converters.DOMString(operation, {
936+
prefix,
937+
context: '1st argument',
938+
});
939+
algorithm = webidl.converters.AlgorithmIdentifier(algorithm, {
940+
prefix,
941+
context: '2nd argument',
942+
});
943+
944+
switch (operation) {
945+
case 'encrypt':
946+
case 'decrypt':
947+
case 'sign':
948+
case 'verify':
949+
case 'digest':
950+
case 'generateKey':
951+
case 'deriveKey':
952+
case 'deriveBits':
953+
case 'importKey':
954+
case 'exportKey':
955+
case 'wrapKey':
956+
case 'unwrapKey':
957+
break;
958+
default:
959+
return false;
960+
}
961+
962+
let length;
963+
let additionalAlgorithm;
964+
if (operation === 'deriveKey') {
965+
additionalAlgorithm = webidl.converters.AlgorithmIdentifier(lengthOrAdditionalAlgorithm, {
966+
prefix,
967+
context: '3rd argument',
968+
});
969+
970+
if (!check('importKey', additionalAlgorithm)) {
971+
return false;
972+
}
973+
974+
try {
975+
length = getKeyLength(normalizeAlgorithm(additionalAlgorithm, 'get key length'));
976+
} catch {
977+
return false;
978+
}
979+
980+
operation = 'deriveBits';
981+
} else if (operation === 'wrapKey') {
982+
additionalAlgorithm = webidl.converters.AlgorithmIdentifier(lengthOrAdditionalAlgorithm, {
983+
prefix,
984+
context: '3rd argument',
985+
});
986+
987+
if (!check('exportKey', additionalAlgorithm)) {
988+
return false;
989+
}
990+
} else if (operation === 'unwrapKey') {
991+
additionalAlgorithm = webidl.converters.AlgorithmIdentifier(lengthOrAdditionalAlgorithm, {
992+
prefix,
993+
context: '3rd argument',
994+
});
995+
996+
if (!check('importKey', additionalAlgorithm)) {
997+
return false;
998+
}
999+
} else if (operation === 'deriveBits') {
1000+
length = lengthOrAdditionalAlgorithm;
1001+
if (length !== null) {
1002+
length = webidl.converters['unsigned long'](length, {
1003+
prefix,
1004+
context: '3rd argument',
1005+
});
1006+
}
1007+
}
1008+
1009+
return check(operation, algorithm, length);
1010+
}
1011+
}
1012+
1013+
function check(op, alg, length) {
1014+
let normalizedAlgorithm;
1015+
try {
1016+
normalizedAlgorithm = normalizeAlgorithm(alg, op);
1017+
} catch {
1018+
if (op === 'wrapKey') {
1019+
return check('encrypt', alg);
1020+
}
1021+
1022+
if (op === 'unwrapKey') {
1023+
return check('decrypt', alg);
1024+
}
1025+
1026+
return false;
1027+
}
1028+
1029+
switch (op) {
1030+
case 'encrypt':
1031+
case 'decrypt':
1032+
case 'sign':
1033+
case 'verify':
1034+
case 'digest':
1035+
case 'generateKey':
1036+
case 'importKey':
1037+
case 'exportKey':
1038+
case 'wrapKey':
1039+
case 'unwrapKey':
1040+
return true;
1041+
case 'deriveBits': {
1042+
if (normalizedAlgorithm.name === 'HKDF') {
1043+
try {
1044+
require('internal/crypto/hkdf').validateHkdfDeriveBitsLength(length);
1045+
} catch {
1046+
return false;
1047+
}
1048+
}
1049+
1050+
if (normalizedAlgorithm.name === 'PBKDF2') {
1051+
try {
1052+
require('internal/crypto/pbkdf2').validatePbkdf2DeriveBitsLength(length);
1053+
} catch {
1054+
return false;
1055+
}
1056+
}
1057+
1058+
return true;
1059+
}
1060+
case 'get key length':
1061+
try {
1062+
getKeyLength(alg);
1063+
return true;
1064+
} catch {
1065+
return false;
1066+
}
1067+
default: {
1068+
const assert = require('internal/assert');
1069+
assert.fail('Unreachable code');
1070+
}
1071+
}
9261072
}
1073+
9271074
const subtle = ReflectConstruct(function() {}, [], SubtleCrypto);
9281075

9291076
class Crypto {

0 commit comments

Comments
 (0)