mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Merge main
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
@@ -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 } },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7130-webcrypto-wrap-key.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7131-webcrypto-wrap-key-errors.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7132-jose-pbes2/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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, ¶ms, data);
|
||||
done:
|
||||
if (data) scr_bytes_release(data);
|
||||
scr_wc_cipher_params_drop(¶ms);
|
||||
@@ -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(¶ms, 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", ¶ms);
|
||||
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, ¶ms, payload);
|
||||
done:
|
||||
if (payload) scr_bytes_release(payload);
|
||||
scr_wc_cipher_params_drop(¶ms);
|
||||
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(¶ms, 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", ¶ms);
|
||||
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, ¶ms, 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(¶ms);
|
||||
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
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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"]));
|
||||
@@ -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
@@ -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.
|
||||
+34
@@ -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>>;
|
||||
+57
@@ -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
@@ -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
|
||||
}
|
||||
+182
@@ -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';
|
||||
});
|
||||
+13
@@ -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
|
||||
};
|
||||
+45
@@ -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
|
||||
};
|
||||
+75
@@ -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
|
||||
};
|
||||
Generated
Vendored
+90
@@ -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
|
||||
};
|
||||
+40
@@ -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
|
||||
};
|
||||
+116
@@ -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
|
||||
};
|
||||
+187
@@ -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
|
||||
};
|
||||
+75
@@ -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
|
||||
};
|
||||
+146
@@ -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
@@ -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
|
||||
};
|
||||
+9
@@ -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
|
||||
};
|
||||
+196
@@ -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
|
||||
};
|
||||
+122
@@ -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
|
||||
};
|
||||
+16
@@ -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
|
||||
};
|
||||
+109
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+4
-4
@@ -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 }
|
||||
@@ -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$/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user