UNPKG

@web5/agent

Version:
294 lines 17.8 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { Convert } from '@web5/common'; import { LocalKeyManager, CryptoUtils } from '@web5/crypto'; import { isCipher } from '../utils.js'; import { AgentCryptoApi } from '../../../crypto-api.js'; import { JweKeyManagement, isValidJweHeader } from './jwe.js'; import { hasDuplicateProperties } from '../../common/object.js'; import { CryptoError, CryptoErrorCode } from '../crypto-error.js'; /** * A helper utility function used internally to decode a JWE header parameter from a Base64 URL * encoded string to a Uint8Array. It's designed to process individual JWE header parameter values, * ensuring they are correctly formatted and decoded. * * @param param - The name of the JWE header parameter being decoded; used for error messaging. * @param value - The Base64 URL encoded string value of the header parameter to decode. * @returns The decoded parameter as a Uint8Array, or undefined if the input value is undefined. * @throws {@link CryptoError} if the value is not a properly encoded Base64 URL string or if it's * not a string. */ function decodeHeaderParam(param, value) { // If the parameter value is not present, return undefined. if (value === undefined) return undefined; try { if (typeof value !== 'string') throw new Error(); return Convert.base64Url(value).toUint8Array(); } catch (_a) { throw new CryptoError(CryptoErrorCode.InvalidJwe, `Failed to decode the JWE Header parameter '${param}' from Base64 URL format to ` + 'Uint8Array. Ensure the value is properly encoded in Base64 URL format without padding.'); } } /** * The `FlattenedJwe` class handles the encryption and decryption of JSON Web Encryption (JWE) * objects in the flattened serialization format. This format is a compact, URL-safe means of * representing encrypted content, typically used when dealing with a single recipient or when * bandwidth efficiency is important. * * This class provides methods to encrypt plaintext to a flattened JWE and decrypt a flattened JWE * back to plaintext, utilizing a variety of supported cryptographic algorithms as specified in the * JWE header parameters. * * @example * ```ts * // Example usage of encrypt method * const plaintext = new TextEncoder().encode("Secret Message"); * const key = { kty: "oct", k: "your-secret-key" }; // Example symmetric key * const protectedHeader = { alg: "dir", enc: "A256GCM" }; * const encryptedJwe = await FlattenedJwe.encrypt({ * plaintext, * protectedHeader, * key, * }); * ``` * * @example * // Decryption example * const { plaintext, protectedHeader } = await FlattenedJwe.decrypt({ * jwe: yourFlattenedJweObject, * key: yourDecryptionKey, * crypto: new YourCryptoApi(), * }); */ export class FlattenedJwe { constructor(params) { /** Base64URL encoded ciphertext. */ this.ciphertext = ''; Object.assign(this, params); } static decrypt({ jwe, key, keyManager = new LocalKeyManager(), crypto = new AgentCryptoApi(), options = {} }) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { // Verify that the provided Crypto API supports the decrypt operation before proceeding. if (!isCipher(crypto)) { throw new CryptoError(CryptoErrorCode.OperationNotSupported, 'Crypto API does not support the "encrypt" operation.'); } // Verify that the provided Key Manager supports the decrypt operation before proceeding. if (!isCipher(keyManager)) { throw new CryptoError(CryptoErrorCode.OperationNotSupported, 'Key Manager does not support the "decrypt" operation.'); } // Verify that at least one of the JOSE header objects is present. if (!jwe.protected && !jwe.header && !jwe.unprotected) { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'JWE is missing the required JOSE header parameters. ' + 'Please provide at least one of the following: "protected", "header", or "unprotected"'); } // Verify that the JWE Ciphertext is present. if (typeof jwe.ciphertext !== 'string') { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'JWE Ciphertext is missing or not a string.'); } // Parse the JWE Protected Header, if present. let parsedProtectedHeader; if (jwe.protected) { try { parsedProtectedHeader = Convert.base64Url(jwe.protected).toObject(); } catch (_c) { throw new Error('JWE Protected Header is invalid'); } } // Per {@link https://www.rfc-editor.org/rfc/rfc7516#section-5.2 | RFC7516 Section 5.2} // the resulting JOSE Header MUST NOT contain duplicate Header Parameter names. In other words, // the same Header Parameter name MUST NOT occur in the `header`, `protected`, and // `unprotected` JSON object values that together comprise the JOSE Header. if (hasDuplicateProperties(parsedProtectedHeader, jwe.header, jwe.unprotected)) { throw new Error('Duplicate properties detected. Please ensure that each parameter is defined only once ' + 'across the JWE "header", "protected", and "unprotected" objects.'); } // The JOSE Header is the union of the members of the JWE Protected Header (`protected`), the // JWE Shared Unprotected Header (`unprotected`), and the corresponding JWE Per-Recipient // Unprotected Header (`header`). const joseHeader = Object.assign(Object.assign(Object.assign({}, parsedProtectedHeader), jwe.header), jwe.unprotected); if (!isValidJweHeader(joseHeader)) { throw new Error('JWE Header is missing required "alg" (Algorithm) and/or "enc" (Encryption) Header Parameters'); } if (Array.isArray(options.allowedAlgValues) && !options.allowedAlgValues.includes(joseHeader.alg)) { throw new Error(`"alg" (Algorithm) Header Parameter value not allowed: ${joseHeader.alg}`); } if (Array.isArray(options.allowedEncValues) && !options.allowedEncValues.includes(joseHeader.enc)) { throw new Error(`"enc" (Encryption Algorithm) Header Parameter value not allowed: ${joseHeader.enc}`); } let cek; try { const encryptedKey = jwe.encrypted_key ? Convert.base64Url(jwe.encrypted_key).toUint8Array() : undefined; cek = yield JweKeyManagement.decrypt({ key, encryptedKey, joseHeader, keyManager, crypto }); } catch (error) { // If the error is a CryptoError with code "InvalidJwe" or "AlgorithmNotSupported", re-throw. if (error instanceof CryptoError && (error.code === CryptoErrorCode.InvalidJwe || error.code === CryptoErrorCode.AlgorithmNotSupported)) { throw error; } // Otherwise, generate a random CEK and proceed to the next step. // As noted in // {@link https://datatracker.ietf.org/doc/html/rfc7516#section-11.5 | RFC 7516 Section 11.5}, // to mitigate the attacks described in // {@link https://datatracker.ietf.org/doc/html/rfc3218 | RFC 3218}, the recipient MUST NOT // distinguish between format, padding, and length errors of encrypted keys. It is strongly // recommended, in the event of receiving an improperly formatted key, that the recipient // substitute a randomly generated CEK and proceed to the next step, to mitigate timing // attacks. cek = typeof key === 'string' ? yield keyManager.generateKey({ algorithm: joseHeader.enc }) : yield crypto.generateKey({ algorithm: joseHeader.enc }); } // If present, decode the JWE Initialization Vector (IV) and Authentication Tag. const iv = decodeHeaderParam('iv', jwe.iv); const tag = decodeHeaderParam('tag', jwe.tag); // Decode the JWE Ciphertext to a byte array, and if present, append the Authentication Tag. const ciphertext = tag !== undefined ? new Uint8Array([ ...Convert.base64Url(jwe.ciphertext).toUint8Array(), ...(tag !== null && tag !== void 0 ? tag : []) ]) : Convert.base64Url(jwe.ciphertext).toUint8Array(); // If the JWE Additional Authenticated Data (AAD) is present, the Additional Authenticated Data // input to the Content Encryption Algorithm is // ASCII(Encoded Protected Header || '.' || BASE64URL(JWE AAD)). If the JWE AAD is absent, the // Additional Authenticated Data is ASCII(BASE64URL(UTF8(JWE Protected Header))). const additionalData = jwe.aad !== undefined ? new Uint8Array([ ...Convert.string((_a = jwe.protected) !== null && _a !== void 0 ? _a : '').toUint8Array(), ...Convert.string('.').toUint8Array(), ...Convert.string(jwe.aad).toUint8Array() ]) : Convert.string((_b = jwe.protected) !== null && _b !== void 0 ? _b : '').toUint8Array(); // Decrypt the JWE using the Content Encryption Key (CEK) with: // - Key Manager: If the CEK is a Key Identifier. // - Crypto API: If the CEK is a JWK. const plaintext = typeof cek === 'string' ? yield keyManager.decrypt({ keyUri: cek, data: ciphertext, iv, additionalData }) : yield crypto.decrypt({ key: cek, data: ciphertext, iv, additionalData }); return { plaintext, protectedHeader: parsedProtectedHeader, additionalAuthenticatedData: decodeHeaderParam('aad', jwe.aad), sharedUnprotectedHeader: jwe.unprotected, unprotectedHeader: jwe.header }; }); } static encrypt({ key, plaintext, additionalAuthenticatedData, protectedHeader, sharedUnprotectedHeader, unprotectedHeader, keyManager = new LocalKeyManager(), crypto = new AgentCryptoApi(), }) { return __awaiter(this, void 0, void 0, function* () { // Verify that the provided Crypto API supports the decrypt operation before proceeding. if (!isCipher(crypto)) { throw new CryptoError(CryptoErrorCode.OperationNotSupported, 'Crypto API does not support the "encrypt" operation.'); } // Verify that the provided Key Manager supports the decrypt operation before proceeding. if (!isCipher(keyManager)) { throw new CryptoError(CryptoErrorCode.OperationNotSupported, 'Key Manager does not support the "decrypt" operation.'); } // Verify that at least one of the JOSE header objects is present. if (!protectedHeader && !sharedUnprotectedHeader && !unprotectedHeader) { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'JWE is missing the required JOSE header parameters. ' + 'Please provide at least one of the following: "protectedHeader", "sharedUnprotectedHeader", or "unprotectedHeader"'); } // Verify that the Plaintext is present. if (!(plaintext instanceof Uint8Array)) { throw new CryptoError(CryptoErrorCode.InvalidJwe, 'Plaintext is missing or not a byte array.'); } // Per {@link https://www.rfc-editor.org/rfc/rfc7516#section-5.2 | RFC7516 Section 5.2} // the resulting JOSE Header MUST NOT contain duplicate Header Parameter names. In other words, // the same Header Parameter name MUST NOT occur in the `header`, `protected`, and // `unprotected` JSON object values that together comprise the JOSE Header. if (hasDuplicateProperties(protectedHeader, sharedUnprotectedHeader, unprotectedHeader)) { throw new Error('Duplicate properties detected. Please ensure that each parameter is defined only once ' + 'across the JWE "protectedHeader", "sharedUnprotectedHeader", and "unprotectedHeader" objects.'); } // The JOSE Header is the union of the members of the JWE Protected Header (`protectedHeader`), // the JWE Shared Unprotected Header (`sharedUnprotectedHeader`), and the corresponding JWE // Per-Recipient Unprotected Header (`unprotectedHeader`). const joseHeader = Object.assign(Object.assign(Object.assign({}, protectedHeader), sharedUnprotectedHeader), unprotectedHeader); if (!isValidJweHeader(joseHeader)) { throw new Error('JWE Header is missing required "alg" (Algorithm) and/or "enc" (Encryption) Header Parameters'); } const { cek, encryptedKey } = yield JweKeyManagement.encrypt({ key, joseHeader, keyManager, crypto }); // If required for the Content Encryption Algorithm, generate a random JWE Initialization // Vector (IV) of the correct size; otherwise, let the JWE Initialization Vector be the empty // octet sequence. let iv; switch (joseHeader.enc) { case 'A128GCM': case 'A192GCM': case 'A256GCM': iv = CryptoUtils.randomBytes(12); break; default: iv = new Uint8Array(0); } // Compute the Encoded Protected Header value BASE64URL(UTF8(JWE Protected Header)). If the JWE // Protected Header is not present, let this value be the empty string. const encodedProtectedHeader = protectedHeader ? Convert.object(protectedHeader).toBase64Url() : ''; // If the JWE Additional Authenticated Data (AAD) is present, the Additional Authenticated Data // input to the Content Encryption Algorithm is // ASCII(Encoded Protected Header || '.' || BASE64URL(JWE AAD)). If the JWE AAD is absent, the // Additional Authenticated Data is ASCII(BASE64URL(UTF8(JWE Protected Header))). let additionalData; let encodedAad; if (additionalAuthenticatedData) { encodedAad = Convert.uint8Array(additionalAuthenticatedData).toBase64Url(); additionalData = Convert.string(encodedProtectedHeader + '.' + encodedAad).toUint8Array(); } else { additionalData = Convert.string(encodedProtectedHeader).toUint8Array(); } // Encrypt the plaintext using the CEK, the JWE Initialization Vector, and the Additional // Authenticated Data value using the specified content encryption algorithm to create the JWE // Ciphertext value and the JWE Authentication Tag. const ciphertextWithTag = typeof cek === 'string' ? yield keyManager.encrypt({ keyUri: cek, data: plaintext, iv, additionalData }) : yield crypto.encrypt({ key: cek, data: plaintext, iv, additionalData }); const ciphertext = ciphertextWithTag.slice(0, -16); const authenticationTag = ciphertextWithTag.slice(-16); // Create the Flattened JWE JSON Serialization output, which is based upon the General syntax, // but flattens it, optimizing it for the single-recipient case. It flattens it by removing the // "recipients" member and instead placing those members defined for use in the "recipients" // array (the "header" and "encrypted_key" members) in the top-level JSON object (at the same // level as the "ciphertext" member). const jwe = new FlattenedJwe({ ciphertext: Convert.uint8Array(ciphertext).toBase64Url(), }); if (encryptedKey) jwe.encrypted_key = Convert.uint8Array(encryptedKey).toBase64Url(); if (protectedHeader) jwe.protected = encodedProtectedHeader; if (sharedUnprotectedHeader) jwe.unprotected = sharedUnprotectedHeader; if (unprotectedHeader) jwe.header = unprotectedHeader; if (iv) jwe.iv = Convert.uint8Array(iv).toBase64Url(); if (encodedAad) jwe.aad = encodedAad; if (authenticationTag) jwe.tag = Convert.uint8Array(authenticationTag).toBase64Url(); return jwe; }); } } //# sourceMappingURL=jwe-flattened.js.map