UNPKG

@web5/agent

Version:
653 lines (593 loc) 26.5 kB
import type { JoseHeaderParams, Jwk, KeyIdentifier } from '@web5/crypto'; import { Convert } from '@web5/common'; import type { CryptoApi } from '../types/crypto-api.js'; import type { KeyManager } from '../types/key-manager.js'; import { CryptoError, CryptoErrorCode } from '../crypto-error.js'; /** * Specifies options for decrypting a JWE, allowing the caller to define constraints on the JWE * decryption process, particularly regarding the algorithms used. * * These options ensure that only expected and permitted algorithms are utilized during the * decryption, enhancing security by preventing unexpected algorithm usage. */ export interface JweDecryptOptions { /** * The allowed "alg" (Algorithm) Header Parameter values. * * These values specify the cryptographic algorithms that are permissible for decrypting * the Content Encryption Key (CEK) or for key agreement to determine the CEK. * * Note: If not specified, all algorithm values are considered allowed, which might not be * desirable in all contexts. */ allowedAlgValues?: string[]; /** * The allowed "enc" (Encryption) Header Parameter values. * * These values determine the cryptographic algorithms that can be used for decrypting the * ciphertext and protecting the integrity of the plaintext and Additional Authenticated Data. * * Note: If left unspecified, it implies that all encryption algorithms are acceptable, which may * not be secure in every scenario. * */ allowedEncValues?: string[]; } /** * Placeholder for specifying options during the JWE encryption process. Currently, this interface * does not define any specific options but can be extended in the future to include parameters * that control various aspects of the JWE encryption workflow. */ export interface JweEncryptOptions {} /** * JSON Web Encryption (JWE) Header Parameters * * The Header Parameter names for use in JWEs are registered in the IANA "JSON Web Signature and * Encryption Header Parameters" registry. * * @see {@link https://datatracker.ietf.org/doc/html/rfc7516#section-4.1 | RFC 7516, Section 4.1} */ export interface JweHeaderParams extends JoseHeaderParams { /** * Algorithm Header Parameter * * Identifies the cryptographic algorithm used to encrypt or determine the value of the Content * Encryption Key (CEK). The encrypted content is not usable if the "alg" value does not represent * a supported algorithm, or if the recipient does not have a key that can be used with that * algorithm. * * "alg" values should either be registered in the IANA "JSON Web Signature and Encryption * Algorithms" registry or be a value that contains a Collision-Resistant Name. The "alg" value is * a case-sensitive ASCII string. This Header Parameter MUST be present and MUST be understood * and processed by implementations. * * @see {@link https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.1 | RFC 7516, Section 4.1.1} */ alg: // AES Key Wrap with default initial value using 128-bit key | 'A128KW' // AES Key Wrap with default initial value using 192-bit key | 'A192KW' // AES Key Wrap with default initial value using 256-bit key | 'A256KW' // Direct use of a shared symmetric key as the CEK | 'dir' // Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF | 'ECDH-ES' // ECDH-ES using Concat KDF and CEK wrapped with "A128KW" | 'ECDH-ES+A128KW' // ECDH-ES using Concat KDF and CEK wrapped with "A192KW" | 'ECDH-ES+A192KW' // ECDH-ES using Concat KDF and CEK wrapped with "A256KW" | 'ECDH-ES+A256KW' // Key wrapping with AES GCM using 128-bit key | 'A128GCMKW' // Key wrapping with AES GCM using 192-bit key | 'A192GCMKW' // Key wrapping with AES GCM using 256-bit key | 'A256GCMKW' // PBES2 with HMAC SHA-256 and "A128KW" wrapping | 'PBES2-HS256+A128KW' // PBES2 with HMAC SHA-384 and "A192KW" wrapping | 'PBES2-HS384+A192KW' // PBES2 with HMAC SHA-512 and "A256KW" wrapping | 'PBES2-HS512+A256KW' // PBES2 with HMAC SHA-512 and "XC20PKW" wrapping | 'PBES2-HS512+XC20PKW' // an unregistered, case-sensitive, collision-resistant string | string; /** * Agreement PartyUInfo Header Parameter * * The "apu" (agreement PartyUInfo) value is a base64url-encoded octet sequence containing * information about the producer of the JWE. This information is used by the recipient to * determine the key agreement algorithm and key encryption algorithm to use to decrypt the JWE. * * Note: This parameter is intended only for use when the recipient is a key agreement algorithm * that uses public key cryptography. */ apu?: string; /** * Agreement PartyVInfo Header Parameter * * The "apv" (agreement PartyVInfo) value is a base64url-encoded octet sequence containing * information about the recipient of the JWE. This information is used by the recipient to * determine the key agreement algorithm and key encryption algorithm to use to decrypt the JWE. * * Note: This parameter is intended only for use when the recipient is a key agreement algorithm * that uses public key cryptography. */ apv?: string; /** * Critical Header Parameter * * Indicates that extensions to JOSE RFCs are being used that MUST be understood and processed. */ crit?: string[]; /** * Encryption Algorithm Header Parameter * * Identifies the content encryption algorithm used to encrypt and integrity-protect (also * known as "authenticated encryption") the plaintext and to integrity-protect the Additional * Authenticated Data (AAD), if any. This algorithm MUST be an AEAD algorithm with a specified * key length. * * The encrypted content is not usable if the "enc" value does not represent a supported * algorithm. "enc" values should either be registered in the IANA "JSON Web Signature and * Encryption Algorithms" registry or be a value that contains a Collision-Resistant Name. The * "enc" value is a case-sensitive ASCII string containing a StringOrURI value. This Header * Parameter MUST be present and MUST be understood and processed by implementations. * * @see {@link https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.2 | RFC 7516, Section 4.1.2} */ enc: // AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm, // as defined in RFC 7518, Section 5.2.3 | 'A128CBC-HS256' // AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm, // as defined in RFC 7518, Section 5.2.4 | 'A192CBC-HS384' // AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm, // as defined in RFC 7518, Section 5.2.5 | 'A256CBC-HS512' // AES GCM using 128-bit key | 'A128GCM' // AES GCM using 192-bit key | 'A192GCM' // AES GCM using 256-bit key | 'A256GCM' // XChaCha20-Poly1305 authenticated encryption algorithm | 'XC20P' // an unregistered, case-sensitive, collision-resistant string | string; /** * Ephemeral Public Key Header Parameter * * The "epk" (ephemeral public key) value created by the originator for the use in key agreement * algorithms. It is the ephemeral public key that corresponds to the key used to encrypt the * JWE. This value is represented as a JSON Web Key (JWK). * * Note: This parameter is intended only for use when the recipient is a key agreement algorithm * that uses public key cryptography. */ epk?: Jwk; /** * Initialization Vector Header Parameter * * The "iv" (initialization vector) value is a base64url-encoded octet sequence used by the * specified "enc" algorithm. The length of this Initialization Vector value MUST be exactly * equal to the value that would be produced by the "enc" algorithm. * * Note: With symmetric encryption algorithms such as AES GCM, this Header Parameter MUST * be present and MUST be understood and processed by implementations. */ iv?: string; /** * PBES2 Count Header Parameter * * The "p2c" (PBES2 count) value is an integer indicating the number of iterations of the PBKDF2 * algorithm performed during key derivation. * * Note: The iteration count adds computational expense, ideally compounded by the possible range * of keys introduced by the salt. A minimum iteration count of 1000 is RECOMMENDED. */ p2c?: number; /** * PBES2 Salt Input Header Parameter * * The "p2s" (PBES2 salt) value is a base64url-encoded octet sequence used as the salt value * input to the PBKDF2 algorithm during key derivation. * * The salt value used is (UTF8(Alg) || 0x00 || Salt Input), where Alg is the "alg" (algorithm) * Header Parameter value. * * Note: The salt value is used to ensure that each key derived from the master key is * independent of every other key. A suitable source of salt value is a sequence of * cryptographically random bytes containing 8 or more octets. */ p2s?: string; /** * Authentication Tag Header Parameter * * The "tag" value is a base64url-encoded octet sequence containing the value of the * Authentication Tag output by the specified "enc" algorithm. The length of this * Authentication Tag value MUST be exactly equal to the value that would be produced by the * "enc" algorithm. * * Note: With authenticated encryption algorithms such as AES GCM, this Header Parameter MUST * be present and MUST be understood and processed by implementations. */ tag?: string; /** * Additional Public or Private Header Parameter names. */ [key: string]: unknown; } /** * Represents the result of the JWE key management encryption process, encapsulating the Content * Encryption Key (CEK) and optionally the encrypted CEK. */ export interface JweKeyManagementEncryptResult { /** * The Content Encryption Key (CEK) used for encrypting the JWE payload. It can be a Key * Identifier such as a KMS URI or a JSON Web Key (JWK). */ cek: KeyIdentifier | Jwk; /** * The encrypted version of the CEK, provided as a byte array. The encrypted version of the CEK * is returned for all key management modes other than "dir" (Direct Encryption Mode). */ encryptedKey?: Uint8Array; } /** * Defines the parameters required to decrypt a JWE encrypted key, including the key management * details. * * @typeParam TKeyManager - The Key Manager used to manage cryptographic keys. * @typeParam TCrypto - The Crypto API used to perform cryptographic operations. */ export interface JweKeyManagementDecryptParams<TKeyManager, TCrypto> { /** * The decryption key which can be a Key Identifier such as a KMS key URI, a JSON Web Key (JWK), * or raw key material represented as a byte array. */ key: KeyIdentifier | Jwk | Uint8Array; /** * The encrypted key extracted from the JWE, represented as a byte array. This parameter is * optional and is used when the key is wrapped. */ encryptedKey?: Uint8Array; /** * The JWE header parameters that define the characteristics of the decryption process, specifying * the algorithm and encryption method among other settings. */ joseHeader: JweHeaderParams; /** Key Manager instanceß responsible for managing cryptographic keys. */ keyManager: TKeyManager; /** Crypto API instance that provides the necessary cryptographic operations. */ crypto: TCrypto; } /** * Defines the parameters required for encrypting a JWE CEK, including the key management details. * * @typeParam TKeyManager - The Key Manager used to manage cryptographic keys. * @typeParam TCrypto - The Crypto API used to perform cryptographic operations. */ export interface JweKeyManagementEncryptParams<TKeyManager, TCrypto> { /** * The encryption key which can be a Key Identifier such as a KMS key URI, a JSON Web Key (JWK), * or raw key material represented as a byte array. */ key: KeyIdentifier | Jwk | Uint8Array; /** * The JWE header parameters that define the characteristics of the encryption process, specifying * the algorithm and encryption method among other settings. */ joseHeader: JweHeaderParams; /** Key Manager instanceß responsible for managing cryptographic keys. */ keyManager: TKeyManager; /** Crypto API instance that provides the necessary cryptographic operations. */ crypto: TCrypto; } /** * Checks if the provided object is a valid JWE (JSON Web Encryption) header. * * This function evaluates whether the given object adheres to the structure expected for * a JWE header, specifically looking for the presence and proper format of the "alg" (algorithm) * and "enc" (encryption algorithm) properties, which are essential for defining the JWE's * cryptographic operations. * * @example * ```ts * const header = { * alg: 'dir', * enc: 'A256GCM' * }; * * if (isValidJweHeader(header)) { * console.log('The object is a valid JWE header.'); * } else { * console.log('The object is not a valid JWE header.'); * } * ``` * * @param obj - The object to be validated as a JWE header. * @returns Returns `true` if the object is a valid JWE header, otherwise `false`. */ export function isValidJweHeader(obj: unknown): obj is JweHeaderParams { return typeof obj === 'object' && obj !== null && 'alg' in obj && obj.alg !== undefined && 'enc' in obj && obj.enc !== undefined; } /** * The `JweKeyManagement` class implements the key management aspects of JSON Web Encryption (JWE) * as specified in {@link https://datatracker.ietf.org/doc/html/rfc7516 | RFC 7516}. * * It supports algorithms for encrypting and decrypting keys, thereby enabling the secure * transmission of information where the payload is encrypted, and the encryption key is also * encrypted or agreed upon using key agreement techniques. * * The choice of algorithm is determined by the "alg" parameter in the JWE * header, and the class is designed to handle the intricacies associated with each algorithm, * ensuring the secure handling of the encryption keys. * * Supported algorithms include: * - `"dir"`: Direct Encryption Mode * - `"PBES2-HS256+A128KW"`, `"PBES2-HS384+A192KW"`, `"PBES2-HS512+A256KW"`: Password-Based * Encryption Mode with Key Wrapping (PBES2) using HMAC-SHA and AES Key Wrap algorithms for key * wrapping and encryption. * * @example * // To encrypt a key: * const keyEncryptionKey = Convert.string(passphrase).toUint8Array() * const { cek, encryptedKey: encryptedCek } = await JweKeyManagement.encrypt({ * key: keyEncryptionKey, * joseHeader: { * alg: 'PBES2-HS512+A256KW', * enc: 'A256GCM', * p2c : 210_000, p2s : Convert.uint8Array(saltInput).toBase64Url() * }, * crypto: new AgentCryptoApi(), * }); * * // To decrypt a key: * const cek = await JweKeyManagement.decrypt({ * key: keyEncryptionKey, * encryptedKey: encryptedCek, * joseHeader: { * alg: 'PBES2-HS512+A256KW', * enc: 'A256GCM', * p2c : 210_000, p2s : Convert.uint8Array(saltInput).toBase64Url() * }, * crypto: new AgentCryptoApi(), * }); */ export class JweKeyManagement { /** * Decrypts the encrypted key (JWE Encrypted Key) using the specified key encryption algorithm * defined in the JWE Header's "alg" parameter. * * This method supports multiple key management algorithms, including Direct Encryption (dir) and * PBES2 schemes with key wrapping. * * The method takes a key, which can be a Key Identifier, JWK, or raw byte array, and the * encrypted key along with the JWE header. It returns the decrypted Content Encryption Key (CEK) * which can then be used to decrypt the JWE ciphertext. * * @example * ```ts * // Decrypting the CEK with the PBES2-HS512+A256KW algorithm * const cek = await JweKeyManagement.decrypt({ * key: Convert.string(passphrase).toUint8Array(), * encryptedKey: encryptedCek, * joseHeader: { * alg: 'PBES2-HS512+A256KW', * enc: 'A256GCM', * p2c: 210_000, * p2s: Convert.uint8Array(saltInput).toBase64Url(), * }, * crypto: new AgentCryptoApi() * }); * ``` * * @param params - The decryption parameters. * @throws Throws an error if the key management algorithm is not supported or if required * parameters are missing or invalid. */ public static async decrypt<TKeyManager extends KeyManager, TCrypto extends CryptoApi>({ key, encryptedKey, joseHeader, crypto }: JweKeyManagementDecryptParams<TKeyManager, TCrypto> ): Promise<KeyIdentifier | Jwk> { // Determine the Key Management Mode employed by the algorithm specified by the "alg" // (algorithm) Header Parameter. switch (joseHeader.alg) { case 'dir': { // In Direct Encryption mode, a JWE "Encrypted Key" is not provided. Instead, the // provided key management `key` is directly used as the Content Encryption Key (CEK) to // decrypt the JWE payload. // Verify that the JWE Encrypted Key value is empty. if (encryptedKey !== undefined) { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'JWE "encrypted_key" is not allowed when using "dir" (Direct Encryption Mode).'); } // Verify the key management `key` is a Key Identifier or JWK. if (key instanceof Uint8Array) { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'Key management "key" must be a Key URI or JWK when using "dir" (Direct Encryption Mode).'); } // return the key management `key` as the CEK. return key; } case 'PBES2-HS256+A128KW': case 'PBES2-HS384+A192KW': case 'PBES2-HS512+A256KW': { // In Key Encryption mode (PBES2) with key wrapping (A128KW, A192KW, A256KW), the given // passphrase, salt (p2s), and iteration count (p2c) are used with the PBKDF2 key derivation // function to derive the Key Encryption Key (KEK). The KEK is then used to decrypt the JWE // Encrypted Key to obtain the Content Encryption Key (CEK). if (typeof joseHeader.p2c !== 'number') { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'JOSE Header "p2c" (PBES2 Count) is missing or not a number.'); } if (typeof joseHeader.p2s !== 'string') { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'JOSE Header "p2s" (PBES2 salt) is missing or not a string.'); } // Throw an error if the key management `key` is not a byte array. For PBES2, the key is // expected to be a low-entropy passphrase as a byte array. if (!(key instanceof Uint8Array)) { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'Key management "key" must be a Uint8Array when using "PBES2" (Key Encryption Mode).'); } // Verify that the JWE Encrypted Key value is present. if (encryptedKey === undefined) { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'JWE "encrypted_key" is required when using "PBES2" (Key Encryption Mode).'); } // Per {@link https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8.1.1 | RFC 7518, Section 4.8.1.1}, // the salt value used with PBES2 should be of the format (UTF8(Alg) || 0x00 || Salt Input), // where Alg is the "alg" (algorithm) Header Parameter value. This reduces the potential for // a precomputed dictionary attack (also known as a rainbow table attack). let salt: Uint8Array; try { salt = new Uint8Array([ ...Convert.string(joseHeader.alg).toUint8Array(), 0x00, ...Convert.base64Url(joseHeader.p2s).toUint8Array() ]); } catch { throw new CryptoError(CryptoErrorCode.EncodingError, 'Failed to decode the JOSE Header "p2s" (PBES2 salt) value.'); } // Derive the Key Encryption Key (KEK) from the given passphrase, salt, and iteration count. const kek = await crypto.deriveKey({ algorithm : joseHeader.alg, baseKeyBytes : key, iterations : joseHeader.p2c, salt }); if (!(kek.alg && ['A128KW', 'A192KW', 'A256KW'].includes(kek.alg))) { throw new CryptoError(CryptoErrorCode.AlgorithmNotSupported, `Unsupported Key Encryption Algorithm (alg) value: ${kek.alg}`); } // Decrypt the Content Encryption Key (CEK) with the derived KEK. return await crypto.unwrapKey({ decryptionKey : kek, wrappedKeyBytes : encryptedKey, wrappedKeyAlgorithm : joseHeader.enc }); } default: { throw new CryptoError( CryptoErrorCode.AlgorithmNotSupported, `Unsupported "alg" (Algorithm) Header Parameter value: ${joseHeader.alg}` ); } } } /** * Encrypts a Content Encryption Key (CEK) using the key management algorithm specified in the * JWE Header's "alg" parameter. * * This method supports various key management algorithms, including Direct Encryption (dir) and * PBES2 with key wrapping. * * It generates a random CEK for the specified encryption algorithm in the JWE header, which * can then be used to encrypt the actual payload. For algorithms that require an encrypted key, * it returns the CEK along with the encrypted key. * * @example * ```ts * // Encrypting the CEK with the PBES2-HS512+A256KW algorithm * const { cek, encryptedKey } = await JweKeyManagement.encrypt({ * key: Convert.string(passphrase).toUint8Array(), * joseHeader: { * alg: 'PBES2-HS512+A256KW', * enc: 'A256GCM', * p2c: 210_000, * p2s: Convert.uint8Array(saltInput).toBase64Url(), * }, * crypto: crypto: new AgentCryptoApi() * }); * ``` * * @param params - The encryption parameters. * @returns The encrypted key result containing the CEK and optionally the encrypted CEK * (JWE Encrypted Key). * @throws Throws an error if the key management algorithm is not supported or if required * parameters are missing or invalid. */ public static async encrypt<TKeyManager extends KeyManager, TCrypto extends CryptoApi>({ key, joseHeader, crypto }: JweKeyManagementEncryptParams<TKeyManager, TCrypto> ): Promise<JweKeyManagementEncryptResult> { let cek: KeyIdentifier | Jwk; let encryptedKey: Uint8Array | undefined; // Determine the Key Management Mode employed by the algorithm specified by the "alg" // (algorithm) Header Parameter. switch (joseHeader.alg) { case 'dir': { // In Direct Encryption mode (dir), a JWE "Encrypted Key" is not provided. Instead, the // provided key management `key` is directly used as the Content Encryption Key (CEK) to // decrypt the JWE payload. // Verify that the JWE Encrypted Key value is empty. if (encryptedKey !== undefined) { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'JWE "encrypted_key" is not allowed when using "dir" (Direct Encryption Mode).'); } // Verify the key management `key` is a Key Identifier or JWK. if (key instanceof Uint8Array) { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'Key management "key" must be a Key URI or JWK when using "dir" (Direct Encryption Mode).'); } // Set the CEK to the key management `key`. cek = key; break; } case 'PBES2-HS256+A128KW': case 'PBES2-HS384+A192KW': case 'PBES2-HS512+A256KW': { // In Key Encryption mode (PBES2) with key wrapping (A128KW, A192KW, A256KW), a randomly // generated Content Encryption Key (CEK) is encrypted with a Key Encryption Key (KEK) // derived from the given passphrase, salt (p2s), and iteration count (p2c) using the // PBKDF2 key derivation function. if (typeof joseHeader.p2c !== 'number') { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'JOSE Header "p2c" (PBES2 Count) is missing or not a number.'); } if (typeof joseHeader.p2s !== 'string') { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'JOSE Header "p2s" (PBES2 salt) is missing or not a string.'); } // Throw an error if the key management `key` is not a byte array. if (!(key instanceof Uint8Array)) { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'Key management "key" must be a Uint8Array when using "PBES2" (Key Encryption Mode).'); } // Generate a random Content Encryption Key (CEK) using the algorithm specified by the "enc" // (encryption) Header Parameter. cek = await crypto.generateKey({ algorithm: joseHeader.enc }); // Per {@link https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8.1.1 | RFC 7518, Section 4.8.1.1}, // the salt value used with PBES2 should be of the format (UTF8(Alg) || 0x00 || Salt Input), // where Alg is the "alg" (algorithm) Header Parameter value. This reduces the potential for // a precomputed dictionary attack (also known as a rainbow table attack). let salt: Uint8Array; try { salt = new Uint8Array([ ...Convert.string(joseHeader.alg).toUint8Array(), 0x00, ...Convert.base64Url(joseHeader.p2s).toUint8Array() ]); } catch { throw new CryptoError(CryptoErrorCode.EncodingError, 'Failed to decode the JOSE Header "p2s" (PBES2 salt) value.'); } // Derive a Key Encryption Key (KEK) from the given passphrase, salt, and iteration count. const kek = await crypto.deriveKey({ algorithm : joseHeader.alg, baseKeyBytes : key, iterations : joseHeader.p2c, salt }); // Encrypt the randomly generated CEK with the derived Key Encryption Key (KEK). encryptedKey = await crypto.wrapKey({ encryptionKey: kek, unwrappedKey: cek }); break; } default: { throw new CryptoError( CryptoErrorCode.AlgorithmNotSupported, `Unsupported "alg" (Algorithm) Header Parameter value: ${joseHeader.alg}` ); } } return { cek, encryptedKey }; } }