Merge main

This commit is contained in:
Chris Tate
2026-10-22 07:44:22 -05:00
51 changed files with 3215 additions and 112 deletions
@@ -36,9 +36,10 @@
Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report.
`publicEncrypt`/`privateDecrypt` 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()`.
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 and `wrapKey()` and `unwrapKey()` of secret keys with AES-KW or those ciphers 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.
`generateKey()`, `importKey()`, `exportKey()`, `sign()`, and `verify()` also support ECDSA (P-256, P-384, and P-521), Ed25519, RSASSA-PKCS1-v1_5, and RSA-PSS, with the `spki`, `pkcs8`, and `jwk` formats and `raw` for ECDSA and Ed25519 public keys; ECDSA, Ed25519, and RSA are refused in wasm32-wasi builds.
ECDSA, Ed25519, and RSA link when a string literal in the program names the algorithm; a name built only at run time throws a coded SC2020 error.
`wrapKey()` and `unwrapKey()` of ECDSA, Ed25519, and RSA keys throw a coded SC2020 error.
Other SubtleCrypto methods, algorithms, and key formats are refused or throw a coded SC2020 error.
Native derivations finish before their deferred callbacks, so computation blocks the caller.
@@ -80277,9 +80277,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"
}
},
{
@@ -80323,9 +80323,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"
}
},
{
@@ -106971,6 +106971,9 @@
"tests/corpus/7127-webcrypto-aes-errors.mjs",
"tests/corpus/7128-webcrypto-derive.ts",
"tests/corpus/7129-webcrypto-derive-errors.mjs",
"tests/corpus/7130-webcrypto-wrap-key.ts",
"tests/corpus/7131-webcrypto-wrap-key-errors.mjs",
"tests/corpus/7132-jose-pbes2/main.mjs",
"tests/corpus/node24-crypto-kdf-errors.ts",
"tests/corpus/node24-crypto-kdf-options.ts",
"tests/corpus/node24-crypto-kdf.ts",
@@ -110843,6 +110846,9 @@
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.decrypt",
"tests": [
"tests/corpus/7130-webcrypto-wrap-key.ts",
"tests/corpus/7131-webcrypto-wrap-key-errors.mjs",
"tests/corpus/7132-jose-pbes2/main.mjs",
"tests/corpus/7128-webcrypto-derive.ts",
"tests/corpus/7129-webcrypto-derive-errors.mjs",
"tests/corpus/7126-webcrypto-aes.ts",
@@ -110884,6 +110890,9 @@
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.deriveBits",
"tests": [
"tests/corpus/7130-webcrypto-wrap-key.ts",
"tests/corpus/7131-webcrypto-wrap-key-errors.mjs",
"tests/corpus/7132-jose-pbes2/main.mjs",
"tests/corpus/7128-webcrypto-derive.ts",
"tests/corpus/7129-webcrypto-derive-errors.mjs",
"tests/corpus/7126-webcrypto-aes.ts",
@@ -110925,6 +110934,9 @@
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.deriveKey",
"tests": [
"tests/corpus/7130-webcrypto-wrap-key.ts",
"tests/corpus/7131-webcrypto-wrap-key-errors.mjs",
"tests/corpus/7132-jose-pbes2/main.mjs",
"tests/corpus/7128-webcrypto-derive.ts",
"tests/corpus/7129-webcrypto-derive-errors.mjs",
"tests/corpus/7126-webcrypto-aes.ts",
@@ -110966,6 +110978,9 @@
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.digest",
"tests": [
"tests/corpus/7130-webcrypto-wrap-key.ts",
"tests/corpus/7131-webcrypto-wrap-key-errors.mjs",
"tests/corpus/7132-jose-pbes2/main.mjs",
"tests/corpus/7128-webcrypto-derive.ts",
"tests/corpus/7129-webcrypto-derive-errors.mjs",
"tests/corpus/7126-webcrypto-aes.ts",
@@ -111053,6 +111068,9 @@
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.encrypt",
"tests": [
"tests/corpus/7130-webcrypto-wrap-key.ts",
"tests/corpus/7131-webcrypto-wrap-key-errors.mjs",
"tests/corpus/7132-jose-pbes2/main.mjs",
"tests/corpus/7128-webcrypto-derive.ts",
"tests/corpus/7129-webcrypto-derive-errors.mjs",
"tests/corpus/7126-webcrypto-aes.ts",
@@ -111094,6 +111112,9 @@
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.exportKey",
"tests": [
"tests/corpus/7130-webcrypto-wrap-key.ts",
"tests/corpus/7131-webcrypto-wrap-key-errors.mjs",
"tests/corpus/7132-jose-pbes2/main.mjs",
"tests/corpus/7128-webcrypto-derive.ts",
"tests/corpus/7129-webcrypto-derive-errors.mjs",
"tests/corpus/7126-webcrypto-aes.ts",
@@ -111158,6 +111179,9 @@
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.generateKey",
"tests": [
"tests/corpus/7130-webcrypto-wrap-key.ts",
"tests/corpus/7131-webcrypto-wrap-key-errors.mjs",
"tests/corpus/7132-jose-pbes2/main.mjs",
"tests/corpus/7128-webcrypto-derive.ts",
"tests/corpus/7129-webcrypto-derive-errors.mjs",
"tests/corpus/7126-webcrypto-aes.ts",
@@ -111199,6 +111223,9 @@
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.importKey",
"tests": [
"tests/corpus/7130-webcrypto-wrap-key.ts",
"tests/corpus/7131-webcrypto-wrap-key-errors.mjs",
"tests/corpus/7132-jose-pbes2/main.mjs",
"tests/corpus/7128-webcrypto-derive.ts",
"tests/corpus/7129-webcrypto-derive-errors.mjs",
"tests/corpus/7126-webcrypto-aes.ts",
@@ -111240,6 +111267,9 @@
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.sign",
"tests": [
"tests/corpus/7130-webcrypto-wrap-key.ts",
"tests/corpus/7131-webcrypto-wrap-key-errors.mjs",
"tests/corpus/7132-jose-pbes2/main.mjs",
"tests/corpus/7128-webcrypto-derive.ts",
"tests/corpus/7129-webcrypto-derive-errors.mjs",
"tests/corpus/7126-webcrypto-aes.ts",
@@ -111278,8 +111308,29 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.unwrapKey",
"tests": [
"tests/corpus/7130-webcrypto-wrap-key.ts",
"tests/corpus/7131-webcrypto-wrap-key-errors.mjs",
"tests/corpus/7132-jose-pbes2/main.mjs",
"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",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
]
},
"anchorSource": "exact"
},
@@ -111304,6 +111355,9 @@
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.verify",
"tests": [
"tests/corpus/7130-webcrypto-wrap-key.ts",
"tests/corpus/7131-webcrypto-wrap-key-errors.mjs",
"tests/corpus/7132-jose-pbes2/main.mjs",
"tests/corpus/7128-webcrypto-derive.ts",
"tests/corpus/7129-webcrypto-derive-errors.mjs",
"tests/corpus/7126-webcrypto-aes.ts",
@@ -111342,8 +111396,29 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.wrapKey",
"tests": [
"tests/corpus/7130-webcrypto-wrap-key.ts",
"tests/corpus/7131-webcrypto-wrap-key-errors.mjs",
"tests/corpus/7132-jose-pbes2/main.mjs",
"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",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
]
},
"anchorSource": "exact"
},
+2 -2
View File
@@ -190,14 +190,14 @@
"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/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.subtle": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "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.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", "tests/corpus/7081-webcrypto-ed25519.mjs", "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"] },
{ "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.wrapKey", "subtle.unwrapKey", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "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", "tests/corpus/7081-webcrypto-ed25519.mjs", "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"] },
{ "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/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"] },
+18 -2
View File
@@ -546,8 +546,9 @@ declare var DOMException: {
/* WebCrypto (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, generateKey, importKey, exportKey, sign
* keys), AES-GCM/CBC/CTR encrypt and decrypt, wrapKey and unwrapKey of
* secret keys (AES-KW or those ciphers), PBKDF2 and HKDF deriveBits and
* deriveKey, HMAC sign/verify, generateKey, importKey, exportKey, sign
* and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and the
* SHA-1/SHA-2 digests, typed as exactly that surface (the declarations
* mirror @types/node's webcrypto shapes), plus the global's randomUUID and
@@ -753,12 +754,27 @@ interface SubtleCrypto {
key: CryptoKey,
data: BufferSource,
): Promise<ArrayBuffer>;
unwrapKey(
format: KeyFormat,
wrappedKey: BufferSource,
unwrappingKey: CryptoKey,
unwrapAlgorithm: AlgorithmIdentifier | AesGcmParams | AesCbcParams | AesCtrParams,
unwrappedKeyAlgorithm: AlgorithmIdentifier | HmacImportParams,
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
verify(
algorithm: AlgorithmIdentifier | EcdsaParams | RsaPssParams,
key: CryptoKey,
signature: BufferSource,
data: BufferSource,
): Promise<boolean>;
wrapKey(
format: KeyFormat,
key: CryptoKey,
wrappingKey: CryptoKey,
wrapAlgorithm: AlgorithmIdentifier | AesGcmParams | AesCbcParams | AesCtrParams,
): Promise<ArrayBuffer>;
}
interface Crypto {
readonly subtle: SubtleCrypto;
@@ -622,6 +622,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"webcrypto.exportKey": "scr_webcrypto_export_key",
"webcrypto.encrypt": "scr_webcrypto_encrypt",
"webcrypto.decrypt": "scr_webcrypto_decrypt",
"webcrypto.wrapKey": "scr_webcrypto_wrap_key",
"webcrypto.unwrapKey": "scr_webcrypto_unwrap_key",
"webcrypto.deriveBits": "scr_webcrypto_derive_bits",
"webcrypto.deriveKey": "scr_webcrypto_derive_key",
"webcrypto.sign": "scr_webcrypto_sign",
@@ -1609,6 +1611,8 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"webcrypto.generateKey",
"webcrypto.encrypt",
"webcrypto.decrypt",
"webcrypto.wrapKey",
"webcrypto.unwrapKey",
"webcrypto.deriveBits",
"webcrypto.deriveKey",
"webcrypto.sign",
@@ -212,10 +212,15 @@ export function moduleWasiUnavailableSurface(
};
return false;
}
if (node.fn === "webcrypto.encrypt" || node.fn === "webcrypto.decrypt") {
if (
node.fn === "webcrypto.encrypt" ||
node.fn === "webcrypto.decrypt" ||
node.fn === "webcrypto.wrapKey" ||
node.fn === "webcrypto.unwrapKey"
) {
found = {
surface:
"WebCrypto encrypt and decrypt (the wasm32-wasi runtime does not include the mbedTLS library)",
"WebCrypto encrypt, decrypt, wrapKey and unwrapKey (the wasm32-wasi runtime does not include the mbedTLS library)",
loc,
};
return false;
@@ -8,16 +8,15 @@ import {
function harness() {
const members = new Map<number, SemanticTypeData[]>();
const snapshot = new SemanticSnapshot(1, {
text(method, payload) {
const query = JSON.parse(payload) as { objectId: number };
if (method !== "getTypesOfType" || !members.has(query.objectId))
throw new Error(`unexpected request: ${method}`);
return JSON.stringify(members.get(query.objectId));
},
binary(method) {
const reply = (method: string, payload: string) => {
const query = JSON.parse(payload) as { objectId: number };
if (method !== "getTypesOfType" || !members.has(query.objectId))
throw new Error(`unexpected request: ${method}`);
},
return JSON.stringify(members.get(query.objectId));
};
const snapshot = new SemanticSnapshot(1, {
text: reply,
binary: (method, payload) => new TextEncoder().encode(reply(method, payload)),
});
const project = snapshot.addProject("literal-values", () => undefined);
let id = 0;
@@ -10,8 +10,8 @@ import { isWebCryptoReceiver } from "./crypto.js";
/* WebCrypto (scr_webcrypto.c, with encrypt and decrypt in
* scr_webcrypto_aes.c and the ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS
* algorithms in scr_webcrypto_pki.c): `crypto.subtle`'s `importKey`,
* `generateKey`, `exportKey`, `encrypt`, `decrypt`, `deriveBits`,
* `deriveKey`, `sign`, `verify` and `digest`
* `generateKey`, `exportKey`, `encrypt`, `decrypt`, `wrapKey`,
* `unwrapKey`, `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
@@ -26,6 +26,8 @@ const SUBTLE_LOWERED: ReadonlyMap<string, IrLibFn> = new Map([
["exportKey", "webcrypto.exportKey"],
["encrypt", "webcrypto.encrypt"],
["decrypt", "webcrypto.decrypt"],
["wrapKey", "webcrypto.wrapKey"],
["unwrapKey", "webcrypto.unwrapKey"],
["deriveBits", "webcrypto.deriveBits"],
["deriveKey", "webcrypto.deriveKey"],
["sign", "webcrypto.sign"],
@@ -34,7 +36,7 @@ const SUBTLE_LOWERED: ReadonlyMap<string, IrLibFn> = new Map([
]);
const 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, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, 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, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle";
/** Whether `node` is a SubtleCrypto object: `subtle` on a WebCrypto
* Crypto object (the global, globalThis.crypto or node:crypto's
@@ -2232,13 +2232,15 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
"webcrypto.exportKey",
"webcrypto.encrypt",
"webcrypto.decrypt",
"webcrypto.wrapKey",
"webcrypto.unwrapKey",
"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, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, 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, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused",
},
{
id: "stdlib.webcrypto.CryptoKey",
@@ -2883,6 +2883,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"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.wrapKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"webcrypto.unwrapKey": { 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 } },
+15 -3
View File
@@ -709,9 +709,17 @@ 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. WebCrypto's encrypt and decrypt (scr_webcrypto_aes.c)
* link the same archive. */
cipher: rule({ fns: ["crypto.cipherNew", "webcrypto.encrypt", "webcrypto.decrypt"] }),
* inspect unit. WebCrypto's encrypt, decrypt, wrapKey and unwrapKey
* (scr_webcrypto_aes.c) link the same archive. */
cipher: rule({
fns: [
"crypto.cipherNew",
"webcrypto.encrypt",
"webcrypto.decrypt",
"webcrypto.wrapKey",
"webcrypto.unwrapKey",
],
}),
copying: rule({
expr: (node, _tables) =>
(node.kind === "arrIntrinsic" && COPYING_ARR_METHODS.has(node.method)) ||
@@ -747,6 +755,8 @@ const RUNTIME_FEATURE_RULES = {
"crypto.cipherNew",
"webcrypto.encrypt",
"webcrypto.decrypt",
"webcrypto.wrapKey",
"webcrypto.unwrapKey",
"crypto.ecdhNew",
],
prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."],
@@ -858,6 +868,8 @@ const RUNTIME_FEATURE_RULES = {
"crypto.cipherNew",
"webcrypto.encrypt",
"webcrypto.decrypt",
"webcrypto.wrapKey",
"webcrypto.unwrapKey",
"crypto.ecdhNew",
"crypto.ecdhConvertKey",
"crypto.dhNew",
+1 -1
View File
@@ -5696,7 +5696,7 @@
"kind": "stdlib",
"name": "globalThis.crypto.subtle",
"status": "static",
"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, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, 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, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused"
},
{
"id": "syntax.async-generators",
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7130-webcrypto-wrap-key.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7131-webcrypto-wrap-key-errors.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7132-jose-pbes2/main.mjs"
],
"diags": []
}
+7
View File
@@ -252,6 +252,13 @@ typedef ScrBytes *(*ScrWebCryptoAesFn)(const char *name, bool decrypt, const uns
ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn aes);
ScrPromise *scr_webcrypto_encrypt(ScrDyn *args);
ScrPromise *scr_webcrypto_decrypt(ScrDyn *args);
/* wrapKey and unwrapKey run the same way (AES-KW wrapping, or the AES
* ciphers): wrapKey fulfills with an ArrayBuffer and unwrapKey with a
* CryptoKey on a later loop turn. */
ScrPromise *scr_webcrypto_wrap(ScrDyn *args, ScrWebCryptoAesFn aes);
ScrPromise *scr_webcrypto_unwrap(ScrDyn *args, ScrWebCryptoAesFn aes);
ScrPromise *scr_webcrypto_wrap_key(ScrDyn *args);
ScrPromise *scr_webcrypto_unwrap_key(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
+296 -54
View File
@@ -106,6 +106,14 @@ static const char *scr_wc_native_hash(const char *name) {
const char *const scr_wc_ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"};
/* An argument's ordinal: unwrapKey's seven arguments run past the shared
* table. */
static const char *scr_wc_ordinal(size_t index) {
static const char *const later[] = {"6th", "7th"};
size_t shared = SCR_WC_COUNT(scr_wc_ordinals);
return index < shared ? scr_wc_ordinals[index] : later[index - shared];
}
void scr_wc_type_error(const char *code, const char *fmt, ...) {
char message[512];
va_list ap;
@@ -169,7 +177,7 @@ ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *value)
scr_wc_type_error("ERR_INVALID_ARG_TYPE",
"%s: %s argument is not instance of ArrayBuffer, Buffer, TypedArray, or "
"DataView.",
prefix, scr_wc_ordinals[index]);
prefix, scr_wc_ordinal(index));
}
scr_dyn_release(plain);
return view;
@@ -220,7 +228,7 @@ long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) {
if (!plain) return -1;
if (plain->kind != SCR_DYN_ARR) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s argument cannot be converted to sequence.",
prefix, scr_wc_ordinals[index]);
prefix, scr_wc_ordinal(index));
scr_dyn_release(plain);
return -1;
}
@@ -238,7 +246,7 @@ long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) {
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),
prefix, scr_wc_ordinal(index), i, (int)(s->len > 200 ? 200 : s->len),
s->data);
scr_str_release(s);
scr_dyn_release(plain);
@@ -885,6 +893,18 @@ static const char *scr_wc_asymmetric_name(const ScrDyn *args, size_t index) {
return found;
}
/* Whether `value` is an ECDSA, Ed25519 or RSA CryptoKey, which
* scr_webcrypto_pki.c holds; wrapKey and unwrapKey wrap secret keys only. */
static bool scr_wc_is_asymmetric_key(const ScrDyn *value) {
return scr_wc_asymmetric && scr_wc_asymmetric->is_key(value);
}
static bool scr_wc_is_asymmetric_algorithm(const char *name) {
for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_asymmetric_names); i++)
if (strcmp(name, scr_wc_asymmetric_names[i]) == 0) return true;
return false;
}
/* The asymmetric algorithms run in scr_webcrypto_pki.c, which the compiler
* links when a string literal in the program names one of them. */
static ScrPromise *scr_wc_asymmetric_missing(const char *method, const char *name) {
@@ -1182,6 +1202,47 @@ done:
return scr_wc_deferred(key);
}
/* exportKeySync's checks for a secret key: false with Node's error
* pending. raw, raw-secret and jwk are the formats a secret key exports. */
static bool scr_wc_exportable(const char *format, const ScrCryptoKey *key) {
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);
return false;
}
if (!key->extractable) {
scr_throw_domex("InvalidAccessError", "key is not extractable");
return false;
}
if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0 &&
strcmp(format, "jwk") != 0) {
char message[96];
snprintf(message, sizeof message, "Unable to export %s secret key using %s format", key->name,
format);
scr_throw_domex("NotSupportedError", message);
return false;
}
return true;
}
/* The key bytes as a fresh u8 view (+1). */
static ScrBytes *scr_wc_key_bytes(const ScrCryptoKey *key) {
ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len);
if (key->len) memcpy(bytes->data, key->data, key->len);
return bytes;
}
/* A JWK's k: the key bytes in unpadded base64url (+1). */
static ScrStr *scr_wc_jwk_k(const ScrCryptoKey *key) {
ScrBytes *bytes = scr_wc_key_bytes(key);
ScrStr *encoding = scr_str_new("base64url", 9);
ScrStr *k = scr_bytes_to_str(bytes, encoding);
scr_str_release(encoding);
scr_bytes_release(bytes);
return k;
}
/* exportKeySync for secret keys: raw and raw-secret answer an ArrayBuffer
* of the key bytes, jwk an object { key_ops, ext, alg, kty, k }. An ECDSA,
* Ed25519 or RSA key sets *asymmetric after the format check instead
@@ -1203,30 +1264,11 @@ static ScrDyn *scr_wc_export(const ScrDyn *args, bool *asymmetric) {
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) {
if (!scr_wc_exportable(format, key)) goto done;
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);
ScrStr *k = scr_wc_jwk_k(key);
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));
@@ -1235,10 +1277,9 @@ static ScrDyn *scr_wc_export(const ScrDyn *args, bool *asymmetric) {
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);
ScrBytes *bytes = scr_wc_key_bytes(key);
result = scr_array_buffer_from_bytes(bytes);
scr_bytes_release(bytes);
}
done:
scr_wc_args_drop(&a, 2);
@@ -1598,6 +1639,43 @@ static ScrPromise *scr_wc_deferred_buffer(ScrBytes *bytes) {
return p;
}
/* The cipher over `data` (cipherOrWrap's job): +1 output bytes, or NULL
* with Node's OperationError pending. */
static ScrBytes *scr_wc_cipher_bytes(ScrWebCryptoAesFn aes, const char *name, bool decrypt,
const ScrCryptoKey *key, ScrWcCipherParams *params,
ScrBytes *data) {
if (!params->iv) params->iv = scr_bytes_new(SCR_BYTES_U8, 0);
if (!params->aad) params->aad = scr_bytes_new(SCR_BYTES_U8, 0);
ScrBytes *view = data;
SCR_BYTES_SNAPSHOT(view);
ScrBytes *iv = params->iv;
SCR_BYTES_SNAPSHOT(iv);
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)
scr_throw_domex("OperationError", "The operation failed for an operation-specific reason");
return out;
}
/* The cipher's promise: an ArrayBuffer of the output, or the job's
* OperationError, on a later loop turn. */
static ScrPromise *scr_wc_run_cipher(ScrWebCryptoAesFn aes, const char *name, bool decrypt,
const ScrCryptoKey *key, ScrWcCipherParams *params,
ScrBytes *data) {
ScrBytes *out = scr_wc_cipher_bytes(aes, name, decrypt, key, params, data);
return out ? scr_wc_deferred_buffer(out) : scr_wc_deferred_rejection();
}
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'"
@@ -1632,31 +1710,7 @@ ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn a
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();
}
}
result = scr_wc_run_cipher(aes, name, decrypt, key, &params, data);
done:
if (data) scr_bytes_release(data);
scr_wc_cipher_params_drop(&params);
@@ -1664,6 +1718,194 @@ done:
return result ? result : scr_wc_rejected();
}
/* ── wrapKey and unwrapKey ────────────────────────────────────────────
* wrapKey is exportKey followed by encrypt (or AES-KW's wrap), and
* unwrapKey is decrypt (or AES-KW's unwrap) followed by importKey, as in
* Node's webcrypto.js. The cipher runs in scr_webcrypto_aes.c. */
/* normalizeAlgorithm(algorithm, 'wrapKey' / 'unwrapKey'), falling back to
* the 'encrypt' / 'decrypt' registry as Node does: AES-KW (no
* parameters), or the AES ciphers with their dictionaries. */
static const char *scr_wc_normalize_wrap(ScrDyn *identifier, const char *method,
ScrWcCipherParams *params) {
memset(params, 0, sizeof *params);
ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm");
if (!name) return NULL;
bool kw = scr_wc_ascii_ieq(name, "AES-KW");
scr_str_release(name);
if (kw) return "AES-KW";
return scr_wc_normalize_cipher(identifier, method, params);
}
/* The bytes wrapKey encrypts (+1): the raw key, or the JWK's JSON text,
* padded with spaces to a multiple of 8 bytes for AES-KW. */
static ScrBytes *scr_wc_wrap_payload(const char *format, const ScrCryptoKey *key, bool kw) {
if (strcmp(format, "jwk") != 0) return scr_wc_key_bytes(key);
char buf[16];
const char *alg = scr_wc_jwk_alg(key->name, key->hash, key->length, buf, sizeof buf);
ScrStr *k = scr_wc_jwk_k(key);
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "{\"key_ops\":[");
bool first = true;
for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) {
if (!(key->usages & (1u << i))) continue;
if (!first) scr_jb_puts(&b, ",");
first = false;
scr_jb_puts(&b, "\"");
scr_jb_puts(&b, scr_wc_usages[i]);
scr_jb_puts(&b, "\"");
}
scr_jb_puts(&b, key->extractable ? "],\"ext\":true" : "],\"ext\":false");
if (alg) {
scr_jb_puts(&b, ",\"alg\":\"");
scr_jb_puts(&b, alg);
scr_jb_puts(&b, "\"");
}
scr_jb_puts(&b, ",\"kty\":\"oct\",\"k\":\"");
ScrStr *json_prefix = scr_jb_finish(&b);
size_t len = json_prefix->len + k->len + 2;
size_t padded = kw && len % 8 ? len + 8 - len % 8 : len;
ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)padded);
memcpy(out->data, json_prefix->data, json_prefix->len);
memcpy(out->data + json_prefix->len, k->data, k->len);
memcpy(out->data + json_prefix->len + k->len, "\"}", 2);
memset(out->data + len, ' ', padded - len);
scr_str_release(json_prefix);
scr_str_release(k);
return out;
}
/* wrapKey(format, key, wrappingKey, wrapAlgorithm). */
ScrPromise *scr_webcrypto_wrap(ScrDyn *args, ScrWebCryptoAesFn aes) {
static const char prefix[] = "Failed to execute 'wrapKey' on 'SubtleCrypto'";
if (!scr_wc_required(prefix, args, 4)) return scr_wc_rejected();
ScrWcArgs a;
scr_wc_args_take(&a, args, 4);
ScrWcCipherParams params;
memset(&params, 0, sizeof params);
ScrBytes *payload = NULL;
ScrPromise *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_is_asymmetric_key(a.v[1])) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix);
goto done;
}
ScrCryptoKey *wrapping = scr_crypto_key_of(a.v[2]);
if (!wrapping && !scr_wc_is_asymmetric_key(a.v[2])) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 3rd argument is not of type CryptoKey.", prefix);
goto done;
}
if (!scr_wc_algorithm_identifier(a.v[3])) goto done;
const char *name = scr_wc_normalize_wrap(a.v[3], "wrapKey", &params);
if (!name) goto done;
/* An ECDSA, Ed25519 or RSA key never has an AES algorithm. */
if (!wrapping || strcmp(wrapping->name, name) != 0) {
scr_throw_domex("InvalidAccessError", "Key algorithm mismatch");
goto done;
}
if (!(wrapping->usages & SCR_WC_USAGE_WRAP_KEY)) {
scr_throw_domex("InvalidAccessError", "Unable to use this key to wrapKey");
goto done;
}
if (!key) {
scr_wc_unsupported("wrapKey", "an ECDSA, Ed25519 or RSA key");
goto done;
}
if (!scr_wc_exportable(format, key)) goto done;
payload = scr_wc_wrap_payload(format, key, strcmp(name, "AES-KW") == 0);
result = scr_wc_run_cipher(aes, name, false, wrapping, &params, payload);
done:
if (payload) scr_bytes_release(payload);
scr_wc_cipher_params_drop(&params);
scr_wc_args_drop(&a, 4);
return result ? result : scr_wc_rejected();
}
/* parseJwk: the unwrapped bytes as UTF-8 JSON converted to a JsonWebKey
* with a kty member, or DataError "Invalid wrapped JWK key". */
static bool scr_wc_parse_wrapped_jwk(ScrBytes *bytes, ScrWcJwk *jwk) {
static const char prefix[] = "Failed to execute 'unwrapKey' on 'SubtleCrypto'";
ScrStr *text = scr_text_decode_options(bytes, true, false);
ScrDyn *parsed = text ? scr_json_parse(text) : NULL;
if (text) scr_str_release(text);
bool ok = parsed && scr_wc_is_object(parsed) && scr_wc_jwk_convert(prefix, parsed, jwk) && jwk->kty;
if (parsed) scr_dyn_release(parsed);
if (!ok) {
if (scr_exc_pending()) scr_caught_release(scr_exc_take());
scr_wc_jwk_drop(jwk);
scr_throw_domex("DataError", "Invalid wrapped JWK key");
}
return ok;
}
/* unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm,
* unwrappedKeyAlgorithm, extractable, keyUsages): argument and key errors
* reject at once; the cipher's failure and the import's errors reject on
* a later loop turn, after Node's job. */
ScrPromise *scr_webcrypto_unwrap(ScrDyn *args, ScrWebCryptoAesFn aes) {
static const char prefix[] = "Failed to execute 'unwrapKey' on 'SubtleCrypto'";
if (!scr_wc_required(prefix, args, 7)) return scr_wc_rejected();
ScrDyn *v[7];
for (size_t i = 0; i < 7; i++) v[i] = scr_dyn_arr_at(args, (double)i);
ScrWcCipherParams params;
memset(&params, 0, sizeof params);
ScrBytes *wrapped = NULL;
ScrPromise *result = NULL;
const char *format = scr_wc_key_format(prefix, v[0]);
if (!format) goto done;
wrapped = scr_wc_buffer_source(prefix, 1, v[1]);
if (!wrapped) goto done;
ScrCryptoKey *unwrapping = scr_crypto_key_of(v[2]);
if (!unwrapping && !scr_wc_is_asymmetric_key(v[2])) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 3rd argument is not of type CryptoKey.", prefix);
goto done;
}
if (!scr_wc_algorithm_identifier(v[3]) || !scr_wc_algorithm_identifier(v[4])) goto done;
bool extractable = scr_dyn_truthy(v[5]);
long usages = scr_wc_key_usages(prefix, 6, v[6]);
if (usages < 0) goto done;
const char *name = scr_wc_normalize_wrap(v[3], "unwrapKey", &params);
if (!name) goto done;
ScrWcAlgorithm imported;
if (!scr_wc_normalize(v[4], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names),
SCR_WC_OP_IMPORT, &imported))
goto done;
if (!unwrapping || strcmp(unwrapping->name, name) != 0) {
scr_throw_domex("InvalidAccessError", "Key algorithm mismatch");
goto done;
}
if (!(unwrapping->usages & SCR_WC_USAGE_UNWRAP_KEY)) {
scr_throw_domex("InvalidAccessError", "Unable to use this key to unwrapKey");
goto done;
}
if (scr_wc_is_asymmetric_algorithm(imported.name)) {
scr_wc_unsupported_algorithm("unwrapKey", imported.name);
goto done;
}
if (!scr_wc_max_length(wrapped, "data")) goto done;
ScrBytes *bytes = scr_wc_cipher_bytes(aes, name, true, unwrapping, &params, wrapped);
if (!bytes) {
result = scr_wc_deferred_rejection();
goto done;
}
ScrWcJwk jwk;
memset(&jwk, 0, sizeof jwk);
ScrDyn *key = NULL;
if (strcmp(format, "jwk") != 0 || scr_wc_parse_wrapped_jwk(bytes, &jwk))
key = scr_wc_import_key_sync(format, bytes, &jwk, &imported, extractable, usages);
scr_wc_jwk_drop(&jwk);
scr_bytes_release(bytes);
result = key ? scr_wc_deferred(key) : scr_wc_deferred_rejection();
done:
if (wrapped) scr_bytes_release(wrapped);
scr_wc_cipher_params_drop(&params);
for (size_t i = 0; i < 7; i++) scr_dyn_release(v[i]);
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
+67
View File
@@ -122,11 +122,74 @@ static ScrBytes *scr_wc_aes_ctr(const unsigned char *key, size_t key_len,
return out;
}
/* RFC 3394 AES key wrap with the default IV: wrapping takes a multiple of
* 8 bytes, at least 16; unwrapping a multiple of 8, at least 24, and fails
* when the integrity check value does not match (compared in constant
* time). */
static ScrBytes *scr_wc_aes_kw(bool unwrap, const unsigned char *key, size_t key_len,
const unsigned char *in, size_t len) {
static const unsigned char iv[8] = {0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6};
if (len % 8 != 0 || len < (unwrap ? 24u : 16u)) return NULL;
size_t n = unwrap ? len / 8 - 1 : len / 8;
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
int rc = unwrap ? mbedtls_aes_setkey_dec(&aes, key, (unsigned)(key_len * 8))
: mbedtls_aes_setkey_enc(&aes, key, (unsigned)(key_len * 8));
if (rc != 0) {
mbedtls_aes_free(&aes);
return NULL;
}
ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)(unwrap ? len - 8 : len + 8));
unsigned char a[8], block[16];
unsigned char *r = unwrap ? out->data : out->data + 8;
if (unwrap) {
memcpy(a, in, 8);
memcpy(r, in + 8, len - 8);
for (int j = 5; j >= 0; j--) {
for (size_t i = n; i >= 1; i--) {
uint64_t t = (uint64_t)n * (uint64_t)j + i;
for (int k = 0; k < 8; k++) block[k] = (unsigned char)(a[k] ^ (t >> (56 - 8 * k)));
memcpy(block + 8, r + 8 * (i - 1), 8);
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_DECRYPT, block, block);
memcpy(a, block, 8);
memcpy(r + 8 * (i - 1), block + 8, 8);
}
}
} else {
memcpy(a, iv, 8);
memcpy(r, in, len);
for (int j = 0; j <= 5; j++) {
for (size_t i = 1; i <= n; i++) {
memcpy(block, a, 8);
memcpy(block + 8, r + 8 * (i - 1), 8);
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, block, block);
uint64_t t = (uint64_t)n * (uint64_t)j + i;
for (int k = 0; k < 8; k++) a[k] = (unsigned char)(block[k] ^ (t >> (56 - 8 * k)));
memcpy(r + 8 * (i - 1), block + 8, 8);
}
}
memcpy(out->data, a, 8);
}
mbedtls_platform_zeroize(block, sizeof block);
mbedtls_aes_free(&aes);
if (unwrap) {
unsigned char diff = 0;
for (int k = 0; k < 8; k++) diff |= (unsigned char)(a[k] ^ iv[k]);
if (diff) {
mbedtls_platform_zeroize(out->data, len - 8);
scr_bytes_release(out);
return NULL;
}
}
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-KW") == 0) return scr_wc_aes_kw(decrypt, key, key_len, in, 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);
@@ -135,3 +198,7 @@ static ScrBytes *scr_wc_aes(const char *name, bool decrypt, const unsigned char
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); }
ScrPromise *scr_webcrypto_wrap_key(ScrDyn *args) { return scr_webcrypto_wrap(args, scr_wc_aes); }
ScrPromise *scr_webcrypto_unwrap_key(ScrDyn *args) { return scr_webcrypto_unwrap(args, scr_wc_aes); }
+69
View File
@@ -0,0 +1,69 @@
// crypto.subtle.wrapKey and unwrapKey: AES-KW wrapping matches RFC 3394's
// test vectors, raw and JSON Web Key payloads round-trip (AES-KW pads the
// JWK's JSON with spaces to a multiple of 8 bytes), and AES-GCM, AES-CBC
// and AES-CTR wrap keys the way encrypt does. jose's A128KW and
// PBES2-HS256+A128KW content-key wrapping is the shape used here.
const encoder = new TextEncoder();
function hex(buffer: ArrayBuffer): string {
return Buffer.from(buffer).toString("hex");
}
type Key = Awaited<ReturnType<typeof crypto.subtle.importKey>>;
function describe(key: Key): string {
return JSON.stringify({ algorithm: key.algorithm, usages: key.usages, extractable: key.extractable });
}
// RFC 3394 section 4: 128-, 192- and 256-bit key data under each KEK size.
const vectors: [string, string, string][] = [
["000102030405060708090A0B0C0D0E0F", "00112233445566778899AABBCCDDEEFF", "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5"],
["000102030405060708090A0B0C0D0E0F1011121314151617", "00112233445566778899AABBCCDDEEFF", "96778b25ae6ca435f92b5b97c050aed2468ab8a17ad84e5d"],
["000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", "00112233445566778899AABBCCDDEEFF", "64e8c3f9ce0f5ba263e9777905818a2a93c8191e7d6e8ae7"],
["000102030405060708090A0B0C0D0E0F1011121314151617", "00112233445566778899AABBCCDDEEFF0001020304050607", "031d33264e15d33268f24ec260743edce1c6c7ddee725a936ba814915c6762d2"],
["000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", "00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F", "28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21"],
];
for (const [kekHex, dataHex, expected] of vectors) {
const kek = await crypto.subtle.importKey("raw", Buffer.from(kekHex, "hex"), "AES-KW", false, ["wrapKey", "unwrapKey"]);
const data = await crypto.subtle.importKey("raw", Buffer.from(dataHex, "hex"), "AES-CBC", true, ["encrypt"]);
const wrapped = await crypto.subtle.wrapKey("raw", data, kek, "AES-KW");
console.log(hex(wrapped) === expected, hex(wrapped));
const unwrapped = await crypto.subtle.unwrapKey("raw", wrapped, kek, { name: "aes-kw" }, "AES-CBC", true, ["decrypt"]);
console.log(describe(unwrapped), hex(await crypto.subtle.exportKey("raw", unwrapped)));
}
// jose's content-key wrap: an HMAC key wrapped raw, then unwrapped and exported.
const kek = await crypto.subtle.importKey("raw", new Uint8Array(16).fill(7), "AES-KW", true, ["wrapKey", "unwrapKey"]);
const cek = await crypto.subtle.importKey("raw", new Uint8Array(32).fill(9), { name: "HMAC", hash: "SHA-256" }, true, ["sign"]);
const wrappedCek = await crypto.subtle.wrapKey("raw", cek, kek, "AES-KW");
console.log(hex(wrappedCek));
const back = await crypto.subtle.unwrapKey("raw", wrappedCek, kek, "AES-KW", { hash: "SHA-256", name: "HMAC" }, true, ["sign"]);
console.log(describe(back), hex(await crypto.subtle.exportKey("raw", back)));
// JSON Web Key payloads, padded for AES-KW.
const wrappedJwk = await crypto.subtle.wrapKey("jwk", cek, kek, "AES-KW");
console.log(wrappedJwk.byteLength, hex(wrappedJwk));
const fromJwk = await crypto.subtle.unwrapKey("jwk", wrappedJwk, kek, "AES-KW", { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
console.log(describe(fromJwk));
const asRaw = await crypto.subtle.unwrapKey("raw", wrappedJwk, kek, "AES-KW", { name: "HMAC", hash: "SHA-256" }, true, ["sign"]);
const padded = new TextDecoder().decode(await crypto.subtle.exportKey("raw", asRaw));
console.log(JSON.stringify(padded), padded.length);
// The AES ciphers as wrapping algorithms.
const gcmKey = await crypto.subtle.importKey("raw", new Uint8Array(16).fill(3), "AES-GCM", false, ["wrapKey", "unwrapKey"]);
const iv = new Uint8Array(12).fill(1);
const gcmWrapped = await crypto.subtle.wrapKey("jwk", cek, gcmKey, { name: "AES-GCM", iv });
console.log(gcmWrapped.byteLength, hex(gcmWrapped).slice(0, 64));
console.log(describe(await crypto.subtle.unwrapKey("jwk", gcmWrapped, gcmKey, { name: "AES-GCM", iv }, { name: "HMAC", hash: "SHA-256" }, true, ["sign"])));
const cbcKey = await crypto.subtle.importKey("raw", new Uint8Array(32).fill(4), "AES-CBC", false, ["wrapKey", "unwrapKey"]);
const cbcWrapped = await crypto.subtle.wrapKey("raw", cek, cbcKey, { name: "AES-CBC", iv: new Uint8Array(16) });
console.log(hex(cbcWrapped));
console.log(describe(await crypto.subtle.unwrapKey("raw", cbcWrapped, cbcKey, { name: "AES-CBC", iv: new Uint8Array(16) }, "AES-GCM", false, ["encrypt"])));
const ctrKey = await crypto.subtle.importKey("raw", new Uint8Array(24).fill(5), "AES-CTR", false, ["wrapKey", "unwrapKey"]);
const ctr = { name: "AES-CTR", counter: new Uint8Array(16), length: 64 };
const ctrWrapped = await crypto.subtle.wrapKey("raw", cek, ctrKey, ctr);
console.log(hex(ctrWrapped));
const fromCtr = await crypto.subtle.unwrapKey("raw", ctrWrapped, ctrKey, ctr, { name: "HMAC", hash: "SHA-1" }, true, ["verify"]);
console.log(describe(fromCtr));
const signature = await crypto.subtle.sign("HMAC", back, encoder.encode("message"));
console.log(hex(signature).slice(0, 32));
@@ -0,0 +1,65 @@
// Rejections of crypto.crypto.subtle.wrapKey and unwrapKey in Node's order:
// argument conversion, wrap algorithm normalization (AES-KW, then the
// encrypt registry), the wrapping key's algorithm and usages, the export
// or import of the wrapped key, and the cipher's OperationErrors (AES-KW's
// length rules and integrity check), which reject on a later turn.
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.usages]) : value;
console.log(label, "resolves", shown);
} catch (e) {
console.log(label, e.name, e.code, e.message);
}
}
const kek = await crypto.subtle.importKey("raw", new Uint8Array(16).fill(1), "AES-KW", true, ["wrapKey", "unwrapKey"]);
const unwrapOnly = await crypto.subtle.importKey("raw", new Uint8Array(16).fill(1), "AES-KW", true, ["unwrapKey"]);
const gcm = await crypto.subtle.importKey("raw", new Uint8Array(16), "AES-GCM", true, ["wrapKey", "unwrapKey"]);
const hmac20 = await crypto.subtle.importKey("raw", new Uint8Array(20).fill(2), { name: "HMAC", hash: "SHA-256" }, true, ["sign"]);
const hmac32 = await crypto.subtle.importKey("raw", new Uint8Array(32).fill(2), { name: "HMAC", hash: "SHA-256" }, true, ["sign"]);
const locked = await crypto.subtle.importKey("raw", new Uint8Array(16), "AES-CBC", false, ["encrypt"]);
const password = await crypto.subtle.importKey("raw", new Uint8Array(4), "PBKDF2", false, ["deriveBits"]);
const hs256 = { name: "HMAC", hash: "SHA-256" };
await attempt("wrap too few", () => crypto.subtle.wrapKey("raw", hmac32, kek));
await attempt("wrap bad format", () => crypto.subtle.wrapKey("pem", hmac32, kek, "AES-KW"));
await attempt("wrap key not a key", () => crypto.subtle.wrapKey("raw", JSON.parse("{}"), kek, "AES-KW"));
await attempt("wrap wrapping key not a key", () => crypto.subtle.wrapKey("raw", hmac32, JSON.parse("{}"), "AES-KW"));
await attempt("wrap unknown algorithm", () => crypto.subtle.wrapKey("raw", hmac32, kek, "AES-XTS"));
await attempt("wrap no name", () => crypto.subtle.wrapKey("raw", hmac32, kek, {}));
await attempt("wrap gcm without iv", () => crypto.subtle.wrapKey("raw", hmac32, gcm, "AES-GCM"));
await attempt("wrap mismatch", () => crypto.subtle.wrapKey("raw", hmac32, gcm, "AES-KW"));
await attempt("wrap ctr on gcm key", () => crypto.subtle.wrapKey("raw", hmac32, gcm, { name: "AES-CTR", counter: new Uint8Array(16), length: 64 }));
await attempt("wrap missing usage", () => crypto.subtle.wrapKey("raw", hmac32, unwrapOnly, "AES-KW"));
await attempt("wrap pbkdf2 key", () => crypto.subtle.wrapKey("raw", password, kek, "AES-KW"));
await attempt("wrap non-extractable", () => crypto.subtle.wrapKey("raw", locked, kek, "AES-KW"));
await attempt("wrap spki", () => crypto.subtle.wrapKey("spki", hmac32, kek, "AES-KW"));
await attempt("wrap 20 bytes with AES-KW", () => crypto.subtle.wrapKey("raw", hmac20, kek, "AES-KW"));
await attempt("wrap 20 bytes with AES-GCM", () => crypto.subtle.wrapKey("raw", hmac20, gcm, { name: "AES-GCM", iv: new Uint8Array(12) }));
const wrapped = new Uint8Array(await crypto.subtle.wrapKey("raw", hmac32, kek, "AES-KW"));
const tampered = wrapped.slice();
tampered[3] ^= 1;
const notJson = await crypto.subtle.wrapKey("raw", hmac32, kek, "AES-KW");
await attempt("unwrap too few", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", hs256, true));
await attempt("unwrap data string", () => crypto.subtle.unwrapKey("raw", "x", kek, "AES-KW", hs256, true, ["sign"]));
await attempt("unwrap not a key", () => crypto.subtle.unwrapKey("raw", wrapped, JSON.parse("{}"), "AES-KW", hs256, true, ["sign"]));
await attempt("unwrap bad usage name", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", hs256, true, ["fly"]));
await attempt("unwrap hmac without hash", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", { name: "HMAC" }, true, ["sign"]));
await attempt("unwrap unknown key algorithm", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", "TWOFISH", true, ["sign"]));
await attempt("unwrap mismatch", () => crypto.subtle.unwrapKey("raw", wrapped, gcm, "AES-KW", hs256, true, ["sign"]));
await attempt("unwrap missing usage", async () => crypto.subtle.unwrapKey("raw", wrapped, await crypto.subtle.importKey("raw", new Uint8Array(16).fill(1), "AES-KW", true, ["wrapKey"]), "AES-KW", hs256, true, ["sign"]));
await attempt("unwrap tampered", () => crypto.subtle.unwrapKey("raw", tampered, kek, "AES-KW", hs256, true, ["sign"]));
await attempt("unwrap 16 bytes", () => crypto.subtle.unwrapKey("raw", new Uint8Array(16), kek, "AES-KW", hs256, true, ["sign"]));
await attempt("unwrap 28 bytes", () => crypto.subtle.unwrapKey("raw", new Uint8Array(28), kek, "AES-KW", hs256, true, ["sign"]));
await attempt("unwrap bad key usage", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", hs256, true, ["encrypt"]));
await attempt("unwrap empty usages", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", hs256, true, []));
await attempt("unwrap as pbkdf2", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", "PBKDF2", false, ["deriveBits"]));
await attempt("unwrap as extractable pbkdf2", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", "PBKDF2", true, ["deriveBits"]));
await attempt("unwrap raw bytes as jwk", () => crypto.subtle.unwrapKey("jwk", notJson, kek, "AES-KW", hs256, true, ["sign"]));
const jwkWrapped = await crypto.subtle.wrapKey("jwk", hmac32, kek, "AES-KW");
await attempt("unwrap jwk with other usages", () => crypto.subtle.unwrapKey("jwk", jwkWrapped, kek, "AES-KW", hs256, true, ["sign", "verify"]));
await attempt("unwrap jwk as AES", () => crypto.subtle.unwrapKey("jwk", jwkWrapped, kek, "AES-KW", "AES-GCM", true, ["encrypt"]));
await attempt("unwrap jwk ok", () => crypto.subtle.unwrapKey("jwk", jwkWrapped, kek, "AES-KW", hs256, false, ["sign"]));
+50
View File
@@ -0,0 +1,50 @@
// The real jose@6.2.12 PBES2 and AES key-wrap paths: EncryptJWT and
// jwtDecrypt from jose's public jose/jwt/encrypt, jose/jwt/decrypt and
// jose/errors entry points, over crypto.subtle importKey, deriveBits,
// wrapKey, unwrapKey, exportKey, encrypt and decrypt. node_modules/jose
// vendors the dist/webapi files those entry points load and their
// declarations, unmodified (MIT, LICENSE.md), with a package.json that
// exports only those entry points. Content keys and IVs are random, so
// tokens are checked by shape and by what they decrypt to.
import { EncryptJWT } from "jose/jwt/encrypt";
import { jwtDecrypt } from "jose/jwt/decrypt";
import * as errors from "jose/errors";
const encoder = new TextEncoder();
const password = encoder.encode("correct horse battery staple");
const kek = new Uint8Array(32);
for (let i = 0; i < kek.length; i++) kek[i] = i * 13 + 5;
async function roundTrip(label, alg, enc, key, header = {}) {
const token = await new EncryptJWT({ sub: "alice", scope: "read", n: 1 })
.setProtectedHeader({ alg, enc, ...header })
.setIssuedAt(1_799_999_000)
.encrypt(key);
const parts = token.split(".");
const { payload, protectedHeader } = await jwtDecrypt(token, key, {
keyManagementAlgorithms: [alg],
currentDate: new Date(1_800_000_000_000),
});
console.log(label, parts.length, parts[1].length > 0, payload.sub, payload.scope, payload.iat);
console.log(label, protectedHeader.alg, protectedHeader.enc, protectedHeader.p2c ?? "-");
return token;
}
const pbes = await roundTrip("pbes2 256", "PBES2-HS256+A128KW", "A256GCM", password);
await roundTrip("pbes2 384", "PBES2-HS384+A192KW", "A128CBC-HS256", password);
await roundTrip("pbes2 512", "PBES2-HS512+A256KW", "A256CBC-HS512", password);
await roundTrip("a128kw", "A128KW", "A128GCM", kek.subarray(0, 16));
await roundTrip("a256kw", "A256KW", "A256CBC-HS512", kek);
try {
await jwtDecrypt(pbes, encoder.encode("wrong password"), { keyManagementAlgorithms: ["PBES2-HS256+A128KW"] });
console.log("wrong password accepted");
} catch (e) {
console.log("wrong password", e instanceof errors.JWEDecryptionFailed, e.code);
}
try {
await jwtDecrypt(pbes, password);
console.log("unlisted algorithm accepted");
} catch (e) {
console.log("unlisted algorithm", e instanceof errors.JOSEAlgNotAllowed, e.code);
}
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Filip Skokan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,34 @@
import type * as t from '../types.d.ts';
/** JWE decryption and JWT Claims Set validation options. */
export interface JWTDecryptOptions extends t.DecryptOptions, t.JWTClaimVerificationOptions {
}
/**
* Resolves a JWT decryption key. No token components have been authenticated when this function is
* called.
*/
export interface JWTDecryptGetKey<KeyType extends t.CryptoKey | Uint8Array = t.CryptoKey | Uint8Array> extends t.GetKeyFunction<t.CompactJWEHeaderParameters, t.FlattenedJWE, KeyType | t.KeyObject | t.JWK> {
}
/**
* Decrypts a Compact JWE-formatted JWT and validates its Claims Set.
*
* @param key Private key or shared secret to decrypt and verify the JWT with. See
* {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}.
*/
export declare function jwtDecrypt<PayloadType = t.JWTPayload>(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTDecryptOptions): Promise<t.JWTDecryptResult<PayloadType>>;
/**
* Decrypts a JWT and validates its claims, returning the dynamically
* {@link t.ResolvedKey.key resolved key}.
*
* @param getKey Function resolving a private key or shared secret to decrypt and verify the JWT
* with. See {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}.
*/
export declare function jwtDecrypt<PayloadType = t.JWTPayload, KeyType extends t.CryptoKey | Uint8Array = t.CryptoKey | Uint8Array>(jwt: string | Uint8Array, getKey: JWTDecryptGetKey<KeyType>, options?: JWTDecryptOptions): Promise<t.JWTDecryptResult<PayloadType> & t.ResolvedKey<KeyType>>;
/**
* Accepts a key or key resolver. Use this overload when forwarding either form; the result includes
* `key` only when a resolver was used.
*
* @param key Private key or shared secret, or a function resolving one, to decrypt and verify the
* JWT with. See
* {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}.
*/
export declare function jwtDecrypt<PayloadType = t.JWTPayload>(jwt: string | Uint8Array, key: t.KeyInput | JWTDecryptGetKey, options?: JWTDecryptOptions): Promise<t.JWTDecryptResult<PayloadType> & Partial<t.ResolvedKey>>;
@@ -0,0 +1,57 @@
import type * as t from '../types.d.ts';
/** @param payload Initial JWT Claims Set. Defaults to an empty object. */
declare const EncryptJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT;
/** Builds and encrypts Compact JWE-formatted JSON Web Tokens. */
export declare class EncryptJWT extends EncryptJWT_base {
#private;
/**
* Sets the JWE Protected Header. May only be called once.
*
* @param protectedHeader JWE Protected Header. Must contain an "alg" (JWE Algorithm) and "enc"
* (JWE Encryption Algorithm) properties.
*/
setProtectedHeader(protectedHeader: t.CompactJWEHeaderParameters): this;
/**
* Sets JWE Key Management parameters, such as ECDH-ES "apu" and "apv" or PBES2 "p2c", and adds
* them to the appropriate JOSE Header. Use this instead of the header setters for algorithm
* inputs. May only be called once.
*/
setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this;
/**
* Sets the content encryption key. By default, a suitable random key is generated for the JWE
* "enc" (Encryption Algorithm). May only be called once.
*
* @deprecated For testing and vector validation only; allow random generation in production.
*/
setContentEncryptionKey(cek: Uint8Array): this;
/**
* Sets the Initialization Vector for content encryption. By default, a suitable random IV is
* generated for the JWE "enc" (Encryption Algorithm). May only be called once.
*
* @deprecated For testing and vector validation only; allow random generation in production.
*/
setInitializationVector(iv: Uint8Array): this;
/**
* Replicates the "iss" (Issuer) Claim in the JWE Protected Header, exposing it without
* decryption.
*/
replicateIssuerAsHeader(): this;
/**
* Replicates the "sub" (Subject) Claim in the JWE Protected Header, exposing it without
* decryption.
*/
replicateSubjectAsHeader(): this;
/**
* Replicates the "aud" (Audience) Claim in the JWE Protected Header, exposing it without
* decryption.
*/
replicateAudienceAsHeader(): this;
/**
* Encrypts and returns the JWT.
*
* @param key Public key or shared secret to encrypt the JWT with. See
* {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}.
*/
encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise<string>;
}
export {};
+736
View File
@@ -0,0 +1,736 @@
/** Supported JWS "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */
export type JWSAlgorithm =
| 'HS256'
| 'HS384'
| 'HS512'
| 'RS256'
| 'RS384'
| 'RS512'
| 'PS256'
| 'PS384'
| 'PS512'
| 'ES256'
| 'ES384'
| 'ES512'
| 'EdDSA'
| 'Ed25519'
| 'ML-DSA-44'
| 'ML-DSA-65'
| 'ML-DSA-87'
| (string & {})
/** Supported JWE "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */
export type JWEKeyManagementAlgorithm =
| 'dir'
| 'A128KW'
| 'A192KW'
| 'A256KW'
| 'A128GCMKW'
| 'A192GCMKW'
| 'A256GCMKW'
| 'ECDH-ES'
| 'ECDH-ES+A128KW'
| 'ECDH-ES+A192KW'
| 'ECDH-ES+A256KW'
| 'RSA-OAEP'
| 'RSA-OAEP-256'
| 'RSA-OAEP-384'
| 'RSA-OAEP-512'
| 'PBES2-HS256+A128KW'
| 'PBES2-HS384+A192KW'
| 'PBES2-HS512+A256KW'
| (string & {})
/**
* Supported JWE "enc" (Encryption Algorithm) Header Parameter values. Availability depends on the
* runtime.
*/
export type JWEContentEncryptionAlgorithm =
| 'A128CBC-HS256'
| 'A192CBC-HS384'
| 'A256CBC-HS512'
| 'A128GCM'
| 'A192GCM'
| 'A256GCM'
| (string & {})
/** JWK "kty" (Key Type) Parameter values supported by this module. */
export type JWKKeyType = 'EC' | 'RSA' | 'OKP' | 'AKP' | 'oct' | (string & {})
/**
* Generic JSON Web Key Parameters.
*
* > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index
* > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`.
*/
export type JWKParameters = {
/** JWK "kty" (Key Type) Parameter */
kty?: JWKKeyType
/** JWK "alg" (Algorithm) Parameter */
alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm
/** Permitted key operations. */
key_ops?: string[]
/** Whether the key may be exported. */
ext?: boolean
/** JWK "use" (Public Key Use) Parameter */
use?: 'sig' | 'enc' | (string & {})
/** X.509 certificate chain. */
x5c?: string[]
/** X.509 certificate SHA-1 thumbprint. */
x5t?: string
/** X.509 certificate SHA-256 thumbprint. */
'x5t#S256'?: string
/** X.509 certificate URL. */
x5u?: string
/** JWK "kid" (Key ID) Parameter */
kid?: string
}
/** Convenience interface for public OKP JSON Web Keys. */
export interface JWK_OKP_Public extends JWKParameters {
/** Key pair subtype. */
crv: string
/** Public key. */
x: string
}
/** Convenience interface for private OKP JSON Web Keys. */
export interface JWK_OKP_Private extends JWK_OKP_Public {
/** Private key. */
d: string
}
/** Convenience interface for public AKP JSON Web Keys. */
export interface JWK_AKP_Public extends JWKParameters {
/** JWK "alg" (Algorithm) Parameter */
alg: string
/** AKP JWK "pub" (The Public key) Parameter */
pub: string
}
/** Convenience interface for private AKP JSON Web Keys. */
export interface JWK_AKP_Private extends JWK_AKP_Public {
/** AKP JWK "priv" (The Private Key) Parameter */
priv: string
}
/** Convenience interface for public EC JSON Web Keys. */
export interface JWK_EC_Public extends JWKParameters {
/** Curve. */
crv: string
/** Public key X coordinate. */
x: string
/** Public key Y coordinate. */
y: string
}
/** Convenience interface for private EC JSON Web Keys. */
export interface JWK_EC_Private extends JWK_EC_Public {
/** Private key. */
d: string
}
/** Convenience interface for public RSA JSON Web Keys. */
export interface JWK_RSA_Public extends JWKParameters {
/** Public exponent. */
e: string
/** Modulus. */
n: string
}
/** Convenience interface for private RSA JSON Web Keys. */
export interface JWK_RSA_Private extends JWK_RSA_Public {
/** Private exponent. */
d: string
/** First factor CRT exponent. */
dp: string
/** Second factor CRT exponent. */
dq: string
/** First prime factor. */
p: string
/** Second prime factor. */
q: string
/** First CRT coefficient. */
qi: string
}
/** Convenience interface for "oct" JSON Web Keys. */
export interface JWK_oct extends JWKParameters {
/** Symmetric key value. */
k: string
}
/**
* JSON Web Key ({@link https://www.rfc-editor.org/info/rfc7517/ JWK}). "RSA", "EC", "OKP", "AKP",
* and "oct" key types are supported.
*
* > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index
* > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. It spells out the
* > {@link JWKParameters} members rather than intersecting them so that every JWK member is documented
* > in one place.
*/
export type JWK = {
/** JWK "kty" (Key Type) Parameter */
kty?: JWKKeyType
/** JWK "alg" (Algorithm) Parameter */
alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm
/** Permitted key operations. */
key_ops?: string[]
/** Whether the key may be exported. */
ext?: boolean
/** JWK "use" (Public Key Use) Parameter */
use?: 'sig' | 'enc' | (string & {})
/** X.509 certificate chain. */
x5c?: string[]
/** X.509 certificate SHA-1 thumbprint. */
x5t?: string
/** X.509 certificate SHA-256 thumbprint. */
'x5t#S256'?: string
/** X.509 certificate URL. */
x5u?: string
/** JWK "kid" (Key ID) Parameter */
kid?: string
/** EC curve or OKP key pair subtype. */
crv?: string
/** Private RSA exponent, EC key, or OKP key. */
d?: string
/** RSA first factor CRT exponent. */
dp?: string
/** RSA second factor CRT exponent. */
dq?: string
/** RSA public exponent. */
e?: string
/** Symmetric key value. */
k?: string
/** RSA modulus. */
n?: string
/** RSA first prime factor. */
p?: string
/** RSA second prime factor. */
q?: string
/** RSA first CRT coefficient. */
qi?: string
/** EC public key X coordinate or OKP public key. */
x?: string
/** EC public key Y coordinate. */
y?: string
/** AKP JWK "pub" (Public Key) Parameter */
pub?: string
/** AKP JWK "priv" (Private key) Parameter */
priv?: string
/** Additional RSA prime factors. */
oth?: Array<{
/** Factor CRT exponent. */
d?: string
/** Prime factor. */
r?: string
/** Factor CRT coefficient. */
t?: string
}>
}
/** Discriminated union of supported JSON Web Key shapes, narrowed by the "kty" (Key Type) Parameter. */
// Intersect "kty" into each member separately: typedoc omits the parentheses in `X & (A | B)`.
export type AnyJWK =
| (JWK_EC_Private & { kty: 'EC' })
| (JWK_EC_Public & { kty: 'EC' })
| (JWK_RSA_Private & { kty: 'RSA' })
| (JWK_RSA_Public & { kty: 'RSA' })
| (JWK_OKP_Private & { kty: 'OKP' })
| (JWK_OKP_Public & { kty: 'OKP' })
| (JWK_AKP_Private & { kty: 'AKP' })
| (JWK_AKP_Public & { kty: 'AKP' })
| (JWK_oct & { kty: 'oct' })
/** Key or secret input accepted by all sign, verify, encrypt, and decrypt operations. */
export type KeyInput = CryptoKey | KeyObject | JWK | Uint8Array
/** Dynamic key resolver for consuming operations. */
export interface GetKeyFunction<IProtectedHeader, IToken, KeyTypes extends KeyInput = KeyInput> {
/**
* Resolves a key for an unverified token. Throw if no suitable key can be resolved.
*
* @param token The consumed JWE or JWS token; none of its components have been verified.
*/
(protectedHeader: IProtectedHeader, token: IToken): Promise<KeyTypes> | KeyTypes
}
/** Flattened JWS verification input. */
export interface FlattenedJWSInput {
/** JWS Unprotected Header as a JSON object. Not integrity protected; omit when empty. */
header?: JWSHeaderParameters
/** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */
payload: string | Uint8Array
/** Base64url-encoded UTF-8 JWS Protected Header. Integrity protected; omit when empty. */
protected?: string
/** Base64url-encoded signature or MAC. */
signature: string
}
/** General JWS verification input. */
export interface GeneralJWSInput {
/** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */
payload: string | Uint8Array
/**
* The "signatures" member value MUST be an array of JSON objects. Each object represents a
* signature or MAC over the JWS Payload and the JWS Protected Header.
*/
signatures: Omit<FlattenedJWSInput, 'payload'>[]
}
/**
* Flattened JWS JSON Serialization token. The payload is an empty string when the
* {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used.
*/
export interface FlattenedJWS extends Partial<FlattenedJWSInput> {
payload: string
signature: string
}
/**
* General JWS JSON Serialization token. The payload is an empty string when the
* {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used.
*/
export interface GeneralJWS {
payload: string
signatures: Omit<FlattenedJWSInput, 'payload'>[]
}
/** Header Parameters common to JWE and JWS. */
export interface JoseHeaderParameters {
/** "kid" (Key ID) Header Parameter */
kid?: string
/** X.509 certificate SHA-1 thumbprint. */
x5t?: string
/** X.509 certificate chain. */
x5c?: string[]
/** X.509 certificate URL. */
x5u?: string
/** JWK Set URL. */
jku?: string
/** Public JWK only; private and symmetric key parameters are not permitted. */
jwk?: Omit<JWK, 'd' | 'dp' | 'dq' | 'k' | 'p' | 'q' | 'qi' | 'priv' | 'oth'>
/** "typ" (Type) Header Parameter */
typ?: string
/** Content type. */
cty?: string
}
/** Recognized JWS Header Parameters; additional members may also be present. */
export interface JWSHeaderParameters extends JoseHeaderParameters {
/** JWS "alg" (Algorithm) Header Parameter */
alg?: JWSAlgorithm
/**
* Controls payload encoding and the JWS signing input as defined by
* {@link https://www.rfc-editor.org/info/rfc7797/ RFC7797}. Set to `false` and list `b64` in
* `crit` to use an unencoded payload.
*/
b64?: boolean
/** Extension parameters that must be recognized. */
crit?: string[]
/** Any other JWS Header member. */
[propName: string]: unknown
}
/** Recognized JWE Key Management-related Header Parameters. */
export interface JWEKeyManagementHeaderParameters {
/** ECDH-ES Agreement PartyUInfo bytes, used in ConcatKDF and added to the JOSE header. */
apu?: Uint8Array
/** ECDH-ES Agreement PartyVInfo bytes, used in ConcatKDF and added to the JOSE header. */
apv?: Uint8Array
/** PBES2 PBKDF2 iteration count, added to the JOSE header. */
p2c?: number
/** @deprecated For testing and vector validation only. */
p2s?: Uint8Array
/** @deprecated For testing and vector validation only. */
iv?: Uint8Array
/** @deprecated For testing and vector validation only. */
epk?: CryptoKey | KeyObject
}
/** Flattened JWE JSON Serialization token. */
export interface FlattenedJWE {
/**
* Base64url-encoded additional authenticated data; integrity protected but not encrypted. Omit
* when empty.
*/
aad?: string
/** Base64url-encoded ciphertext. */
ciphertext: string
/** Base64url-encoded encrypted key. Omit when empty. */
encrypted_key?: string
/**
* JWE Per-Recipient Unprotected Header as a JSON object. Not integrity protected; omit when
* empty.
*/
header?: JWEHeaderParameters
/** Base64url-encoded initialization vector. Omit when empty. */
iv?: string
/** Base64url-encoded UTF-8 JWE Protected Header. Integrity protected; omit when empty. */
protected?: string
/** Base64url-encoded authentication tag. Omit when empty. */
tag?: string
/** JWE Shared Unprotected Header as a JSON object. Not integrity protected; omit when empty. */
unprotected?: JWEHeaderParameters
}
/** General JWE JSON Serialization token. */
export interface GeneralJWE extends Omit<FlattenedJWE, 'encrypted_key' | 'header'> {
recipients: Pick<FlattenedJWE, 'encrypted_key' | 'header'>[]
}
/** Recognized JWE Header Parameters; additional members may also be present. */
export interface JWEHeaderParameters extends JoseHeaderParameters {
/** JWE "alg" (Algorithm) Header Parameter */
alg?: JWEKeyManagementAlgorithm
/** JWE "enc" (Encryption Algorithm) Header Parameter */
enc?: JWEContentEncryptionAlgorithm
/** Extension parameters that must be recognized. */
crit?: string[]
/**
* JWE compression algorithm. Only `"DEF"` (DEFLATE) is supported, requiring the runtime's
* `CompressionStream` / `DecompressionStream` APIs.
*/
zip?: 'DEF' | (string & {})
/** Any other JWE Header member. */
[propName: string]: unknown
}
/** Shared "crit" option for signing, verification, encryption, and decryption. */
export interface CritOption {
/**
* Recognized "crit" (Critical) Header Parameter names. Set each value to `true` to require
* integrity protection, or `false` when protection is optional. The JWS `b64` extension is always
* recognized and processed.
*
* > Warning: Other extensions are only checked for syntax and optional integrity protection. Their presence
* > is not required by this option. You must check their presence and process them according to the
* > profile's validation steps after the operation succeeds.
*/
crit?: {
[propName: string]: boolean
}
}
/** JWE Decryption options. */
export interface DecryptOptions extends CritOption {
/**
* Accepted JWE "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable
* to the key or secret except PBES2, which must be explicitly allowed.
*/
keyManagementAlgorithms?: JWEKeyManagementAlgorithm[]
/**
* Accepted JWE "enc" (Encryption Algorithm) Header Parameter values. Defaults to all algorithms
* applicable to the key or secret.
*/
contentEncryptionAlgorithms?: JWEContentEncryptionAlgorithm[]
/**
* Maximum "p2c" (PBES2 Count) Header Parameter value, limiting PBKDF2 iterations and their
* computational expense. Defaults to 10000; must be a positive safe integer or `Infinity` to
* disable the limit.
*/
maxPBES2Count?: number
/**
* Maximum decompressed plaintext size in bytes. Defaults to 250000; `0` rejects compressed JWEs,
* and `Infinity` disables the limit. Other values must be positive safe integers.
*/
maxDecompressedLength?: number
}
/** JWE Encryption options. */
export interface EncryptOptions extends CritOption {}
/** JWT Claims Set verification options. */
export interface JWTClaimVerificationOptions {
/** Expected JWT "aud" (Audience) Claim value(s). Requires the claim to be present. */
audience?: string | string[]
/**
* Clock skew tolerance in seconds or a duration string (e.g. "5 seconds"). Applies to the "nbf"
* (Not Before) and "exp" (Expiration Time) claims, and to "iat" (Issued At) when
* {@link maxTokenAge} is set.
*/
clockTolerance?: string | number
/** Expected JWT "iss" (Issuer) Claim value(s). Requires the claim to be present. */
issuer?: string | string[]
/**
* Maximum time since the JWT "iat" (Issued At) Claim, in seconds or a duration string (e.g. "2
* hours"). Requires the claim to be present.
*/
maxTokenAge?: string | number
/** Expected JWT "sub" (Subject) Claim value. Requires the claim to be present. */
subject?: string
/** Expected JWT "typ" (Type) Header Parameter value. Requires the parameter to be present. */
typ?: string
/** Date for NumericDate comparisons. Defaults to `new Date()`. */
currentDate?: Date
/**
* Additional claim names required in the JWT Claims Set. The {@link issuer}, {@link audience},
* {@link subject}, and {@link maxTokenAge} options independently require "iss", "aud", "sub", and
* "iat", respectively.
*/
requiredClaims?: string[]
}
/** JWS Verification options. */
export interface VerifyOptions extends CritOption {
/**
* Accepted JWS "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable
* to the key or secret. Unsecured JWTs (`alg: "none"`) are never accepted.
*/
algorithms?: JWSAlgorithm[]
}
/** JWS Signing options. */
export interface SignOptions extends CritOption {}
/** Recognized JWT Claims Set members; additional members may also be present. */
export interface JWTPayload {
/** JWT Issuer */
iss?: string
/** JWT Subject */
sub?: string
/** JWT Audience */
aud?: string | string[]
/** JWT ID */
jti?: string
/** Not valid before this Unix timestamp in seconds. */
nbf?: number
/** Expiration Unix timestamp in seconds. */
exp?: number
/** Issued-at Unix timestamp in seconds. */
iat?: number
/** Any other JWT Claim Set member. */
[propName: string]: unknown
}
/** Flattened JWE JSON Serialization decryption result. */
export interface FlattenedDecryptResult {
/** JWE Additional Authenticated Data, integrity protected but not encrypted. */
additionalAuthenticatedData?: Uint8Array
/** Plaintext. */
plaintext: Uint8Array
/** JWE Protected Header. */
protectedHeader?: JWEHeaderParameters
/** JWE Shared Unprotected Header. Not integrity protected. */
sharedUnprotectedHeader?: JWEHeaderParameters
/** JWE Per-Recipient Unprotected Header. Not integrity protected. */
unprotectedHeader?: JWEHeaderParameters
}
/** General JWE JSON Serialization decryption result. */
export interface GeneralDecryptResult extends FlattenedDecryptResult {}
/** Compact JWE decryption result. */
export interface CompactDecryptResult {
/** Plaintext. */
plaintext: Uint8Array
/** JWE Protected Header. */
protectedHeader: CompactJWEHeaderParameters
}
/** Flattened JWS JSON Serialization verification result. */
export interface FlattenedVerifyResult {
/** JWS Payload. */
payload: Uint8Array
/** JWS Protected Header. */
protectedHeader?: JWSHeaderParameters
/** JWS Unprotected Header. Not integrity protected. */
unprotectedHeader?: JWSHeaderParameters
}
/** General JWS JSON Serialization verification result. */
export interface GeneralVerifyResult extends FlattenedVerifyResult {}
/** Compact JWS verification result. */
export interface CompactVerifyResult {
/** JWS Payload. */
payload: Uint8Array
/** JWS Protected Header. */
protectedHeader: CompactJWSHeaderParameters
}
/** Signed JSON Web Token (JWT) verification result. */
export interface JWTVerifyResult<PayloadType = JWTPayload> {
/** JWT Claims Set. */
payload: PayloadType &
JWTPayload &
([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never)
/** JWS Protected Header. */
protectedHeader: JWTHeaderParameters
}
/** Encrypted JSON Web Token (JWT) decryption result. */
export interface JWTDecryptResult<PayloadType = JWTPayload> {
/** JWT Claims Set. */
payload: PayloadType &
JWTPayload &
([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never)
/** JWE Protected Header. */
protectedHeader: CompactJWEHeaderParameters
}
/** Key resolver result metadata. */
export interface ResolvedKey<KeyType extends CryptoKey | Uint8Array = CryptoKey | Uint8Array> {
/** The resolved key, present only when a key resolver is used. */
key: KeyType
}
/** Recognized Compact JWS Header Parameters; additional members may also be present. */
export interface CompactJWSHeaderParameters extends JWSHeaderParameters {
alg: JWSAlgorithm
}
/** Recognized signed JWT Header Parameters; additional members may also be present. */
export interface JWTHeaderParameters extends CompactJWSHeaderParameters {
b64?: boolean
}
/** Recognized Compact JWE Header Parameters; additional members may also be present. */
export interface CompactJWEHeaderParameters extends JWEHeaderParameters {
alg: JWEKeyManagementAlgorithm
enc: JWEContentEncryptionAlgorithm
}
/** JSON Web Key Set. */
export interface JSONWebKeySet {
keys: JWK[]
}
/** Node.js {@link !KeyObject} representation accepted as key input. */
export interface KeyObject {
type: string
}
/** The runtime's Web Crypto {@link !CryptoKey} representation accepted as key input. */
export type CryptoKey = typeof globalThis extends {
crypto: { subtle: { generateKey(...args: any[]): Promise<infer R> } }
}
? Extract<R, { type: string }>
: CryptoKeyStructuralFallback
/** Structural fallback used when the host {@link !CryptoKey} type cannot be inferred. */
export interface CryptoKeyStructuralFallback {
readonly algorithm: { name: string }
readonly extractable: boolean
readonly type: string
readonly usages: string[]
}
/** Shared fluent API for JWT-producing classes. */
export interface ProduceJWT {
/**
* Set the "iss" (Issuer) Claim.
*
* @param issuer "Issuer" Claim value to set on the JWT Claims Set.
*/
setIssuer(issuer: string): this
/**
* Set the "sub" (Subject) Claim.
*
* @param subject "sub" (Subject) Claim value to set on the JWT Claims Set.
*/
setSubject(subject: string): this
/**
* Set the "aud" (Audience) Claim.
*
* @param audience "aud" (Audience) Claim value to set on the JWT Claims Set.
*/
setAudience(audience: string | string[]): this
/**
* Set the "jti" (JWT ID) Claim.
*
* @param jwtId "jti" (JWT ID) Claim value to set on the JWT Claims Set.
*/
setJti(jwtId: string): this
/**
* Set the "nbf" (Not Before) Claim. Numbers are Unix timestamps in seconds; Dates are converted
* to seconds. Strings are relative to now, using seconds, minutes, hours, days, weeks, or years
* (365.25 days; no months). Prefix `-` or suffix `ago` subtracts the duration.
*
* @param input "nbf" (Not Before) Claim value as a timestamp, Date, or relative duration.
*/
setNotBefore(input: number | string | Date): this
/**
* Set the "exp" (Expiration Time) Claim. Accepts a Unix timestamp in seconds, a Date, or a
* duration relative to now using the same formats as {@link setNotBefore}.
*
* @param input "exp" (Expiration Time) Claim value as a timestamp, Date, or relative duration.
*/
setExpirationTime(input: number | string | Date): this
/**
* Set the "iat" (Issued At) Claim. Defaults to the current Unix timestamp in seconds. Accepts a
* Unix timestamp in seconds, a Date, or a duration relative to now using the same formats as
* {@link setNotBefore}.
*
* @param input "iat" (Issued At) Claim value as a timestamp, Date, or relative duration.
*/
setIssuedAt(input?: number | string | Date): this
}
@@ -0,0 +1,182 @@
import type * as t from '../types.d.ts';
/** Stable error codes used by this module. */
export type JOSEErrorCode = 'ERR_JOSE_ALG_NOT_ALLOWED' | 'ERR_JOSE_GENERIC' | 'ERR_JOSE_NOT_SUPPORTED' | 'ERR_JWE_DECRYPTION_FAILED' | 'ERR_JWE_INVALID' | 'ERR_JWK_INVALID' | 'ERR_JWKS_INVALID' | 'ERR_JWKS_MULTIPLE_MATCHING_KEYS' | 'ERR_JWKS_NO_MATCHING_KEY' | 'ERR_JWKS_TIMEOUT' | 'ERR_JWS_INVALID' | 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' | 'ERR_JWT_CLAIM_VALIDATION_FAILED' | 'ERR_JWT_EXPIRED' | 'ERR_JWT_INVALID';
/** Shared details of JWT claim or header validation failures. */
export interface JWTClaimValidationFailure {
/** Claim or header that failed validation. */
claim: string;
/** Reason code for the validation failure. */
reason: JWTClaimValidationReason;
/** The parsed JWT Claims Set; validation of other claims may be incomplete. */
payload: t.JWTPayload;
}
/** Reason codes produced by JWT Claims Set validation. */
export type JWTClaimValidationReason = 'check_failed' | 'invalid' | 'mismatch' | 'missing' | 'unspecified' | (string & {});
/** Base class for JOSE errors. */
export declare class JOSEError extends Error {
/** Stable code identifying the error class. */
static code: JOSEErrorCode | (string & {});
/** Stable code identifying the error class. Use {@link AnyJOSEError} to narrow subclasses by code. */
code: JOSEErrorCode | (string & {});
constructor(message?: string, options?: {
cause?: unknown;
});
}
/**
* Thrown when JWT claim or header validation fails. Expiration is reported separately as
* {@link JWTExpired}.
*/
export declare class JWTClaimValidationFailed extends JOSEError implements JWTClaimValidationFailure {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
/** The {@link JWTClaimValidationFailure} details carried by this error. */
cause: JWTClaimValidationFailure;
/** Claim or header that failed validation. */
claim: string;
/** Reason code for the validation failure. */
reason: JWTClaimValidationReason;
/**
* The parsed JWT Claims Set; validation of other claims may be incomplete. With
* {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token
* authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not
* authenticate tokens.
*/
payload: t.JWTPayload;
constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason);
}
/** Thrown when a JWT has expired or exceeds the configured maximum token age. */
export declare class JWTExpired extends JOSEError implements JWTClaimValidationFailure {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
/** The {@link JWTClaimValidationFailure} details carried by this error. */
cause: JWTClaimValidationFailure;
/** Claim or header that failed validation. */
claim: string;
/** Reason code for the validation failure. */
reason: JWTClaimValidationReason;
/**
* The parsed JWT Claims Set; validation of other claims may be incomplete. With
* {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token
* authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not
* authenticate tokens.
*/
payload: t.JWTPayload;
constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason);
}
/** Thrown when an algorithm is disallowed by configuration. */
export declare class JOSEAlgNotAllowed extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
}
/** Thrown when a feature or algorithm is unsupported. */
export declare class JOSENotSupported extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
}
/** Thrown when JWE ciphertext decryption or authentication fails. */
export declare class JWEDecryptionFailed extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
constructor(message?: string, options?: {
cause?: unknown;
});
}
/** Thrown when a JWE is invalid. */
export declare class JWEInvalid extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
}
/** Thrown when a JWS is invalid. */
export declare class JWSInvalid extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
}
/** Thrown when a JWT is invalid. */
export declare class JWTInvalid extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
}
/** Thrown when a JWK is invalid. */
export declare class JWKInvalid extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
}
/** Thrown when a JWKS is invalid. */
export declare class JWKSInvalid extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
}
/** Thrown when no keys match in a JWKS. */
export declare class JWKSNoMatchingKey extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
constructor(message?: string, options?: {
cause?: unknown;
});
}
/** Thrown when multiple keys match in a JWKS. */
export declare class JWKSMultipleMatchingKeys extends JOSEError {
/**
* Yields public keys matching the JWS Header so verification can be attempted with each. See the
* {@link jwks/remote.createRemoteJWKSet createRemoteJWKSet} and
* {@link jwks/local.createLocalJWKSet createLocalJWKSet} examples. Empty on manually constructed
* errors.
*/
[Symbol.asyncIterator]: () => AsyncIterableIterator<t.CryptoKey>;
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
constructor(message?: string, options?: {
cause?: unknown;
});
}
/** Thrown when fetching a remote JWKS times out. */
export declare class JWKSTimeout extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
constructor(message?: string, options?: {
cause?: unknown;
});
}
/** Thrown when JWS signature verification fails. */
export declare class JWSSignatureVerificationFailed extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
constructor(message?: string, options?: {
cause?: unknown;
});
}
/** Errors thrown during JWT Claims Set validation. */
export type JWTClaimValidationError = JWTClaimValidationFailed | JWTExpired;
/**
* Discriminated union of specific {@link JOSEError} subclasses, narrowed by `code`. Excludes base
* {@link JOSEError} instances; use `instanceof JOSEError` to catch all module errors.
*/
export type AnyJOSEError = (JOSEAlgNotAllowed & {
code: 'ERR_JOSE_ALG_NOT_ALLOWED';
}) | (JOSENotSupported & {
code: 'ERR_JOSE_NOT_SUPPORTED';
}) | (JWEDecryptionFailed & {
code: 'ERR_JWE_DECRYPTION_FAILED';
}) | (JWEInvalid & {
code: 'ERR_JWE_INVALID';
}) | (JWKInvalid & {
code: 'ERR_JWK_INVALID';
}) | (JWKSInvalid & {
code: 'ERR_JWKS_INVALID';
}) | (JWKSMultipleMatchingKeys & {
code: 'ERR_JWKS_MULTIPLE_MATCHING_KEYS';
}) | (JWKSNoMatchingKey & {
code: 'ERR_JWKS_NO_MATCHING_KEY';
}) | (JWKSTimeout & {
code: 'ERR_JWKS_TIMEOUT';
}) | (JWSInvalid & {
code: 'ERR_JWS_INVALID';
}) | (JWSSignatureVerificationFailed & {
code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED';
}) | (JWTClaimValidationFailed & {
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED';
}) | (JWTExpired & {
code: 'ERR_JWT_EXPIRED';
}) | (JWTInvalid & {
code: 'ERR_JWT_INVALID';
});
@@ -0,0 +1,13 @@
import { prepareDecrypt, decryptCompact } from "../lib/jwe_decrypt.js";
import { validateClaimsSet } from "../lib/jwt_claims_set.js";
import { JWTClaimValidationFailed } from "../util/errors.js";
async function jwtDecrypt(jwt, key, options) {
const { plaintext, ...result } = await decryptCompact(jwt, prepareDecrypt(options), key), { protectedHeader } = result, payload = validateClaimsSet(protectedHeader, plaintext, options);
for (const claim of ["iss", "sub", "aud"])
if (protectedHeader[claim] !== void 0 && (claim === "aud" ? JSON.stringify(protectedHeader.aud) !== JSON.stringify(payload.aud) : protectedHeader[claim] !== payload[claim]))
throw new JWTClaimValidationFailed(`replicated "${claim}" claim header parameter mismatch`, payload, claim, "mismatch");
return { payload, ...result };
}
export {
jwtDecrypt
};
@@ -0,0 +1,45 @@
import { compactJWE, createJWE } from "../lib/jwe_encrypt.js";
import { JWTClaimsBuilder, jwtClaim, jwtData } from "../lib/jwt_claims_set.js";
import { assertNotSet } from "../lib/validate.js";
const EncryptJWT_base = JWTClaimsBuilder;
class EncryptJWT extends EncryptJWT_base {
#input = [void 0];
#replicateIssuerAsHeader;
#replicateSubjectAsHeader;
#replicateAudienceAsHeader;
setProtectedHeader(protectedHeader) {
return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this;
}
setKeyManagementParameters(parameters) {
return assertNotSet(this.#input[7], "setKeyManagementParameters"), this.#input[7] = parameters, this;
}
setContentEncryptionKey(cek) {
return assertNotSet(this.#input[5], "setContentEncryptionKey"), this.#input[5] = cek, this;
}
setInitializationVector(iv) {
return assertNotSet(this.#input[6], "setInitializationVector"), this.#input[6] = iv, this;
}
replicateIssuerAsHeader() {
return this.#replicateIssuerAsHeader = !0, this;
}
replicateSubjectAsHeader() {
return this.#replicateSubjectAsHeader = !0, this;
}
replicateAudienceAsHeader() {
return this.#replicateAudienceAsHeader = !0, this;
}
async encrypt(key, options) {
const plaintext = jwtData(this);
this.#input[1] && (this.#replicateIssuerAsHeader || this.#replicateSubjectAsHeader || this.#replicateAudienceAsHeader) && (this.#input[1] = {
...this.#input[1],
iss: this.#replicateIssuerAsHeader ? jwtClaim(this, "iss") : void 0,
sub: this.#replicateSubjectAsHeader ? jwtClaim(this, "sub") : void 0,
aud: this.#replicateAudienceAsHeader ? jwtClaim(this, "aud") : void 0
});
const input = [...this.#input];
return input[0] = plaintext, compactJWE(await createJWE(input, key, options));
}
}
export {
EncryptJWT
};
@@ -0,0 +1,75 @@
const encoder = new TextEncoder(), decoder = new TextDecoder(), strictDecoder = new TextDecoder("utf-8", { fatal: !0 }), MAX_INT32 = 2 ** 32;
function concat(...buffers) {
const size = buffers.reduce((acc, { length }) => acc + length, 0), buf = new Uint8Array(size);
let i = 0;
for (const buffer of buffers)
buf.set(buffer, i), i += buffer.length;
return buf;
}
function writeUInt32BE(buf, value, offset) {
if (value < 0 || value >= MAX_INT32)
throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`);
buf.set([value >>> 24, value >>> 16, value >>> 8, value & 255], offset);
}
function uint64be(value) {
const high = Math.floor(value / MAX_INT32), low = value % MAX_INT32, buf = new Uint8Array(8);
return writeUInt32BE(buf, high, 0), writeUInt32BE(buf, low, 4), buf;
}
function uint32be(value) {
const buf = new Uint8Array(4);
return writeUInt32BE(buf, value), buf;
}
const NON_ASCII = /[^\x00-\x7f]/;
function encode(string) {
if (typeof string == "string" && string.length >= 128) {
if (NON_ASCII.test(string))
throw new TypeError("non-ASCII string encountered in encode()");
return encoder.encode(string);
}
const bytes = new Uint8Array(string.length);
for (let i = 0; i < string.length; i++) {
const code = string.charCodeAt(i);
if (code > 127)
throw new TypeError("non-ASCII string encountered in encode()");
bytes[i] = code;
}
return bytes;
}
function encodeBase64(input, url = !1) {
if (Uint8Array.prototype.toBase64)
return input.toBase64({ alphabet: url ? "base64url" : "base64", omitPadding: url });
const CHUNK_SIZE = 32768, arr = [];
for (let i = 0; i < input.length; i += CHUNK_SIZE)
arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
const encoded = btoa(arr.join(""));
return url ? encoded.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") : encoded;
}
function decodeBase64(encoded, url = !1) {
if (Uint8Array.fromBase64)
return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" });
if (url) {
if (encoded.includes("+") || encoded.includes("/"))
throw new TypeError("Invalid base64url");
encoded = encoded.replace(/-/g, "+").replace(/_/g, "/");
}
const binary = atob(encoded), bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++)
bytes[i] = binary.charCodeAt(i);
return bytes;
}
async function digest(algorithm, data) {
const subtleDigest = `SHA-${algorithm.slice(-3)}`;
return new Uint8Array(await crypto.subtle.digest(subtleDigest, data));
}
export {
concat,
decodeBase64,
decoder,
digest,
encode,
encodeBase64,
encoder,
strictDecoder,
uint32be,
uint64be
};
@@ -0,0 +1,90 @@
import { concat, uint64be } from "./buffer_utils.js";
import { rawKey, invalidKeyInput, isCryptoKey } from "./key.js";
import { JWEDecryptionFailed, JWEInvalid } from "../util/errors.js";
const generateCek = (enc) => crypto.getRandomValues(new Uint8Array(enc.cekBits >> 3));
function checkCekLength(cek, expected) {
const actual = cek.byteLength << 3;
if (actual !== expected)
throw new JWEInvalid(`Invalid Content Encryption Key length. Expected ${expected} bits, got ${actual} bits`);
}
const generateIv = (enc) => crypto.getRandomValues(new Uint8Array(enc.ivBits >> 3));
function checkIvLength(enc, iv) {
if (iv.length << 3 !== enc.ivBits)
throw new JWEInvalid("Invalid Initialization Vector length");
}
async function cbcKeySetup(enc, cek, usage) {
if (!(cek instanceof Uint8Array))
throw new TypeError(invalidKeyInput(cek, "Uint8Array"));
const keySize = enc.cekBits >> 1, encKey = await crypto.subtle.importKey("raw", cek.subarray(keySize >> 3), "AES-CBC", !1, [usage]), macKey = await crypto.subtle.importKey("raw", cek.subarray(0, keySize >> 3), {
hash: `SHA-${keySize << 1}`,
name: "HMAC"
}, !1, ["sign"]);
return [encKey, macKey, keySize];
}
async function cbcHmacTag(macKey, macData, keySize) {
return new Uint8Array((await crypto.subtle.sign("HMAC", macKey, macData)).slice(0, keySize >> 3));
}
async function cbcEncrypt(enc, plaintext, cek, iv, aad) {
const [encKey, macKey, keySize] = await cbcKeySetup(enc, cek, "encrypt"), ciphertext = new Uint8Array(await crypto.subtle.encrypt({
iv,
name: "AES-CBC"
}, encKey, plaintext)), macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8)), tag = await cbcHmacTag(macKey, macData, keySize);
return { ciphertext, tag, iv };
}
async function timingSafeEqual(a, b) {
const algorithm = { name: "HMAC", hash: "SHA-256" }, key = await crypto.subtle.generateKey(algorithm, !1, ["sign", "verify"]), aHmac = await crypto.subtle.sign(algorithm, key, a);
return crypto.subtle.verify(algorithm, key, aHmac, b);
}
async function cbcDecrypt(enc, cek, ciphertext, iv, tag, aad) {
const [encKey, macKey, keySize] = await cbcKeySetup(enc, cek, "decrypt"), macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8)), expectedTag = await cbcHmacTag(macKey, macData, keySize);
try {
if (await timingSafeEqual(tag, expectedTag))
return new Uint8Array(await crypto.subtle.decrypt({ iv, name: "AES-CBC" }, encKey, ciphertext));
} catch {
}
throw new JWEDecryptionFailed();
}
async function encrypt(enc, plaintext, cek, iv, aad) {
if (!isCryptoKey(cek) && !(cek instanceof Uint8Array))
throw new TypeError(invalidKeyInput(cek, "CryptoKey", "KeyObject", "Uint8Array", "JSON Web Key"));
if (iv ? checkIvLength(enc, iv) : iv = generateIv(enc), cek instanceof Uint8Array && checkCekLength(cek, enc.cekBits), enc.cbc)
return cbcEncrypt(enc, plaintext, cek, iv, aad);
const encKey = await rawKey(cek, enc.subtle, "encrypt"), encrypted = new Uint8Array(await crypto.subtle.encrypt({
additionalData: aad,
iv,
name: "AES-GCM",
tagLength: 128
}, encKey, plaintext));
return { ciphertext: encrypted.subarray(0, -16), tag: encrypted.subarray(-16), iv };
}
async function decrypt(enc, cek, ciphertext, iv, tag, aad) {
if (!isCryptoKey(cek) && !(cek instanceof Uint8Array))
throw new TypeError(invalidKeyInput(cek, "CryptoKey", "KeyObject", "Uint8Array", "JSON Web Key"));
if (!iv)
throw new JWEInvalid("JWE Initialization Vector missing");
if (!tag)
throw new JWEInvalid("JWE Authentication Tag missing");
if (!enc.cbc && tag.length !== 16)
throw new JWEInvalid("Invalid Authentication Tag length");
if (checkIvLength(enc, iv), cek instanceof Uint8Array && checkCekLength(cek, enc.cekBits), enc.cbc)
return cbcDecrypt(enc, cek, ciphertext, iv, tag, aad);
const encKey = await rawKey(cek, enc.subtle, "decrypt");
try {
return new Uint8Array(await crypto.subtle.decrypt({
additionalData: aad,
iv,
name: "AES-GCM",
tagLength: 128
}, encKey, concat(ciphertext, tag)));
} catch {
throw new JWEDecryptionFailed();
}
}
export {
checkCekLength,
checkIvLength,
decrypt,
encrypt,
generateCek,
generateIv
};
@@ -0,0 +1,40 @@
import { JOSENotSupported, JWEInvalid } from "../util/errors.js";
import { concat } from "./buffer_utils.js";
function validateZip(joseHeader, protectedHeader) {
if (joseHeader.zip !== void 0 && joseHeader.zip !== "DEF")
throw new JOSENotSupported('Unsupported JWE "zip" (Compression Algorithm) Header Parameter value.');
if (joseHeader.zip !== void 0 && !protectedHeader?.zip)
throw new JWEInvalid('JWE "zip" (Compression Algorithm) Header Parameter MUST be in a protected header.');
}
function supported(name) {
if (typeof globalThis[name] > "u")
throw new JOSENotSupported(`JWE "zip" (Compression Algorithm) Header Parameter requires the ${name} API.`);
}
async function transform(stream, input, maxLength = 1 / 0) {
const writer = stream.writable.getWriter();
writer.write(input).catch(() => {
}), writer.close().catch(() => {
});
const chunks = [];
let length = 0;
const reader = stream.readable.getReader();
for (; ; ) {
const { value, done } = await reader.read();
if (done)
break;
if (chunks.push(value), length += value.byteLength, maxLength !== 1 / 0 && length > maxLength)
throw new JWEInvalid("Decompressed plaintext exceeded the configured limit");
}
return concat(...chunks);
}
async function compress(input) {
return supported("CompressionStream"), transform(new CompressionStream("deflate-raw"), input);
}
async function decompress(input, maxLength) {
return supported("DecompressionStream"), transform(new DecompressionStream("deflate-raw"), input, maxLength);
}
export {
compress,
decompress,
validateZip
};
@@ -0,0 +1,116 @@
import { JOSENotSupported } from "../util/errors.js";
import { table } from "./key_descriptor.js";
const wrap = [
["encrypt", "wrapKey"],
["decrypt", "unwrapKey"]
], derive = [[], ["deriveBits"]], none = [[], []];
function rsaes(bits) {
return {
kty: ["RSA"],
mode: "key-encryption",
subtle: { name: "RSA-OAEP", hash: `SHA-${bits}` },
usages: wrap,
ops: ["wrapKey", "unwrapKey"]
};
}
function ecdh(mode) {
return {
kty: ["EC", "OKP"],
mode,
subtle: { name: "ECDH" },
resolve: ({ kty, crv, asymmetricKeyType }) => {
if (crv === "X25519" || asymmetricKeyType === "x25519")
return { name: "X25519" };
if (kty === "OKP")
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
return { name: "ECDH", namedCurve: crv };
},
usages: derive,
ops: [void 0, "deriveBits"]
};
}
function aeskw(bits, gcm = !1) {
return {
kty: ["oct"],
mode: "key-wrapping",
secret: !0,
subtle: { name: gcm ? "AES-GCM" : "AES-KW", length: bits },
usages: none,
ops: gcm ? ["encrypt", "decrypt"] : ["wrapKey", "unwrapKey"]
};
}
function pbes2() {
return {
kty: ["oct"],
mode: "key-wrapping",
secret: !0,
subtle: { name: "PBKDF2" },
usages: none,
ops: ["deriveBits", "deriveBits"]
};
}
const JWE = table({
dir: {
kty: ["oct"],
mode: "direct-encryption",
secret: !0,
subtle: { name: "AES-GCM" },
usages: none,
ops: ["encrypt", "decrypt"]
},
"RSA-OAEP": rsaes(1),
"RSA-OAEP-256": rsaes(256),
"RSA-OAEP-384": rsaes(384),
"RSA-OAEP-512": rsaes(512),
"ECDH-ES": ecdh("direct-key-agreement"),
"ECDH-ES+A128KW": ecdh("key-agreement-with-key-wrapping"),
"ECDH-ES+A192KW": ecdh("key-agreement-with-key-wrapping"),
"ECDH-ES+A256KW": ecdh("key-agreement-with-key-wrapping"),
A128KW: aeskw(128),
A192KW: aeskw(192),
A256KW: aeskw(256),
A128GCMKW: aeskw(128, !0),
A192GCMKW: aeskw(192, !0),
A256GCMKW: aeskw(256, !0),
"PBES2-HS256+A128KW": pbes2(),
"PBES2-HS384+A192KW": pbes2(),
"PBES2-HS512+A256KW": pbes2()
}), contentOps = ["encrypt", "decrypt"];
function contentEncryption(bits, cbc = !1) {
return {
kty: ["oct"],
secret: !0,
subtle: { name: cbc ? "AES-CBC" : "AES-GCM", length: bits },
usages: none,
ops: contentOps,
cekBits: bits,
ivBits: cbc ? 128 : 96,
cbc
};
}
const ENC = table({
A128GCM: contentEncryption(128),
A192GCM: contentEncryption(192),
A256GCM: contentEncryption(256),
"A128CBC-HS256": contentEncryption(256, !0),
"A192CBC-HS384": contentEncryption(384, !0),
"A256CBC-HS512": contentEncryption(512, !0)
});
function unsupported(parameter, name) {
throw new JOSENotSupported(`Invalid or unsupported "${parameter}" (JWE ${name}) header value`);
}
function jweAlgorithm(alg) {
return (typeof alg == "string" ? JWE[alg] : void 0) ?? unsupported("alg", "Algorithm");
}
function isJWECEKTransport(algorithm) {
return algorithm.mode === "key-wrapping" || algorithm.mode === "key-encryption" || algorithm.mode === "key-agreement-with-key-wrapping";
}
function jweEncryption(enc) {
return (typeof enc == "string" ? ENC[enc] : void 0) ?? unsupported("enc", "Encryption Algorithm");
}
export {
JWE,
isJWECEKTransport,
jweAlgorithm,
jweEncryption
};
@@ -0,0 +1,187 @@
import { decrypt, generateCek } from "./content_encryption.js";
import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateCrit, validateAlgorithms, JWE_RECOGNIZED } from "./validate.js";
import { JOSEAlgNotAllowed, JOSENotSupported, JWEInvalid } from "../util/errors.js";
import { decryptKeyManagement } from "./key_management.js";
import { decoder } from "./buffer_utils.js";
import { prepareKey } from "./key.js";
import { JWE, isJWECEKTransport, jweAlgorithm, jweEncryption } from "./jwe_algorithms.js";
import { decompress, validateZip } from "./deflate.js";
function snapshotSharedJWE(jwe) {
const { aad, ciphertext, iv, protected: encodedProtected, tag, unprotected } = jwe;
if (iv !== void 0 && (typeof iv != "string" || !iv))
throw new JWEInvalid("JWE Initialization Vector incorrect type");
if (typeof ciphertext != "string")
throw new JWEInvalid("JWE Ciphertext missing or incorrect type");
if (tag !== void 0 && (typeof tag != "string" || !tag))
throw new JWEInvalid("JWE Authentication Tag incorrect type");
if (encodedProtected !== void 0 && typeof encodedProtected != "string")
throw new JWEInvalid("JWE Protected Header incorrect type");
if (aad !== void 0 && (typeof aad != "string" || !aad))
throw new JWEInvalid("JWE AAD incorrect type");
if (unprotected !== void 0 && !isObject(unprotected))
throw new JWEInvalid("JWE Shared Unprotected Header incorrect type");
return {
aad,
ciphertext,
iv,
protected: encodedProtected,
tag,
unprotected: unprotected === void 0 ? void 0 : { ...unprotected }
};
}
function snapshotRecipientJWE(recipient) {
let header, headerAlg;
try {
const { header: inputHeader } = recipient;
if (isObject(inputHeader)) {
headerAlg = inputHeader.alg;
const parameters = Object.keys(inputHeader);
parameters.includes("alg") || (headerAlg = void 0), header = Object.fromEntries(parameters.map((parameter) => [
parameter,
parameter === "alg" ? headerAlg : inputHeader[parameter]
]));
} else
header = inputHeader;
const { encrypted_key: encryptedKey } = recipient;
return [{ encrypted_key: encryptedKey, header }, headerAlg];
} catch (error) {
return [void 0, headerAlg, error];
}
}
function checkRecipient(jwe) {
const { encrypted_key: encryptedKey, header } = jwe;
if (encryptedKey !== void 0 && typeof encryptedKey != "string")
throw new JWEInvalid("JWE Encrypted Key incorrect type");
if (header !== void 0 && !isObject(header))
throw new JWEInvalid("JWE Per-Recipient Unprotected Header incorrect type");
if (jwe.protected === void 0 && header === void 0 && jwe.unprotected === void 0)
throw new JWEInvalid("JOSE Header missing");
}
function shareJWE(jwe) {
const { protected: encodedProtected, ciphertext, iv, tag, aad } = jwe;
let parsedProt;
return encodedProtected !== void 0 && (parsedProt = parseJoseHeader(encodedProtected, JWEInvalid, "JWE Protected Header is invalid")), [
parsedProt,
decodeBase64url(ciphertext, "ciphertext", JWEInvalid),
iv !== void 0 ? decodeBase64url(iv, "iv", JWEInvalid) : void 0,
tag !== void 0 ? decodeBase64url(tag, "tag", JWEInvalid) : void 0,
encodeBase64url((encodedProtected ?? "") + (aad !== void 0 ? `.${aad}` : ""), "aad", JWEInvalid)
];
}
function prepareDecrypt(options) {
return [
options && validateAlgorithms("keyManagementAlgorithms", options.keyManagementAlgorithms),
options && validateAlgorithms("contentEncryptionAlgorithms", options.contentEncryptionAlgorithms),
options?.crit,
options?.maxPBES2Count,
options?.maxDecompressedLength
];
}
async function decryptJWE(jwe, shared, key, token = shareJWE(jwe)) {
const [parsedProt, ciphertext, iv, tag, additionalData] = token, { header, unprotected, aad } = jwe;
let joseHeader;
if (header !== void 0 || unprotected !== void 0) {
if (!isDisjoint(parsedProt, header, unprotected))
throw new JWEInvalid("JWE Protected, JWE Unprotected Header, and JWE Per-Recipient Unprotected Header Parameter names must be disjoint");
joseHeader = { ...parsedProt, ...header, ...unprotected };
} else
joseHeader = parsedProt ?? {};
const [keyManagementAlgorithms, contentEncryptionAlgorithms, crit, maxPBES2Count, maxDecompressedLength] = shared, { encrypted_key: encodedKey } = jwe;
validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, parsedProt, joseHeader), validateZip(joseHeader, parsedProt);
const { alg, enc } = joseHeader;
if (typeof alg != "string" || !alg)
throw new JWEInvalid("missing JWE Algorithm (alg) in JWE Header");
const selected = JWE[alg];
if (encodedKey === "" && (!selected || !isJWECEKTransport(selected)))
throw new JWEInvalid("JWE Encrypted Key incorrect type");
const integrated = selected?.mode === "integrated-encryption";
if (!integrated && (typeof enc != "string" || !enc))
throw new JWEInvalid("missing JWE Encryption Algorithm (enc) in JWE Header");
if (keyManagementAlgorithms && !keyManagementAlgorithms.has(alg) || !keyManagementAlgorithms && alg.startsWith("PBES2"))
throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed');
let encEntry;
if (integrated) {
if (enc !== void 0)
throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must not be present for integrated encryption');
if (iv?.byteLength)
throw new JWEInvalid("JWE Initialization Vector must be empty for integrated encryption");
if (tag?.byteLength)
throw new JWEInvalid("JWE Authentication Tag must be empty for integrated encryption");
} else {
if (contentEncryptionAlgorithms && !contentEncryptionAlgorithms.has(enc))
throw new JOSEAlgNotAllowed('"enc" (Encryption Algorithm) Header Parameter value not allowed');
encEntry = jweEncryption(enc);
}
let encryptedKey;
if (encodedKey !== void 0)
try {
encryptedKey = decodeBase64url(encodedKey, "encrypted_key", JWEInvalid);
} catch (error) {
if (!selected || !isJWECEKTransport(selected))
throw error;
encryptedKey = new Uint8Array();
}
let resolvedKey = !1;
typeof key == "function" && (key = await key(parsedProt, jwe), resolvedKey = !0);
const algEntry = selected ?? jweAlgorithm(alg);
isJWECEKTransport(algEntry) && encryptedKey === void 0 && (encryptedKey = new Uint8Array());
const k = await prepareKey(algEntry.mode === "direct-encryption" ? encEntry : algEntry, key, "decrypt");
let plaintext;
if (algEntry.mode === "integrated-encryption")
plaintext = await algEntry.decrypt(k, encryptedKey, ciphertext, additionalData, parsedProt, joseHeader);
else {
const encryption = encEntry;
let cek;
try {
cek = await decryptKeyManagement(algEntry, encryption, k, encryptedKey, joseHeader, maxPBES2Count), isJWECEKTransport(algEntry) && cek instanceof Uint8Array && cek.byteLength << 3 !== encryption.cekBits && (cek = generateCek(encryption));
} catch (err) {
if (err instanceof TypeError || err instanceof JWEInvalid || err instanceof JOSENotSupported)
throw err;
cek = generateCek(encryption);
}
plaintext = await decrypt(encryption, cek, ciphertext, iv, tag, additionalData);
}
if (joseHeader.zip === "DEF") {
const decompressionLimit = maxDecompressedLength ?? 25e4;
if (decompressionLimit === 0)
throw new JOSENotSupported('JWE "zip" (Compression Algorithm) Header Parameter is not supported.');
if (decompressionLimit !== 1 / 0 && (!Number.isSafeInteger(decompressionLimit) || decompressionLimit < 1))
throw new TypeError("maxDecompressedLength must be 0, a positive safe integer, or Infinity");
plaintext = await decompress(plaintext, decompressionLimit).catch((cause) => {
throw cause instanceof JWEInvalid ? cause : new JWEInvalid("Failed to decompress plaintext", { cause });
});
}
return {
plaintext,
...parsedProt && { protectedHeader: parsedProt },
...aad !== void 0 && {
additionalAuthenticatedData: decodeBase64url(aad, "aad", JWEInvalid)
},
...unprotected && { sharedUnprotectedHeader: unprotected },
...header && { unprotectedHeader: header },
...resolvedKey && { key: k }
};
}
async function decryptCompact(jwe, shared, key) {
if (jwe instanceof Uint8Array && (jwe = decoder.decode(jwe)), typeof jwe != "string")
throw new JWEInvalid("Compact JWE must be a string or Uint8Array");
const { 0: protectedHeader, 1: encryptedKey, 2: iv, 3: ciphertext, 4: tag, length } = jwe.split(".");
if (length !== 5)
throw new JWEInvalid("Invalid Compact JWE");
return decryptJWE({
ciphertext,
iv: iv || void 0,
protected: protectedHeader,
tag: tag || void 0,
encrypted_key: encryptedKey || void 0
}, shared, key);
}
export {
checkRecipient,
decryptCompact,
decryptJWE,
prepareDecrypt,
shareJWE,
snapshotRecipientJWE,
snapshotSharedJWE
};
@@ -0,0 +1,75 @@
import { encode as b64u } from "../util/base64url.js";
import { encrypt } from "./content_encryption.js";
import { encryptKeyManagement } from "./key_management.js";
import { JWEInvalid } from "../util/errors.js";
import { assertUint8Array, isDisjoint, isObject, serializeJoseHeader, validateCrit, validateCritDuplicates, JWE_RECOGNIZED } from "./validate.js";
import { encode } from "./buffer_utils.js";
import { prepareKey } from "./key.js";
import { JWE, jweAlgorithm, jweEncryption } from "./jwe_algorithms.js";
import { compress, validateZip } from "./deflate.js";
function checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader) {
if (!isDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader))
throw new JWEInvalid("JWE Protected, JWE Shared Unprotected and JWE Per-Recipient Header Parameter names must be disjoint");
}
function checkEncryptHeaders(input, options, sharedHeadersNormalized = !1) {
if (!input[1] && !input[2] && !input[3])
throw new JWEInvalid("either setProtectedHeader, setUnprotectedHeader, or sharedUnprotectedHeader must be called before #encrypt()");
options !== void 0 && (input[8] = options?.crit);
let [, protectedHeader, unprotectedHeader, sharedUnprotectedHeader, aad, cek, iv, keyManagementParameters, crit] = input;
if (aad !== void 0 && assertUint8Array(aad, "JWE Additional Authenticated Data"), cek !== void 0 && assertUint8Array(cek, "JWE Content Encryption Key"), iv !== void 0 && assertUint8Array(iv, "JWE Initialization Vector"), !sharedHeadersNormalized && protectedHeader !== void 0 && (protectedHeader = serializeJoseHeader(JWEInvalid, protectedHeader)[0], input[1] = protectedHeader), unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWEInvalid, unprotectedHeader)[0], input[2] = unprotectedHeader), !sharedHeadersNormalized && sharedUnprotectedHeader !== void 0 && (sharedUnprotectedHeader = serializeJoseHeader(JWEInvalid, sharedUnprotectedHeader)[0], input[3] = sharedUnprotectedHeader), keyManagementParameters !== void 0 && !isObject(keyManagementParameters))
throw new TypeError("JWE Key Management Parameters must be an object");
checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader);
const joseHeader = {
...protectedHeader,
...unprotectedHeader,
...sharedUnprotectedHeader
};
validateCritDuplicates(JWEInvalid, protectedHeader), validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, protectedHeader, joseHeader), validateZip(joseHeader, protectedHeader);
const { alg, enc } = joseHeader;
if (typeof alg != "string" || !alg)
throw new JWEInvalid('JWE "alg" (Algorithm) Header Parameter missing or invalid');
const algEntry = JWE[alg];
if (algEntry?.mode === "integrated-encryption") {
if (enc !== void 0)
throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must not be present for integrated encryption');
if (cek !== void 0)
throw new TypeError(`setContentEncryptionKey cannot be called with JWE "alg" (Algorithm) Header ${alg}`);
if (iv !== void 0)
throw new TypeError(`setInitializationVector cannot be called with JWE "alg" (Algorithm) Header ${alg}`);
return [joseHeader, void 0, algEntry];
}
if (typeof enc != "string" || !enc)
throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter missing or invalid');
return [joseHeader, jweEncryption(enc), algEntry];
}
async function encryptJWE(input, checked, key) {
const [joseHeader, encEntry, selected] = checked, [inputPlaintext, inputProtectedHeader, inputUnprotectedHeader, sharedUnprotectedHeader, aad, providedCek, inputIv, keyManagementParameters, , unprotectedParameters] = input;
let protectedHeader = inputProtectedHeader, unprotectedHeader = inputUnprotectedHeader;
const algEntry = selected ?? jweAlgorithm(joseHeader.alg);
let encryptedKey, parameters, cek;
algEntry.mode === "integrated-encryption" ? cek = await prepareKey(algEntry, key, "encrypt") : [cek, encryptedKey, parameters] = await encryptKeyManagement(algEntry, encEntry, key, joseHeader, providedCek, keyManagementParameters), parameters && (unprotectedParameters ? unprotectedHeader = unprotectedHeader ? { ...unprotectedHeader, ...parameters } : parameters : protectedHeader = protectedHeader ? { ...protectedHeader, ...parameters } : parameters, checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader));
const protectedHeaderS = protectedHeader ? b64u(JSON.stringify(protectedHeader)) : "", aadMember = aad?.byteLength ? b64u(aad) : void 0, additionalData = encode(aadMember ? `${protectedHeaderS}.${aadMember}` : protectedHeaderS);
let plaintext = inputPlaintext;
joseHeader.zip === "DEF" && (plaintext = await compress(plaintext).catch((cause) => {
throw new JWEInvalid("Failed to compress plaintext", { cause });
}));
let ciphertext, tag, iv;
algEntry.mode === "integrated-encryption" ? [encryptedKey, ciphertext] = await algEntry.encrypt(cek, plaintext, additionalData, protectedHeader, joseHeader, keyManagementParameters) : { ciphertext, tag, iv } = await encrypt(encEntry, plaintext, cek, inputIv, additionalData);
const jwe = {
ciphertext: b64u(ciphertext)
};
return iv && (jwe.iv = b64u(iv)), tag && (jwe.tag = b64u(tag)), encryptedKey?.byteLength && (jwe.encrypted_key = b64u(encryptedKey)), aadMember && (jwe.aad = aadMember), protectedHeader && (jwe.protected = protectedHeaderS), sharedUnprotectedHeader && (jwe.unprotected = sharedUnprotectedHeader), unprotectedHeader && (jwe.header = unprotectedHeader), jwe;
}
async function createJWE(input, key, options) {
return encryptJWE(input, checkEncryptHeaders(input, options), key);
}
function compactJWE(jwe) {
return [jwe.protected, jwe.encrypted_key, jwe.iv, jwe.ciphertext, jwe.tag].join(".");
}
export {
checkDisjoint,
checkEncryptHeaders,
compactJWE,
createJWE,
encryptJWE
};
@@ -0,0 +1,146 @@
import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "../util/errors.js";
import { encoder, strictDecoder } from "./buffer_utils.js";
import { isObject } from "./validate.js";
const epoch = (date) => Math.floor(date.getTime() / 1e3), multipliers = {
s: 1,
m: 60,
h: 3600,
d: 86400,
w: 604800,
y: 31557600
}, REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i, checkFailed = "check_failed";
function invalidDuration() {
throw new TypeError("Invalid time period format");
}
function secs(str) {
typeof str != "string" && invalidDuration();
const matched = REGEX.exec(str);
(!matched || matched[4] && matched[1]) && invalidDuration();
const value = parseFloat(matched[2]), numericDate2 = Math.round(value * multipliers[matched[3][0].toLowerCase()]);
return Number.isFinite(numericDate2) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -numericDate2 : numericDate2;
}
function validateInput(label, input) {
if (!Number.isFinite(input))
throw new TypeError(`Invalid ${label} input`);
return input;
}
function validateStringClaim(claim, value) {
if (typeof value != "string")
throw new TypeError(`"${claim}" claim must be a string`);
}
function validateAudienceClaim(value) {
if (typeof value != "string" && (!Array.isArray(value) || Array.from(value).some((member) => typeof member != "string")))
throw new TypeError('"aud" claim must be a string or an array of strings');
}
function numericDate(value, label) {
return typeof value == "number" ? validateInput(label, value) : value instanceof Date ? validateInput(label, epoch(value)) : epoch(/* @__PURE__ */ new Date()) + secs(value);
}
const normalizeTyp = (value) => {
const normalized = value.toLowerCase();
return value.includes("/") ? normalized : `application/${normalized}`;
}, checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : !1;
function validateNumericDate(payload, claim, required = !1) {
const value = payload[claim];
if (!(value === void 0 && !required)) {
if (typeof value != "number")
throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, payload, claim, "invalid");
return value;
}
}
function unexpectedClaim(payload, claim) {
throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, payload, claim, checkFailed);
}
function validateClaimsSet(protectedHeader, encodedPayload, options = {}) {
let payload;
try {
payload = JSON.parse(strictDecoder.decode(encodedPayload));
} catch {
}
if (!isObject(payload))
throw new JWTInvalid("JWT Claims Set must be a top-level JSON object");
const { typ } = options;
if (typ !== void 0 && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ)))
throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", checkFailed);
const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims];
maxTokenAge !== void 0 && presenceCheck.push("iat"), audience !== void 0 && presenceCheck.push("aud"), subject !== void 0 && presenceCheck.push("sub"), issuer !== void 0 && presenceCheck.push("iss");
for (const claim of new Set(presenceCheck.reverse()))
if (!Object.hasOwn(payload, claim))
throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing");
issuer !== void 0 && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss) && unexpectedClaim(payload, "iss"), subject !== void 0 && payload.sub !== subject && unexpectedClaim(payload, "sub"), audience !== void 0 && !checkAudiencePresence(payload.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(payload, "aud");
const { clockTolerance } = options;
let tolerance = 0;
if (typeof clockTolerance == "string")
tolerance = secs(clockTolerance);
else if (clockTolerance !== void 0) {
if (typeof clockTolerance != "number")
throw new TypeError("Invalid clockTolerance option type");
tolerance = clockTolerance;
}
validateInput("clockTolerance option", tolerance);
const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === void 0 ? /* @__PURE__ */ new Date() : currentDate)), iat = validateNumericDate(payload, "iat", maxTokenAge !== void 0), nbf = validateNumericDate(payload, "nbf");
if (nbf !== void 0 && nbf > now + tolerance)
throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed);
const exp = validateNumericDate(payload, "exp");
if (exp !== void 0 && exp <= now - tolerance)
throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed);
if (maxTokenAge !== void 0) {
const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge));
if (age - tolerance > max)
throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", checkFailed);
if (age < -tolerance)
throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", checkFailed);
}
return payload;
}
let producerPayloads;
function producerPayload(producer) {
return producerPayloads.get(producer);
}
function jwtData(producer) {
const payload = producerPayload(producer);
for (const claim of ["iat", "nbf", "exp"]) {
const value = payload[claim];
if (typeof value == "number" && !Number.isFinite(value))
throw new TypeError(`"${claim}" claim must be a finite number`);
}
return encoder.encode(JSON.stringify(payload));
}
function jwtClaim(producer, claim) {
return producerPayload(producer)[claim];
}
class JWTClaimsBuilder {
constructor(payload = {}) {
if (!isObject(payload))
throw new TypeError("JWT Claims Set MUST be an object");
(producerPayloads ||= /* @__PURE__ */ new WeakMap()).set(this, structuredClone(payload));
}
setIssuer(value) {
return validateStringClaim("iss", value), producerPayload(this).iss = value, this;
}
setSubject(value) {
return validateStringClaim("sub", value), producerPayload(this).sub = value, this;
}
setAudience(value) {
return validateAudienceClaim(value), producerPayload(this).aud = value, this;
}
setJti(value) {
return validateStringClaim("jti", value), producerPayload(this).jti = value, this;
}
setNotBefore(value) {
return producerPayload(this).nbf = numericDate(value, "setNotBefore"), this;
}
setExpirationTime(value) {
return producerPayload(this).exp = numericDate(value, "setExpirationTime"), this;
}
setIssuedAt(value) {
const payload = producerPayload(this);
return value === void 0 ? payload.iat = epoch(/* @__PURE__ */ new Date()) : typeof value == "string" ? payload.iat = validateInput("setIssuedAt", epoch(/* @__PURE__ */ new Date()) + secs(value)) : payload.iat = numericDate(value, "setIssuedAt"), this;
}
}
export {
JWTClaimsBuilder,
jwtClaim,
jwtData,
secs,
validateClaimsSet
};
+155
View File
@@ -0,0 +1,155 @@
import { isObject } from "./validate.js";
import { decode } from "../util/base64url.js";
import { JOSENotSupported } from "../util/errors.js";
const tag = (key) => key[Symbol.toStringTag], jwkMatchesOp = (entry, key, usage) => {
const { alg } = entry;
if (key.use !== void 0) {
const expected = usage === "sign" || usage === "verify" ? "sig" : "enc";
if (key.use !== expected)
throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`);
}
if (key.alg !== void 0 && key.alg !== alg)
throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`);
if (Array.isArray(key.key_ops)) {
const expectedKeyOp = usage === "encrypt" || usage === "decrypt" ? entry.ops?.[usage === "encrypt" ? 0 : 1] : usage;
if (expectedKeyOp && !key.key_ops.includes(expectedKeyOp))
throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`);
}
};
async function prepareKey(entry, key, usage) {
const { alg, secret } = entry, privateKey = usage === "decrypt" || usage === "sign";
if (secret && key instanceof Uint8Array)
return key;
let normalized, keyObject;
if (isObject(key)) {
if (normalized = normalizeJwk(key), typeof normalized.kty != "string")
throw invalidKeyType(alg, key, secret);
if (!(secret ? normalized.kty === "oct" && typeof normalized.k == "string" : normalized.kty !== "oct" && (privateKey ? normalized.kty === "AKP" && typeof normalized.priv == "string" || typeof normalized.d == "string" : normalized.d === void 0 && normalized.priv === void 0)))
throw new TypeError(secret ? 'JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present' : `JSON Web Key for this operation must be a ${privateKey ? "private" : "public"} JWK`);
if (jwkMatchesOp(entry, normalized, usage), normalized.kty === "oct")
return decode(normalized.k);
if (!Object.isFrozen(key)) {
const { key_ops } = key;
Array.isArray(key_ops) && Object.freeze(key_ops), Object.freeze(key);
}
} else {
if (!isKeyLike(key))
throw invalidKeyType(alg, key, secret);
const expectedType = secret ? "secret" : privateKey ? "private" : "public";
if (key.type !== expectedType && (secret || ["secret", "public", "private"].includes(key.type)))
throw new TypeError(`${tag(key)} instances must be of type "${expectedType}" for the ${alg} algorithm`);
if (isCryptoKey(key))
return key;
if (keyObject = key, keyObject.type === "secret")
return keyObject.export();
}
cache ||= /* @__PURE__ */ new WeakMap();
const cacheKey = key;
let cached = cache.get(cacheKey);
if (cached?.[alg])
return cached[alg];
if (cached || cache.set(cacheKey, cached = {}), keyObject && typeof keyObject.toCryptoKey == "function") {
const isPublic = keyObject.type === "public", crv = nist[keyObject.asymmetricKeyDetails?.namedCurve], params = entry.resolve?.({ crv, asymmetricKeyType: keyObject.asymmetricKeyType }) ?? entry.subtle;
return cached[alg] = keyObject.toCryptoKey(params, isPublic, entry.usages[isPublic ? 0 : 1]);
}
return normalized ??= keyObject.export({ format: "jwk" }), normalized.alg = alg, cached[alg] = await jwkToKey(entry, normalized);
}
let cache;
const nist = {
__proto__: null,
prime256v1: "P-256",
secp384r1: "P-384",
secp521r1: "P-521"
};
function assertCryptoKey(key) {
if (!isCryptoKey(key))
throw new Error("CryptoKey instance expected");
}
const isCryptoKey = (key) => {
if (key?.[Symbol.toStringTag] === "CryptoKey")
return !0;
try {
return key instanceof CryptoKey;
} catch {
return !1;
}
}, isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject", isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key);
function message(msg, actual, ...types) {
if (types.length > 2) {
const last = types.pop();
msg += `one of type ${types.join(", ")}, or ${last}.`;
} else types.length === 2 ? msg += `one of type ${types[0]} or ${types[1]}.` : msg += `of type ${types[0]}.`;
return actual == null ? msg += ` Received ${actual}` : typeof actual == "function" && actual.name ? msg += ` Received function ${actual.name}` : typeof actual == "object" && actual != null && actual.constructor?.name && (msg += ` Received an instance of ${actual.constructor.name}`), msg;
}
const invalidKeyInput = (actual, ...types) => message("Key must be ", actual, ...types);
function invalidKeyType(alg, actual, secret) {
const types = ["CryptoKey", "KeyObject", "JSON Web Key"];
return secret && types.push("Uint8Array"), new TypeError(message(`Key for the ${alg} algorithm must be `, actual, ...types));
}
const unusable = (name, prop = "algorithm.name") => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
function checkUsage(key, usage) {
if (usage && !key.usages.includes(usage))
throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
}
function checkModulusLength(alg, key) {
const { modulusLength } = key.algorithm;
if (typeof modulusLength != "number" || modulusLength < 2048)
throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
}
function checkCryptoKey(key, expected, usage) {
const algorithm = key.algorithm;
if (algorithm.name !== expected.name)
throw unusable(expected.name);
if (expected.hash && algorithm.hash?.name !== expected.hash)
throw unusable(expected.hash, "algorithm.hash");
if (expected.namedCurve && algorithm.namedCurve !== expected.namedCurve)
throw unusable(expected.namedCurve, "algorithm.namedCurve");
if (expected.length !== void 0 && algorithm.length !== expected.length)
throw unusable(expected.length, "algorithm.length");
checkUsage(key, usage);
}
function snapshotJwk(jwk) {
return { __proto__: null, ...jwk };
}
function normalizeJwk(jwk) {
const normalized = snapshotJwk(jwk);
if (normalized.ext !== void 0 && typeof normalized.ext != "boolean")
throw new TypeError('"ext" (Extractable) Parameter must be a boolean');
if (normalized.key_ops !== void 0) {
const value = normalized.key_ops, keyOps = Array.isArray(value) ? [...value] : void 0;
if (!keyOps || keyOps.some((operation) => typeof operation != "string") || new Set(keyOps).size !== keyOps.length)
throw new TypeError('"key_ops" (Key Operations) Parameter must be an array of unique strings');
normalized.key_ops = keyOps;
}
return normalized;
}
function validateExtractableOption(extractable) {
if (extractable !== void 0 && typeof extractable != "boolean")
throw new TypeError('"extractable" option must be a boolean');
return extractable;
}
async function jwkToKey(entry, jwk, extractable) {
if (!entry.kty.includes(jwk.kty))
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
const algorithm = entry.resolve?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle, isPrivate = !!(jwk.d || jwk.priv), keyData = { ...jwk, ext: extractable ?? jwk.ext };
return keyData.kty !== "AKP" && delete keyData.alg, delete keyData.use, crypto.subtle.importKey("jwk", keyData, algorithm, keyData.ext ?? !isPrivate, jwk.key_ops ?? entry.usages[isPrivate ? 1 : 0]);
}
async function rawKey(key, expected, usage, extractable = !1) {
return key instanceof Uint8Array && (key = await crypto.subtle.importKey("raw", key, expected, extractable, [usage])), checkCryptoKey(key, expected, usage), key;
}
export {
assertCryptoKey,
checkCryptoKey,
checkModulusLength,
checkUsage,
invalidKeyInput,
isCryptoKey,
isKeyLike,
isKeyObject,
jwkToKey,
normalizeJwk,
prepareKey,
rawKey,
snapshotJwk,
validateExtractableOption
};
@@ -0,0 +1,9 @@
function table(entries) {
const out = { __proto__: null };
for (const alg in entries)
out[alg] = { ...entries[alg], alg };
return out;
}
export {
table
};
@@ -0,0 +1,196 @@
import { encode as b64u } from "../util/base64url.js";
import { prepareKey, rawKey, jwkToKey, checkCryptoKey, checkModulusLength, checkUsage, assertCryptoKey } from "./key.js";
import { jweAlgorithm, jweEncryption, isJWECEKTransport } from "./jwe_algorithms.js";
import { JOSENotSupported, JWEInvalid } from "../util/errors.js";
import { decodeBase64url, assertUint8Array, isObject } from "./validate.js";
import { digest, concat, encode, uint32be } from "./buffer_utils.js";
import { checkCekLength, generateCek, encrypt, decrypt } from "./content_encryption.js";
function checkEcdhCryptoKey(key, usage) {
if (key.algorithm.name !== "ECDH" && key.algorithm.name !== "X25519")
throw new TypeError("CryptoKey does not support this operation, its algorithm.name must be ECDH or X25519");
checkUsage(key, usage);
}
async function aeskwWrap(alg, key, cek) {
const cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "wrapKey", !0), cryptoKeyCek = await crypto.subtle.importKey("raw", cek, { hash: "SHA-256", name: "HMAC" }, !0, ["sign"]);
return new Uint8Array(await crypto.subtle.wrapKey("raw", cryptoKeyCek, cryptoKey, "AES-KW"));
}
async function aeskwUnwrap(alg, key, encryptedKey) {
const cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "unwrapKey", !0), cryptoKeyCek = await crypto.subtle.unwrapKey("raw", encryptedKey, cryptoKey, "AES-KW", { hash: "SHA-256", name: "HMAC" }, !0, ["sign"]);
return new Uint8Array(await crypto.subtle.exportKey("raw", cryptoKeyCek));
}
function checkRsaKey(alg, key, usage) {
checkCryptoKey(key, jweAlgorithm(alg).subtle, usage), checkModulusLength(alg, key);
}
async function deriveKey(p2s, alg, p2c, key) {
if (!(p2s instanceof Uint8Array) || p2s.length < 8)
throw new JWEInvalid("PBES2 Salt Input must be 8 or more octets");
if (!Number.isSafeInteger(p2c) || Math.sign(p2c) !== 1)
throw new JWEInvalid("PBES2 Count Input must be a positive integer");
const salt = concat(encode(alg), Uint8Array.of(0), p2s), keylen = parseInt(alg.slice(13, 16), 10), subtleAlg = {
hash: `SHA-${alg.slice(8, 11)}`,
iterations: p2c,
name: "PBKDF2",
salt
}, cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "deriveBits");
return new Uint8Array(await crypto.subtle.deriveBits(subtleAlg, cryptoKey, keylen));
}
function lengthAndInput(input) {
return concat(uint32be(input.length), input);
}
async function concatKdf(Z, L, OtherInfo) {
const dkLen = L >> 3, hashLen = 32, reps = Math.ceil(dkLen / hashLen), dk = new Uint8Array(reps * hashLen);
for (let i = 1; i <= reps; i++) {
const hashResult = await digest("sha256", concat(uint32be(i), Z, OtherInfo));
dk.set(hashResult, (i - 1) * hashLen);
}
return dk.slice(0, dkLen);
}
async function ecdhesDeriveKey(publicKey, privateKey, algorithm, keyLength, apu = new Uint8Array(), apv = new Uint8Array()) {
checkEcdhCryptoKey(publicKey), checkEcdhCryptoKey(privateKey, "deriveBits");
const otherInfo = concat(lengthAndInput(encode(algorithm)), lengthAndInput(apu), lengthAndInput(apv), uint32be(keyLength)), Z = new Uint8Array(await crypto.subtle.deriveBits({
name: publicKey.algorithm.name,
public: publicKey
}, privateKey, publicKey.algorithm.name === "X25519" ? 256 : Math.ceil(parseInt(publicKey.algorithm.namedCurve.slice(-3), 10) / 8) << 3));
return concatKdf(Z, keyLength, otherInfo);
}
function assertEcdhKey(key) {
assertCryptoKey(key);
const curve = key.algorithm.namedCurve;
if (curve !== "P-256" && curve !== "P-384" && curve !== "P-521" && key.algorithm.name !== "X25519")
throw new JOSENotSupported("ECDH with the provided key is not allowed or not supported by your javascript runtime");
}
function partyInfo(joseHeader, name) {
const value = joseHeader[name];
if (value !== void 0) {
if (typeof value != "string")
throw new JWEInvalid(`JOSE Header "${name}" (Agreement Party${name === "apu" ? "U" : "V"}Info) invalid`);
return decodeBase64url(value, name, JWEInvalid);
}
}
function checkPartyInfo(apu, apv) {
if (!(apu === void 0 || apv === void 0 || apu.byteLength !== apv.byteLength)) {
for (let i = 0; i < apu.byteLength; i++)
if (apu[i] !== apv[i])
return;
throw new JWEInvalid('JOSE Header "apu" and "apv" values must be distinct');
}
}
function assertEncryptedKey(encryptedKey) {
if (encryptedKey === void 0)
throw new JWEInvalid("JWE Encrypted Key missing");
}
function assertNoEncryptedKey(encryptedKey) {
if (encryptedKey !== void 0)
throw new JWEInvalid("Encountered unexpected JWE Encrypted Key");
}
function validateMaxPBES2Count(value) {
if (value !== void 0 && value !== 1 / 0 && (!Number.isSafeInteger(value) || value < 1))
throw new TypeError("maxPBES2Count must be a positive safe integer or Infinity");
}
async function decryptKeyManagement(entry, enc, key, encryptedKey, joseHeader, maxPBES2Count) {
const { alg } = entry, mode = entry.mode;
if (mode === "direct-encryption")
return assertNoEncryptedKey(encryptedKey), key;
const direct = mode === "direct-key-agreement";
switch (direct ? assertNoEncryptedKey(encryptedKey) : assertEncryptedKey(encryptedKey), entry.subtle.name) {
case "ECDH": {
const { epk } = joseHeader;
if (!isObject(epk) || ["d", "k", "p", "q", "dp", "dq", "qi", "oth", "priv"].some((parameter) => Object.hasOwn(epk, parameter)))
throw new JWEInvalid('JOSE Header "epk" (Ephemeral Public Key) missing or invalid');
assertEcdhKey(key);
const ephemeralPublicKey = await jwkToKey(entry, epk), partyUInfo = partyInfo(joseHeader, "apu"), partyVInfo = partyInfo(joseHeader, "apv");
checkPartyInfo(partyUInfo, partyVInfo);
const sharedSecret = await ecdhesDeriveKey(ephemeralPublicKey, key, direct ? enc.alg : alg, direct ? enc.cekBits : parseInt(alg.slice(-5, -2), 10), partyUInfo, partyVInfo);
if (direct)
return sharedSecret;
key = sharedSecret;
break;
}
case "RSA-OAEP":
return assertCryptoKey(key), checkRsaKey(alg, key, "decrypt"), new Uint8Array(await crypto.subtle.decrypt("RSA-OAEP", key, encryptedKey));
case "PBKDF2": {
if (typeof joseHeader.p2c != "number")
throw new JWEInvalid('JOSE Header "p2c" (PBES2 Count) missing or invalid');
validateMaxPBES2Count(maxPBES2Count);
const p2cLimit = maxPBES2Count ?? 1e4;
if (joseHeader.p2c > p2cLimit)
throw new JWEInvalid('JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds');
if (typeof joseHeader.p2s != "string")
throw new JWEInvalid('JOSE Header "p2s" (PBES2 Salt) missing or invalid');
const p2s = decodeBase64url(joseHeader.p2s, "p2s", JWEInvalid);
key = await deriveKey(p2s, alg, joseHeader.p2c, key);
break;
}
case "AES-GCM": {
if (typeof joseHeader.iv != "string")
throw new JWEInvalid('JOSE Header "iv" (Initialization Vector) missing or invalid');
if (typeof joseHeader.tag != "string")
throw new JWEInvalid('JOSE Header "tag" (Authentication Tag) missing or invalid');
const iv = decodeBase64url(joseHeader.iv, "iv", JWEInvalid), tag = decodeBase64url(joseHeader.tag, "tag", JWEInvalid);
if (iv.byteLength !== 12)
throw new JWEInvalid("Invalid Initialization Vector length");
if (tag.byteLength !== 16)
throw new JWEInvalid("Invalid Authentication Tag length");
return decrypt(jweEncryption(alg.slice(0, -2)), key, encryptedKey, iv, tag, new Uint8Array());
}
}
return aeskwUnwrap(alg.slice(-6), key, encryptedKey);
}
async function encryptKeyManagement(entry, enc, inputKey, joseHeader, providedCek, providedParameters = {}) {
const { alg, mode } = entry, transport = isJWECEKTransport(entry);
if (providedCek !== void 0 && !transport)
throw new TypeError(`setContentEncryptionKey cannot be called with JWE "alg" (Algorithm) Header ${alg}`);
let key = await prepareKey(mode === "direct-encryption" ? enc : entry, inputKey, "encrypt");
if (mode === "direct-encryption")
return [key, void 0, void 0];
const cek = transport ? providedCek ?? generateCek(enc) : void 0;
cek && checkCekLength(cek, enc.cekBits);
let encryptedKey, parameters;
switch (entry.subtle.name) {
case "ECDH": {
assertEcdhKey(key);
const { apu: providedApu, apv: providedApv } = providedParameters;
providedApu !== void 0 && assertUint8Array(providedApu, '"apu"'), providedApv !== void 0 && assertUint8Array(providedApv, '"apv"');
const apu = providedApu ?? partyInfo(joseHeader, "apu"), apv = providedApv ?? partyInfo(joseHeader, "apv");
checkPartyInfo(apu, apv);
let ephemeralKey;
providedParameters.epk !== void 0 ? ephemeralKey = await prepareKey(entry, providedParameters.epk, "decrypt") : ephemeralKey = (await crypto.subtle.generateKey(key.algorithm, !0, ["deriveBits"])).privateKey;
const subtle = crypto.subtle;
let exportableEpk = ephemeralKey;
if (!exportableEpk.extractable) {
if (typeof subtle.getPublicKey != "function")
throw new TypeError('CryptoKey for "epk" must be extractable');
exportableEpk = await subtle.getPublicKey(ephemeralKey, []);
}
const { x, y, crv, kty } = await subtle.exportKey("jwk", exportableEpk), direct = mode === "direct-key-agreement", sharedSecret = await ecdhesDeriveKey(key, ephemeralKey, direct ? enc.alg : alg, direct ? enc.cekBits : parseInt(alg.slice(-5, -2), 10), apu, apv), epk = { x, crv, kty };
if (kty === "EC" && (epk.y = y), parameters = { epk }, providedApu !== void 0 && (parameters.apu = b64u(providedApu)), providedApv !== void 0 && (parameters.apv = b64u(providedApv)), direct)
return [sharedSecret, void 0, parameters];
key = sharedSecret;
break;
}
case "RSA-OAEP": {
assertCryptoKey(key), checkRsaKey(alg, key, "encrypt"), encryptedKey = new Uint8Array(await crypto.subtle.encrypt("RSA-OAEP", key, cek));
break;
}
case "PBKDF2": {
const { p2c = 2048, p2s = crypto.getRandomValues(new Uint8Array(16)) } = providedParameters;
key = await deriveKey(p2s, alg, p2c, key), parameters = { p2c, p2s: b64u(p2s) };
break;
}
case "AES-GCM": {
const iv = providedParameters.iv === void 0 ? crypto.getRandomValues(new Uint8Array(12)) : providedParameters.iv;
if (!(iv instanceof Uint8Array))
throw new TypeError('"iv" must be an instance of Uint8Array');
const wrapped = await encrypt(jweEncryption(alg.slice(0, -2)), cek, key, iv, new Uint8Array());
encryptedKey = wrapped.ciphertext, parameters = { iv: b64u(wrapped.iv), tag: b64u(wrapped.tag) };
}
}
if (encryptedKey ??= await aeskwWrap(alg.slice(-6), key, cek), !(encryptedKey instanceof Uint8Array) || !encryptedKey.byteLength)
throw new TypeError("JWE key management algorithm did not produce an Encrypted Key");
return [cek, encryptedKey, parameters];
}
export {
decryptKeyManagement,
encryptKeyManagement,
validateMaxPBES2Count
};
@@ -0,0 +1,122 @@
import { JOSENotSupported, JWSInvalid } from "../util/errors.js";
import { decode } from "../util/base64url.js";
import { encode, strictDecoder } from "./buffer_utils.js";
function assertUint8Array(input, label) {
if (!(input instanceof Uint8Array))
throw new TypeError(`${label} must be an instance of Uint8Array`);
}
function isObject(input) {
if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]")
return !1;
const prototype = Object.getPrototypeOf(input);
return prototype === null || Object.getPrototypeOf(prototype) === null;
}
function isJwkSet(input) {
return isObject(input) && Array.isArray(input.keys) && Array.from(input.keys).every(isObject);
}
function isDisjoint(...headers) {
const parameters = /* @__PURE__ */ new Set();
for (const header of headers)
if (header)
for (const parameter of Object.keys(header)) {
if (parameters.has(parameter))
return !1;
parameters.add(parameter);
}
return !0;
}
function assertNotSet(value, name) {
if (value !== void 0)
throw new TypeError(`${name} can only be called once`);
}
function decodeBase64url(value, label, ErrorClass) {
try {
return decode(value);
} catch {
throw new ErrorClass(`Failed to base64url decode the ${label}`);
}
}
function encodeBase64url(value, label, ErrorClass) {
try {
return encode(value);
} catch {
throw new ErrorClass(`The ${label} is not a valid base64url string`);
}
}
function parseJoseHeader(b64, ErrorClass, message) {
let parsed;
try {
parsed = JSON.parse(strictDecoder.decode(decode(b64)));
} catch {
throw new ErrorClass(message);
}
if (!isObject(parsed))
throw new ErrorClass(message);
return parsed;
}
const JWS_RECOGNIZED = { __proto__: null, b64: !0 }, JWE_RECOGNIZED = { __proto__: null };
function validateAlgorithms(option, algorithms) {
if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s != "string")))
throw new TypeError(`"${option}" option must be an array of strings`);
return algorithms === void 0 ? void 0 : new Set(algorithms);
}
function validateCritDuplicates(Err, protectedHeader) {
const { crit } = protectedHeader ?? {};
if (Array.isArray(crit) && new Set(crit).size !== crit.length)
throw new Err('"crit" (Critical) Header Parameter MUST NOT contain duplicate values');
}
function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) {
if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0)
throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected');
if (!protectedHeader || protectedHeader.crit === void 0)
return [];
if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input != "string" || input.length === 0))
throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present');
const recognized = recognizedOption === void 0 ? recognizedDefault : { __proto__: null, ...recognizedOption, ...recognizedDefault };
for (const parameter of protectedHeader.crit) {
if (!(parameter in recognized))
throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`);
if (!Object.hasOwn(joseHeader, parameter) || joseHeader[parameter] === void 0)
throw new Err(`Extension Header Parameter "${parameter}" is missing`);
if (recognized[parameter] && (!Object.hasOwn(protectedHeader, parameter) || protectedHeader[parameter] === void 0))
throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`);
}
return protectedHeader.crit;
}
function validateB64(protectedHeader, extensions) {
if (extensions.includes("b64")) {
const b64 = protectedHeader.b64;
if (typeof b64 != "boolean")
throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean');
return b64;
}
return !0;
}
function serializeJoseHeader(Err, header) {
let serialized, parsed;
try {
serialized = JSON.stringify(header), parsed = JSON.parse(serialized);
} catch (cause) {
throw new Err("JOSE Header is not valid JSON", { cause });
}
if (!isObject(parsed))
throw new Err("JOSE Header is not a JSON object");
return [parsed, serialized];
}
export {
JWE_RECOGNIZED,
JWS_RECOGNIZED,
assertNotSet,
assertUint8Array,
decodeBase64url,
encodeBase64url,
isDisjoint,
isJwkSet,
isObject,
parseJoseHeader,
serializeJoseHeader,
validateAlgorithms,
validateB64,
validateCrit,
validateCritDuplicates
};
@@ -0,0 +1,16 @@
import { encoder, decoder, encodeBase64, decodeBase64 } from "../lib/buffer_utils.js";
const invalid = "The input to be decoded is not correctly encoded.";
function decode(input) {
try {
return decodeBase64(typeof input == "string" ? input : decoder.decode(input), !0);
} catch (cause) {
throw new TypeError(invalid, { cause });
}
}
function encode(input) {
return encodeBase64(typeof input == "string" ? encoder.encode(input) : input, !0);
}
export {
decode,
encode
};
@@ -0,0 +1,109 @@
class JOSEError extends Error {
static code = "ERR_JOSE_GENERIC";
code = "ERR_JOSE_GENERIC";
constructor(message, options) {
super(message, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor);
}
}
class JWTClaimValidationFailed extends JOSEError {
static code = "ERR_JWT_CLAIM_VALIDATION_FAILED";
code = "ERR_JWT_CLAIM_VALIDATION_FAILED";
claim;
reason;
payload;
constructor(message, payload, claim = "unspecified", reason = "unspecified") {
super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload;
}
}
class JWTExpired extends JOSEError {
static code = "ERR_JWT_EXPIRED";
code = "ERR_JWT_EXPIRED";
claim;
reason;
payload;
constructor(message, payload, claim = "unspecified", reason = "unspecified") {
super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload;
}
}
class JOSEAlgNotAllowed extends JOSEError {
static code = "ERR_JOSE_ALG_NOT_ALLOWED";
code = "ERR_JOSE_ALG_NOT_ALLOWED";
}
class JOSENotSupported extends JOSEError {
static code = "ERR_JOSE_NOT_SUPPORTED";
code = "ERR_JOSE_NOT_SUPPORTED";
}
class JWEDecryptionFailed extends JOSEError {
static code = "ERR_JWE_DECRYPTION_FAILED";
code = "ERR_JWE_DECRYPTION_FAILED";
constructor(message = "decryption operation failed", options) {
super(message, options);
}
}
class JWEInvalid extends JOSEError {
static code = "ERR_JWE_INVALID";
code = "ERR_JWE_INVALID";
}
class JWSInvalid extends JOSEError {
static code = "ERR_JWS_INVALID";
code = "ERR_JWS_INVALID";
}
class JWTInvalid extends JOSEError {
static code = "ERR_JWT_INVALID";
code = "ERR_JWT_INVALID";
}
class JWKInvalid extends JOSEError {
static code = "ERR_JWK_INVALID";
code = "ERR_JWK_INVALID";
}
class JWKSInvalid extends JOSEError {
static code = "ERR_JWKS_INVALID";
code = "ERR_JWKS_INVALID";
}
class JWKSNoMatchingKey extends JOSEError {
static code = "ERR_JWKS_NO_MATCHING_KEY";
code = "ERR_JWKS_NO_MATCHING_KEY";
constructor(message = "no applicable key found in the JSON Web Key Set", options) {
super(message, options);
}
}
class JWKSMultipleMatchingKeys extends JOSEError {
[Symbol.asyncIterator] = async function* () {
};
static code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS";
code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS";
constructor(message = "multiple matching keys found in the JSON Web Key Set", options) {
super(message, options);
}
}
class JWKSTimeout extends JOSEError {
static code = "ERR_JWKS_TIMEOUT";
code = "ERR_JWKS_TIMEOUT";
constructor(message = "request timed out", options) {
super(message, options);
}
}
class JWSSignatureVerificationFailed extends JOSEError {
static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED";
code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED";
constructor(message = "signature verification failed", options) {
super(message, options);
}
}
export {
JOSEAlgNotAllowed,
JOSEError,
JOSENotSupported,
JWEDecryptionFailed,
JWEInvalid,
JWKInvalid,
JWKSInvalid,
JWKSMultipleMatchingKeys,
JWKSNoMatchingKey,
JWKSTimeout,
JWSInvalid,
JWSSignatureVerificationFailed,
JWTClaimValidationFailed,
JWTExpired,
JWTInvalid
};
+17
View File
@@ -0,0 +1,17 @@
{
"name": "jose",
"version": "6.2.12",
"license": "MIT",
"type": "module",
"sideEffects": false,
"exports": {
"./jwt/*": {
"types": "./dist/types/jwt/*.d.ts",
"default": "./dist/webapi/jwt/*.js"
},
"./errors": {
"types": "./dist/types/util/errors.d.ts",
"default": "./dist/webapi/util/errors.js"
}
}
}
+4 -4
View File
@@ -107,11 +107,11 @@ declare const dhKey: KeyObject;
diffieHellman({ privateKey: dhKey, publicKey: dhKey });
/* 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<void> {
await crypto.subtle.wrapKey("raw", key, key, "AES-KW");
* getPublicKey, fence by name. */
async function publicKeyWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise<void> {
await crypto.subtle.getPublicKey(key, ["verify"]);
}
void wrapWithWebCrypto;
void publicKeyWithWebCrypto;
/* Node's navigator global: typeof navigator and navigator.userAgent
* compile; the other members fence by name. */
const cores = navigator.hardwareConcurrency;
@@ -241,14 +241,14 @@ fenced.ts:107:1 - error SC2020: 'crypto.diffieHellman' is typed by @types/node b
hint: crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets
fenced.ts:112:9 - error SC2020: 'crypto.subtle.wrapKey' is part of the standard library types but has no scriptc lowering yet
fenced.ts:112:9 - error SC2020: 'crypto.subtle.getPublicKey' is part of the standard library types but has no scriptc lowering yet
111 | async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise<void> {
112 | await crypto.subtle.wrapKey("raw", key, key, "AES-KW");
| ^~~~~~~~~~~~~~~~~~~~~
111 | async function publicKeyWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise<void> {
112 | await crypto.subtle.getPublicKey(key, ["verify"]);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~
113 | }
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, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, 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, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle
fenced.ts:117:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet
@@ -5,7 +5,7 @@ webcrypto-refusals.ts:6:16 - error SC2020: 'crypto.subtle as a value' is part of
| ^~~~~~~~~~~~~
7 | console.log(typeof 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, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, 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, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle
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,4 +14,4 @@ webcrypto-refusals.ts:9:20 - error SC2020: 'crypto.subtle.sign as a value' is pa
| ^~~~~~~~~~~~~~~~~~
10 |
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, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, 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, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle
@@ -0,0 +1 @@
{ "plain": 5334, "san": 8182 }
@@ -0,0 +1 @@
{ "plain": 5838, "san": 8032 }
@@ -0,0 +1 @@
{ "plain": 30866, "san": 122276 }
+19 -5
View File
@@ -1806,6 +1806,16 @@ await attempt("sha3 hmac", () =>
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));
await attempt("rsa-oaep wrap", () => crypto.subtle.wrapKey("raw", key, key, { name: "RSA-OAEP" }));
const pair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
const kw = await crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, false, ["wrapKey", "unwrapKey"]);
await attempt("wrap an ecdsa key", () => crypto.subtle.wrapKey("spki", pair.publicKey, kw, "AES-KW"));
await attempt("wrap with an ecdsa key", () => crypto.subtle.wrapKey("raw", aes, pair.privateKey, "AES-KW"));
await attempt("unwrap an ecdsa key", () =>
crypto.subtle.unwrapKey("spki", new Uint8Array(24), kw, "AES-KW", { name: "ECDSA", namedCurve: "P-256" }, true, [
"verify",
]),
);
`,
"mjs",
);
@@ -1817,23 +1827,27 @@ await attempt("ecdh bits", () => crypto.subtle.deriveBits({ name: "ECDH" }, key,
"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" +
"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",
"ecdh bits Error SC2020 crypto.subtle.deriveBits with the ECDH algorithm is not supported yet\n" +
"rsa-oaep wrap Error SC2020 crypto.subtle.wrapKey with the RSA-OAEP algorithm is not supported yet\n" +
"wrap an ecdsa key Error SC2020 crypto.subtle.wrapKey with an ECDSA, Ed25519 or RSA key is not supported yet\n" +
"wrap with an ecdsa key InvalidAccessError 15 Key algorithm mismatch\n" +
"unwrap an ecdsa key Error SC2020 crypto.subtle.unwrapKey with the ECDSA algorithm is not supported yet\n",
);
});
test("a method outside the slice is a named fence where it runs", async () => {
const r = await compileAndRun(
"webcrypto-wrap-key",
"webcrypto-get-public-key",
`console.log("before");
const key = await crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, true, ["wrapKey"]);
await crypto.subtle.wrapKey("raw", key, key, "AES-KW");
const key = await crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-256" }, true, ["sign"]);
await crypto.subtle.getPublicKey(key, ["verify"]);
`,
"mjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("before\n");
expect(r.stderr).toMatch(
/^Uncaught Error: 'crypto\.subtle\.wrapKey' is part of the standard library types but has no scriptc lowering yet.*\n$/,
/^Uncaught Error: 'crypto\.subtle\.getPublicKey' is part of the standard library types but has no scriptc lowering yet.*\n$/,
);
});
});
+17 -8
View File
@@ -8,7 +8,12 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, describe, expect, test } from "vitest";
import { loadLibraryProfile, profileRemediation, profileTeaching } from "@scriptc/compiler";
import {
loadLibraryProfile,
profileRemediation,
profileTeaching,
resolveLibraryFences,
} from "@scriptc/compiler";
const dir = mkdtempSync(join(tmpdir(), "scriptc-lib-profile-"));
afterAll(() => rmSync(dir, { recursive: true, force: true }));
@@ -475,6 +480,13 @@ describe("library profile fences", () => {
});
test("a fence remediation feeds the trap-remediation lookup through covered codes", () => {
// A crypto member that still refuses with SC2020, read from the
// manifest: members leave that set as they gain lowerings.
const builtins = resolveLibraryFences([{ path: "fences[0]", prefix: "node-builtin.crypto." }]);
expect(builtins.ok).toBe(true);
if (!builtins.ok) return;
const unlowered = builtins.fences[0]!.surfaces.find((s) => s.code === "SC2020");
expect(unlowered).toBeDefined();
const r = loadLibraryProfile(
writeProfile({
...good,
@@ -482,10 +494,7 @@ describe("library profile fences", () => {
remediations: { SC2012: "the explicit map key wins" },
fences: [
{ id: "diagnostic.sc2012", remediation: "request it as an effect" },
{
id: "node-builtin.crypto.createDiffieHellman",
remediation: "key agreement comes from the host",
},
{ id: unlowered!.id, remediation: "the host provides it" },
],
},
}),
@@ -495,9 +504,9 @@ describe("library profile fences", () => {
// Explicit remediations keys win over fence-supplied text.
expect(profileRemediation(r.profile, "SC2012")).toBe("the explicit map key wins");
// A fence's remediation answers for its covered entries' codes
// (crypto.createDiffieHellman is SC2020) — spec §3's "in a fence entry or a
// remediations map", one lookup either way.
expect(profileRemediation(r.profile, "SC2020")).toBe("key agreement comes from the host");
// (SC2020 here) — spec §3's "in a fence entry or a remediations map",
// one lookup either way.
expect(profileRemediation(r.profile, "SC2020")).toBe("the host provides it");
expect(profileRemediation(r.profile, "SC4014")).toBeUndefined();
});