UNPKG

@digitalcredentials/minimal-cipher

Version:
137 lines (122 loc) 4.2 kB
/*! * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. */ import * as base64url from 'base64url-universal'; import * as fipsAlgorithm from './algorithms/fips.js'; import * as recAlgorithm from './algorithms/recommended.js'; import {stringToUint8Array} from './util.js'; // support `C20P` for backwards compatibility import * as c20p from './algorithms/c20p.js'; const CIPHER_ALGORITHMS = { [fipsAlgorithm.cipher.JWE_ENC]: fipsAlgorithm.cipher, [recAlgorithm.cipher.JWE_ENC]: recAlgorithm.cipher, // backwards compatibility for decryption only (*not* encryption) [c20p.JWE_ENC]: c20p }; // only supported key algorithm const KEY_ALGORITHM = 'ECDH-ES+A256KW'; export class DecryptTransformer { constructor({ keyAgreement, keyAgreementKey } = {}) { if(!keyAgreement) { throw new TypeError('"keyAgreement" is a required parameter.'); } if(!keyAgreementKey) { throw new TypeError('"keyAgreementKey" is a required parameter.'); } this.keyAgreement = keyAgreement; this.keyAgreementKey = keyAgreementKey; } async transform(chunk, controller) { // assumes `chunk` is an object with a JWE under the `jwe` property if(!(chunk && typeof chunk === 'object')) { throw new TypeError('"chunk" must be an object.'); } const {jwe} = chunk; const data = await this.decrypt(jwe); if(data === null) { const error = new Error('Invalid decryption key.'); error.name = 'DataError'; throw error; } controller.enqueue(data); } async decrypt(jwe) { // validate JWE if(!(jwe && typeof jwe === 'object')) { throw new TypeError('"jwe" must be an object.'); } if(typeof jwe.protected !== 'string') { throw new TypeError('"jwe.protected" is missing or not a string.'); } if(typeof jwe.iv !== 'string') { throw new Error('Invalid or missing "iv".'); } if(typeof jwe.ciphertext !== 'string') { throw new Error('Invalid or missing "ciphertext".'); } if(typeof jwe.tag !== 'string') { throw new Error('Invalid or missing "tag".'); } // validate encryption header let header; let additionalData; try { // ASCII(BASE64URL(UTF8(JWE Protected Header))) additionalData = stringToUint8Array(jwe.protected); header = JSON.parse(new TextDecoder().decode( base64url.decode(jwe.protected))); } catch(e) { throw new Error('Invalid JWE "protected" header.'); } if(!(header.enc && typeof header.enc === 'string')) { throw new Error('Invalid JWE "enc" header.'); } const cipher = CIPHER_ALGORITHMS[header.enc]; if(!cipher) { throw new Error('Unsupported encryption algorithm "${header.enc}".'); } if(!Array.isArray(jwe.recipients)) { throw new TypeError('"jwe.recipients" must be an array.'); } // find `keyAgreementKey` matching recipient const {keyAgreementKey} = this; const recipient = _findRecipient(jwe.recipients, keyAgreementKey); if(!recipient) { throw new Error('No matching recipient found for key agreement key.'); } // get wrapped CEK const {encrypted_key: wrappedKey} = recipient; if(typeof wrappedKey !== 'string') { throw new Error('Invalid or missing "encrypted_key".'); } // TODO: consider a cache of encrypted_key => CEKs to reduce unwrapping // calls which may even need to hit the network (e.g., Web KMS) // derive KEK and unwrap CEK const {epk} = recipient.header; const {keyAgreement} = this; const {kek} = await keyAgreement.kekFromEphemeralPeer( {keyAgreementKey, epk}); const cek = await kek.unwrapKey({wrappedKey}); if(!cek) { // failed to unwrap key return null; } // decrypt content const {ciphertext, iv, tag} = jwe; return cipher.decrypt({ ciphertext: base64url.decode(ciphertext), iv: base64url.decode(iv), tag: base64url.decode(tag), additionalData, cek }); } } function _findRecipient(recipients, key) { return recipients.find( e => e.header && e.header.kid === key.id && (!key.algorithm && e.header.alg === KEY_ALGORITHM)); }