Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: add SubtleCrypto.supports feature detection in Web Crypto API #57270

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions doc/api/webcrypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,74 @@ async function digest(data, algorithm = 'SHA-512') {
}
```

### Checking for runtime algorithm support

> Stability: 1.0 - Early development. SubleCrypto.supports is an experimental
> implementation based on [Modern Algorithms in the Web Cryptography API][] as
> of 8 January 2025

This example derives a key from a password using Argon2, if available,
or PBKDF2, otherwise; and then encrypts and decrypts some text with it
using AES-OCB, if available, and AES-GCM, otherwise.

```mjs
const password = 'correct horse battery staple';
const derivationAlg =
SubtleCrypto.supports?.('importKey', 'Argon2id') ?
'Argon2id' :
'PBKDF2';
const encryptionAlg =
SubtleCrypto.supports?.('importKey', 'AES-OCB') ?
'AES-OCB' :
'AES-GCM';
const passwordKey = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
derivationAlg,
false,
['deriveKey'],
);
const nonce = crypto.getRandomValues(new Uint8Array(16));
const derivationParams =
derivationAlg === 'Argon2id' ?
{
nonce,
parallelism: 4,
memory: 2 ** 21,
passes: 1,
} :
{
salt: nonce,
iterations: 100_000,
hash: 'SHA-256',
};
const key = await crypto.subtle.deriveKey(
{
name: derivationAlg,
...derivationParams,
},
passwordKey,
{
name: encryptionAlg,
length: 256,
},
false,
['encrypt', 'decrypt'],
);
const plaintext = 'Hello, world!';
const iv = crypto.getRandomValues(new Uint8Array(16));
const encrypted = await crypto.subtle.encrypt(
{ name: encryptionAlg, iv },
key,
new TextEncoder().encode(plaintext),
);
const decrypted = new TextDecoder().decode(await crypto.subtle.decrypt(
{ name: encryptionAlg, iv },
key,
encrypted,
));
```

## Algorithm matrix

The table details the algorithms supported by the Node.js Web Crypto API
Expand Down Expand Up @@ -549,6 +617,28 @@ added: v15.0.0
added: v15.0.0
-->

### Static method: `SubtleCrypto.supports(operation, algorithm[, lengthOrAdditionalAlgorithm])`

> Stability: 1.0 - Early development. An experimental implementation of SubtleCrypto.supports from
> [Modern Algorithms in the Web Cryptography API][] as of 8 January 2025

<!-- YAML
added: REPLACEME
-->

<!--lint disable maximum-line-length remark-lint-->

* `operation`: {string} "encrypt", "decrypt", "sign", "verify", "digest", "generateKey", "deriveKey", "deriveBits", "importKey", "exportKey", "wrapKey" or "unwrapKey"
* `algorithm`: {AesCbcParams|AesCtrParams|AesGcmParams|AesKeyGenParams|AlgorithmIdentifier|EcdhKeyDeriveParams|EcdsaParams|EcKeyGenParams|EcKeyImportParams|Ed448Params|HkdfParams|HmacImportParams|HmacKeyGenParams|Pbkdf2Params|RsaHashedImportParams|RsaHashedKeyGenParams|RsaOaepParams|RsaPssParams|string}
* `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"
* Returns: {boolean} Indicating whether the implementation supports the given operation

<!--lint enable maximum-line-length remark-lint-->

Allows feature detection in Web Crypto API, which can be used to detect whether
a given algorithm identifier (including any of its parameters) is supported for
the given operation.

### `subtle.decrypt(algorithm, key, data)`

<!-- YAML
Expand Down Expand Up @@ -1653,6 +1743,7 @@ The length (in bytes) of the random salt to use.

[JSON Web Key]: https://tools.ietf.org/html/rfc7517
[Key usages]: #cryptokeyusages
[Modern Algorithms in the Web Cryptography API]: https://twiss.github.io/webcrypto-modern-algos/
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
[RFC 4122]: https://www.rfc-editor.org/rfc/rfc4122.txt
[Secure Curves in the Web Cryptography API]: https://wicg.github.io/webcrypto-secure-curves/
Expand Down
8 changes: 4 additions & 4 deletions lib/internal/crypto/aes.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ const {
generateKey: _generateKey,
} = require('internal/crypto/keygen');

const kMaxCounterLength = 128;
const kTagLengths = [32, 64, 96, 104, 112, 120, 128];
const generateKey = promisify(_generateKey);

Expand Down Expand Up @@ -110,10 +109,10 @@ function getVariant(name, length) {
}

function asyncAesCtrCipher(mode, key, data, { counter, length }) {
validateByteLength(counter, 'algorithm.counter', 16);
validateByteLength(counter, 16, 'algorithm.counter');
// The length must specify an integer between 1 and 128. While
// there is no default, this should typically be 64.
if (length === 0 || length > kMaxCounterLength) {
if (length === 0 || length > 128) {
throw lazyDOMException(
'AES-CTR algorithm.length must be between 1 and 128',
'OperationError');
Expand All @@ -130,7 +129,7 @@ function asyncAesCtrCipher(mode, key, data, { counter, length }) {
}

function asyncAesCbcCipher(mode, key, data, { iv }) {
validateByteLength(iv, 'algorithm.iv', 16);
validateByteLength(iv, 16, 'algorithm.iv');
return jobPromise(() => new AESCipherJob(
kCryptoJobAsync,
mode,
Expand Down Expand Up @@ -343,4 +342,5 @@ module.exports = {
aesGenerateKey,
aesImportKey,
getAlgorithmName,
kTagLengths,
};
18 changes: 17 additions & 1 deletion lib/internal/crypto/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,20 @@ const kSupportedAlgorithms = {
'SHA-384': null,
'SHA-512': null,
},
'exportKey': {
'RSASSA-PKCS1-v1_5': null,
'RSA-PSS': null,
'RSA-OAEP': null,
'ECDSA': null,
'ECDH': null,
'HMAC': null,
'AES-CTR': null,
'AES-CBC': null,
'AES-GCM': null,
'AES-KW': null,
'Ed25519': null,
'X25519': null,
},
'generateKey': {
'RSASSA-PKCS1-v1_5': 'RsaHashedKeyGenParams',
'RSA-PSS': 'RsaHashedKeyGenParams',
Expand Down Expand Up @@ -263,12 +277,14 @@ const experimentalAlgorithms = ObjectEntries({
generateKey: null,
importKey: null,
deriveBits: 'EcdhKeyDeriveParams',
exportKey: null,
},
'Ed448': {
generateKey: null,
sign: 'Ed448Params',
verify: 'Ed448Params',
importKey: null,
exportKey: null,
},
});

Expand Down Expand Up @@ -432,7 +448,7 @@ function validateBitLength(length, name, required = false) {
}
}

function validateByteLength(buf, name, target) {
function validateByteLength(buf, target, name) {
if (buf.byteLength !== target) {
throw lazyDOMException(
`${name} must contain exactly ${target} bytes`,
Expand Down
Loading