@web5/agent
Version:
294 lines • 17.8 kB
JavaScript
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