diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 3c830c63d..b00ea438e 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -16,4 +16,7 @@ A call on an ECDH value whose arguments no @types/node overload accepts (`getPublicKey("HEX")`) is refused at compile time, as is `computeSecret(view, encoding)`: Node reads that encoding as the input encoding and answers a Buffer where @types/node declares a string. ECDH property reads and writes throw a coded SC2020 error. Other public-key operations remain unsupported. + The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript. + Its `subtle` object, also node:crypto's `subtle` export, supports `generateKey()` and `exportKey()` for HMAC and AES keys, `importKey()` for HMAC, AES, PBKDF2, and HKDF keys, `encrypt()` and `decrypt()` with AES-GCM, AES-CBC, and AES-CTR on native targets, `deriveBits()` and `deriveKey()` with PBKDF2 and HKDF, HMAC `sign()` and `verify()`, and SHA-1 and SHA-2 `digest()`. + Secret keys import and export in the `raw`, `raw-secret`, and `jwk` formats; other SubtleCrypto methods, algorithms, and key formats are refused. Native derivations finish before their deferred callbacks, so computation blocks the caller. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 66b0ba3dd..2c09eb9be 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -12442,9 +12442,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -12465,9 +12465,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -79656,9 +79656,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -79680,7 +79680,7 @@ }, "static": { "status": "supported", - "detail": "Implemented by a dedicated static compiler/runtime path.", + "detail": "Implemented for the documented scriptc module-loader subset.", "verification": "test-backed" } }, @@ -79703,7 +79703,7 @@ }, "static": { "status": "supported", - "detail": "Implemented by a dedicated static compiler/runtime path.", + "detail": "Implemented for the documented scriptc module-loader subset.", "verification": "test-backed" } }, @@ -80001,9 +80001,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80024,9 +80024,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80047,9 +80047,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80139,9 +80139,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80162,9 +80162,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80208,9 +80208,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index c6a7a102c..c9f6dc6be 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19313,6 +19313,8 @@ "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", + "tests/corpus/7122-webcrypto-node-crypto.ts", + "tests/corpus/7123-webcrypto-node-crypto.cjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -21215,8 +21217,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:subtle", + "tests": [ + "tests/corpus/7122-webcrypto-node-crypto.ts", + "tests/corpus/7123-webcrypto-node-crypto.cjs" + ] }, "anchorSource": "ancestor" }, @@ -21238,8 +21244,12 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.webcrypto" + "status": "partial", + "evidence": "compiler-dedicated:crypto.webcrypto", + "tests": [ + "tests/corpus/7122-webcrypto-node-crypto.ts", + "tests/corpus/7123-webcrypto-node-crypto.cjs" + ] }, "anchorSource": "exact" }, @@ -106607,8 +106617,6 @@ "tests/corpus/1534-crypto-hash-chain.ts", "tests/corpus/2556-crypto-introspection.ts", "tests/corpus/2890-crypto-static-utility.ts", - "tests/corpus/5442-crypto-random-values.ts", - "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", @@ -106616,11 +106624,20 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/harness/npm-static.test.ts" ] }, @@ -110078,8 +110095,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.Crypto", + "tests": [ + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", + "tests/corpus/6293-webcrypto-digest.ts" + ] }, "anchorSource": "exact" }, @@ -110102,8 +110124,10 @@ }, "static": { "status": "supported", - "evidence": "compiler-dedicated:crypto.getRandomValues", + "evidence": "compiler-feature:webcrypto.crypto.getRandomValues", "tests": [ + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js" ] @@ -110129,11 +110153,12 @@ }, "static": { "status": "supported", - "evidence": "compiler-dedicated:crypto.randomUUID", + "evidence": "compiler-feature:webcrypto.crypto.randomUUID", "tests": [ - "tests/corpus/5442-crypto-random-values.ts", - "tests/corpus/5443-crypto-random-arguments.js", - "tests/corpus/1358-crypto-random.ts" + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", + "tests/corpus/6204-crypto-get-random-values.ts", + "tests/corpus/6205-crypto-get-random-values.js" ] }, "anchorSource": "exact" @@ -110189,6 +110214,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.CryptoKey", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110217,6 +110243,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.cryptoKey.algorithm", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110245,6 +110272,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.extractable", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110273,6 +110301,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.type", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110301,6 +110330,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.usages", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110471,8 +110501,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.decrypt", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110494,8 +110539,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.deriveBits", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110517,8 +110577,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.deriveKey", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110543,6 +110618,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.digest", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110617,8 +110699,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.encrypt", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110640,8 +110737,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.exportKey", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110686,8 +110798,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.generateKey", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110712,6 +110839,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.importKey", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110743,6 +110877,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.sign", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110797,6 +110938,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.verify", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 23c7a159b..e7b3486c4 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -187,16 +187,18 @@ "stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"], "stdlib.headers": ["tests/harness/fetch-conformance.test.ts"], "stdlib.request": ["tests/harness/fetch-conformance.test.ts"], - "stdlib.webcrypto.subtle": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], - "stdlib.webcrypto.CryptoKey": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], + "stdlib.webcrypto.subtle": ["tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], + "stdlib.webcrypto.CryptoKey": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], "stdlib.response": ["tests/harness/fetch-conformance.test.ts"], "stdlib.readable-stream": ["tests/harness/fetch-conformance.test.ts"], "stdlib.abort": ["tests/harness/fetch-conformance.test.ts"] }, "features": [ - { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, + { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, { "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, - { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "webcrypto", "signatures": ["Class: `Crypto`"], "status": "partial", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6293-webcrypto-digest.ts"] }, + { "chapter": "webcrypto", "symbols": ["crypto.randomUUID", "crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] }, { "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] }, { "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `SubtleCrypto`"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, @@ -413,6 +415,7 @@ { "symbols": ["assert.deepEqual", "assert.notDeepEqual"], "status": "partial", "evidence": ["tests/corpus/5513-assert-deep-equal-loose.js", "tests/corpus/5514-assert-deep-equal-typed.ts", "tests/corpus/5516-assert-deep-equal-edges.js"] }, { "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] }, { "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/fixtures/node-types/cipher.ts"] }, + { "chapter": "crypto", "symbols": ["crypto.webcrypto", "subtle"], "status": "partial", "evidence": ["tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs"] }, { "chapter": "crypto", "symbols": ["ECDH", "ecdh.computeSecret", "ecdh.generateKeys", "ecdh.getPrivateKey", "ecdh.getPublicKey", "ecdh.setPrivateKey", "ecdh.setPublicKey"], "status": "partial", "evidence": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"] }, { "chapter": "errors", "symbols": ["ERR_CRYPTO_ECDH_INVALID_FORMAT", "ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY"], "status": "partial", "evidence": ["tests/corpus/6702-ecdh-errors.js"] }, { "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index dbb3cd8fd..489af1700 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -544,12 +544,15 @@ declare var DOMException: { readonly prototype: DOMException; }; -/* WebCrypto's HMAC slice (scr_webcrypto.c): crypto.subtle's importKey for - * raw HMAC keys, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as +/* WebCrypto's secret-key slice (scr_webcrypto.c): crypto.subtle's + * importKey, generateKey and exportKey for HMAC and AES keys (importKey + * also for PBKDF2 and HKDF keys), AES-GCM/CBC/CTR encrypt and decrypt, + * PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as * exactly that surface (the declarations mirror @types/node's webcrypto - * shapes). Every other SubtleCrypto method and crypto member is absent, - * so a use is an honest type error. CryptoKey values are native handles - * whose four members read through the handle. */ + * shapes), plus the global's randomUUID and getRandomValues. Every other + * SubtleCrypto method and crypto member is absent, so a use is an honest + * type error. CryptoKey values are native handles whose four members read + * through the handle. */ declare var crypto: Crypto; /* The Crypto and SubtleCrypto constructors (feature probes only) stay * `unknown`; the values keep this declaration order. */ @@ -582,9 +585,44 @@ interface HmacImportParams extends Algorithm { hash: HashAlgorithmIdentifier; length?: number; } +interface HmacKeyGenParams extends Algorithm { + hash: HashAlgorithmIdentifier; + length?: number; +} +interface AesKeyGenParams extends Algorithm { + length: number; +} +interface AesGcmParams extends Algorithm { + additionalData?: BufferSource; + iv: BufferSource; + tagLength?: number; +} +interface AesCbcParams extends Algorithm { + iv: BufferSource; +} +interface AesCtrParams extends Algorithm { + counter: BufferSource; + length: number; +} +interface Pbkdf2Params extends Algorithm { + hash: HashAlgorithmIdentifier; + iterations: number; + salt: BufferSource; +} +interface HkdfParams extends Algorithm { + hash: HashAlgorithmIdentifier; + info: BufferSource; + salt: BufferSource; +} +interface AesDerivedKeyParams extends Algorithm { + length: number; +} interface KeyAlgorithm { name: string; } +interface AesKeyAlgorithm extends KeyAlgorithm { + length: number; +} interface HmacKeyAlgorithm extends KeyAlgorithm { hash: KeyAlgorithm; length: number; @@ -623,10 +661,37 @@ interface JsonWebKey { x?: string; y?: string; } -/* The 'jwk' form type-checks like @types/node's; the compiler refuses it - * (the JsonWebKey import is not implemented). */ interface SubtleCrypto { + decrypt( + algorithm: AesGcmParams | AesCbcParams | AesCtrParams, + key: CryptoKey, + data: BufferSource, + ): Promise; + deriveBits( + algorithm: Pbkdf2Params | HkdfParams, + baseKey: CryptoKey, + length?: number | null, + ): Promise; + deriveKey( + algorithm: Pbkdf2Params | HkdfParams, + baseKey: CryptoKey, + derivedKeyType: AlgorithmIdentifier | AesDerivedKeyParams | HmacImportParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise; + encrypt( + algorithm: AesGcmParams | AesCbcParams | AesCtrParams, + key: CryptoKey, + data: BufferSource, + ): Promise; + exportKey(format: "jwk", key: CryptoKey): Promise; + exportKey(format: Exclude, key: CryptoKey): Promise; + generateKey( + algorithm: AesKeyGenParams | HmacKeyGenParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; importKey( format: "jwk", keyData: JsonWebKey, @@ -651,6 +716,21 @@ interface SubtleCrypto { } interface Crypto { readonly subtle: SubtleCrypto; + getRandomValues< + T extends + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | BigInt64Array + | BigUint64Array, + >( + typedArray: T, + ): T; + randomUUID(): `${string}-${string}-${string}-${string}-${string}`; } /* Capability-gated globals harness code probes behind feature guards @@ -2784,8 +2864,7 @@ declare module "crypto" { b: ArrayBufferView | ArrayBufferLike, ): boolean; /* WebCrypto's getRandomValues (also the webcrypto object's): integer - * typed arrays, filled in place and returned. The crypto GLOBAL stays - * unknown in fallback builds. */ + * typed arrays, filled in place and returned. */ type RandomValuesArray = | Int8Array | Uint8Array @@ -2797,9 +2876,9 @@ declare module "crypto" { | BigInt64Array | BigUint64Array; export function getRandomValues(typedArray: T): T; - export const webcrypto: { - getRandomValues(typedArray: T): T; - }; + /* The WebCrypto global's object and its SubtleCrypto. */ + export const webcrypto: Crypto; + export const subtle: SubtleCrypto; export function randomFillSync( buffer: T, offset?: number, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8bb8bc3aa..507624652 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -615,10 +615,17 @@ export const LIB_FN_SYMS: Record = { "crypto.keyObjectInput": "scr_crypto_key_object_input", "crypto.isKeyObject": "scr_crypto_is_key_object", "webcrypto.importKey": "scr_webcrypto_import_key", + "webcrypto.generateKey": "scr_webcrypto_generate_key", + "webcrypto.exportKey": "scr_webcrypto_export_key", + "webcrypto.encrypt": "scr_webcrypto_encrypt", + "webcrypto.decrypt": "scr_webcrypto_decrypt", + "webcrypto.deriveBits": "scr_webcrypto_derive_bits", + "webcrypto.deriveKey": "scr_webcrypto_derive_key", "webcrypto.sign": "scr_webcrypto_sign", "webcrypto.verify": "scr_webcrypto_verify", "webcrypto.digest": "scr_webcrypto_digest", "webcrypto.isCryptoKey": "scr_webcrypto_is_crypto_key", + "webcrypto.object": "scr_webcrypto_object", "crypto.randomUUIDv7": "scr_crypto_random_uuid_v7", "crypto.pbkdf2": "scr_crypto_pbkdf2", "crypto.hkdf": "scr_crypto_hkdf", @@ -1573,6 +1580,11 @@ export const USES_TIMERS_LIB_FNS = new Set([ // ecdh.setPublicKey reports DEP0031 on the next tick. "crypto.ecdhNew", // WebCrypto results settle through the deferred crypto queue. + "webcrypto.generateKey", + "webcrypto.encrypt", + "webcrypto.decrypt", + "webcrypto.deriveBits", + "webcrypto.deriveKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index a476e1f7f..55301948b 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -84,8 +84,9 @@ export interface NativeLinkFeatures { foreignFfi: boolean; nodeTest: boolean; tls: boolean; - /** crypto.createCipheriv/createDecipheriv: scr_cipher.c plus the mbedTLS - * archive. */ + /** crypto.createCipheriv/createDecipheriv and WebCrypto's encrypt and + * decrypt: scr_cipher.c plus the mbedTLS archive (and, with webcrypto, + * scr_webcrypto_aes.c). */ cipher: boolean; /** crypto.createECDH and ECDH.convertKey: scr_ecdh.c plus the mbedTLS * archive. */ diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 06a41c9b2..8df233585 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -243,8 +243,9 @@ export interface CcOptions { * The unit also compiles whenever `tls` does — scr_tls.c consults its * default-set override and shared Windows-certificate enumerator. */ tlsCa?: boolean; - /** crypto.createCipheriv/createDecipheriv: compiles scr_cipher.c and links - * the mbedTLS archive (shared with tls). */ + /** crypto.createCipheriv/createDecipheriv and WebCrypto's encrypt and + * decrypt: compiles scr_cipher.c (and, with webcrypto, + * scr_webcrypto_aes.c) and links the mbedTLS archive (shared with tls). */ cipher?: boolean; /** crypto.createECDH and ECDH.convertKey: compiles scr_ecdh.c and links * the mbedTLS archive (shared with tls). */ diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index a551ad600..229b2f967 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -297,6 +297,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"), unit((u) => u.opts.nodeTest, "scr_test.c"), unit((u) => u.opts.webcrypto, "scr_webcrypto.c"), + // The crypto global's object extends scr_async_dyn.c's. + unit((u) => u.opts.webcrypto && u.opts.dynAsync, "scr_webcrypto_object.c"), // The CA-store unit rides its own gate OR the tls one: scr_tls.c // references its default-set override unconditionally. unit((u) => u.tlsCa, "scr_tls_ca.c"), @@ -308,6 +310,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ ...(u.tls ? [u.rt(join(u.rtDir, "scr_tls.c"))] : []), ...(u.cipher ? [u.rt(join(u.rtDir, "scr_cipher.c"))] : []), ...(u.ecdh ? [u.rt(join(u.rtDir, "scr_ecdh.c"))] : []), + ...(u.cipher && u.opts.webcrypto ? [u.rt(join(u.rtDir, "scr_webcrypto_aes.c"))] : []), u.tlsArchive, // mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h). // The unconditional win32 libs above do not carry it. diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts index b7049f6fe..eb70a0d2c 100644 --- a/packages/compiler/src/backend/target-diagnostics.ts +++ b/packages/compiler/src/backend/target-diagnostics.ts @@ -208,6 +208,14 @@ export function moduleWasiUnavailableSurface( }; return false; } + if (node.fn === "webcrypto.encrypt" || node.fn === "webcrypto.decrypt") { + found = { + surface: + "WebCrypto encrypt and decrypt (the wasm32-wasi runtime does not include the mbedTLS library)", + loc, + }; + return false; + } if (node.fn === "crypto.ecdhNew" || node.fn === "crypto.ecdhConvertKey") { found = { surface: "ECDH (the wasm32-wasi runtime does not include the mbedTLS library)", diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index f4270dbe8..fc1502958 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -7,6 +7,7 @@ import { type Lowerer, own } from "../lowerer.js"; import { locOf } from "../../program.js"; import { CIPHER_TYPE_NAMES } from "../../type-mapper.js"; import { CRYPTO_CIPHERS, CRYPTO_CURVES, CRYPTO_HASHES } from "../crypto-tables.js"; +import { stdlibGlobalNameOf } from "../surfaces.js"; import { BOOL, BYTES_U8, @@ -1316,8 +1317,10 @@ export function lowerCryptoGetRandomValues( } /** Whether `receiver` names a WebCrypto Crypto object: the `crypto` - * global (also through globalThis) or node:crypto's `webcrypto`. */ + * global (also through globalThis or an alias of it) or node:crypto's + * `webcrypto`. */ export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): boolean { + if (stdlibGlobalNameOf(lowerer, receiver) === "crypto") return true; if (ts.isIdentifier(receiver)) { if (receiver.text === "crypto" && lowerer.isStdlibGlobal(receiver, "crypto")) return true; const imported = lowerer.builtinImportOf(receiver); diff --git a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts index 93cba84c5..fe310294c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts @@ -5,34 +5,57 @@ import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js"; import { lowerDynObjectLiteral } from "../expressions/object-literals.js"; import { stdlibGlobalNameOf } from "../surfaces.js"; import { WEBCRYPTO_DYN_TYPES } from "../../type-mapper.js"; +import { isWebCryptoReceiver } from "./crypto.js"; -/* WebCrypto's HMAC slice (scr_webcrypto.c): `crypto.subtle.importKey`, - * `sign`, `verify` and `digest` called on the crypto global (also through - * globalThis) lower to runtime entry points that take the argument vector - * as one dyn array and answer a promise, so Web IDL conversion, algorithm - * normalization and every error stay Node-shaped at run time. CryptoKey +/* WebCrypto's secret-key slice (scr_webcrypto.c, with encrypt and + * decrypt in scr_webcrypto_aes.c): `crypto.subtle`'s `importKey`, + * `generateKey`, `exportKey`, `encrypt`, `decrypt`, `deriveBits`, + * `deriveKey`, `sign`, `verify` and `digest` + * called on the crypto global (also through + * globalThis, node:crypto's webcrypto and node:crypto's subtle) lower to + * runtime entry points that take the argument vector as one dyn array and + * answer a promise, so Web IDL conversion, algorithm normalization and + * every error stay Node-shaped at run time. CryptoKey * values are native handles; their type maps to dyn and their members * are handle reads. Other SubtleCrypto methods are refused by name. */ const SUBTLE_LOWERED: ReadonlyMap = new Map([ ["importKey", "webcrypto.importKey"], + ["generateKey", "webcrypto.generateKey"], + ["exportKey", "webcrypto.exportKey"], + ["encrypt", "webcrypto.encrypt"], + ["decrypt", "webcrypto.decrypt"], + ["deriveBits", "webcrypto.deriveBits"], + ["deriveKey", "webcrypto.deriveKey"], ["sign", "webcrypto.sign"], ["verify", "webcrypto.verify"], ["digest", "webcrypto.digest"], ]); const SUBTLE_HINT = - "the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; + "the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; -/** Whether `node` is `crypto.subtle` on the WebCrypto global. */ +/** Whether `node` is a SubtleCrypto object: `subtle` on a WebCrypto + * Crypto object (the global, globalThis.crypto or node:crypto's + * `webcrypto`) or node:crypto's own `subtle` export, named or through + * the namespace. */ function isSubtleReceiver(lowerer: Lowerer, node: ts.Expression): boolean { while (ts.isParenthesizedExpression(node)) node = node.expression; - return ( - ts.isPropertyAccessExpression(node) && - !node.questionDotToken && - node.name.text === "subtle" && - stdlibGlobalNameOf(lowerer, node.expression) === "crypto" - ); + if (ts.isIdentifier(node)) { + const imported = lowerer.builtinImportOf(node); + return imported?.module === "crypto" && imported.member === "subtle"; + } + if ( + !ts.isPropertyAccessExpression(node) || + node.questionDotToken || + node.name.text !== "subtle" + ) { + return false; + } + if (stdlibGlobalNameOf(lowerer, node.expression) === "crypto") return true; + if (isWebCryptoReceiver(lowerer, node.expression)) return true; + const member = lowerer.builtinMemberOf(node); + return member?.module === "crypto" && member.member === "subtle"; } /** `crypto.subtle.(...args)`: the lowered methods become their @@ -51,17 +74,6 @@ export function lowerWebCryptoCall(lowerer: Lowerer, call: ts.CallExpression): I if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`crypto.subtle.${member} with spread arguments`, call, SUBTLE_HINT); } - // JsonWebKey imports are not implemented: a literal 'jwk' format is - // refused here (the runtime refuses a computed one). - const format = call.arguments.nodes[0]; - if ( - member === "importKey" && - format !== undefined && - ts.isStringLiteralLike(format) && - format.text === "jwk" - ) { - lowerer.noLowering("crypto.subtle.importKey with the 'jwk' key format", format, SUBTLE_HINT); - } const loc = locOf(call); const args = call.arguments.nodes.map((arg) => ts.isObjectLiteralExpression(arg) diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index b5f99d682..16cca3b13 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -549,6 +549,9 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: const callable = lowerer.lowerBuiltinCallableValue(bi, loc); if (callable) return callable; } + // node:crypto's webcrypto is the WebCrypto global's object. + if (bi.module === "crypto" && bi.member === "webcrypto") + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; // JavaScript sources: an otherwise unsupported builtin VALUE is // the same identity-token story as stdlib globals above (the // harness adds worker_threads.Worker to its identity Set). @@ -721,7 +724,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: return { kind: "closure", fnName: name, captures: [], type, loc }; } if (lowerer.isStdlibGlobal(expr, "crypto")) - return { kind: "libCall", fn: "crypto.native", args: [], type: DYN, loc }; + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; const nativeAlias = stdlibGlobalNameOf(lowerer, expr); if (nativeAlias === "Buffer" && expr.text !== "Buffer") { return { kind: "strLit", value: "[builtin Buffer]", type: STRING, loc }; diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index f0b980da0..5cd70d2a5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -162,6 +162,7 @@ import { lowerUnionFieldRead } from "./union-fields.js"; import { errorPropertyRead } from "../error-methods.js"; import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js"; import { isZlibStreamMember, lowerZlibStreamBytesWritten } from "../builtins/compression.js"; +import { isWebCryptoReceiver } from "../builtins/crypto.js"; import { isFsStreamMember, lowerFsStreamProperty } from "../builtins/fs-streams.js"; import { keyedFieldRead } from "./keyed-field-targets.js"; import { promisePrototypeValue } from "../thenables.js"; @@ -842,8 +843,16 @@ export function lowerPropertyAccessExpr( if (runtimeOptionalClassField(lowerer, handled, expr)) return handled; return iteratorResultValueRead(lowerer, handled, expr) ?? lowerer.maybeNarrow(handled, expr); } + // `globalThis.crypto` is the WebCrypto object the bare `crypto` answers. + if ( + !expr.questionDotToken && + stdlibGlobalNameOf(lowerer, expr.expression) === "globalThis" && + stdlibGlobalNameOf(lowerer, expr) === "crypto" + ) { + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; + } // `globalThis.` that no lowering above claimed - // (globalThis.crypto, globalThis.SubtleCrypto, globalThis.localStorage + // (globalThis.SubtleCrypto, globalThis.localStorage // — the harness's capability-conditional knownGlobals adds): the // PROPERTY spelling of the identifier chokepoint's JS rule — a stdlib // global taken as a VALUE in a JavaScript source is the same opaque @@ -2959,6 +2968,9 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi if ( receiverIr?.kind !== "object" && propSym && + // The WebCrypto object's generic getRandomValues reads off its + // dynamic object. + !isWebCryptoReceiver(lowerer, expr.expression) && isGenericCallableMemberType(lowerer.checker.getTypeOfSymbol(propSym), lowerer.checker) && // CLASS members keep the class-path fences (a poisoned class's own // diagnostics, the bound-method fence above). diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 7102f37ab..c503f0c9c 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1542,9 +1542,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.fenceStaticReadableStreamMember(expr.expression, "call") ?? lowerDisposableStackUseCall(lowerer, expr, expr.expression) ?? lowerer.lowerStdlibMethodCall(expr, expr.expression) ?? - // crypto.getRandomValues on the WebCrypto global (untyped in - // fallback builds, so before the dyn receiver path) or webcrypto. - lowerWebCryptoGetRandomValues(lowerer, expr, expr.expression) ?? + // crypto.getRandomValues / randomUUID on the WebCrypto global + // (untyped in fallback builds, so before the dyn receiver path) or + // webcrypto. + lowerWebCryptoMethodCall(lowerer, expr, expr.expression) ?? // Dyn receivers (JSON.parse-derived `unknown`/`any` values) — lowerDynReceiverMethodCall(lowerer, expr, expr.expression) ?? @@ -10492,20 +10493,40 @@ export function lowerStaticNumberGlobal( return null; } -/** `crypto.getRandomValues(view)` on the WebCrypto global or webcrypto. */ -function lowerWebCryptoGetRandomValues( +/** `crypto.getRandomValues(view)` and `crypto.randomUUID()` on the + * WebCrypto global or webcrypto. Other argument counts (JavaScript only) + * invoke the dynamic object's method, which ignores extra arguments and + * throws Node's missing-argument TypeError. */ +function lowerWebCryptoMethodCall( lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { + const member = access.name.text; if ( - access.name.text !== "getRandomValues" || + (member !== "getRandomValues" && member !== "randomUUID") || call.questionDotToken || access.questionDotToken || !isWebCryptoReceiver(lowerer, access.expression) ) return null; - return lowerCryptoGetRandomValues(lowerer, call); + const args = call.arguments.nodes; + if (member === "getRandomValues" && args.length === 1) { + return lowerCryptoGetRandomValues(lowerer, call); + } + if (member === "randomUUID" && args.length === 0) { + return { kind: "libCall", fn: "crypto.randomUUID", args: [], type: STRING, loc: locOf(call) }; + } + if (args.some(ts.isSpreadElement)) return null; + return { + kind: "dynInvoke", + recv: lowerer.lowerExprExpecting(access.expression, DYN), + method: member, + calleeName: v8CalleeText(access), + args: args.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + type: DYN, + loc: locOf(call), + }; } /** A then handler's promise-armed result (`.then(() => cached ?? load())` diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index e3480cf9c..7f0854523 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -3964,9 +3964,13 @@ function stdlibGlobalTokenDestructure( const canonical = ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member); token = `[builtin ${canonical}]`; alias = canonical; - // Builtin constructors read as their interned function values, the - // same values the bare and `globalThis.X` spellings read. - value = builtinGlobalValue(canonical, locOf(el), true); + // Builtin constructors read as their interned function values, and + // crypto as the WebCrypto object: the same values the bare and + // `globalThis.X` spellings read. + value = + canonical === "crypto" + ? { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc: locOf(el) } + : builtinGlobalValue(canonical, locOf(el), true); } else if (globalName === "console" && !CONSOLE_CALL_MEMBERS.has(propName)) { token = `[builtin console.${propName}]`; } else if (TOKEN_OPAQUE_GLOBALS.has(globalName)) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ee8ba7ecb..3fbb110ed 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -13410,6 +13410,10 @@ export class Lowerer { const callable = this.lowerBuiltinCallableValue(bi, loc); if (callable) return callable; } + // node:crypto's webcrypto is the WebCrypto global's object. + if (bi.module === "crypto" && bi.member === "webcrypto") { + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; + } // node:stream/web's exports are the web stream globals themselves: in // JavaScript sources the same identity token as the global spelling // (`require("stream/web").ReadableStream === globalThis.ReadableStream`). diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index c77938337..67950a53f 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2210,15 +2210,26 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ id: "stdlib.webcrypto.subtle", kind: "stdlib", name: "globalThis.crypto.subtle", - fns: ["webcrypto.importKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest"], - note: "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", + fns: [ + "webcrypto.importKey", + "webcrypto.generateKey", + "webcrypto.exportKey", + "webcrypto.encrypt", + "webcrypto.decrypt", + "webcrypto.deriveBits", + "webcrypto.deriveKey", + "webcrypto.sign", + "webcrypto.verify", + "webcrypto.digest", + ], + note: "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", }, { id: "stdlib.webcrypto.CryptoKey", kind: "stdlib", name: "CryptoKey", fns: ["webcrypto.isCryptoKey"], - note: "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey", + note: "secret HMAC, AES, PBKDF2 and HKDF keys with type, extractable, algorithm and usages, and instanceof CryptoKey", }, { id: "stdlib.atomics", @@ -3088,9 +3099,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< setFips: "a compiled binary has no FIPS provider to enable, and Node itself throws on setFips(true) " + "in a non-FIPS build — getFips() answers 0 here", - webcrypto: - "the WebCrypto object has no lowering — the lowered crypto surface is randomUUID, " + - "randomBytes, and the createHash chain", }, zlib: { brotliCompressSync: BROTLI_HINT, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index a233f0186..a491edf95 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -146,6 +146,16 @@ export const WEBCRYPTO_DYN_TYPES: ReadonlySet = new Set([ "KeyAlgorithm", "HmacImportParams", "HmacKeyAlgorithm", + "HmacKeyGenParams", + "AesKeyGenParams", + "AesKeyAlgorithm", + "JsonWebKey", + "AesGcmParams", + "AesCbcParams", + "AesCtrParams", + "Pbkdf2Params", + "HkdfParams", + "AesDerivedKeyParams", ]); export const FETCH_AMBIENT_TYPES = [ diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fa7876579..416d5f18e 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2846,10 +2846,17 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * one dyn array and answers a promise that argument and validation * failures reject. CryptoKey values are native handles. */ "webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.generateKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.exportKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.encrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.decrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.deriveBits": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.deriveKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.sign": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.verify": { argTypes: [DYN], result: { kind: "promise", inner: BOOL } }, "webcrypto.digest": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.isCryptoKey": { argTypes: [DYN], result: BOOL }, + "webcrypto.object": { argTypes: [], result: DYN }, "crypto.randomUUIDv7": { argTypes: [DYN], result: STRING }, "crypto.pbkdf2": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: BYTES_U8 }, "crypto.randomBytesCb": { argTypes: [F64, null], result: VOID }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 4142b6efe..61e04177b 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -75,6 +75,8 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "child.propsInstall", "child.propsResolve", "crypto.native", + // The crypto global's object extends crypto.native's. + "webcrypto.object", "dyn.promiseAll", "dyn.promiseAllSettled", "dyn.promiseAny", @@ -647,8 +649,9 @@ const RUNTIME_FEATURE_RULES = { }), /** scr_cipher.c over the mbedTLS archive; DEP0182 reports through the * process-warning unit, and unknown output encodings render through the - * inspect unit. */ - cipher: rule({ fns: ["crypto.cipherNew"] }), + * inspect unit. WebCrypto's encrypt and decrypt (scr_webcrypto_aes.c) + * link the same archive. */ + cipher: rule({ fns: ["crypto.cipherNew", "webcrypto.encrypt", "webcrypto.decrypt"] }), copying: rule({ expr: (node, _tables) => (node.kind === "arrIntrinsic" && COPYING_ARR_METHODS.has(node.method)) || @@ -675,12 +678,15 @@ const RUNTIME_FEATURE_RULES = { ), // The legacy url functions, crypto.createCipheriv and // ecdh.setPublicKey emit their deprecation warnings through the - // dyn-async unit; scr_wasm.c boxes its handles and promises through it. + // dyn-async unit (WebCrypto's encrypt and decrypt link scr_cipher.c + // too); scr_wasm.c boxes its handles and promises through it. fns: [ ...DYN_ASYNC_LIB_FNS, ...READABLE_DYN_ASYNC_LIB_FNS, ...LEGACY_URL_LIB_FNS, "crypto.cipherNew", + "webcrypto.encrypt", + "webcrypto.decrypt", "crypto.ecdhNew", ], prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."], @@ -787,6 +793,8 @@ const RUNTIME_FEATURE_RULES = { "global.native", "util.styleText", "crypto.cipherNew", + "webcrypto.encrypt", + "webcrypto.decrypt", "crypto.ecdhNew", "crypto.ecdhConvertKey", ...BUILTIN_CLASS_LIB_FNS, diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index c05e994a8..3e8b9df73 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -762,14 +762,6 @@ "code": "SC2020", "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" }, - { - "id": "node-builtin.crypto.webcrypto", - "kind": "node-builtin", - "name": "crypto.webcrypto", - "status": "unsupported", - "code": "SC2020", - "note": "the WebCrypto object has no lowering — the lowered crypto surface is randomUUID, randomBytes, and the createHash chain" - }, { "id": "node-builtin.dgram", "kind": "node-builtin", @@ -5719,14 +5711,14 @@ "kind": "stdlib", "name": "CryptoKey", "status": "static", - "note": "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey" + "note": "secret HMAC, AES, PBKDF2 and HKDF keys with type, extractable, algorithm and usages, and instanceof CryptoKey" }, { "id": "stdlib.webcrypto.subtle", "kind": "stdlib", "name": "globalThis.crypto.subtle", "status": "static", - "note": "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" + "note": "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" }, { "id": "syntax.async-generators", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7120-webcrypto-random-uuid.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7120-webcrypto-random-uuid.ts.json new file mode 100644 index 000000000..c70fdfa63 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7120-webcrypto-random-uuid.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7120-webcrypto-random-uuid.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7121-webcrypto-crypto-object.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7121-webcrypto-crypto-object.mjs.json new file mode 100644 index 000000000..b152699ba --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7121-webcrypto-crypto-object.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7121-webcrypto-crypto-object.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-webcrypto-node-crypto.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-webcrypto-node-crypto.ts.json new file mode 100644 index 000000000..02451a516 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-webcrypto-node-crypto.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7122-webcrypto-node-crypto.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-webcrypto-node-crypto.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-webcrypto-node-crypto.cjs.json new file mode 100644 index 000000000..821fb378d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-webcrypto-node-crypto.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7123-webcrypto-node-crypto.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-webcrypto-secret-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-webcrypto-secret-keys.ts.json new file mode 100644 index 000000000..8fc0dab2a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-webcrypto-secret-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7124-webcrypto-secret-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-webcrypto-key-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-webcrypto-key-errors.mjs.json new file mode 100644 index 000000000..942de8c10 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-webcrypto-key-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7125-webcrypto-key-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-webcrypto-aes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-webcrypto-aes.ts.json new file mode 100644 index 000000000..f113a3ca9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-webcrypto-aes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7126-webcrypto-aes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-webcrypto-aes-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-webcrypto-aes-errors.mjs.json new file mode 100644 index 000000000..3ec3d26f2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-webcrypto-aes-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7127-webcrypto-aes-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-webcrypto-derive.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-webcrypto-derive.ts.json new file mode 100644 index 000000000..b613fbce6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-webcrypto-derive.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7128-webcrypto-derive.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7129-webcrypto-derive-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7129-webcrypto-derive-errors.mjs.json new file mode 100644 index 000000000..11fc8c721 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7129-webcrypto-derive-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7129-webcrypto-derive-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-keys.ts.json new file mode 100644 index 000000000..19f14ed02 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/webcrypto-keys.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 85c5ffb00..3944b023c 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -172,7 +172,8 @@ async function build() { ...(unit.source === "scr_regex.c" ? ["-I", quickjs] : []), ...(unit.source === "scr_tls.c" || unit.source === "scr_cipher.c" || - unit.source === "scr_ecdh.c" + unit.source === "scr_ecdh.c" || + unit.source === "scr_webcrypto_aes.c" ? ["-I", join(mbedtls, "include")] : []), ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 0f40218a1..3529df137 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -72,6 +72,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_js_object.c", "dynTier"], ["scr_event_target.c", any("eventTarget", "nativeFetch")], ["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "fileHandle", "nativeFetch")], + // The crypto global's object extends scr_async_dyn.c's crypto object. + ["scr_webcrypto_object.c", all("webcrypto", "dynAsync")], // The performance timeline emits its buffer warning through scr_async_dyn.c. ["scr_perf.c", "dynAsync"], // perf_hooks event-loop metrics, only for programs that read them. @@ -151,6 +153,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_ecdh.c", "ecdh"], ["scr_wasm.c", "wasm"], ["scr_webcrypto.c", "webcrypto"], + // WebCrypto's AES ciphers run over the cipher feature's mbedTLS archive. + ["scr_webcrypto_aes.c", all("cipher", "webcrypto")], ["scr_fetch.c", "nativeFetch"], ]; diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index b242b78aa..f59a2431f 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -223,7 +223,8 @@ async function build() { ...(unit.source === "scr_regex.c" ? ["-I", quickjs] : []), ...(unit.source === "scr_tls.c" || unit.source === "scr_cipher.c" || - unit.source === "scr_ecdh.c" + unit.source === "scr_ecdh.c" || + unit.source === "scr_webcrypto_aes.c" ? ["-I", join(mbedtls, "include")] : []), ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" diff --git a/packages/runtime-wasm32-wasi/scripts/build.mjs b/packages/runtime-wasm32-wasi/scripts/build.mjs index 2dd983973..0a8699b23 100644 --- a/packages/runtime-wasm32-wasi/scripts/build.mjs +++ b/packages/runtime-wasm32-wasi/scripts/build.mjs @@ -22,6 +22,7 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ "scr_tls.c", "scr_cipher.c", "scr_ecdh.c", + "scr_webcrypto_aes.c", "scr_wasm.c", "scr_fetch.c", ], diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index d12858abf..ddc306598 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -20,6 +20,9 @@ ScrStr *scr_crypto_hash_digest_bytes(ScrStr *alg, ScrBytes *data, ScrStr *enc); typedef struct ScrCryptoHash ScrCryptoHash; ScrCryptoHash *scr_crypto_hash_new(ScrStr *alg); ScrDyn *scr_crypto_native(void); +/* The crypto global as a value: scr_crypto_native's object with the Crypto + * methods (scr_webcrypto_object.c, linked with the webcrypto feature). */ +ScrDyn *scr_webcrypto_object(void); ScrCryptoHash *scr_crypto_hmac_new_str(ScrStr *alg, ScrStr *key); ScrCryptoHash *scr_crypto_hmac_new_bytes(ScrStr *alg, ScrBytes *key); ScrCryptoHash *scr_crypto_hash_retain(ScrCryptoHash *h); @@ -202,19 +205,48 @@ size_t scr_crypto_digest_raw(const char *alg, const unsigned char *data, size_t size_t scr_crypto_hmac_raw(const char *alg, const unsigned char *key, size_t keylen, const unsigned char *data, size_t len, unsigned char out[64]); /* ── WebCrypto (scr_webcrypto.c) ───────────────────────────────────── - * crypto.subtle.importKey (raw HMAC keys), sign and verify (HMAC) and - * digest (SHA-1/SHA-2) over node:crypto's digest core. Each takes the + * crypto.subtle.importKey (HMAC, AES, PBKDF2 and HKDF keys, raw and jwk), + * generateKey and exportKey (HMAC and AES keys), sign and verify (HMAC) + * and digest (SHA-1/SHA-2) over node:crypto's digest core. Each takes the * call's argument vector as one dyn array (borrowed) and answers a +1 * promise that every argument or validation failure rejects with Node's * TypeError or DOMException; algorithms and formats outside the slice - * reject with a coded SC2020 Error. importKey settles at once with a - * CryptoKey handle; sign and digest fulfill with an ArrayBuffer and - * verify with a boolean on a later loop turn. scr_webcrypto_is_crypto_key - * answers `value instanceof CryptoKey`. */ + * reject with a coded SC2020 Error. importKey and exportKey settle at + * once with a CryptoKey handle, an ArrayBuffer or a JWK object; + * generateKey fulfills with a CryptoKey, sign and digest with an + * ArrayBuffer and verify with a boolean on a later loop turn. + * scr_webcrypto_is_crypto_key answers `value instanceof CryptoKey`. */ ScrPromise *scr_webcrypto_import_key(ScrDyn *args); +ScrPromise *scr_webcrypto_generate_key(ScrDyn *args); +ScrPromise *scr_webcrypto_export_key(ScrDyn *args); ScrPromise *scr_webcrypto_sign(ScrDyn *args); ScrPromise *scr_webcrypto_verify(ScrDyn *args); ScrPromise *scr_webcrypto_digest(ScrDyn *args); +/* encrypt and decrypt (scr_webcrypto_aes.c, linked with the cipher + * feature's mbedTLS archive) run scr_webcrypto_cipher with the AES cipher + * as `aes`, which answers the +1 output bytes or NULL when the operation + * fails. encrypt and decrypt fulfill with an ArrayBuffer, or reject with + * a job failure, on a later loop turn. */ +typedef struct { + const unsigned char *iv; /* the IV, or the CTR counter block */ + size_t iv_len; + const unsigned char *aad; + size_t aad_len; + size_t tag_len; /* bytes */ + unsigned counter_bits; +} ScrWebCryptoAesParams; +typedef ScrBytes *(*ScrWebCryptoAesFn)(const char *name, bool decrypt, const unsigned char *key, + size_t key_len, const ScrWebCryptoAesParams *params, + const unsigned char *in, size_t len); +ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn aes); +ScrPromise *scr_webcrypto_encrypt(ScrDyn *args); +ScrPromise *scr_webcrypto_decrypt(ScrDyn *args); +/* deriveBits and deriveKey (PBKDF2 and HKDF) over node:crypto's KDF core: + * deriveBits fulfills with an ArrayBuffer (at once for a zero length) and + * deriveKey with a CryptoKey of the derived key type, on a later loop + * turn. */ +ScrPromise *scr_webcrypto_derive_bits(ScrDyn *args); +ScrPromise *scr_webcrypto_derive_key(ScrDyn *args); bool scr_webcrypto_is_crypto_key(const ScrDyn *value); /* The composed `new crypto.X509Certificate(data).fingerprint` read (the diff --git a/packages/runtime/src/scr_webcrypto.c b/packages/runtime/src/scr_webcrypto.c index 546b24151..4d2c27be4 100644 --- a/packages/runtime/src/scr_webcrypto.c +++ b/packages/runtime/src/scr_webcrypto.c @@ -1,22 +1,27 @@ -/* WebCrypto's HMAC slice: crypto.subtle.importKey (raw HMAC keys), - * sign, verify and the SHA-1/SHA-2 digests, plus the CryptoKey values - * importKey answers. +/* WebCrypto's secret-key slice: crypto.subtle.importKey, generateKey and + * exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF + * keys), HMAC sign and verify, the SHA-1/SHA-2 digests, PBKDF2 and HKDF + * deriveBits and deriveKey, the argument and key handling of AES encrypt + * and decrypt (whose cipher runs in scr_webcrypto_aes.c), and the + * CryptoKey values those methods answer. * * This unit is only the WebCrypto layer, modeled on Node 24's - * lib/internal/crypto/webcrypto.js and mac.js: Web IDL argument - * conversion, algorithm normalization, the CryptoKey handle, promise - * wrapping and Node's error shapes. The MACs and digests themselves come - * from node:crypto's digest core (scr_crypto_hmac_raw and - * scr_crypto_digest_raw in scr_lib.c), and results settle through the - * deferred crypto queue that node:crypto's callback forms use, so sign, - * verify and digest settle on a later loop turn like Node's thread-pool - * jobs while importKey settles at once. + * lib/internal/crypto/webcrypto.js, mac.js, aes.js and keys.js: Web IDL + * argument conversion, algorithm normalization, the CryptoKey handle, the + * JSON Web Key forms, promise wrapping and Node's error shapes. The MACs + * and digests themselves come from node:crypto's digest core + * (scr_crypto_hmac_raw and scr_crypto_digest_raw in scr_lib.c), and + * results settle through the deferred crypto queue that node:crypto's + * callback forms use, so sign, verify, digest and generateKey settle on a + * later loop turn like Node's thread-pool jobs while importKey and + * exportKey settle at once. * * Every argument and validation failure rejects the returned promise. * Algorithms Node recognizes but this slice does not implement (RSA, - * ECDSA, AES, SHA-3, ...) and the 'jwk' key format reject with a coded - * SC2020 Error instead of silently diverging. The compiler links the unit - * only into programs that call these methods (the webcrypto feature). */ + * ECDSA, AES-OCB, SHA-3, ...) and the key formats of asymmetric keys + * reject with a coded SC2020 Error instead of silently diverging. The + * compiler links the unit only into programs that call these methods + * (the webcrypto feature). */ #include "scr_runtime.h" #include @@ -28,8 +33,9 @@ /* ── algorithm registry ─────────────────────────────────────────────── * The names Node 24 registers per operation (util.js's * kAlgorithmDefinitions with the conditional algorithms its OpenSSL - * build enables), matched case-insensitively. Only HMAC and the SHA-1/ - * SHA-2 digests are implemented. */ + * build enables), matched case-insensitively. HMAC, AES-CBC/CTR/GCM/KW + * keys, PBKDF2 and HKDF key imports and the SHA-1/SHA-2 digests are + * implemented. */ static const char *const scr_wc_import_names[] = { "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", "Argon2d", "Argon2i", "Argon2id", "ChaCha20-Poly1305", @@ -43,6 +49,15 @@ static const char *const scr_wc_sign_names[] = { "ECDSA", "Ed25519", "Ed448", "HMAC", "KMAC128", "KMAC256", "ML-DSA-44", "ML-DSA-65", "ML-DSA-87", "RSA-PSS", "RSASSA-PKCS1-v1_5", }; +/* generateKey and exportKey register the same algorithms. */ +static const char *const scr_wc_key_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", + "ChaCha20-Poly1305", "ECDH", "ECDSA", "Ed25519", "Ed448", + "HMAC", "KMAC128", "KMAC256", "ML-DSA-44", "ML-DSA-65", + "ML-DSA-87", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024", "MLKEM768-P256", + "MLKEM768-X25519", "MLKEM1024-P384", "RSA-OAEP", "RSA-PSS", "RSASSA-PKCS1-v1_5", + "X25519", "X448", +}; static const char *const scr_wc_digest_names[] = { "SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", "SHA3-512", "cSHAKE128", "cSHAKE256", "KT128", "KT256", "TurboSHAKE128", @@ -193,7 +208,24 @@ static const char *const scr_wc_usages[] = { "deriveKey", "deriveBits", "wrapKey", "unwrapKey", "encapsulateKey", "encapsulateBits", "decapsulateKey", "decapsulateBits", }; -enum { SCR_WC_USAGE_SIGN = 1u << 2, SCR_WC_USAGE_VERIFY = 1u << 3 }; +enum { + SCR_WC_USAGE_ENCRYPT = 1u << 0, + SCR_WC_USAGE_DECRYPT = 1u << 1, + SCR_WC_USAGE_SIGN = 1u << 2, + SCR_WC_USAGE_VERIFY = 1u << 3, + SCR_WC_USAGE_DERIVE_KEY = 1u << 4, + SCR_WC_USAGE_DERIVE_BITS = 1u << 5, + SCR_WC_USAGE_WRAP_KEY = 1u << 6, + SCR_WC_USAGE_UNWRAP_KEY = 1u << 7, +}; + +/* The usage index of a KeyUsage string, or -1. */ +static int scr_wc_usage_index(const ScrStr *s) { + for (size_t k = 0; k < SCR_WC_COUNT(scr_wc_usages); k++) + if (s->len == strlen(scr_wc_usages[k]) && memcmp(s->data, scr_wc_usages[k], s->len) == 0) + return (int)k; + return -1; +} /* sequence over an array: the usage mask (-1 on failure). */ static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { @@ -215,11 +247,8 @@ static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { scr_dyn_release(plain); return -1; } - size_t k = 0; - while (k < SCR_WC_COUNT(scr_wc_usages) && - !(s->len == strlen(scr_wc_usages[k]) && memcmp(s->data, scr_wc_usages[k], s->len) == 0)) - k++; - if (k == SCR_WC_COUNT(scr_wc_usages)) { + int k = scr_wc_usage_index(s); + if (k < 0) { scr_wc_type_error("ERR_INVALID_ARG_VALUE", "%s: %s argument[%.0f] '%.*s' is not a valid enum value of type KeyUsage.", prefix, scr_wc_ordinals[index], i, (int)(s->len > 200 ? 200 : s->len), @@ -237,10 +266,10 @@ static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { /* ── algorithm normalization ────────────────────────────────────────── * normalizeAlgorithm(algorithm, op) for the members this slice reads: - * the canonical name, and for HMAC imports the hash and length. */ + * the canonical name, the HMAC hash and length, and AES key lengths. */ typedef struct { const char *name; /* canonical registered name */ - const char *hash; /* HMAC import: the normalized hash name */ + const char *hash; /* HMAC: the normalized hash name */ bool has_length; double length; } ScrWcAlgorithm; @@ -261,6 +290,28 @@ static ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) { return v ? v : scr_dyn_retain(scr_dyn_undefined()); } +/* An [EnforceRange] integer conversion: the truncated value in [0, max], + * or false with Node's TypeError pending. `context` names the value + * ("length in passed algorithm", "3rd argument"). */ +static bool scr_wc_enforce_range(const char *prefix, const char *context, const ScrDyn *value, + double max, double *out) { + double x; + if (!scr_dyn_number_coerce_js(value, &x)) return false; + if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s is not a finite number.", prefix, context); + return false; + } + x = trunc(x); + if (x == 0) x = 0; + if (x < 0 || x > max) { + scr_wc_type_error("ERR_OUT_OF_RANGE", "%s: %s is outside the expected range of 0 to %.0f.", + prefix, context, max); + return false; + } + *out = x; + return true; +} + /* The name an AlgorithmIdentifier carries (+1), with the Algorithm * dictionary's missing-name TypeError. `dict` names the dictionary the * value converts to. */ @@ -295,11 +346,13 @@ static const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *metho return canonical; } -/* HmacImportParams over an object: hash (required, a SHA name), then the - * optional [EnforceRange] unsigned long length. */ -static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { +/* HmacImportParams / HmacKeyGenParams (`dict`) over an object: hash + * (required, a SHA name), then the optional [EnforceRange] unsigned long + * length, which cannot be 0 (`zero_error` names that DOMException). */ +static bool scr_wc_hmac_params(ScrDyn *identifier, const char *dict, const char *zero_error, + const char *method, ScrWcAlgorithm *out) { if (!scr_wc_is_object(identifier)) { - scr_wc_missing_member("HmacImportParams", "hash"); + scr_wc_missing_member(dict, "hash"); return false; } ScrDyn *plain = scr_wc_plain(identifier); @@ -307,7 +360,7 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { ScrDyn *hash = scr_wc_member(plain, "hash"); bool ok = true; if (hash->kind == SCR_DYN_UNDEF) { - scr_wc_missing_member("HmacImportParams", "hash"); + scr_wc_missing_member(dict, "hash"); ok = false; } ScrDyn *hash_value = NULL; @@ -335,7 +388,9 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { (hash_name->data[2] | 32) == 'a'; if (hash_name) scr_str_release(hash_name); if (ok && !sha) { - scr_throw_domex("NotSupportedError", "Only SHA hashes are supported in HmacImportParams"); + char message[96]; + snprintf(message, sizeof message, "Only SHA hashes are supported in %s", dict); + scr_throw_domex("NotSupportedError", message); ok = false; } } @@ -343,34 +398,23 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { ScrDyn *length = scr_wc_member(plain, "length"); if (length->kind != SCR_DYN_UNDEF) { double x; - if (!scr_dyn_number_coerce_js(length, &x)) ok = false; - else if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) { - scr_wc_type_error("ERR_INVALID_ARG_TYPE", - "%s: length in passed algorithm is not a finite number.", - scr_wc_normalize_prefix); + if (!scr_wc_enforce_range(scr_wc_normalize_prefix, "length in passed algorithm", length, + 4294967295.0, &x)) + ok = false; + else if (x == 0) { + char message[96]; + snprintf(message, sizeof message, "%s.length cannot be 0", dict); + scr_throw_domex(zero_error, message); ok = false; } else { - x = trunc(x); - if (x == 0) x = 0; - if (x < 0 || x > 4294967295.0) { - scr_wc_type_error("ERR_OUT_OF_RANGE", - "%s: length in passed algorithm is outside the expected range of 0 to " - "4294967295.", - scr_wc_normalize_prefix); - ok = false; - } else if (x == 0) { - scr_throw_domex("DataError", "HmacImportParams.length cannot be 0"); - ok = false; - } else { - out->has_length = true; - out->length = x; - } + out->has_length = true; + out->length = x; } } scr_dyn_release(length); } if (ok) { - out->hash = scr_wc_digest_algorithm(hash_value, "importKey"); + out->hash = scr_wc_digest_algorithm(hash_value, method); if (!out->hash) ok = false; } if (hash_value) scr_dyn_release(hash_value); @@ -379,11 +423,51 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { return ok; } -/* normalizeAlgorithm for importKey, sign/verify (`names`) — false with - * the error pending. Only HMAC parameters are read; any other recognized - * algorithm is refused by the caller. */ -static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_t count, - bool import, ScrWcAlgorithm *out) { +/* AesKeyGenParams / AesDerivedKeyParams (`dict`): the required + * [EnforceRange] unsigned short length, 128, 192 or 256. */ +static bool scr_wc_aes_length_params(ScrDyn *identifier, const char *dict, ScrWcAlgorithm *out) { + ScrDyn *plain = scr_wc_plain(identifier); + if (!plain) return false; + ScrDyn *length = scr_wc_member(plain, "length"); + bool ok = false; + double x; + if (length->kind == SCR_DYN_UNDEF) scr_wc_missing_member(dict, "length"); + else if (scr_wc_enforce_range(scr_wc_normalize_prefix, "length in passed algorithm", length, + 65535.0, &x)) { + if (x != 128 && x != 192 && x != 256) { + scr_throw_domex("OperationError", "AES key length must be 128, 192, or 256 bits"); + } else { + out->has_length = true; + out->length = x; + ok = true; + } + } + scr_dyn_release(length); + scr_dyn_release(plain); + return ok; +} + +static bool scr_wc_is_aes(const char *name) { + return strcmp(name, "AES-CBC") == 0 || strcmp(name, "AES-CTR") == 0 || + strcmp(name, "AES-GCM") == 0 || strcmp(name, "AES-KW") == 0; +} + +static bool scr_wc_is_kdf(const char *name) { + return strcmp(name, "PBKDF2") == 0 || strcmp(name, "HKDF") == 0; +} + +typedef enum { + SCR_WC_OP_IMPORT, + SCR_WC_OP_SIGN, + SCR_WC_OP_GENERATE, +} ScrWcOp; + +/* normalizeAlgorithm for importKey, sign/verify and generateKey (`names`) + * — false with the error pending. Only the dictionaries of the + * implemented algorithms are read; any other recognized algorithm is + * refused by the caller. */ +static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_t count, ScrWcOp op, + ScrWcAlgorithm *out) { memset(out, 0, sizeof *out); ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); if (!name) return false; @@ -393,19 +477,27 @@ static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_ scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); return false; } - if (import && strcmp(out->name, "HMAC") == 0) return scr_wc_hmac_import_params(identifier, out); + bool hmac = strcmp(out->name, "HMAC") == 0; + if (op == SCR_WC_OP_IMPORT && hmac) + return scr_wc_hmac_params(identifier, "HmacImportParams", "DataError", "importKey", out); + if (op == SCR_WC_OP_GENERATE && hmac) + return scr_wc_hmac_params(identifier, "HmacKeyGenParams", "OperationError", "generateKey", out); + if (op == SCR_WC_OP_GENERATE && scr_wc_is_aes(out->name)) + return scr_wc_aes_length_params(identifier, "AesKeyGenParams", out); return true; } /* ── CryptoKey ───────────────────────────────────────────────────────── - * A secret HMAC key: the bytes, the hash, the bit length, the usage mask - * and the extractable flag. The algorithm and usages getters answer the - * same object on every read (Node caches its clones), built on first - * read. The bytes are cleared when the last reference goes. */ + * A secret key: the algorithm name, the bytes, the HMAC hash, the bit + * length (HMAC and AES keys), the usage mask and the extractable flag. + * The algorithm and usages getters answer the same object on every read + * (Node caches its clones), built on first read. The bytes are cleared + * when the last reference goes. */ typedef struct ScrCryptoKey { size_t rc; - const char *hash; /* WebCrypto hash name */ - double length; /* bits */ + const char *name; /* canonical algorithm name */ + const char *hash; /* HMAC: WebCrypto hash name */ + double length; /* HMAC and AES: bits */ unsigned usages; bool extractable; ScrDyn *algorithm; /* cached getter results, or NULL */ @@ -446,26 +538,34 @@ static ScrDyn *scr_wc_cstr(const char *s) { return out; } +/* Node's algorithm objects: { name, length, hash } for HMAC keys, + * { name, length } for AES keys and { name } for PBKDF2 and HKDF keys. */ static ScrDyn *scr_crypto_key_algorithm(ScrCryptoKey *key) { if (!key->algorithm) { - ScrDyn *hash = scr_dyn_new_obj(); - scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(key->hash)); ScrDyn *algorithm = scr_dyn_new_obj(); - scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr("HMAC")); - scr_dyn_obj_set(algorithm, "length", 6, scr_dyn_new_num(key->length)); - scr_dyn_obj_set(algorithm, "hash", 4, hash); + scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr(key->name)); + if (!scr_wc_is_kdf(key->name)) + scr_dyn_obj_set(algorithm, "length", 6, scr_dyn_new_num(key->length)); + if (key->hash) { + ScrDyn *hash = scr_dyn_new_obj(); + scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(key->hash)); + scr_dyn_obj_set(algorithm, "hash", 4, hash); + } key->algorithm = algorithm; } return scr_dyn_retain(key->algorithm); } +/* The usages as a fresh array in Node's canonical order (+1). */ +static ScrDyn *scr_wc_usage_array(unsigned usages) { + ScrDyn *list = scr_dyn_new_arr(); + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) + if (usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i])); + return list; +} + static ScrDyn *scr_crypto_key_usages(ScrCryptoKey *key) { - if (!key->usage_list) { - ScrDyn *list = scr_dyn_new_arr(); - for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) - if (key->usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i])); - key->usage_list = list; - } + if (!key->usage_list) key->usage_list = scr_wc_usage_array(key->usages); return scr_dyn_retain(key->usage_list); } @@ -540,8 +640,9 @@ static const ScrDynHandleOps scr_crypto_key_ops = { .symbol_get = scr_crypto_key_symbol_get, }; -static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const char *hash, - double length, unsigned usages, bool extractable) { +static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const char *name, + const char *hash, double length, unsigned usages, + bool extractable) { static SCR_TL bool installed; if (!installed) { installed = true; @@ -550,6 +651,7 @@ static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const c ScrCryptoKey *key = malloc(sizeof *key + (len ? len : 1)); if (!key) scr_trap("scriptc: out of memory\n"); key->rc = 1; + key->name = name; key->hash = hash; key->length = length; key->usages = usages; @@ -563,6 +665,208 @@ static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const c return box; } +/* numBitsToBytes: whole bytes for `bits`, rounding up. */ +static size_t scr_wc_bits_to_bytes(double bits) { + return (size_t)(floor(bits / 8) + floor((7 + fmod(bits, 8)) / 8)); +} + +/* Clears the unused low bits of the final byte of a `bits`-long key + * (truncateToBitLength). */ +static void scr_wc_truncate_bits(unsigned char *data, size_t len, double bits) { + int extra = (int)fmod(bits, 8); + if (extra && len) data[len - 1] = (unsigned char)(data[len - 1] & (0xff << (8 - extra))); +} + +/* The usages each key algorithm accepts. */ +static unsigned scr_wc_allowed_usages(const char *name) { + if (strcmp(name, "HMAC") == 0) return SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY; + if (strcmp(name, "AES-KW") == 0) return SCR_WC_USAGE_WRAP_KEY | SCR_WC_USAGE_UNWRAP_KEY; + if (scr_wc_is_kdf(name)) return SCR_WC_USAGE_DERIVE_KEY | SCR_WC_USAGE_DERIVE_BITS; + return SCR_WC_USAGE_ENCRYPT | SCR_WC_USAGE_DECRYPT | SCR_WC_USAGE_WRAP_KEY | + SCR_WC_USAGE_UNWRAP_KEY; +} + +/* validateKeyUsages: SyntaxError for a usage outside the algorithm's. */ +static bool scr_wc_check_usages(const char *name, long usages) { + if (!(usages & ~(long)scr_wc_allowed_usages(name))) return true; + char message[96]; + snprintf(message, sizeof message, "Unsupported key usage for %s%s key", + scr_wc_is_kdf(name) ? "a " : "", name); + scr_throw_domex("SyntaxError", message); + return false; +} + +/* The JWK "alg" of an HMAC hash or an AES key, or NULL. */ +static const char *scr_wc_jwk_alg(const char *name, const char *hash, double length, char *buf, + size_t size) { + if (strcmp(name, "HMAC") == 0) { + if (strcmp(hash, "SHA-1") == 0) return "HS1"; + if (strcmp(hash, "SHA-256") == 0) return "HS256"; + if (strcmp(hash, "SHA-384") == 0) return "HS384"; + if (strcmp(hash, "SHA-512") == 0) return "HS512"; + return NULL; + } + if (!scr_wc_is_aes(name)) return NULL; + snprintf(buf, size, "A%.0f%s", length, name + 4); + return buf; +} + +/* ── JSON Web Keys ───────────────────────────────────────────────────── + * The JsonWebKey dictionary members a secret key reads, converted in Web + * IDL's member order (every DOMString member converts, so a Symbol or a + * throwing toString in any of them rejects like Node). */ +typedef struct { + ScrStr *alg, *k, *kty, *use; + bool has_ext, ext; + ScrDyn *key_ops; /* array of strings, or NULL */ +} ScrWcJwk; + +static void scr_wc_jwk_drop(ScrWcJwk *jwk) { + if (jwk->alg) scr_str_release(jwk->alg); + if (jwk->k) scr_str_release(jwk->k); + if (jwk->kty) scr_str_release(jwk->kty); + if (jwk->use) scr_str_release(jwk->use); + if (jwk->key_ops) scr_dyn_release(jwk->key_ops); + memset(jwk, 0, sizeof *jwk); +} + +/* A DOMString dictionary member: NULL with *ok when absent; NULL with + * !*ok and the TypeError pending on failure. */ +static ScrStr *scr_wc_jwk_string(const char *prefix, const ScrDyn *dict, const char *key, bool *ok) { + ScrDyn *value = scr_wc_member(dict, key); + ScrStr *out = NULL; + if (value->kind == SCR_DYN_SYMBOL) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: %s in 2nd argument is a Symbol and cannot be converted to a string.", + prefix, key); + *ok = false; + } else if (value->kind != SCR_DYN_UNDEF) { + out = scr_wc_dom_string(value); + if (!out) *ok = false; + } + scr_dyn_release(value); + return out; +} + +static bool scr_wc_jwk_convert(const char *prefix, ScrDyn *value, ScrWcJwk *out) { + memset(out, 0, sizeof *out); + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) return true; + if (!scr_wc_is_object(value)) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument cannot be converted to a dictionary", + prefix); + return false; + } + ScrDyn *dict = scr_wc_plain(value); + if (!dict) return false; + static const char *const members[] = {"alg", "crv", "d", "dp", "dq", "e", "ext", + "k", "key_ops", "kty", "n", "p", "priv", "pub", + "q", "qi", "use", "x", "y"}; + bool ok = true; + for (size_t i = 0; ok && i < SCR_WC_COUNT(members); i++) { + const char *key = members[i]; + if (strcmp(key, "ext") == 0) { + ScrDyn *ext = scr_wc_member(dict, key); + if (ext->kind != SCR_DYN_UNDEF) { + out->has_ext = true; + out->ext = scr_dyn_truthy(ext); + } + scr_dyn_release(ext); + } else if (strcmp(key, "key_ops") == 0) { + ScrDyn *ops = scr_wc_member(dict, key); + if (ops->kind != SCR_DYN_UNDEF) { + ScrDyn *list = scr_wc_plain(ops); + if (!list) ok = false; + else if (list->kind != SCR_DYN_ARR) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: key_ops in 2nd argument cannot be converted to sequence.", prefix); + scr_dyn_release(list); + ok = false; + } else { + ScrDyn *strings = scr_dyn_new_arr(); + double n = scr_dyn_arr_len(list); + for (double j = 0; ok && j < n; j++) { + ScrDyn *item = scr_dyn_arr_at(list, j); + ScrStr *s = scr_wc_dom_string(item); + scr_dyn_release(item); + if (!s) ok = false; + else { + scr_dyn_arr_push(strings, scr_dyn_new_str(s)); + scr_str_release(s); + } + } + scr_dyn_release(list); + if (ok) out->key_ops = strings; + else scr_dyn_release(strings); + } + } + scr_dyn_release(ops); + } else { + ScrStr *s = scr_wc_jwk_string(prefix, dict, key, &ok); + if (strcmp(key, "alg") == 0) out->alg = s; + else if (strcmp(key, "k") == 0) out->k = s; + else if (strcmp(key, "kty") == 0) out->kty = s; + else if (strcmp(key, "use") == 0) out->use = s; + else if (s) scr_str_release(s); + } + } + scr_dyn_release(dict); + if (!ok) scr_wc_jwk_drop(out); + return ok; +} + +static bool scr_wc_str_is(const ScrStr *s, const char *literal) { + return s && s->len == strlen(literal) && memcmp(s->data, literal, s->len) == 0; +} + +/* validateJwk for an oct key, then the key bytes (+1): k decodes with + * Node's lenient base64 decoder, so any string answers bytes. */ +static ScrBytes *scr_wc_jwk_secret(const ScrWcJwk *jwk, bool extractable, long usages, + const char *expected_use) { + if (!jwk->kty) { + scr_throw_domex("DataError", "Invalid keyData"); + return NULL; + } + if (!scr_wc_str_is(jwk->kty, "oct")) { + scr_throw_domex("DataError", "Invalid JWK \"kty\" Parameter"); + return NULL; + } + if (!jwk->k) { + scr_throw_domex("DataError", "Invalid keyData"); + return NULL; + } + if (usages != 0 && jwk->use && !scr_wc_str_is(jwk->use, expected_use)) { + scr_throw_domex("DataError", "Invalid JWK \"use\" Parameter"); + return NULL; + } + if (jwk->key_ops) { + long seen = 0; + double n = scr_dyn_arr_len(jwk->key_ops); + for (double i = 0; i < n; i++) { + ScrDyn *op = scr_dyn_arr_at(jwk->key_ops, i); + int k = scr_wc_usage_index(op->v.str); + scr_dyn_release(op); + if (k < 0) continue; + if (seen & (1L << k)) { + scr_throw_domex("DataError", "Duplicate key operation"); + return NULL; + } + seen |= 1L << k; + } + if ((seen & usages) != usages) { + scr_throw_domex("DataError", "Key operations and usage mismatch"); + return NULL; + } + } + if (jwk->has_ext && !jwk->ext && extractable) { + scr_throw_domex("DataError", "JWK \"ext\" Parameter and extractable mismatch"); + return NULL; + } + ScrStr *encoding = scr_str_new("base64url", 9); + ScrBytes *bytes = scr_bytes_from_str(jwk->k, encoding); + scr_str_release(encoding); + return bytes; +} + /* ── the operations ─────────────────────────────────────────────────── * Each takes its argument vector as one dyn array (borrowed) and answers * a +1 promise. */ @@ -573,6 +877,19 @@ static ScrPromise *scr_wc_rejected(void) { return p; } +/* A promise already fulfilled with `value` (moves). */ +static ScrPromise *scr_wc_resolved(ScrDyn *value) { + return scr_promise_settled_ref(value, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); +} + +/* A promise that fulfills with `value` (moves) on a later loop turn, like + * Node's crypto jobs. */ +static ScrPromise *scr_wc_deferred(ScrDyn *value) { + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_dyn(value, NULL, scr_crypto_promise_callback(p), scr_crypto_promise_settle_dyn); + return p; +} + /* The argument count check every method starts with (after the receiver * check the compiler's call shape already guarantees). */ static bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required) { @@ -595,9 +912,158 @@ static void scr_wc_args_drop(ScrWcArgs *a, size_t n) { for (size_t i = 0; i < n; i++) scr_dyn_release(a->v[i]); } +/* AlgorithmIdentifier: an object, or a value converted to a DOMString. */ +static bool scr_wc_algorithm_identifier(ScrDyn *value) { + if (scr_wc_is_object(value)) return true; + ScrStr *s = scr_wc_dom_string(value); + if (!s) return false; + scr_str_release(s); + return true; +} + +/* macImportKey: the usage check, the format, then normalizeKeyLength. */ +static ScrDyn *scr_wc_import_hmac(const char *format, ScrBytes *data, const ScrWcJwk *jwk, + const ScrWcAlgorithm *algorithm, bool extractable, long usages, + bool *unsupported) { + if (!scr_wc_check_usages("HMAC", usages)) return NULL; + ScrBytes *bytes = NULL; + if (strcmp(format, "raw") == 0 || strcmp(format, "raw-secret") == 0) { + bytes = scr_bytes_retain(data); + } else if (strcmp(format, "jwk") == 0) { + bytes = scr_wc_jwk_secret(jwk, extractable, usages, "sig"); + if (!bytes) return NULL; + char buf[16]; + const char *expected = scr_wc_jwk_alg("HMAC", algorithm->hash, 0, buf, sizeof buf); + if (jwk->alg && expected && !scr_wc_str_is(jwk->alg, expected)) { + scr_bytes_release(bytes); + scr_throw_domex("DataError", "JWK \"alg\" does not match the requested algorithm"); + return NULL; + } + } else { + *unsupported = true; + return NULL; + } + ScrDyn *result = NULL; + { + ScrBytes *view = bytes; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + double length = (double)len * 8; + if (len == 0) { + scr_throw_domex("DataError", "Zero-length key is not supported"); + goto done; + } + if (algorithm->has_length) { + if (scr_wc_bits_to_bytes(algorithm->length) != len) { + scr_throw_domex("DataError", "Invalid key length"); + goto done; + } + length = algorithm->length; + } + result = scr_crypto_key_new(view->data, len, "HMAC", algorithm->hash, length, + (unsigned)usages, extractable); + ScrCryptoKey *key = scr_crypto_key_of(result); + scr_wc_truncate_bits(key->data, key->len, length); + } +done: + scr_bytes_release(bytes); + return result; +} + +/* aesImportKey: the usage check, then the format and the key length. */ +static ScrDyn *scr_wc_import_aes(const char *format, ScrBytes *data, const ScrWcJwk *jwk, + const char *name, bool extractable, long usages, + bool *unsupported) { + if (!scr_wc_check_usages(name, usages)) return NULL; + ScrBytes *bytes = NULL; + bool from_jwk = strcmp(format, "jwk") == 0; + if (strcmp(format, "raw") == 0 || strcmp(format, "raw-secret") == 0) { + bytes = scr_bytes_retain(data); + } else if (from_jwk) { + bytes = scr_wc_jwk_secret(jwk, extractable, usages, "enc"); + if (!bytes) return NULL; + } else { + *unsupported = true; + return NULL; + } + ScrDyn *result = NULL; + { + ScrBytes *view = bytes; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + double length = (double)len * 8; + if (length != 128 && length != 192 && length != 256) { + scr_throw_domex("DataError", "Invalid key length"); + goto done; + } + char buf[16]; + if (from_jwk && jwk->alg && + !scr_wc_str_is(jwk->alg, scr_wc_jwk_alg(name, NULL, length, buf, sizeof buf))) { + scr_throw_domex("DataError", "JWK \"alg\" does not match the requested algorithm"); + goto done; + } + result = scr_crypto_key_new(view->data, len, name, NULL, length, (unsigned)usages, extractable); + } +done: + scr_bytes_release(bytes); + return result; +} + +/* importGenericSecretKey (PBKDF2, HKDF): never extractable, derivation + * usages only, raw bytes of any length. */ +static ScrDyn *scr_wc_import_kdf(const char *format, ScrBytes *data, const char *name, + bool extractable, long usages, bool *unsupported) { + if (extractable) { + char message[64]; + snprintf(message, sizeof message, "%s keys are not extractable", name); + scr_throw_domex("SyntaxError", message); + return NULL; + } + if (!scr_wc_check_usages(name, usages)) return NULL; + if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0) { + *unsupported = true; + return NULL; + } + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + return scr_crypto_key_new(view->data, (size_t)scr_bytes_byte_len(view), name, NULL, 0, + (unsigned)usages, false); +} + +/* importKeySync over converted arguments: the per-algorithm import, the + * unsupported-format NotSupportedError, then the empty-usages check. */ +static ScrDyn *scr_wc_import_key_sync(const char *format, ScrBytes *data, const ScrWcJwk *jwk, + const ScrWcAlgorithm *algorithm, bool extractable, + long usages) { + bool unsupported = false; + ScrDyn *key; + if (strcmp(algorithm->name, "HMAC") == 0) + key = scr_wc_import_hmac(format, data, jwk, algorithm, extractable, usages, &unsupported); + else if (scr_wc_is_aes(algorithm->name)) + key = scr_wc_import_aes(format, data, jwk, algorithm->name, extractable, usages, &unsupported); + else if (scr_wc_is_kdf(algorithm->name)) + key = scr_wc_import_kdf(format, data, algorithm->name, extractable, usages, &unsupported); + else { + scr_wc_unsupported_algorithm("importKey", algorithm->name); + return NULL; + } + if (unsupported) { + char message[96]; + snprintf(message, sizeof message, "Unable to import %s using %s format", algorithm->name, + format); + scr_throw_domex("NotSupportedError", message); + return NULL; + } + if (key && usages == 0) { + scr_dyn_release(key); + scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a secret key."); + return NULL; + } + return key; +} + /* importKey(format, keyData, algorithm, extractable, keyUsages): Node's - * conversion order, then macImportKey's checks and importKeySync's empty - * usage check. */ + * conversion order, then normalization and importKeySync. */ static ScrDyn *scr_wc_import(const ScrDyn *args) { static const char prefix[] = "Failed to execute 'importKey' on 'SubtleCrypto'"; if (!scr_wc_required(prefix, args, 5)) return NULL; @@ -605,77 +1071,28 @@ static ScrDyn *scr_wc_import(const ScrDyn *args) { scr_wc_args_take(&a, args, 5); ScrDyn *result = NULL; ScrBytes *data = NULL; + ScrWcJwk jwk; + memset(&jwk, 0, sizeof jwk); const char *format = scr_wc_key_format(prefix, a.v[0]); if (!format) goto done; - bool jwk = strcmp(format, "jwk") == 0; - /* The JsonWebKey dictionary is not implemented; that format is refused - * after the remaining arguments convert. */ - if (!jwk) { + if (strcmp(format, "jwk") == 0) { + if (!scr_wc_jwk_convert(prefix, a.v[1], &jwk)) goto done; + } else { data = scr_wc_buffer_source(prefix, 1, a.v[1]); if (!data) goto done; } - if (!scr_wc_is_object(a.v[2])) { - ScrStr *s = scr_wc_dom_string(a.v[2]); - if (!s) goto done; - scr_str_release(s); - } + if (!scr_wc_algorithm_identifier(a.v[2])) goto done; bool extractable = scr_dyn_truthy(a.v[3]); long usages = scr_wc_key_usages(prefix, 4, a.v[4]); if (usages < 0) goto done; ScrWcAlgorithm algorithm; - if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), true, - &algorithm)) + if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), + SCR_WC_OP_IMPORT, &algorithm)) goto done; - if (strcmp(algorithm.name, "HMAC") != 0) { - scr_wc_unsupported_algorithm("importKey", algorithm.name); - goto done; - } - if (usages & ~(long)(SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY)) { - scr_throw_domex("SyntaxError", "Unsupported key usage for HMAC key"); - goto done; - } - if (jwk) { - scr_wc_unsupported("importKey", "the 'jwk' key format"); - goto done; - } - if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0) { - char message[96]; - snprintf(message, sizeof message, "Unable to import HMAC using %s format", format); - scr_throw_domex("NotSupportedError", message); - goto done; - } - { - ScrBytes *view = data; - SCR_BYTES_SNAPSHOT(view); - size_t len = (size_t)scr_bytes_byte_len(view); - if (len == 0) { - scr_throw_domex("DataError", "Zero-length key is not supported"); - goto done; - } - double length = (double)len * 8; - unsigned char last = view->data[len - 1]; - if (algorithm.has_length) { - double bytes = floor(algorithm.length / 8) + floor((7 + fmod(algorithm.length, 8)) / 8); - if (bytes != (double)len) { - scr_throw_domex("DataError", "Invalid key length"); - goto done; - } - /* A length that is not a whole number of bytes clears the unused - * low bits of the final byte (truncateToBitLength). */ - int extra = (int)fmod(algorithm.length, 8); - if (extra) last = (unsigned char)(last & (0xff << (8 - extra))); - length = algorithm.length; - } - if (usages == 0) { - scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a secret key."); - goto done; - } - result = scr_crypto_key_new(view->data, len, algorithm.hash, length, (unsigned)usages, - extractable); - scr_crypto_key_of(result)->data[len - 1] = last; - } + result = scr_wc_import_key_sync(format, data, &jwk, &algorithm, extractable, usages); done: if (data) scr_bytes_release(data); + scr_wc_jwk_drop(&jwk); scr_wc_args_drop(&a, 5); return result; } @@ -683,7 +1100,118 @@ done: ScrPromise *scr_webcrypto_import_key(ScrDyn *args) { ScrDyn *key = scr_wc_import(args); if (!key) return scr_wc_rejected(); - return scr_promise_settled_ref(key, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); + return scr_wc_resolved(key); +} + +/* generateKey(algorithm, extractable, keyUsages) for HMAC and AES keys: + * random bytes of the requested length (an HMAC key defaults to its + * hash's block size; a length that is not a whole number of bytes clears + * the final byte's unused low bits). */ +ScrPromise *scr_webcrypto_generate_key(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'generateKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 3)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 3); + ScrDyn *key = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + bool extractable = scr_dyn_truthy(a.v[1]); + long usages = scr_wc_key_usages(prefix, 2, a.v[2]); + if (usages < 0) goto done; + ScrWcAlgorithm algorithm; + if (!scr_wc_normalize(a.v[0], scr_wc_key_names, SCR_WC_COUNT(scr_wc_key_names), + SCR_WC_OP_GENERATE, &algorithm)) + goto done; + bool hmac = strcmp(algorithm.name, "HMAC") == 0; + if (!hmac && !scr_wc_is_aes(algorithm.name)) { + scr_wc_unsupported_algorithm("generateKey", algorithm.name); + goto done; + } + if (!scr_wc_check_usages(algorithm.name, usages)) goto done; + if (usages == 0) { + scr_throw_domex("SyntaxError", "Usages cannot be empty when creating a key."); + goto done; + } + double length = algorithm.has_length ? algorithm.length + : strcmp(algorithm.hash, "SHA-384") == 0 || strcmp(algorithm.hash, "SHA-512") == 0 + ? 1024 + : 512; + size_t len = scr_wc_bits_to_bytes(length); + unsigned char *bytes = malloc(len ? len : 1); + if (!bytes) scr_trap("scriptc: out of memory\n"); + arc4random_buf(bytes, len); + scr_wc_truncate_bits(bytes, len, length); + key = scr_crypto_key_new(bytes, len, algorithm.name, hmac ? algorithm.hash : NULL, length, + (unsigned)usages, extractable); + volatile unsigned char *wipe = bytes; + for (size_t i = 0; i < len; i++) wipe[i] = 0; + free(bytes); +done: + scr_wc_args_drop(&a, 3); + if (!key) return scr_wc_rejected(); + return scr_wc_deferred(key); +} + +/* exportKeySync for secret keys: raw and raw-secret answer an ArrayBuffer + * of the key bytes, jwk an object { key_ops, ext, alg, kty, k }. */ +static ScrDyn *scr_wc_export(const ScrDyn *args) { + static const char prefix[] = "Failed to execute 'exportKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 2)) return NULL; + ScrWcArgs a; + scr_wc_args_take(&a, args, 2); + ScrDyn *result = NULL; + const char *format = scr_wc_key_format(prefix, a.v[0]); + if (!format) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + if (scr_wc_is_kdf(key->name)) { + char message[64]; + snprintf(message, sizeof message, "%s key export is not supported", key->name); + scr_throw_domex("NotSupportedError", message); + goto done; + } + if (!key->extractable) { + scr_throw_domex("InvalidAccessError", "key is not extractable"); + goto done; + } + if (strcmp(format, "raw") == 0 || strcmp(format, "raw-secret") == 0) { + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len); + if (key->len) memcpy(bytes->data, key->data, key->len); + result = scr_array_buffer_from_bytes(bytes); + scr_bytes_release(bytes); + } else if (strcmp(format, "jwk") == 0) { + char buf[16]; + const char *alg = scr_wc_jwk_alg(key->name, key->hash, key->length, buf, sizeof buf); + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len); + if (key->len) memcpy(bytes->data, key->data, key->len); + ScrStr *encoding = scr_str_new("base64url", 9); + ScrStr *k = scr_bytes_to_str(bytes, encoding); + scr_str_release(encoding); + scr_bytes_release(bytes); + result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "key_ops", 7, scr_wc_usage_array(key->usages)); + scr_dyn_obj_set(result, "ext", 3, scr_dyn_new_bool(key->extractable)); + if (alg) scr_dyn_obj_set(result, "alg", 3, scr_wc_cstr(alg)); + scr_dyn_obj_set(result, "kty", 3, scr_wc_cstr("oct")); + scr_dyn_obj_set(result, "k", 1, scr_dyn_new_str(k)); + scr_str_release(k); + } else { + char message[96]; + snprintf(message, sizeof message, "Unable to export %s secret key using %s format", key->name, + format); + scr_throw_domex("NotSupportedError", message); + } +done: + scr_wc_args_drop(&a, 2); + return result; +} + +ScrPromise *scr_webcrypto_export_key(ScrDyn *args) { + ScrDyn *result = scr_wc_export(args); + if (!result) return scr_wc_rejected(); + return scr_wc_resolved(result); } /* sign(algorithm, key, data) / verify(algorithm, key, signature, data): @@ -717,13 +1245,17 @@ static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char ma data = scr_wc_buffer_source(prefix, verify ? 3 : 2, a.v[verify ? 3 : 2]); if (!data) goto done; ScrWcAlgorithm algorithm; - if (!scr_wc_normalize(a.v[0], scr_wc_sign_names, SCR_WC_COUNT(scr_wc_sign_names), false, - &algorithm)) + if (!scr_wc_normalize(a.v[0], scr_wc_sign_names, SCR_WC_COUNT(scr_wc_sign_names), + SCR_WC_OP_SIGN, &algorithm)) goto done; if (strcmp(algorithm.name, "HMAC") != 0) { scr_wc_unsupported_algorithm(method, algorithm.name); goto done; } + if (strcmp(key->name, algorithm.name) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } if (!(key->usages & (verify ? SCR_WC_USAGE_VERIFY : SCR_WC_USAGE_SIGN))) { char message[64]; snprintf(message, sizeof message, "Unable to use this key to %s", method); @@ -827,3 +1359,563 @@ done: scr_crypto_promise_settle_array_buffer); return p; } + +/* ── encrypt and decrypt ────────────────────────────────────────────── + * The WebCrypto half of encrypt and decrypt: argument conversion, the + * AesCbcParams, AesCtrParams and AeadParams dictionaries, and the key + * checks. The cipher itself runs in scr_webcrypto_aes.c, which links + * with the mbedTLS archive and passes it in. */ +static const char *const scr_wc_cipher_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-OCB", "ChaCha20-Poly1305", "RSA-OAEP", +}; + +/* Node's limit on the data, IV and additional data (kMaxBufferLength). */ +static const double scr_wc_max_buffer = 2147483647.0; + +/* A BufferSource member of an algorithm dictionary: false with the error + * pending; *out stays NULL when an optional member is absent. */ +static bool scr_wc_member_buffer(const ScrDyn *dict, const char *dict_name, const char *key, + bool required, ScrBytes **out) { + ScrDyn *value = scr_wc_member(dict, key); + bool ok = true; + if (value->kind == SCR_DYN_UNDEF) { + if (required) { + scr_wc_missing_member(dict_name, key); + ok = false; + } + } else { + ScrDyn *plain = scr_wc_plain(value); + if (!plain) ok = false; + else if (plain->kind == SCR_DYN_BYTES || scr_array_buffer_is(plain)) { + *out = scr_bytes_buffer_source(plain); + } else { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: %s in passed algorithm is not instance of ArrayBuffer, Buffer, " + "TypedArray, or DataView.", + scr_wc_normalize_prefix, key); + ok = false; + } + if (plain) scr_dyn_release(plain); + } + scr_dyn_release(value); + return ok; +} + +/* validateByteLength / validateMaxBufferLength over a converted member. */ +static bool scr_wc_exact_length(const ScrBytes *bytes, const char *name, double length) { + if (scr_bytes_byte_len(bytes) == length) return true; + char message[96]; + snprintf(message, sizeof message, "%s must contain exactly %.0f bytes", name, length); + scr_throw_domex("OperationError", message); + return false; +} + +static bool scr_wc_max_length(const ScrBytes *bytes, const char *name) { + if (scr_bytes_byte_len(bytes) <= scr_wc_max_buffer) return true; + char message[96]; + snprintf(message, sizeof message, "%s must be at most %.0f bytes", name, scr_wc_max_buffer); + scr_throw_domex("OperationError", message); + return false; +} + +typedef struct { + ScrBytes *iv; /* the IV or the CTR counter block */ + ScrBytes *aad; + double tag_bits; + double counter_bits; +} ScrWcCipherParams; + +static void scr_wc_cipher_params_drop(ScrWcCipherParams *params) { + if (params->iv) scr_bytes_release(params->iv); + if (params->aad) scr_bytes_release(params->aad); + params->iv = params->aad = NULL; +} + +/* An optional or required [EnforceRange] octet member: false with the + * error pending; *present says whether it was given. */ +static bool scr_wc_member_octet(const ScrDyn *dict, const char *dict_name, const char *key, + bool required, bool *present, double *out) { + ScrDyn *value = scr_wc_member(dict, key); + bool ok = true; + *present = value->kind != SCR_DYN_UNDEF; + if (!*present) { + if (required) { + scr_wc_missing_member(dict_name, key); + ok = false; + } + } else { + char context[48]; + snprintf(context, sizeof context, "%s in passed algorithm", key); + ok = scr_wc_enforce_range(scr_wc_normalize_prefix, context, value, 255, out); + } + scr_dyn_release(value); + return ok; +} + +/* normalizeAlgorithm(algorithm, 'encrypt' / 'decrypt'): the canonical + * name and the AES dictionaries' members, in Web IDL member order with + * each member's validator right after its conversion. */ +static const char *scr_wc_normalize_cipher(ScrDyn *identifier, const char *method, + ScrWcCipherParams *params) { + memset(params, 0, sizeof *params); + params->tag_bits = 128; + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return NULL; + const char *canonical = + scr_wc_lookup(scr_wc_cipher_names, SCR_WC_COUNT(scr_wc_cipher_names), name); + scr_str_release(name); + if (!canonical) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return NULL; + } + if (!scr_wc_is_aes(canonical)) { + scr_wc_unsupported_algorithm(method, canonical); + return NULL; + } + ScrDyn *dict = scr_wc_is_object(identifier) ? scr_wc_plain(identifier) + : scr_dyn_retain(scr_dyn_undefined()); + if (!dict) return NULL; + bool ok = true; + bool present; + if (strcmp(canonical, "AES-CBC") == 0) { + ok = scr_wc_member_buffer(dict, "AesCbcParams", "iv", true, ¶ms->iv) && + scr_wc_exact_length(params->iv, "algorithm.iv", 16); + } else if (strcmp(canonical, "AES-CTR") == 0) { + ok = scr_wc_member_buffer(dict, "AesCtrParams", "counter", true, ¶ms->iv) && + scr_wc_exact_length(params->iv, "algorithm.counter", 16) && + scr_wc_member_octet(dict, "AesCtrParams", "length", true, &present, + ¶ms->counter_bits); + if (ok && (params->counter_bits == 0 || params->counter_bits > 128)) { + scr_throw_domex("OperationError", "AES-CTR algorithm.length must be between 1 and 128"); + ok = false; + } + } else { + ok = scr_wc_member_buffer(dict, "AeadParams", "additionalData", false, ¶ms->aad) && + (!params->aad || scr_wc_max_length(params->aad, "algorithm.additionalData")) && + scr_wc_member_buffer(dict, "AeadParams", "iv", true, ¶ms->iv) && + scr_wc_max_length(params->iv, "algorithm.iv") && + scr_wc_member_octet(dict, "AeadParams", "tagLength", false, &present, ¶ms->tag_bits); + if (ok && !present) params->tag_bits = 128; + double t = params->tag_bits; + if (ok && t != 32 && t != 64 && t != 96 && t != 104 && t != 112 && t != 120 && t != 128) { + char message[64]; + snprintf(message, sizeof message, "%.0f is not a valid AES-GCM tag length", t); + scr_throw_domex("OperationError", message); + ok = false; + } + } + scr_dyn_release(dict); + if (!ok) { + scr_wc_cipher_params_drop(params); + return NULL; + } + return canonical; +} + +static void *scr_wc_caught_retain(void *c) { return scr_caught_retain(c); } +static void scr_wc_caught_release(void *c) { scr_caught_release(c); } + +static void scr_wc_settle_rethrow(ScrClosure *cb, ScrError *error, ScrBytes *value) { + (void)error; + (void)value; + ScrPromise *p; + ScrCaught *caught; + memcpy(&p, &cb->caps[0]->slot, sizeof p); + memcpy(&caught, &cb->caps[1]->slot, sizeof caught); + scr_rethrow(caught); + scr_promise_reject_pending(p); +} + +/* A job failure: a fresh promise that rejects with the pending exception + * on a later loop turn, as Node's job promises do (+1). */ +static ScrPromise *scr_wc_deferred_rejection(void) { + ScrPromise *p = scr_promise_new(); + ScrClosure *cb = scr_closure_new(NULL, 2); + cb->caps[0] = scr_box_new_obj(scr_promise_retain_v, scr_promise_release_v, NULL); + scr_box_set_ref(cb->caps[0], scr_promise_retain(p)); + cb->caps[1] = scr_box_new_obj(scr_wc_caught_retain, scr_wc_caught_release, scr_caught_trace_v); + scr_box_set_ref(cb->caps[1], scr_exc_take()); + scr_crypto_defer_bytes(NULL, NULL, cb, scr_wc_settle_rethrow); + return p; +} + +/* A promise that fulfills with an ArrayBuffer of `bytes` (moves) on a + * later loop turn. */ +static ScrPromise *scr_wc_deferred_buffer(ScrBytes *bytes) { + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_bytes(bytes, NULL, scr_crypto_promise_callback(p), + scr_crypto_promise_settle_array_buffer); + return p; +} + +ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn aes) { + const char *method = decrypt ? "decrypt" : "encrypt"; + const char *prefix = decrypt ? "Failed to execute 'decrypt' on 'SubtleCrypto'" + : "Failed to execute 'encrypt' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 3)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 3); + ScrBytes *data = NULL; + ScrWcCipherParams params; + memset(¶ms, 0, sizeof params); + ScrPromise *result = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + data = scr_wc_buffer_source(prefix, 2, a.v[2]); + if (!data) goto done; + const char *name = scr_wc_normalize_cipher(a.v[0], method, ¶ms); + if (!name) goto done; + if (strcmp(key->name, name) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } + if (!(key->usages & (decrypt ? SCR_WC_USAGE_DECRYPT : SCR_WC_USAGE_ENCRYPT))) { + char message[64]; + snprintf(message, sizeof message, "Unable to use this key to %s", method); + scr_throw_domex("InvalidAccessError", message); + goto done; + } + if (!scr_wc_max_length(data, "data")) goto done; + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + ScrBytes *iv = params.iv; + SCR_BYTES_SNAPSHOT(iv); + if (!params.aad) params.aad = scr_bytes_new(SCR_BYTES_U8, 0); + ScrBytes *aad = params.aad; + SCR_BYTES_SNAPSHOT(aad); + ScrWebCryptoAesParams run = { + .iv = iv->data, + .iv_len = (size_t)scr_bytes_byte_len(iv), + .aad = aad->data, + .aad_len = (size_t)scr_bytes_byte_len(aad), + .tag_len = (size_t)(params.tag_bits / 8), + .counter_bits = (unsigned)params.counter_bits, + }; + ScrBytes *out = aes(name, decrypt, key->data, key->len, &run, view->data, + (size_t)scr_bytes_byte_len(view)); + if (out) { + result = scr_wc_deferred_buffer(out); + } else { + scr_throw_domex("OperationError", "The operation failed for an operation-specific reason"); + result = scr_wc_deferred_rejection(); + } + } +done: + if (data) scr_bytes_release(data); + scr_wc_cipher_params_drop(¶ms); + scr_wc_args_drop(&a, 3); + return result ? result : scr_wc_rejected(); +} + +/* ── deriveBits and deriveKey ───────────────────────────────────────── + * PBKDF2 and HKDF over node:crypto's KDF core (scr_crypto_pbkdf2 and + * scr_crypto_hkdf_bytes in scr_lib.c), with Node's Pbkdf2Params and + * HkdfParams dictionaries, length rules and key checks. deriveKey derives + * the bits the derived key type needs and imports them as a raw-secret + * key of that type. */ +static const char *const scr_wc_derive_names[] = { + "Argon2d", "Argon2i", "Argon2id", "ECDH", "HKDF", "PBKDF2", "X25519", "X448", +}; +/* The algorithms 'get key length' registers. */ +static const char *const scr_wc_length_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", "Argon2d", "Argon2i", + "Argon2id", "ChaCha20-Poly1305", "HKDF", "HMAC", "KMAC128", "KMAC256", "PBKDF2", +}; + +typedef struct { + const char *name; + const char *hash; + double iterations; + ScrBytes *salt; + ScrBytes *info; +} ScrWcKdfParams; + +static void scr_wc_kdf_params_drop(ScrWcKdfParams *params) { + if (params->salt) scr_bytes_release(params->salt); + if (params->info) scr_bytes_release(params->info); + params->salt = params->info = NULL; +} + +/* A HashAlgorithmIdentifier member with ensureSHA (`dict` names the + * dictionary); *hash is the member value for the later digest + * normalization (+1). */ +static bool scr_wc_member_hash(const ScrDyn *dict, const char *dict_name, ScrDyn **hash) { + ScrDyn *value = scr_wc_member(dict, "hash"); + if (value->kind == SCR_DYN_UNDEF) { + scr_dyn_release(value); + scr_wc_missing_member(dict_name, "hash"); + return false; + } + ScrStr *name = NULL; + bool ok = true; + if (scr_wc_is_object(value)) { + ScrDyn *plain = scr_wc_plain(value); + ScrDyn *member = plain ? scr_wc_member(plain, "name") : NULL; + if (plain) scr_dyn_release(plain); + if (!member) ok = false; + else { + if (member->kind == SCR_DYN_STR) name = scr_str_retain(member->v.str); + scr_dyn_release(member); + } + *hash = scr_dyn_retain(value); + } else { + name = scr_wc_dom_string(value); + if (!name) ok = false; + *hash = name ? scr_dyn_new_str(name) : NULL; + } + scr_dyn_release(value); + bool sha = name && name->len >= 3 && (name->data[0] | 32) == 's' && + (name->data[1] | 32) == 'h' && (name->data[2] | 32) == 'a'; + if (name) scr_str_release(name); + if (ok && !sha) { + char message[96]; + snprintf(message, sizeof message, "Only SHA hashes are supported in %s", dict_name); + scr_throw_domex("NotSupportedError", message); + ok = false; + } + if (!ok && *hash) { + scr_dyn_release(*hash); + *hash = NULL; + } + return ok; +} + +/* normalizeAlgorithm(algorithm, 'deriveBits') for PBKDF2 and HKDF. */ +static bool scr_wc_normalize_kdf(ScrDyn *identifier, const char *method, ScrWcKdfParams *params) { + memset(params, 0, sizeof *params); + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return false; + params->name = scr_wc_lookup(scr_wc_derive_names, SCR_WC_COUNT(scr_wc_derive_names), name); + scr_str_release(name); + if (!params->name) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return false; + } + if (!scr_wc_is_kdf(params->name)) { + scr_wc_unsupported_algorithm(method, params->name); + return false; + } + bool pbkdf2 = strcmp(params->name, "PBKDF2") == 0; + const char *dict_name = pbkdf2 ? "Pbkdf2Params" : "HkdfParams"; + ScrDyn *dict = scr_wc_is_object(identifier) ? scr_wc_plain(identifier) + : scr_dyn_retain(scr_dyn_undefined()); + if (!dict) return false; + ScrDyn *hash = NULL; + bool ok = scr_wc_member_hash(dict, dict_name, &hash); + if (ok && pbkdf2) { + ScrDyn *iterations = scr_wc_member(dict, "iterations"); + if (iterations->kind == SCR_DYN_UNDEF) { + scr_wc_missing_member(dict_name, "iterations"); + ok = false; + } else if (!scr_wc_enforce_range(scr_wc_normalize_prefix, "iterations in passed algorithm", + iterations, 4294967295.0, ¶ms->iterations)) { + ok = false; + } else if (params->iterations == 0) { + scr_throw_domex("OperationError", "iterations cannot be zero"); + ok = false; + } else if (params->iterations > 2147483647.0) { + scr_throw_domex("NotSupportedError", "iterations exceeds the implementation limit"); + ok = false; + } + scr_dyn_release(iterations); + ok = ok && scr_wc_member_buffer(dict, dict_name, "salt", true, ¶ms->salt); + } else if (ok) { + ok = scr_wc_member_buffer(dict, dict_name, "info", true, ¶ms->info); + if (ok && scr_bytes_byte_len(params->info) > 1024) { + scr_throw_domex("OperationError", "algorithm.info must be at most 1024 bytes"); + ok = false; + } + ok = ok && scr_wc_member_buffer(dict, dict_name, "salt", true, ¶ms->salt); + } + scr_dyn_release(dict); + if (ok) { + params->hash = scr_wc_digest_algorithm(hash, method); + if (!params->hash) ok = false; + } + if (hash) scr_dyn_release(hash); + if (!ok) scr_wc_kdf_params_drop(params); + return ok; +} + +/* The length rules of pbkdf2DeriveBits and hkdfDeriveBits; `null_length` + * says the length was null. */ +static bool scr_wc_kdf_length(const ScrWcKdfParams *params, bool null_length, double length) { + if (null_length) { + scr_throw_domex("OperationError", "length cannot be null"); + return false; + } + if (fmod(length, 8) != 0) { + scr_throw_domex("OperationError", "length must be a multiple of 8"); + return false; + } + if (strcmp(params->name, "HKDF") == 0) { + double digest = strcmp(params->hash, "SHA-1") == 0 ? 20 + : strcmp(params->hash, "SHA-256") == 0 ? 32 + : strcmp(params->hash, "SHA-384") == 0 ? 48 + : 64; + if (length > 255 * digest * 8) { + scr_throw_domex("OperationError", "length exceeds the maximum derived bit length"); + return false; + } + } + return true; +} + +/* The derived bytes (+1); length is a nonzero multiple of 8. */ +static ScrBytes *scr_wc_kdf_derive(const ScrWcKdfParams *params, const ScrCryptoKey *key, + double length) { + ScrBytes *secret = scr_bytes_new(SCR_BYTES_U8, (double)key->len); + if (key->len) memcpy(secret->data, key->data, key->len); + const char *native = scr_wc_native_hash(params->hash); + ScrStr *digest = scr_str_new(native, strlen(native)); + ScrBytes *out; + if (strcmp(params->name, "PBKDF2") == 0) { + out = scr_crypto_pbkdf2(secret, params->salt, params->iterations, length / 8, digest); + } else { + out = scr_crypto_hkdf_bytes(digest, secret, params->salt, params->info, length / 8); + } + scr_str_release(digest); + volatile unsigned char *wipe = secret->data; + for (size_t i = 0; i < key->len; i++) wipe[i] = 0; + scr_bytes_release(secret); + return out; +} + +/* The base key checks deriveBits and deriveKey share. */ +static bool scr_wc_check_base_key(const ScrCryptoKey *key, const char *algorithm, unsigned usage, + const char *usage_name) { + if (!(key->usages & usage)) { + char message[64]; + snprintf(message, sizeof message, "baseKey does not have %s usage", usage_name); + scr_throw_domex("InvalidAccessError", message); + return false; + } + if (strcmp(key->name, algorithm) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + return false; + } + return true; +} + +/* deriveBits(algorithm, baseKey[, length]). */ +ScrPromise *scr_webcrypto_derive_bits(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'deriveBits' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 2)) return scr_wc_rejected(); + size_t argc = (size_t)scr_dyn_arr_len(args); + ScrWcArgs a; + size_t n = argc > 3 ? 3 : argc; + scr_wc_args_take(&a, args, n); + ScrWcKdfParams params; + memset(¶ms, 0, sizeof params); + ScrPromise *result = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + /* length defaults to null when absent or undefined. */ + bool null_length = n < 3 || a.v[2]->kind == SCR_DYN_UNDEF || a.v[2]->kind == SCR_DYN_NULL; + double length = 0; + if (!null_length && !scr_wc_enforce_range(prefix, "3rd argument", a.v[2], 4294967295.0, &length)) + goto done; + if (!scr_wc_normalize_kdf(a.v[0], "deriveBits", ¶ms)) goto done; + if (!scr_wc_check_base_key(key, params.name, SCR_WC_USAGE_DERIVE_BITS, "deriveBits")) goto done; + if (!scr_wc_kdf_length(¶ms, null_length, length)) goto done; + if (length == 0) { + ScrBytes *empty = scr_bytes_new(SCR_BYTES_U8, 0); + result = scr_wc_resolved(scr_array_buffer_from_bytes(empty)); + scr_bytes_release(empty); + goto done; + } + ScrBytes *bits = scr_wc_kdf_derive(¶ms, key, length); + if (bits) result = scr_wc_deferred_buffer(bits); +done: + scr_wc_kdf_params_drop(¶ms); + scr_wc_args_drop(&a, n); + return result ? result : scr_wc_rejected(); +} + +/* getKeyLength for the derived key type: the bits to derive, or *null + * for the KDF types (whose derivation then rejects). */ +static bool scr_wc_derived_length(ScrDyn *identifier, const ScrWcAlgorithm *imported, + bool *null_length, double *length) { + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return false; + const char *canonical = + scr_wc_lookup(scr_wc_length_names, SCR_WC_COUNT(scr_wc_length_names), name); + scr_str_release(name); + if (!canonical) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return false; + } + *null_length = false; + if (scr_wc_is_aes(canonical)) { + ScrWcAlgorithm aes; + memset(&aes, 0, sizeof aes); + if (!scr_wc_aes_length_params(identifier, "AesDerivedKeyParams", &aes)) return false; + *length = aes.length; + return true; + } + if (strcmp(canonical, "HMAC") == 0 && imported->hash) { + *length = imported->has_length ? imported->length + : strcmp(imported->hash, "SHA-384") == 0 || strcmp(imported->hash, "SHA-512") == 0 + ? 1024 + : 512; + return true; + } + *null_length = true; + return true; +} + +/* deriveKey(algorithm, baseKey, derivedKeyType, extractable, keyUsages): + * the derivation's errors reject at once; the import of the derived bits + * settles, or rejects, on a later loop turn. */ +ScrPromise *scr_webcrypto_derive_key(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'deriveKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 5)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 5); + ScrWcKdfParams params; + memset(¶ms, 0, sizeof params); + ScrPromise *result = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + if (!scr_wc_algorithm_identifier(a.v[2])) goto done; + bool extractable = scr_dyn_truthy(a.v[3]); + long usages = scr_wc_key_usages(prefix, 4, a.v[4]); + if (usages < 0) goto done; + if (!scr_wc_normalize_kdf(a.v[0], "deriveKey", ¶ms)) goto done; + ScrWcAlgorithm imported; + if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), + SCR_WC_OP_IMPORT, &imported)) + goto done; + bool null_length; + double length; + if (!scr_wc_derived_length(a.v[2], &imported, &null_length, &length)) goto done; + if (strcmp(imported.name, "HMAC") != 0 && !scr_wc_is_aes(imported.name) && + !scr_wc_is_kdf(imported.name)) { + scr_wc_unsupported_algorithm("deriveKey", imported.name); + goto done; + } + if (!scr_wc_check_base_key(key, params.name, SCR_WC_USAGE_DERIVE_KEY, "deriveKey")) goto done; + if (!scr_wc_kdf_length(¶ms, null_length, length)) goto done; + ScrBytes *bits = scr_wc_kdf_derive(¶ms, key, length); + if (!bits) goto done; + ScrDyn *derived = scr_wc_import_key_sync("raw-secret", bits, NULL, &imported, extractable, usages); + scr_bytes_release(bits); + result = derived ? scr_wc_deferred(derived) : scr_wc_deferred_rejection(); +done: + scr_wc_kdf_params_drop(¶ms); + scr_wc_args_drop(&a, 5); + return result ? result : scr_wc_rejected(); +} diff --git a/packages/runtime/src/scr_webcrypto_aes.c b/packages/runtime/src/scr_webcrypto_aes.c new file mode 100644 index 000000000..7cb63f561 --- /dev/null +++ b/packages/runtime/src/scr_webcrypto_aes.c @@ -0,0 +1,137 @@ +/* WebCrypto's AES ciphers: crypto.subtle.encrypt and decrypt with AES-CBC, + * AES-CTR and AES-GCM keys, over the vendored mbedTLS block cipher. + * + * scr_webcrypto.c converts and validates the arguments, normalizes the + * algorithm and checks the key; this unit runs the cipher itself, the way + * Node 24's src/crypto/crypto_aes.cc does: CBC with PKCS#7 padding, CTR + * with a counter that wraps within its low `length` bits (and fails when + * the input needs more blocks than those bits can count), and GCM with a + * 12 to 128 byte IV and a 4 to 16 byte tag appended to the ciphertext. + * The unit links only into programs that call these methods, alongside + * the cipher feature's mbedTLS archive. */ +#include "scr_runtime.h" + +#include +#include + +#include "mbedtls/aes.h" +#include "mbedtls/gcm.h" +#include "mbedtls/platform_util.h" + +static ScrBytes *scr_wc_aes_gcm(bool decrypt, const unsigned char *key, size_t key_len, + const ScrWebCryptoAesParams *p, const unsigned char *in, + size_t len) { + if (p->iv_len < 12 || p->iv_len > 128) return NULL; + if (decrypt && len < p->tag_len) return NULL; + size_t out_len = decrypt ? len - p->tag_len : len + p->tag_len; + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)out_len); + mbedtls_gcm_context gcm; + mbedtls_gcm_init(&gcm); + int rc = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, (unsigned)(key_len * 8)); + if (rc == 0 && decrypt) { + rc = mbedtls_gcm_auth_decrypt(&gcm, out_len, p->iv, p->iv_len, p->aad, p->aad_len, + in + out_len, p->tag_len, in, out->data); + } else if (rc == 0) { + rc = mbedtls_gcm_crypt_and_tag(&gcm, MBEDTLS_GCM_ENCRYPT, len, p->iv, p->iv_len, p->aad, + p->aad_len, in, out->data, p->tag_len, out->data + len); + } + mbedtls_gcm_free(&gcm); + if (rc != 0) { + mbedtls_platform_zeroize(out->data, out_len); + scr_bytes_release(out); + return NULL; + } + return out; +} + +static ScrBytes *scr_wc_aes_cbc(bool decrypt, const unsigned char *key, size_t key_len, + const ScrWebCryptoAesParams *p, const unsigned char *in, + size_t len) { + if (decrypt && (len == 0 || len % 16 != 0)) return NULL; + size_t padded = decrypt ? len : (len / 16 + 1) * 16; + unsigned char *buf = malloc(padded); + if (!buf) scr_trap("scriptc: out of memory\n"); + memcpy(buf, in, len); + if (!decrypt) memset(buf + len, (int)(padded - len), padded - len); + unsigned char iv[16]; + memcpy(iv, p->iv, 16); + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + int rc = decrypt ? mbedtls_aes_setkey_dec(&aes, key, (unsigned)(key_len * 8)) + : mbedtls_aes_setkey_enc(&aes, key, (unsigned)(key_len * 8)); + if (rc == 0) + rc = mbedtls_aes_crypt_cbc(&aes, decrypt ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, padded, + iv, buf, buf); + mbedtls_aes_free(&aes); + size_t out_len = padded; + if (rc == 0 && decrypt) { + /* PKCS#7: the final byte names 1 to 16 bytes of padding, each of + * which holds that value. Checked without data-dependent branches. */ + unsigned char pad = buf[padded - 1]; + unsigned char diff = (unsigned char)-(pad == 0 || pad > 16); + for (unsigned i = 1; i <= 16; i++) + diff |= (unsigned char)((buf[padded - i] ^ pad) & (unsigned char)-(i <= pad)); + if (diff) rc = -1; + else out_len = padded - pad; + } + ScrBytes *out = NULL; + if (rc == 0) { + out = scr_bytes_new(SCR_BYTES_U8, (double)out_len); + memcpy(out->data, buf, out_len); + } + mbedtls_platform_zeroize(buf, padded); + free(buf); + return out; +} + +/* Adds one to the low `bits` bits of a big-endian counter block, leaving + * the bits above them unchanged. */ +static void scr_wc_ctr_increment(unsigned char block[16], unsigned bits) { + for (int i = 15; i >= 0 && bits > 0; i--) { + unsigned width = bits >= 8 ? 8 : bits; + unsigned mask = (1u << width) - 1; + unsigned low = ((unsigned)block[i] + 1) & mask; + block[i] = (unsigned char)((block[i] & ~mask) | low); + bits -= width; + if (low != 0) return; + } +} + +static ScrBytes *scr_wc_aes_ctr(const unsigned char *key, size_t key_len, + const ScrWebCryptoAesParams *p, const unsigned char *in, + size_t len) { + size_t blocks = (len + 15) / 16; + if (p->counter_bits < 64 && (double)blocks > (double)(1ull << p->counter_bits)) return NULL; + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + if (mbedtls_aes_setkey_enc(&aes, key, (unsigned)(key_len * 8)) != 0) { + mbedtls_aes_free(&aes); + return NULL; + } + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)len); + unsigned char counter[16], stream[16]; + memcpy(counter, p->iv, 16); + for (size_t off = 0; off < len; off += 16) { + mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, counter, stream); + size_t n = len - off < 16 ? len - off : 16; + for (size_t i = 0; i < n; i++) out->data[off + i] = (unsigned char)(in[off + i] ^ stream[i]); + scr_wc_ctr_increment(counter, p->counter_bits); + } + mbedtls_platform_zeroize(stream, sizeof stream); + mbedtls_aes_free(&aes); + return out; +} + +/* The cipher scr_webcrypto_cipher runs: +1 output bytes, or NULL when the + * operation fails (Node's OperationError). */ +static ScrBytes *scr_wc_aes(const char *name, bool decrypt, const unsigned char *key, + size_t key_len, const ScrWebCryptoAesParams *p, + const unsigned char *in, size_t len) { + if (strcmp(name, "AES-GCM") == 0) return scr_wc_aes_gcm(decrypt, key, key_len, p, in, len); + if (strcmp(name, "AES-CBC") == 0) return scr_wc_aes_cbc(decrypt, key, key_len, p, in, len); + return scr_wc_aes_ctr(key, key_len, p, in, len); +} + +ScrPromise *scr_webcrypto_encrypt(ScrDyn *args) { return scr_webcrypto_cipher(args, false, scr_wc_aes); } + +ScrPromise *scr_webcrypto_decrypt(ScrDyn *args) { return scr_webcrypto_cipher(args, true, scr_wc_aes); } diff --git a/packages/runtime/src/scr_webcrypto_object.c b/packages/runtime/src/scr_webcrypto_object.c new file mode 100644 index 000000000..c20ef13dc --- /dev/null +++ b/packages/runtime/src/scr_webcrypto_object.c @@ -0,0 +1,53 @@ +/* The crypto global as a value: node:crypto's dynamic Crypto object + * (scr_crypto_native in scr_async_dyn.c, which carries subtle.digest) with + * the Crypto object's own randomUUID and getRandomValues methods, for + * untyped reads such as uuid's `crypto.randomUUID && crypto.randomUUID()`. + * Node's receiver check (ERR_INVALID_THIS on a detached call) is not + * modeled. There is one object per thread, so every spelling of the + * global (crypto, globalThis.crypto, node:crypto's webcrypto) is the same + * value; it is released at exit. Linked only into programs that read the + * global as a value. */ +#include "scr_runtime.h" + +static ScrDyn *scr_webcrypto_random_uuid_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + (void)args; + (void)argc; + ScrStr *uuid = scr_crypto_random_uuid(); + ScrDyn *result = scr_dyn_new_str(uuid); + scr_str_release(uuid); + return result; +} + +static ScrDyn *scr_webcrypto_get_random_values_call(ScrClosure *closure, ScrDyn *const *args, + size_t argc) { + (void)closure; + if (argc == 0) { + static const char message[] = + "Failed to execute 'getRandomValues' on 'Crypto': 1 argument required, but only 0 present."; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_MISSING_ARGS"); + return NULL; + } + return scr_crypto_get_random_values_dyn(args[0]); +} + +static SCR_TL ScrDyn *scr_webcrypto_object_value; + +static void scr_webcrypto_object_cleanup(void) { + if (scr_webcrypto_object_value) scr_dyn_release(scr_webcrypto_object_value); + scr_webcrypto_object_value = NULL; +} + +ScrDyn *scr_webcrypto_object(void) { + if (scr_webcrypto_object_value) return scr_dyn_retain(scr_webcrypto_object_value); + ScrDyn *result = scr_crypto_native(); + scr_dyn_obj_set(result, "randomUUID", 10, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_webcrypto_random_uuid_call, 0, + "crypto.randomUUID", "randomUUID")); + scr_dyn_obj_set(result, "getRandomValues", 15, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_webcrypto_get_random_values_call, 1, + "crypto.getRandomValues", "getRandomValues")); + scr_webcrypto_object_value = result; + scr_atexit(scr_webcrypto_object_cleanup); + return scr_dyn_retain(result); +} diff --git a/tests/corpus/7120-webcrypto-random-uuid.ts b/tests/corpus/7120-webcrypto-random-uuid.ts new file mode 100644 index 000000000..d35df97cc --- /dev/null +++ b/tests/corpus/7120-webcrypto-random-uuid.ts @@ -0,0 +1,22 @@ +// crypto.randomUUID() on the WebCrypto global, directly and through +// globalThis: version-4 UUIDs, distinct on every call, and inside +// closures and default parameters the way request-id middleware uses it. +const shape = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +const direct: string = crypto.randomUUID(); +console.log(direct.length, shape.test(direct)); +const viaGlobalThis = globalThis.crypto.randomUUID(); +console.log(viaGlobalThis.length, shape.test(viaGlobalThis), viaGlobalThis !== direct); + +const seen = new Set(); +for (let i = 0; i < 100; i++) seen.add(crypto.randomUUID()); +console.log("distinct", seen.size); + +function requestId(generator: () => string = () => crypto.randomUUID()): string { + return generator(); +} +console.log(shape.test(requestId()), requestId(() => "fixed")); + +const ids = ["a", "b", "c"].map((name) => ({ name, id: crypto.randomUUID() })); +console.log(ids.every((entry) => shape.test(entry.id)), ids[0]!.id !== ids[1]!.id); +console.log(typeof crypto.randomUUID()); diff --git a/tests/corpus/7121-webcrypto-crypto-object.mjs b/tests/corpus/7121-webcrypto-crypto-object.mjs new file mode 100644 index 000000000..058e72153 --- /dev/null +++ b/tests/corpus/7121-webcrypto-crypto-object.mjs @@ -0,0 +1,35 @@ +// The crypto global from JavaScript: uuid's feature test +// (`crypto.randomUUID && crypto.randomUUID()`), the methods read as values, +// and getRandomValues through the dynamic object, including its +// missing-argument and non-integer-view errors. +const shape = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +function v4(options) { + if (!options && crypto.randomUUID) { + return crypto.randomUUID(); + } + return "fallback"; +} +console.log(shape.test(v4()), v4({})); +console.log(shape.test(globalThis.crypto.randomUUID())); +console.log(typeof crypto.randomUUID, typeof crypto.getRandomValues, typeof crypto.subtle); +console.log(crypto.randomUUID.name, crypto.getRandomValues.name, crypto.getRandomValues.length); + +const native = typeof crypto !== "undefined" && crypto.randomUUID ? { randomUUID: crypto.randomUUID.bind(crypto) } : {}; +console.log(shape.test(native.randomUUID())); + +const rng = (n) => crypto.getRandomValues(new Uint8Array(n)); +console.log(rng(16).length, rng(16) instanceof Uint8Array); +const words = new Uint32Array(4); +console.log(crypto.getRandomValues(words) === words); + +function attempt(label, run) { + try { + console.log(label, run()); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +attempt("no argument", () => crypto.getRandomValues()); +attempt("float view", () => crypto.getRandomValues(new Float64Array(2))); +attempt("too long", () => crypto.getRandomValues(new Uint8Array(65537))); diff --git a/tests/corpus/7122-webcrypto-node-crypto.ts b/tests/corpus/7122-webcrypto-node-crypto.ts new file mode 100644 index 000000000..c0ada7939 --- /dev/null +++ b/tests/corpus/7122-webcrypto-node-crypto.ts @@ -0,0 +1,38 @@ +// WebCrypto reached through node:crypto: the webcrypto export (named and +// through the namespace), the module's own subtle export, and their +// identity with the crypto global. Each form runs the same HMAC and +// digest calls as the global. +import * as nodeCrypto from "node:crypto"; +import { subtle, webcrypto } from "node:crypto"; + +const encoder = new TextEncoder(); +const message = encoder.encode("node:crypto webcrypto"); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +console.log(webcrypto === crypto, webcrypto === globalThis.crypto, nodeCrypto.webcrypto === crypto); +console.log(hex(await webcrypto.subtle.digest("SHA-256", message))); +console.log(hex(await subtle.digest("SHA-1", message))); +console.log(hex(await nodeCrypto.subtle.digest({ name: "SHA-384" }, message))); +console.log(hex(await nodeCrypto.webcrypto.subtle.digest("SHA-512", message))); + +const secret = encoder.encode("shared secret"); +const params = { name: "HMAC", hash: "SHA-256" }; +const signer = await webcrypto.subtle.importKey("raw", secret, params, false, ["sign"]); +const verifier = await subtle.importKey("raw", secret, params, false, ["verify"]); +const signature = await nodeCrypto.subtle.sign("HMAC", signer, message); +console.log(hex(signature)); +console.log(await nodeCrypto.webcrypto.subtle.verify("HMAC", verifier, signature, message)); +console.log(await subtle.verify("HMAC", verifier, signature, encoder.encode("other"))); +console.log(signer instanceof CryptoKey, signer.algorithm.name, verifier.usages.join(",")); + +const shape = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +console.log(shape.test(webcrypto.randomUUID()), shape.test(nodeCrypto.webcrypto.randomUUID())); +console.log(webcrypto.getRandomValues(new Uint32Array(4)).length); +try { + await subtle.digest("MD5", message); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} diff --git a/tests/corpus/7123-webcrypto-node-crypto.cjs b/tests/corpus/7123-webcrypto-node-crypto.cjs new file mode 100644 index 000000000..b11131d4b --- /dev/null +++ b/tests/corpus/7123-webcrypto-node-crypto.cjs @@ -0,0 +1,31 @@ +// node:crypto's WebCrypto from CommonJS: destructured webcrypto and +// subtle, the module object's members, the +// `globalThis.crypto ?? require("crypto").webcrypto` fallback libraries +// use to find a Crypto object, and crypto destructured from globalThis, +// all the same object. +const nodeCrypto = require("node:crypto"); +const { webcrypto, subtle } = require("crypto"); + +const encoder = new TextEncoder(); +const hex = (buffer) => Buffer.from(new Uint8Array(buffer)).toString("hex"); + +function cryptoObject() { + return globalThis.crypto ?? nodeCrypto.webcrypto; +} + +async function main() { + console.log(webcrypto === globalThis.crypto, cryptoObject() === crypto, typeof subtle); + console.log(hex(await webcrypto.subtle.digest("SHA-256", encoder.encode("abc")))); + console.log(hex(await subtle.digest("SHA-1", encoder.encode("abc")))); + console.log(hex(await nodeCrypto.subtle.digest("SHA-256", encoder.encode("abc")))); + const key = await nodeCrypto.webcrypto.subtle.importKey("raw", encoder.encode("k"), { name: "HMAC", hash: "SHA-1" }, true, ["sign"]); + console.log(hex(await subtle.sign("HMAC", key, encoder.encode("abc")))); + const wc = cryptoObject(); + console.log(wc.randomUUID().length, wc.getRandomValues(new Uint8Array(8)).length); + console.log(typeof webcrypto.randomUUID, typeof nodeCrypto.webcrypto.getRandomValues); + const { crypto: destructured } = globalThis; + console.log(destructured === webcrypto, destructured.randomUUID().length); + console.log(hex(await destructured.subtle.digest("SHA-1", encoder.encode("abc")))); +} + +main(); diff --git a/tests/corpus/7124-webcrypto-secret-keys.ts b/tests/corpus/7124-webcrypto-secret-keys.ts new file mode 100644 index 000000000..8f1f5d601 --- /dev/null +++ b/tests/corpus/7124-webcrypto-secret-keys.ts @@ -0,0 +1,91 @@ +// crypto.crypto.subtle.generateKey, exportKey and importKey for secret keys: +// HMAC and AES keys generated at every length, exported as raw bytes and +// as JSON Web Keys, imported back from both forms, and PBKDF2 and HKDF +// base keys imported from raw bytes. Random key bytes are checked by +// length and round trip, not by value. +const encoder = new TextEncoder(); + +type Key = Awaited>; + +function describe(key: Key): string { + return JSON.stringify({ + type: key.type, + extractable: key.extractable, + algorithm: key.algorithm, + usages: key.usages, + }); +} + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +async function show(label: string, run: () => Promise): Promise { + try { + console.log(label, await run()); + } catch (e) { + const error = e as Error & { code?: string }; + console.log(label, "rejects", error.name, error.code, error.message); + } +} + +for (const hash of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) { + await show(`hmac ${hash}`, async () => { + const key = await crypto.subtle.generateKey({ name: "HMAC", hash }, true, ["sign", "verify"]); + const raw = await crypto.subtle.exportKey("raw", key); + return `${describe(key)} ${raw.byteLength}`; + }); +} +await show("hmac length 100", async () => { + const key = await crypto.subtle.generateKey({ name: "HMAC", hash: { name: "SHA-512" }, length: 100 }, true, ["sign"]); + const raw = new Uint8Array(await crypto.subtle.exportKey("raw", key)); + return `${describe(key)} ${raw.length} ${raw[12]! & 0x0f}`; +}); +for (const name of ["AES-GCM", "AES-CBC", "AES-CTR", "AES-KW"]) { + for (const length of [128, 192, 256]) { + await show(`${name} ${length}`, async () => { + const usages: ("wrapKey" | "unwrapKey" | "encrypt" | "decrypt")[] = + name === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"]; + const key = await crypto.subtle.generateKey({ name, length }, true, usages); + const jwk = await crypto.subtle.exportKey("jwk", key); + return `${describe(key)} ${jwk.alg} ${jwk.kty} ${jwk.k!.length} ${JSON.stringify(jwk.key_ops)} ${jwk.ext}`; + }); + } +} + +// Fixed key bytes: the exported forms are deterministic. +const bytes = new Uint8Array(32); +for (let i = 0; i < bytes.length; i++) bytes[i] = i * 7 + 1; +const hmac = await crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-384" }, true, ["verify", "sign"]); +const hmacJwk = await crypto.subtle.exportKey("jwk", hmac); +console.log(JSON.stringify(hmacJwk), Object.keys(hmacJwk).join(",")); +console.log(hex(await crypto.subtle.exportKey("raw", hmac)) === Buffer.from(bytes).toString("hex")); +const reimported = await crypto.subtle.importKey("jwk", hmacJwk, { name: "HMAC", hash: "SHA-384" }, true, ["sign"]); +console.log(describe(reimported)); +const signature = await crypto.subtle.sign("HMAC", reimported, encoder.encode("payload")); +console.log(await crypto.subtle.verify("HMAC", hmac, signature, encoder.encode("payload"))); + +const short = await crypto.subtle.importKey("raw", bytes.subarray(0, 20), { name: "HMAC", hash: "SHA-1", length: 156 }, true, ["sign"]); +console.log(describe(short), JSON.stringify(await crypto.subtle.exportKey("jwk", short))); +console.log(hex(await crypto.subtle.exportKey("raw-secret", short))); + +for (const length of [16, 24, 32]) { + const key = await crypto.subtle.importKey("raw", bytes.subarray(0, length), "AES-GCM", true, ["encrypt"]); + const jwk = await crypto.subtle.exportKey("jwk", key); + console.log(describe(key), JSON.stringify(jwk)); + const back = await crypto.subtle.importKey("jwk", jwk, { name: "AES-GCM" }, false, ["encrypt"]); + console.log(describe(back)); +} +const cbc = await crypto.subtle.importKey("raw-secret", bytes.subarray(0, 16), "aes-cbc", true, ["decrypt", "unwrapKey"]); +console.log(describe(cbc), hex(await crypto.subtle.exportKey("raw", cbc))); +const wrap = await crypto.subtle.importKey("raw", bytes.subarray(0, 24), "AES-KW", true, ["unwrapKey", "wrapKey"]); +console.log(describe(wrap), JSON.stringify(await crypto.subtle.exportKey("jwk", wrap))); + +const password = encoder.encode("correct horse battery staple"); +const pbkdf2 = await crypto.subtle.importKey("raw", password, "PBKDF2", false, ["deriveBits", "deriveKey"]); +console.log(describe(pbkdf2)); +const hkdf = await crypto.subtle.importKey("raw-secret", bytes, { name: "hkdf" }, false, ["deriveKey"]); +console.log(describe(hkdf)); +const empty = await crypto.subtle.importKey("raw", new Uint8Array(0), "PBKDF2", false, ["deriveBits"]); +console.log(describe(empty)); +console.log(hmac instanceof CryptoKey, pbkdf2 instanceof CryptoKey); diff --git a/tests/corpus/7125-webcrypto-key-errors.mjs b/tests/corpus/7125-webcrypto-key-errors.mjs new file mode 100644 index 000000000..7f14ae0d7 --- /dev/null +++ b/tests/corpus/7125-webcrypto-key-errors.mjs @@ -0,0 +1,95 @@ +// Rejections of crypto.crypto.subtle.generateKey, exportKey and importKey for +// secret keys, in Node's order: argument conversion, algorithm +// normalization, usages, then the per-algorithm checks, and the JSON Web +// Key validation of oct keys. + +async function attempt(label, run) { + try { + const value = await run(); + const shown = value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value instanceof CryptoKey ? JSON.stringify(value.algorithm) : JSON.stringify(value); + console.log(label, "resolves", shown); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +await attempt("generate too few", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true)); +await attempt("generate unknown", () => crypto.subtle.generateKey({ name: "FOO" }, true, ["sign"])); +await attempt("generate PBKDF2", () => crypto.subtle.generateKey({ name: "PBKDF2" }, false, ["deriveBits"])); +await attempt("generate no name", () => crypto.subtle.generateKey({ hash: "SHA-1" }, true, ["sign"])); +await attempt("generate hmac string", () => crypto.subtle.generateKey("HMAC", true, ["sign"])); +await attempt("generate hmac no hash", () => crypto.subtle.generateKey({ name: "HMAC" }, true, ["sign"])); +await attempt("generate hmac md5", () => crypto.subtle.generateKey({ name: "HMAC", hash: "MD5" }, true, ["sign"])); +await attempt("generate hmac bogus hash", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-999" }, true, ["sign"])); +await attempt("generate hmac length 0", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1", length: 0 }, true, ["sign"])); +await attempt("generate hmac length NaN", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1", length: NaN }, true, ["sign"])); +await attempt("generate hmac length -1", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1", length: -1 }, true, ["sign"])); +await attempt("generate hmac encrypt", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, ["encrypt"])); +await attempt("generate hmac no usages", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, [])); +await attempt("generate bad usage", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, ["fly"])); +await attempt("generate usages string", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, "sign")); +await attempt("generate aes no length", () => crypto.subtle.generateKey({ name: "AES-GCM" }, true, ["encrypt"])); +await attempt("generate aes length 100", () => crypto.subtle.generateKey({ name: "AES-GCM", length: 100 }, true, ["encrypt"])); +await attempt("generate aes length 70000", () => crypto.subtle.generateKey({ name: "AES-CBC", length: 70000 }, true, ["encrypt"])); +await attempt("generate aes sign", () => crypto.subtle.generateKey({ name: "AES-CTR", length: 128 }, true, ["sign"])); +await attempt("generate aes-kw encrypt", () => crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, true, ["encrypt"])); +await attempt("generate aes no usages", () => crypto.subtle.generateKey({ name: "AES-GCM", length: 128 }, true, [])); +await attempt("generate aes string length", async () => (await crypto.subtle.generateKey({ name: "aes-gcm", length: "256" }, true, ["encrypt"])).algorithm); + +const hmac = await crypto.subtle.importKey("raw", new Uint8Array(20), { name: "HMAC", hash: "SHA-256" }, true, ["sign"]); +const locked = await crypto.subtle.importKey("raw", new Uint8Array(16), "AES-GCM", false, ["encrypt"]); +const pbkdf2 = await crypto.subtle.importKey("raw", new Uint8Array(4), "PBKDF2", false, ["deriveBits"]); +await attempt("export too few", () => crypto.subtle.exportKey("raw")); +await attempt("export bad format", () => crypto.subtle.exportKey("pem", hmac)); +await attempt("export not a key", () => crypto.subtle.exportKey("raw", { type: "secret" })); +await attempt("export spki", () => crypto.subtle.exportKey("spki", hmac)); +await attempt("export pkcs8", () => crypto.subtle.exportKey("pkcs8", hmac)); +await attempt("export raw-public", () => crypto.subtle.exportKey("raw-public", hmac)); +await attempt("export non-extractable", () => crypto.subtle.exportKey("raw", locked)); +await attempt("export pbkdf2", () => crypto.subtle.exportKey("raw", pbkdf2)); +await attempt("export pbkdf2 jwk", () => crypto.subtle.exportKey("jwk", pbkdf2)); + +const sixteen = new Uint8Array(16); +await attempt("import aes 15 bytes", () => crypto.subtle.importKey("raw", new Uint8Array(15), "AES-GCM", true, ["encrypt"])); +await attempt("import aes sign", () => crypto.subtle.importKey("raw", sixteen, "AES-GCM", true, ["sign"])); +await attempt("import aes-kw encrypt", () => crypto.subtle.importKey("raw", sixteen, "AES-KW", true, ["encrypt"])); +await attempt("import aes no usages", () => crypto.subtle.importKey("raw", sixteen, "AES-CBC", true, [])); +await attempt("import aes spki", () => crypto.subtle.importKey("spki", sixteen, "AES-CBC", true, ["encrypt"])); +await attempt("import aes length ignored", () => crypto.subtle.importKey("raw", sixteen, { name: "AES-CTR", length: 256 }, true, ["encrypt"])); +await attempt("import pbkdf2 extractable", () => crypto.subtle.importKey("raw", sixteen, "PBKDF2", true, ["deriveBits"])); +await attempt("import pbkdf2 sign", () => crypto.subtle.importKey("raw", sixteen, "PBKDF2", false, ["sign"])); +await attempt("import pbkdf2 no usages", () => crypto.subtle.importKey("raw", sixteen, "PBKDF2", false, [])); +await attempt("import hkdf jwk", () => crypto.subtle.importKey("jwk", { kty: "oct", k: "AAAA" }, "HKDF", false, ["deriveKey"])); +await attempt("import hkdf pkcs8", () => crypto.subtle.importKey("pkcs8", sixteen, "HKDF", false, ["deriveKey"])); +await attempt("sign with aes key", () => crypto.subtle.sign("HMAC", locked, sixteen)); + +const hs256 = { name: "HMAC", hash: "SHA-256" }; +const jwk = (data, alg = hs256, extractable = true, usages = ["sign"]) => crypto.subtle.importKey("jwk", data, alg, extractable, usages); +await attempt("jwk ok", () => jwk({ kty: "oct", k: "AAECAwQFBgc" })); +await attempt("jwk alg match", () => jwk({ kty: "oct", k: "AAECAwQFBgc", alg: "HS256" })); +await attempt("jwk alg mismatch", () => jwk({ kty: "oct", k: "AAECAwQFBgc", alg: "HS512" })); +await attempt("jwk no kty", () => jwk({ k: "AAECAwQFBgc" })); +await attempt("jwk kty RSA", () => jwk({ kty: "RSA", k: "AAECAwQFBgc" })); +await attempt("jwk no k", () => jwk({ kty: "oct" })); +await attempt("jwk k number", () => jwk({ kty: "oct", k: 5 })); +await attempt("jwk k empty", () => jwk({ kty: "oct", k: "" })); +await attempt("jwk k standard alphabet", () => jwk({ kty: "oct", k: "AA+/AA==" })); +await attempt("jwk k junk", () => jwk({ kty: "oct", k: "!!!!" })); +await attempt("jwk use enc", () => jwk({ kty: "oct", k: "AAECAwQFBgc", use: "enc" })); +await attempt("jwk use sig", () => jwk({ kty: "oct", k: "AAECAwQFBgc", use: "sig" })); +await attempt("jwk key_ops superset", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: ["sign", "verify", "fly"] })); +await attempt("jwk key_ops missing", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: ["verify"] })); +await attempt("jwk key_ops duplicate", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: ["sign", "sign"] })); +await attempt("jwk key_ops string", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: "sign" })); +await attempt("jwk ext false", () => jwk({ kty: "oct", k: "AAECAwQFBgc", ext: false })); +await attempt("jwk ext false locked", () => jwk({ kty: "oct", k: "AAECAwQFBgc", ext: false }, hs256, false)); +await attempt("jwk not an object", () => jwk("key")); +await attempt("jwk null", () => jwk(null)); +await attempt("jwk kty symbol", () => jwk({ kty: Symbol("oct"), k: "AA" })); +await attempt("jwk hmac length", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, { name: "HMAC", hash: "SHA-1", length: 60 })); +await attempt("jwk hmac length mismatch", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, { name: "HMAC", hash: "SHA-1", length: 128 })); +await attempt("jwk no usages", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, hs256, true, [])); +await attempt("jwk aes", () => jwk({ kty: "oct", k: "AAECAwQFBgcICQoLDA0ODw", alg: "A128GCM" }, "AES-GCM", true, ["encrypt"])); +await attempt("jwk aes alg mismatch", () => jwk({ kty: "oct", k: "AAECAwQFBgcICQoLDA0ODw", alg: "A256GCM" }, "AES-GCM", true, ["encrypt"])); +await attempt("jwk aes use sig", () => jwk({ kty: "oct", k: "AAECAwQFBgcICQoLDA0ODw", use: "sig" }, "AES-GCM", true, ["encrypt"])); +await attempt("jwk aes length", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, "AES-CBC", true, ["encrypt"])); diff --git a/tests/corpus/7126-webcrypto-aes.ts b/tests/corpus/7126-webcrypto-aes.ts new file mode 100644 index 000000000..0e7be7548 --- /dev/null +++ b/tests/corpus/7126-webcrypto-aes.ts @@ -0,0 +1,77 @@ +// crypto.crypto.subtle.encrypt and decrypt with AES-GCM, AES-CBC and AES-CTR: +// fixed keys, IVs and inputs give Node's exact ciphertexts, every key +// length and GCM tag length round-trips, and CTR counters wrap within +// their low `length` bits. +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +function bytes(length: number, seed: number): Uint8Array { + const out = new Uint8Array(length); + for (let i = 0; i < length; i++) out[i] = (i * 31 + seed) & 0xff; + return out; +} + +const plaintext = encoder.encode("The quick brown fox jumps over the lazy dog"); +for (const length of [16, 24, 32]) { + const raw = bytes(length, length); + const gcm = await crypto.subtle.importKey("raw", raw, "AES-GCM", false, ["encrypt", "decrypt"]); + const cbc = await crypto.subtle.importKey("raw", raw, "AES-CBC", false, ["encrypt", "decrypt"]); + const ctr = await crypto.subtle.importKey("raw", raw, "AES-CTR", false, ["encrypt", "decrypt"]); + const iv = bytes(12, 7); + const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, gcm, plaintext); + console.log("gcm", length, sealed.byteLength, hex(sealed)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, gcm, sealed))); + const aad = encoder.encode("header"); + const tagged = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData: aad, tagLength: 96 }, gcm, plaintext); + console.log("gcm aad", length, hex(tagged)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv, additionalData: aad, tagLength: 96 }, gcm, tagged))); + const cbcIv = bytes(16, 3); + const padded = await crypto.subtle.encrypt({ name: "AES-CBC", iv: cbcIv }, cbc, plaintext); + console.log("cbc", length, padded.byteLength, hex(padded)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-CBC", iv: cbcIv }, cbc, padded))); + const counter = bytes(16, 9); + const streamed = await crypto.subtle.encrypt({ name: "AES-CTR", counter, length: 64 }, ctr, plaintext); + console.log("ctr", length, hex(streamed)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-CTR", counter, length: 64 }, ctr, streamed))); +} + +const key = await crypto.subtle.importKey("raw", bytes(16, 1), "AES-GCM", true, ["encrypt", "decrypt"]); +for (const tagLength of [32, 64, 96, 104, 112, 120, 128]) { + const iv = bytes(16, tagLength); + const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv, tagLength }, key, plaintext.subarray(0, 5)); + const opened = await crypto.subtle.decrypt({ name: "AES-GCM", iv, tagLength }, key, sealed); + console.log("tag", tagLength, sealed.byteLength, hex(sealed), decoder.decode(opened)); +} +console.log("gcm empty", hex(await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(0)))); +console.log("gcm long iv", hex(await crypto.subtle.encrypt({ name: "AES-GCM", iv: bytes(128, 5) }, key, plaintext.subarray(0, 3)))); + +const cbcKey = await crypto.subtle.importKey("raw", bytes(16, 2), "AES-CBC", false, ["encrypt", "decrypt"]); +for (const length of [0, 1, 15, 16, 17, 32]) { + const sealed = await crypto.subtle.encrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, cbcKey, bytes(length, 4)); + const opened = await crypto.subtle.decrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, cbcKey, sealed); + console.log("cbc length", length, sealed.byteLength, opened.byteLength, hex(opened) === hex(bytes(length, 4).buffer as ArrayBuffer)); +} + +const ctrKey = await crypto.subtle.importKey("raw", new Uint8Array(16), "AES-CTR", false, ["encrypt"]); +const allOnes = new Uint8Array(16).fill(0xff); +const zeros = new Uint8Array(64); +console.log("ctr wrap 4", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: allOnes, length: 4 }, ctrKey, zeros))); +console.log("ctr wrap 128", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: allOnes, length: 128 }, ctrKey, zeros))); +console.log("ctr wrap 2", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: allOnes, length: 2 }, ctrKey, zeros))); +console.log("ctr 1 bit, 2 blocks", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: new Uint8Array(16), length: 1 }, ctrKey, zeros.subarray(0, 32)))); +console.log("ctr partial", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: bytes(16, 6), length: 128 }, ctrKey, zeros.subarray(0, 21)))); + +const generated = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); +const iv = crypto.getRandomValues(new Uint8Array(12)); +const message = encoder.encode("generated key round trip"); +const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, generated, message); +console.log(sealed.byteLength, decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, generated, sealed))); +const exported = await crypto.subtle.exportKey("jwk", generated); +const reimported = await crypto.subtle.importKey("jwk", exported, "AES-GCM", false, ["decrypt"]); +console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, reimported, sealed))); +console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, generated, new DataView(sealed)))); +console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv.buffer as ArrayBuffer }, generated, Buffer.from(sealed)))); diff --git a/tests/corpus/7127-webcrypto-aes-errors.mjs b/tests/corpus/7127-webcrypto-aes-errors.mjs new file mode 100644 index 000000000..7ce51230a --- /dev/null +++ b/tests/corpus/7127-webcrypto-aes-errors.mjs @@ -0,0 +1,71 @@ +// Rejections of crypto.crypto.subtle.encrypt and decrypt in Node's order: +// argument conversion, the AesCbcParams, AesCtrParams and AeadParams +// dictionaries, the key checks, then the cipher's own OperationErrors +// (bad tags, bad padding, IV and counter limits). + +async function attempt(label, run) { + try { + const value = await run(); + console.log(label, "resolves", value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +const raw = new Uint8Array(16).map((_, i) => i); +const gcm = await crypto.subtle.importKey("raw", raw, "AES-GCM", true, ["encrypt", "decrypt"]); +const cbc = await crypto.subtle.importKey("raw", raw, "AES-CBC", true, ["encrypt", "decrypt"]); +const ctr = await crypto.subtle.importKey("raw", raw, "AES-CTR", true, ["encrypt", "decrypt"]); +const encryptOnly = await crypto.subtle.importKey("raw", raw, "AES-GCM", true, ["encrypt"]); +const hmac = await crypto.subtle.importKey("raw", raw, { name: "HMAC", hash: "SHA-1" }, true, ["sign"]); +const iv12 = new Uint8Array(12); +const iv16 = new Uint8Array(16); +const data = new TextEncoder().encode("hello world"); + +await attempt("too few", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, gcm)); +await attempt("decrypt too few", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 })); +await attempt("not a key", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, JSON.parse("{}"), data)); +await attempt("data string", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, gcm, "hello")); +await attempt("unknown algorithm", () => crypto.subtle.encrypt({ name: "AES-XTS" }, gcm, data)); +await attempt("AES-KW", () => crypto.subtle.encrypt({ name: "AES-KW" }, gcm, data)); +await attempt("no name", () => crypto.subtle.encrypt({ iv: iv12 }, gcm, data)); +await attempt("string algorithm", () => crypto.subtle.encrypt("AES-GCM", gcm, data)); +await attempt("gcm no iv", () => crypto.subtle.encrypt({ name: "AES-GCM" }, gcm, data)); +await attempt("gcm iv string", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: "abc" }, gcm, data)); +await attempt("gcm aad string", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, additionalData: "x" }, gcm, data)); +await attempt("gcm aad null", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, additionalData: null }, gcm, data)); +await attempt("gcm tag 100", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, tagLength: 100 }, gcm, data)); +await attempt("gcm tag 300", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, tagLength: 300 }, gcm, data)); +await attempt("gcm tag NaN", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, tagLength: NaN }, gcm, data)); +await attempt("gcm tag string", () => crypto.subtle.encrypt({ name: "aes-gcm", iv: iv12, tagLength: "96" }, gcm, data)); +await attempt("gcm iv 11 bytes", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(11) }, gcm, data)); +await attempt("gcm iv 129 bytes", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(129) }, gcm, data)); +await attempt("gcm iv empty", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(0) }, gcm, data)); +const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, gcm, data)); +const tampered = sealed.slice(); +tampered[0] ^= 1; +await attempt("gcm tampered", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, gcm, tampered)); +await attempt("gcm wrong aad", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12, additionalData: data }, gcm, sealed)); +await attempt("gcm wrong tag length", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12, tagLength: 96 }, gcm, sealed)); +await attempt("gcm shorter than tag", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, gcm, new Uint8Array(15))); +await attempt("gcm tag only", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, gcm, new Uint8Array(16))); +await attempt("cbc no iv", () => crypto.subtle.encrypt({ name: "AES-CBC" }, cbc, data)); +await attempt("cbc iv 12 bytes", () => crypto.subtle.encrypt({ name: "AES-CBC", iv: iv12 }, cbc, data)); +await attempt("cbc decrypt 15 bytes", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: iv16 }, cbc, new Uint8Array(15))); +await attempt("cbc decrypt empty", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: iv16 }, cbc, new Uint8Array(0))); +const block = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-CBC", iv: iv16 }, cbc, data)); +const badPad = block.slice(); +badPad[15] ^= 0x55; +await attempt("cbc bad padding", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: iv16 }, cbc, badPad)); +await attempt("cbc other iv", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: new Uint8Array(16).fill(1) }, cbc, block)); +await attempt("ctr no counter", () => crypto.subtle.encrypt({ name: "AES-CTR", length: 64 }, ctr, data)); +await attempt("ctr no length", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16 }, ctr, data)); +await attempt("ctr length 0", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 0 }, ctr, data)); +await attempt("ctr length 129", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 129 }, ctr, data)); +await attempt("ctr length 300", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 300 }, ctr, data)); +await attempt("ctr counter 8 bytes", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: new Uint8Array(8), length: 64 }, ctr, data)); +await attempt("ctr 1 bit, 3 blocks", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 1 }, ctr, new Uint8Array(48))); +await attempt("ctr empty", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 1 }, ctr, new Uint8Array(0))); +await attempt("algorithm mismatch", () => crypto.subtle.encrypt({ name: "AES-CBC", iv: iv16 }, gcm, data)); +await attempt("hmac key", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, hmac, data)); +await attempt("missing usage", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, encryptOnly, sealed)); diff --git a/tests/corpus/7128-webcrypto-derive.ts b/tests/corpus/7128-webcrypto-derive.ts new file mode 100644 index 000000000..4badedb66 --- /dev/null +++ b/tests/corpus/7128-webcrypto-derive.ts @@ -0,0 +1,45 @@ +// crypto.crypto.subtle.deriveBits and deriveKey with PBKDF2 and HKDF: Node's +// exact derived bytes for every SHA digest, zero-length derivations, the +// SCRAM-style PBKDF2-then-HMAC chain, and derived AES and HMAC keys that +// encrypt and sign. +const encoder = new TextEncoder(); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +const password = await crypto.subtle.importKey("raw", encoder.encode("pencil"), "PBKDF2", false, ["deriveBits", "deriveKey"]); +const ikm = await crypto.subtle.importKey("raw", encoder.encode("input keying material"), { name: "HKDF" }, false, ["deriveBits", "deriveKey"]); +const salt = encoder.encode("QSXCR+Q6sek8bf92"); +const info = encoder.encode("context"); + +for (const hash of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) { + console.log("pbkdf2", hash, hex(await crypto.subtle.deriveBits({ name: "PBKDF2", hash, salt, iterations: 4096 }, password, 256))); + console.log("hkdf", hash, hex(await crypto.subtle.deriveBits({ name: "HKDF", hash: { name: hash }, salt, info }, ikm, 256))); +} +console.log("pbkdf2 0", (await crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 1 }, password, 0)).byteLength); +console.log("hkdf 0", (await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt, info }, ikm, 0)).byteLength); +console.log("hkdf empty salt and info", hex(await crypto.subtle.deriveBits({ name: "hkdf", hash: "SHA-256", salt: new Uint8Array(0), info: new Uint8Array(0) }, ikm, 128))); +console.log("hkdf max", (await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-1", salt, info }, ikm, 255 * 20 * 8)).byteLength); + +// SCRAM-SHA-256's client proof inputs, the way database drivers compute them. +const saltedPassword = await crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 4096 }, password, 256); +const salted = await crypto.subtle.importKey("raw", saltedPassword, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +const clientKey = await crypto.subtle.sign("HMAC", salted, encoder.encode("Client Key")); +console.log("client key", hex(clientKey)); +console.log("stored key", hex(await crypto.subtle.digest("SHA-256", clientKey))); + +const aes = await crypto.subtle.deriveKey({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 1000 }, password, { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); +console.log(JSON.stringify(aes.algorithm), aes.usages.join(","), aes.extractable); +console.log("aes key", hex(await crypto.subtle.exportKey("raw", aes))); +const iv = new Uint8Array(12); +const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aes, encoder.encode("derived")); +console.log(hex(sealed), new TextDecoder().decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, aes, sealed))); + +const mac = await crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt, info }, ikm, { name: "HMAC", hash: "SHA-384" }, false, ["sign", "verify"]); +console.log(JSON.stringify(mac.algorithm), mac.extractable); +console.log("mac", hex(await crypto.subtle.sign("HMAC", mac, encoder.encode("message")))); +const shortMac = await crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-1", salt, info }, ikm, { name: "HMAC", hash: "SHA-1", length: 128 }, true, ["sign"]); +console.log(JSON.stringify(shortMac.algorithm), hex(await crypto.subtle.exportKey("raw", shortMac))); +const wrap = await crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-512", salt, info }, ikm, { name: "AES-KW", length: 128 }, true, ["wrapKey"]); +console.log(JSON.stringify(wrap.algorithm), JSON.stringify(await crypto.subtle.exportKey("jwk", wrap))); diff --git a/tests/corpus/7129-webcrypto-derive-errors.mjs b/tests/corpus/7129-webcrypto-derive-errors.mjs new file mode 100644 index 000000000..83149b61b --- /dev/null +++ b/tests/corpus/7129-webcrypto-derive-errors.mjs @@ -0,0 +1,72 @@ +// Rejections of crypto.crypto.subtle.deriveBits and deriveKey in Node's order: +// argument conversion, the Pbkdf2Params and HkdfParams dictionaries, the +// base key's usages and algorithm, the length rules, and for deriveKey +// the derived key type's normalization and import. +const encoder = new TextEncoder(); + +async function attempt(label, run) { + try { + const value = await run(); + const shown = value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value instanceof CryptoKey ? JSON.stringify(value.algorithm) : value; + console.log(label, "resolves", shown); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +const password = await crypto.subtle.importKey("raw", encoder.encode("password"), "PBKDF2", false, ["deriveBits", "deriveKey"]); +const ikm = await crypto.subtle.importKey("raw", encoder.encode("input key"), "HKDF", false, ["deriveBits", "deriveKey"]); +const bitsOnly = await crypto.subtle.importKey("raw", encoder.encode("x"), "PBKDF2", false, ["deriveBits"]); +const keyOnly = await crypto.subtle.importKey("raw", encoder.encode("x"), "PBKDF2", false, ["deriveKey"]); +const hmac = await crypto.subtle.importKey("raw", encoder.encode("x"), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]); +const salt = encoder.encode("salt"); +const pbkdf2 = (extra = {}) => ({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 1000, ...extra }); +const hkdf = (extra = {}) => ({ name: "HKDF", hash: "SHA-256", salt, info: encoder.encode("info"), ...extra }); + +await attempt("bits too few", () => crypto.subtle.deriveBits(pbkdf2())); +await attempt("bits not a key", () => crypto.subtle.deriveBits(pbkdf2(), JSON.parse("{}"), 64)); +await attempt("bits length null", () => crypto.subtle.deriveBits(pbkdf2(), password, null)); +await attempt("bits length undefined", () => crypto.subtle.deriveBits(pbkdf2(), password, undefined)); +await attempt("bits length absent", () => crypto.subtle.deriveBits(pbkdf2(), password)); +await attempt("bits length 12", () => crypto.subtle.deriveBits(pbkdf2(), password, 12)); +await attempt("bits length -1", () => crypto.subtle.deriveBits(pbkdf2(), password, -1)); +await attempt("bits length NaN", () => crypto.subtle.deriveBits(pbkdf2(), password, NaN)); +await attempt("bits length string", () => crypto.subtle.deriveBits(pbkdf2(), password, "64")); +await attempt("bits unknown", () => crypto.subtle.deriveBits({ name: "SCRYPT" }, password, 64)); +await attempt("bits no name", () => crypto.subtle.deriveBits({ hash: "SHA-1" }, password, 64)); +await attempt("pbkdf2 iterations 0", () => crypto.subtle.deriveBits(pbkdf2({ iterations: 0 }), password, 64)); +await attempt("pbkdf2 iterations 2^31", () => crypto.subtle.deriveBits(pbkdf2({ iterations: 2 ** 31 }), password, 64)); +await attempt("pbkdf2 iterations -1", () => crypto.subtle.deriveBits(pbkdf2({ iterations: -1 }), password, 64)); +await attempt("pbkdf2 no iterations", () => crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt }, password, 64)); +await attempt("pbkdf2 no salt", () => crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", iterations: 1 }, password, 64)); +await attempt("pbkdf2 no hash", () => crypto.subtle.deriveBits({ name: "PBKDF2", salt, iterations: 1 }, password, 64)); +await attempt("pbkdf2 string", () => crypto.subtle.deriveBits("PBKDF2", password, 64)); +await attempt("pbkdf2 md5", () => crypto.subtle.deriveBits(pbkdf2({ hash: "MD5" }), password, 64)); +await attempt("pbkdf2 bogus sha", () => crypto.subtle.deriveBits(pbkdf2({ hash: "SHA-999" }), password, 64)); +await attempt("pbkdf2 salt string", () => crypto.subtle.deriveBits(pbkdf2({ salt: "salt" }), password, 64)); +await attempt("pbkdf2 hkdf key", () => crypto.subtle.deriveBits(pbkdf2(), ikm, 64)); +await attempt("pbkdf2 hmac key", () => crypto.subtle.deriveBits(pbkdf2(), hmac, 64)); +await attempt("pbkdf2 no deriveBits usage", () => crypto.subtle.deriveBits(pbkdf2(), keyOnly, 64)); +await attempt("hkdf no info", () => crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt }, ikm, 64)); +await attempt("hkdf no salt", () => crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", info: salt }, ikm, 64)); +await attempt("hkdf info 1025 bytes", () => crypto.subtle.deriveBits(hkdf({ info: new Uint8Array(1025) }), ikm, 64)); +await attempt("hkdf over maximum", () => crypto.subtle.deriveBits(hkdf({ hash: "SHA-1" }), ikm, 255 * 20 * 8 + 8)); +await attempt("hkdf length 4", () => crypto.subtle.deriveBits(hkdf(), ikm, 4)); +await attempt("hkdf null", () => crypto.subtle.deriveBits(hkdf(), ikm, null)); + +const aes = { name: "AES-GCM", length: 128 }; +await attempt("key too few", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, true)); +await attempt("key not a key", () => crypto.subtle.deriveKey(pbkdf2(), "key", aes, true, ["encrypt"])); +await attempt("key no deriveKey usage", () => crypto.subtle.deriveKey(pbkdf2(), bitsOnly, aes, true, ["encrypt"])); +await attempt("key mismatch", () => crypto.subtle.deriveKey(hkdf(), password, aes, true, ["encrypt"])); +await attempt("key aes no length", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "AES-GCM" }, true, ["encrypt"])); +await attempt("key aes length 100", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "AES-CBC", length: 100 }, true, ["encrypt"])); +await attempt("key aes sign usage", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, true, ["sign"])); +await attempt("key aes no usages", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, true, [])); +await attempt("key hmac no hash", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "HMAC" }, true, ["sign"])); +await attempt("key hmac length 0", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "HMAC", hash: "SHA-1", length: 0 }, true, ["sign"])); +await attempt("key hmac length 100", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "HMAC", hash: "SHA-1", length: 100 }, true, ["sign"])); +await attempt("key hkdf type", () => crypto.subtle.deriveKey(pbkdf2(), password, "HKDF", false, ["deriveBits"])); +await attempt("key unknown type", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "TWOFISH" }, true, ["encrypt"])); +await attempt("key pbkdf2 type", () => crypto.subtle.deriveKey(hkdf(), ikm, { name: "PBKDF2" }, false, ["deriveKey"])); +await attempt("key ok", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, false, ["encrypt"])); diff --git a/tests/diagnostics/webcrypto-refusals.ts b/tests/diagnostics/webcrypto-refusals.ts index d162872fb..309e70c4e 100644 --- a/tests/diagnostics/webcrypto-refusals.ts +++ b/tests/diagnostics/webcrypto-refusals.ts @@ -1,17 +1,8 @@ -// The native WebCrypto surface is HMAC importKey('raw'), sign and verify, -// and the SHA-1/SHA-2 digests, called directly on crypto.subtle. The 'jwk' -// key format and crypto.subtle or its methods taken as values are refused -// by name; the bundled declarations omit the other SubtleCrypto methods -// (node-types/fenced.ts pins their fence under @types/node). -const fromJwk = await crypto.subtle.importKey( - "jwk", - { kty: "oct", k: "c2VjcmV0" }, - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"], -); -console.log(fromJwk.type); - +// The native WebCrypto surface is crypto.subtle's secret-key slice, called +// directly on crypto.subtle. crypto.subtle or its methods taken as values +// are refused by name; the bundled declarations omit the other +// SubtleCrypto methods (node-types/fenced.ts pins their fence under +// @types/node). const subtle = crypto.subtle; console.log(typeof subtle); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 16a74081f..dfd07d446 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -103,13 +103,13 @@ cipherStream.on("data", () => {}); import { createECDH } from "node:crypto"; createECDH("sect163k1"); createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); -/* WebCrypto beyond the HMAC slice: @types/node declares the whole - * SubtleCrypto surface, and the methods outside importKey, sign, verify - * and digest fence by name. */ -async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); +/* WebCrypto beyond the secret-key slice: @types/node declares the whole + * SubtleCrypto surface, and the methods outside the slice, such as + * wrapKey, fence by name. */ +async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); } -void encryptWithWebCrypto; +void wrapWithWebCrypto; /* Node's navigator global: typeof navigator and navigator.userAgent * compile; the other members fence by name. */ const cores = navigator.hardwareConcurrency; diff --git a/tests/fixtures/node-types/webcrypto-keys.ts b/tests/fixtures/node-types/webcrypto-keys.ts new file mode 100644 index 000000000..be16271d8 --- /dev/null +++ b/tests/fixtures/node-types/webcrypto-keys.ts @@ -0,0 +1,32 @@ +/* WebCrypto secret keys under @types/node: generateKey, exportKey and + * importKey for HMAC and AES keys, and PBKDF2 key imports, lower to the + * same native values as under the bundled declarations. @types/node + * names the dictionary, key and JSON Web Key types in node:crypto's + * webcrypto namespace. */ +import type { webcrypto } from "node:crypto"; + +async function main(): Promise { + const hmacParams: webcrypto.HmacKeyGenParams = { name: "HMAC", hash: "SHA-256", length: 256 }; + const hmac: webcrypto.CryptoKey = await crypto.subtle.generateKey(hmacParams, true, ["sign", "verify"]); + const hmacAlgorithm = hmac.algorithm as webcrypto.HmacKeyAlgorithm; + console.log(hmac.type, hmacAlgorithm.name, hmacAlgorithm.length, hmacAlgorithm.hash.name, hmac.usages.join(",")); + const raw: ArrayBuffer = await crypto.subtle.exportKey("raw", hmac); + console.log(raw.byteLength); + + const aesParams: webcrypto.AesKeyGenParams = { name: "AES-GCM", length: 192 }; + const aes = await crypto.subtle.generateKey(aesParams, true, ["encrypt", "decrypt"]); + const aesAlgorithm = aes.algorithm as webcrypto.AesKeyAlgorithm; + console.log(aesAlgorithm.name, aesAlgorithm.length, aes.extractable); + const jwk: webcrypto.JsonWebKey = await crypto.subtle.exportKey("jwk", aes); + console.log(jwk.kty, jwk.alg, jwk.ext, jwk.key_ops?.join(","), jwk.k?.length); + const back = await crypto.subtle.importKey("jwk", jwk, "AES-GCM", false, ["decrypt"]); + console.log(back.algorithm.name, back.extractable, back.usages.join(",")); + + const fixed = new Uint8Array(16).fill(9); + const imported = await crypto.subtle.importKey("raw", fixed, { name: "AES-CBC" }, true, ["encrypt"]); + console.log(Buffer.from(await crypto.subtle.exportKey("raw", imported)).toString("hex")); + const password = await crypto.subtle.importKey("raw", new TextEncoder().encode("pw"), "PBKDF2", false, ["deriveBits"]); + console.log(password.algorithm.name, password.extractable, password.usages.join(",")); +} + +void main(); diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 0bd13963d..2f1f8cb2e 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -228,18 +228,18 @@ fenced.ts:105:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer 104 | createECDH("sect163k1"); 105 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 106 | /* WebCrypto beyond the HMAC slice: @types/node declares the whole + 106 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole hint: Node reads the second argument as the key's input encoding and answers a Buffer; pass the output encoding third: computeSecret(key, undefined, encoding) -fenced.ts:110:9 - error SC2020: 'crypto.subtle.encrypt' is part of the standard library types but has no scriptc lowering yet +fenced.ts:110:9 - error SC2020: 'crypto.subtle.wrapKey' is part of the standard library types but has no scriptc lowering yet - 109 | async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - 110 | await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); + 109 | async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + 110 | await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); | ^~~~~~~~~~~~~~~~~~~~~ 111 | } - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle fenced.ts:115:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet diff --git a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt index 521edff3c..e46099f69 100644 --- a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt +++ b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt @@ -1,26 +1,17 @@ -webcrypto-refusals.ts:7:3 - error SC2020: 'crypto.subtle.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet +webcrypto-refusals.ts:6:16 - error SC2020: 'crypto.subtle as a value' is part of the standard library types but has no scriptc lowering yet - 6 | const fromJwk = await crypto.subtle.importKey( - 7 | "jwk", - | ^~~~~ - 8 | { kty: "oct", k: "c2VjcmV0" }, + 5 | // @types/node). + 6 | const subtle = crypto.subtle; + | ^~~~~~~~~~~~~ + 7 | console.log(typeof subtle); - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle -webcrypto-refusals.ts:15:16 - error SC2020: 'crypto.subtle as a value' is part of the standard library types but has no scriptc lowering yet +webcrypto-refusals.ts:9:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet - 14 | - 15 | const subtle = crypto.subtle; - | ^~~~~~~~~~~~~ - 16 | console.log(typeof subtle); - - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle - -webcrypto-refusals.ts:18:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet - - 17 | - 18 | console.log(typeof crypto.subtle.sign); + 8 | + 9 | console.log(typeof crypto.subtle.sign); | ^~~~~~~~~~~~~~~~~~ - 19 | + 10 | - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file diff --git a/tests/harness/corpus-costs/7120-webcrypto-random-uuid.ts.json b/tests/harness/corpus-costs/7120-webcrypto-random-uuid.ts.json new file mode 100644 index 000000000..ce52548ca --- /dev/null +++ b/tests/harness/corpus-costs/7120-webcrypto-random-uuid.ts.json @@ -0,0 +1 @@ +{ "plain": 4431, "san": 4365 } diff --git a/tests/harness/corpus-costs/7121-webcrypto-crypto-object.mjs.json b/tests/harness/corpus-costs/7121-webcrypto-crypto-object.mjs.json new file mode 100644 index 000000000..db431c2ca --- /dev/null +++ b/tests/harness/corpus-costs/7121-webcrypto-crypto-object.mjs.json @@ -0,0 +1 @@ +{ "plain": 5043, "san": 4705 } diff --git a/tests/harness/corpus-costs/7122-webcrypto-node-crypto.ts.json b/tests/harness/corpus-costs/7122-webcrypto-node-crypto.ts.json new file mode 100644 index 000000000..46469f2a9 --- /dev/null +++ b/tests/harness/corpus-costs/7122-webcrypto-node-crypto.ts.json @@ -0,0 +1 @@ +{ "plain": 4805, "san": 6923 } diff --git a/tests/harness/corpus-costs/7123-webcrypto-node-crypto.cjs.json b/tests/harness/corpus-costs/7123-webcrypto-node-crypto.cjs.json new file mode 100644 index 000000000..682ba704a --- /dev/null +++ b/tests/harness/corpus-costs/7123-webcrypto-node-crypto.cjs.json @@ -0,0 +1 @@ +{ "plain": 4728, "san": 6725 } diff --git a/tests/harness/corpus-costs/7124-webcrypto-secret-keys.ts.json b/tests/harness/corpus-costs/7124-webcrypto-secret-keys.ts.json new file mode 100644 index 000000000..73f1ce847 --- /dev/null +++ b/tests/harness/corpus-costs/7124-webcrypto-secret-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 5860, "san": 9241 } diff --git a/tests/harness/corpus-costs/7125-webcrypto-key-errors.mjs.json b/tests/harness/corpus-costs/7125-webcrypto-key-errors.mjs.json new file mode 100644 index 000000000..ceff344eb --- /dev/null +++ b/tests/harness/corpus-costs/7125-webcrypto-key-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 6582, "san": 9054 } diff --git a/tests/harness/corpus-costs/7126-webcrypto-aes.ts.json b/tests/harness/corpus-costs/7126-webcrypto-aes.ts.json new file mode 100644 index 000000000..b1680985c --- /dev/null +++ b/tests/harness/corpus-costs/7126-webcrypto-aes.ts.json @@ -0,0 +1 @@ +{ "plain": 4563, "san": 6500 } diff --git a/tests/harness/corpus-costs/7127-webcrypto-aes-errors.mjs.json b/tests/harness/corpus-costs/7127-webcrypto-aes-errors.mjs.json new file mode 100644 index 000000000..1ebb743b0 --- /dev/null +++ b/tests/harness/corpus-costs/7127-webcrypto-aes-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 4385, "san": 8939 } diff --git a/tests/harness/corpus-costs/7128-webcrypto-derive.ts.json b/tests/harness/corpus-costs/7128-webcrypto-derive.ts.json new file mode 100644 index 000000000..2888d22eb --- /dev/null +++ b/tests/harness/corpus-costs/7128-webcrypto-derive.ts.json @@ -0,0 +1 @@ +{ "plain": 5042, "san": 9143 } diff --git a/tests/harness/corpus-costs/7129-webcrypto-derive-errors.mjs.json b/tests/harness/corpus-costs/7129-webcrypto-derive-errors.mjs.json new file mode 100644 index 000000000..b2f70bf57 --- /dev/null +++ b/tests/harness/corpus-costs/7129-webcrypto-derive-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 5549, "san": 7441 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 8af6998ca..00ee40043 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1634,14 +1634,14 @@ console.log(typeof promisePrototype, typeof promisePrototype.then, promiseProtot }); }); -// WebCrypto's native slice is HMAC importKey('raw'), sign and verify, and -// the SHA-1/SHA-2 digests. Algorithms Node recognizes outside that slice -// reject with a coded SC2020 Error instead of a Node-shaped success, and a -// JavaScript call with the 'jwk' format meets its named fence when it runs. +// WebCrypto's native slice is crypto.subtle's secret-key methods. +// Algorithms Node recognizes outside that slice reject with a coded SC2020 +// Error instead of a Node-shaped success, and a JavaScript call of a +// method outside it meets its named fence when it runs. // Node's own rejections (unknown names, bad usages) are corpus-pinned // (6292, 6294); these refusals have no Node twin, so they live here. describe("WebCrypto refusals (JS lane)", () => { - test("recognized algorithms outside the HMAC slice reject with SC2020", async () => { + test("recognized algorithms outside the secret-key slice reject with SC2020", async () => { const r = await compileAndRun( "webcrypto-unsupported-algorithms", `const bytes = new TextEncoder().encode("secret"); @@ -1662,10 +1662,10 @@ await attempt("sha3", () => crypto.subtle.digest("SHA3-256", bytes)); await attempt("sha3 hmac", () => crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA3-256" }, false, ["sign"]), ); -const format = JSON.parse('"jwk"'); -await attempt("computed jwk", () => - crypto.subtle.importKey(format, { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), -); +await attempt("ed25519 key", () => crypto.subtle.generateKey({ name: "Ed25519" }, false, ["sign"])); +const aes = await crypto.subtle.generateKey({ name: "AES-GCM", length: 128 }, false, ["encrypt"]); +await attempt("aes-ocb", () => crypto.subtle.encrypt({ name: "AES-OCB", iv: new Uint8Array(12) }, aes, bytes)); +await attempt("ecdh bits", () => crypto.subtle.deriveBits({ name: "ECDH" }, key, 256)); `, "mjs", ); @@ -1676,22 +1676,25 @@ await attempt("computed jwk", () => "ecdsa Error SC2020 crypto.subtle.sign with the ECDSA algorithm is not supported yet\n" + "sha3 Error SC2020 crypto.subtle.digest with the SHA3-256 algorithm is not supported yet\n" + "sha3 hmac Error SC2020 crypto.subtle.importKey with the SHA3-256 algorithm is not supported yet\n" + - "computed jwk Error SC2020 crypto.subtle.importKey with the 'jwk' key format is not supported yet\n", + "ed25519 key Error SC2020 crypto.subtle.generateKey with the Ed25519 algorithm is not supported yet\n" + + "aes-ocb Error SC2020 crypto.subtle.encrypt with the AES-OCB algorithm is not supported yet\n" + + "ecdh bits Error SC2020 crypto.subtle.deriveBits with the ECDH algorithm is not supported yet\n", ); }); - test("a literal 'jwk' import is a named fence where it runs", async () => { + test("a method outside the slice is a named fence where it runs", async () => { const r = await compileAndRun( - "webcrypto-jwk-literal", + "webcrypto-wrap-key", `console.log("before"); -await crypto.subtle.importKey("jwk", { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +const key = await crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, true, ["wrapKey"]); +await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); `, "mjs", ); expect(r.exitCode).toBe(1); expect(r.stdout).toBe("before\n"); expect(r.stderr).toMatch( - /^Uncaught Error: 'crypto\.subtle\.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet.*\n$/, + /^Uncaught Error: 'crypto\.subtle\.wrapKey' is part of the standard library types but has no scriptc lowering yet.*\n$/, ); }); }); diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index dabb9b0bf..8532ad6e9 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -1132,6 +1132,24 @@ test("node-types: WebCrypto HMAC matches Node under @types/node", async () => { expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: WebCrypto secret keys match Node under @types/node", async () => { + const outDir = outDirFor("node-webcrypto-keys"); + const entry = join(nodeTypesDir, "webcrypto-keys.ts"); + const result = await compile(entry, { + outPath: join(outDir, "webcrypto-keys"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: the navigator global's runtime probe matches Node under @types/node", async () => { const outDir = outDirFor("node-navigator"); const entry = join(nodeTypesDir, "navigator.ts");