UNPKG

@microsoft/useragent-sdk

Version:

SDK for building decentralized identity wallets and enterprise agents.

643 lines 30.5 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); const ProtectionFormat_1 = require("../../../keyStore/ProtectionFormat"); const JweRecipient_1 = require("./JweRecipient"); const typescript_map_1 = require("typescript-map"); const JoseHelpers_1 = require("../JoseHelpers"); const KeyTypeFactory_1 = require("../../../keys/KeyTypeFactory"); const JoseConstants_1 = require("../JoseConstants"); const CryptoHelpers_1 = require("../../../utilities/CryptoHelpers"); const SubtleCryptoExtension_1 = require("../../../plugin/SubtleCryptoExtension"); const base64url_1 = require("base64url"); const CryptoProtocolError_1 = require("../../CryptoProtocolError"); const JoseProtocol_1 = require("../JoseProtocol"); const JoseToken_1 = require("../JoseToken"); /** * Class for containing Jwe token operations. * This class hides the JOSE and crypto library dependencies to allow support for additional crypto algorithms. * Crypto calls always happen via CryptoFactory */ class JweToken { /** * Create an Jwe token object * @param options Set of Jwe token options */ constructor(options) { /** * The protected header. */ this.protected = new typescript_map_1.TSMap(); /** * The unprotected header. */ this.unprotected = new typescript_map_1.TSMap(); /** * The initial vector. */ this.iv = Buffer.from(''); /** * The additional authenticated data. */ this.aad = Buffer.from(''); /** * The encrypted data. */ this.ciphertext = Buffer.from(''); /** * The authentication tag used by GCM. */ this.tag = Buffer.from(''); /** * Signatures on content */ this.recipients = []; /** * Get the request serialization format */ this.format = ProtectionFormat_1.ProtectionFormat.JweGeneralJson; this.options = options; } //#region serialization /** * Serialize a Jwe token object from a token * @param format Optional specify the serialization format. If not specified, use default format. */ serialize(format) { if (format === undefined) { format = this.format; } switch (format) { case ProtectionFormat_1.ProtectionFormat.JweGeneralJson: return JweToken.serializeJweGeneralJson(this); case ProtectionFormat_1.ProtectionFormat.JweCompactJson: return JweToken.serializeJweCompact(this); case ProtectionFormat_1.ProtectionFormat.JweFlatJson: return JweToken.serializeJweFlatJson(this); } throw new CryptoProtocolError_1.default(JoseConstants_1.default.Jwe, `The format '${this.format}' is not supported`); } /** * Serialize a Jwe token object from a token in General Json format * @param token Jwe base object */ static serializeJweGeneralJson(token) { let json = { recipients: [], aad: base64url_1.default.encode(token.aad), iv: base64url_1.default.encode(token.iv), ciphertext: base64url_1.default.encode(token.ciphertext), tag: base64url_1.default.encode(token.tag) }; if (JoseHelpers_1.default.headerHasElements(token.protected)) { json.protected = JoseHelpers_1.default.encodeHeader(token.protected); } if (JoseHelpers_1.default.headerHasElements(token.unprotected)) { json.unprotected = JoseHelpers_1.default.encodeHeader(token.unprotected, false); } for (let inx = 0; inx < token.recipients.length; inx++) { const recipient = { encrypted_key: base64url_1.default.encode(token.recipients[inx].encrypted_key) }; if (JoseHelpers_1.default.headerHasElements(token.recipients[inx].header)) { recipient.header = JoseHelpers_1.default.encodeHeader(token.recipients[inx].header, false); } json.recipients.push(recipient); } return JSON.stringify(json); } /** * Serialize a Jwe token object from a token in Flat Json format * @param token Jwe base object */ static serializeJweFlatJson(token) { let json = { encrypted_key: base64url_1.default.encode(token.recipients[0].encrypted_key), aad: base64url_1.default.encode(token.aad), iv: base64url_1.default.encode(token.iv), ciphertext: base64url_1.default.encode(token.ciphertext), tag: base64url_1.default.encode(token.tag) }; if (JoseHelpers_1.default.headerHasElements(token.protected)) { json.protected = JoseHelpers_1.default.encodeHeader(token.protected); } if (JoseHelpers_1.default.headerHasElements(token.unprotected)) { json.unprotected = JoseHelpers_1.default.encodeHeader(token.unprotected); } if (JoseHelpers_1.default.headerHasElements(token.recipients[0].header)) { json.header = JoseHelpers_1.default.encodeHeader(token.recipients[0].header, false); } return JSON.stringify(json); } /** * Serialize a Jwe token object from a token in Compact format * @param token Jwe base object */ static serializeJweCompact(token) { let encodedProtected = ''; if (JoseHelpers_1.default.headerHasElements(token.protected)) { encodedProtected = JoseHelpers_1.default.encodeHeader(token.protected); } const encryptedKey = base64url_1.default.encode(token.recipients[0].encrypted_key); const iv = base64url_1.default.encode(token.iv); const cipher = base64url_1.default.encode(token.ciphertext); const tag = base64url_1.default.encode(token.tag); return `${encodedProtected}.${encryptedKey}.${iv}.${cipher}.${tag}`; } //#endregion //#region deserialization /** * Deserialize a Jwe token object */ static deserialize(token, options) { const jweToken = new JweToken(options); // check for JWE compact format if (typeof token === 'string') { const parts = token.split('.'); if (parts.length === 5) { jweToken.protected = JweToken.setProtected(parts[0]); const recipient = new JweRecipient_1.default(); recipient.encrypted_key = base64url_1.default.toBuffer(parts[1]); jweToken.recipients = [recipient]; jweToken.ciphertext = base64url_1.default.toBuffer(parts[3]); jweToken.iv = base64url_1.default.toBuffer(parts[2]); jweToken.tag = base64url_1.default.toBuffer(parts[4]); jweToken.aad = base64url_1.default.toBuffer(parts[0]); return jweToken; } } else { throw new CryptoProtocolError_1.default(JoseConstants_1.default.Jwe, `The presented object is not deserializable.`); } // Flat or general format let jsonObject; try { jsonObject = JSON.parse(token); } catch (error) { throw new CryptoProtocolError_1.default(JoseConstants_1.default.Jwe, `The presented object is not deserializable and is no compact format.`); } // Try to handle token as IJweGeneralJSon let decodeStatus = jweToken.setGeneralParts(jsonObject); if (decodeStatus.result) { return jweToken; } else { console.debug(`Failed parsing as IJweGeneralJSon. Reason: ${decodeStatus.reason}`); } // Try to handle token as IJweFlatJson decodeStatus = jweToken.setFlatParts(jsonObject); if (decodeStatus.result) { return jweToken; } else { console.debug(`Failed parsing as IJweFlatJson. Reason: ${decodeStatus.reason}`); } throw new CryptoProtocolError_1.default(JoseConstants_1.default.Jwe, `The content does not represent a valid jwe token`); } /** * Try to parse the input token and set the properties of this JswToken * @param content Alledged IJweGeneralJSon token * @returns true if valid token was parsed */ setGeneralParts(content) { if (content) { if (content.recipients) { this.ciphertext = base64url_1.default.toBuffer(content.ciphertext); this.aad = base64url_1.default.toBuffer(content.aad); this.iv = base64url_1.default.toBuffer(content.iv); if (content.protected) { this.protected = JweToken.setProtected(content.protected); } if (content.unprotected && content.unprotected !== 'e30') { this.unprotected = JweToken.setUnprotected(content.unprotected); } this.tag = base64url_1.default.toBuffer(content.tag); this.recipients = []; for (let inx = 0; inx < content.recipients.length; inx++) { const recipient = new JweRecipient_1.default(); recipient.encrypted_key = base64url_1.default.toBuffer(content.recipients[inx].encrypted_key); if (content.recipients[inx].header) { recipient.header = JweToken.setUnprotected(content.recipients[inx].header); } this.recipients.push(recipient); } } else { // manadatory field return { result: false, reason: 'missing recipients' }; } return this.isValidToken(); } return { result: false, reason: 'no content passed' }; } /** * Try to parse the input token and set the properties of this JswToken * @param content Alledged IJweFlatJson token * @returns true if valid token was parsed */ setFlatParts(content) { if (content) { const recipient = new JweRecipient_1.default(); if (content.ciphertext) { this.ciphertext = base64url_1.default.toBuffer(content.ciphertext); } else { // manadatory field return { result: false, reason: 'missing ciphertext' }; } if (content.encrypted_key) { recipient.encrypted_key = base64url_1.default.toBuffer(content.encrypted_key); } else { // manadatory field return { result: false, reason: 'missing encrypted_key' }; } if (content.iv) { this.iv = base64url_1.default.toBuffer(content.iv); } else { // manadatory field return { result: false, reason: 'missing iv' }; } if (content.aad) { this.aad = base64url_1.default.toBuffer(content.aad); } if (JoseHelpers_1.default.headerHasElements(content.protected)) { this.protected = JweToken.setProtected(content.protected); } if (JoseHelpers_1.default.headerHasElements(content.unprotected)) { this.unprotected = JweToken.setUnprotected(content.unprotected); } if (JoseHelpers_1.default.headerHasElements(content.header)) { recipient.header = JweToken.setUnprotected(content.header); } this.recipients = [recipient]; return this.isValidToken(); } return { result: false, reason: 'no content passed' }; } /** * Check if a valid token was found after decoding */ isValidToken() { if (!this.ciphertext) { return { result: false, reason: 'missing ciphertext' }; } if (!this.iv) { return { result: false, reason: 'missing iv' }; } if (!this.recipients) { return { result: false, reason: 'missing recipients' }; } if (this.recipients.length == 0) { return { result: false, reason: 'recipients array is empty' }; } for (let inx = 0; inx < this.recipients.length; inx++) { const recipient = this.recipients[inx]; if (!recipient.encrypted_key) { return { result: false, reason: `recipient ${inx} is missing encrypted_key` }; } if (!this.protected && !recipient.header) { return { result: false, reason: `recipient ${inx} is missing header and protected is also missing` }; } } return { result: true, reason: '' }; } //#endregion //#region options section /** * Get the CryptoFactory to be used * @param newOptions Options passed in after the constructure * @param manadatory True if property needs to be defined */ getCryptoFactory(newOptions, manadatory = true) { return JoseHelpers_1.default.getOptionsProperty('cryptoFactory', this.options, newOptions, manadatory); } /** * Get the key encryption key for testing * @param newOptions Options passed in after the constructure * @param manadatory True if property needs to be defined */ getContentEncryptionKey(newOptions, manadatory = true) { return JoseHelpers_1.default.getOptionsProperty('contentEncryptionKey', this.options, newOptions, manadatory); } /** * Get the initial vector for testing * @param newOptions Options passed in after the constructure * @param manadatory True if property needs to be defined */ getInitialVector(newOptions, manadatory = true) { return JoseHelpers_1.default.getOptionsProperty('initialVector', this.options, newOptions, manadatory); } /** * Get the content encryption algorithm from the options * @param newOptions Options passed in after the constructure * @param manadatory True if property needs to be defined */ getContentEncryptionAlgorithm(newOptions, manadatory = true) { return JoseHelpers_1.default.getOptionsProperty('contentEncryptionAlgorithm', this.options, newOptions, manadatory); } //#endregion //#region encryption functions /** * Encrypt content using the given public keys in JWK format. * The key type enforces the key encryption algorithm. * The options can override certain algorithm choices. * * @param recipients List of recipients' public keys. * @param payload to encrypt. * @param format of the final serialization. * @param options used for the signature. These options override the options provided in the constructor. * @returns JweToken with encrypted payload. */ // tslint:disable-next-line: max-func-body-length async encrypt(recipients, payload, format, options) { const cryptoFactory = this.getCryptoFactory(options); const contentEncryptionAlgorithm = this.getContentEncryptionAlgorithm(options, false) || JoseConstants_1.default.AesGcm256; this.format = format; // encoded protected header let encodedProtected = ''; // Set the resulting token const jweToken = new JweToken(options || this.options); // Get the encryptor extensions const encryptor = new SubtleCryptoExtension_1.default(cryptoFactory); let randomGenerator = CryptoHelpers_1.default.jwaToWebCrypto(contentEncryptionAlgorithm); const generator = CryptoHelpers_1.default.getSubtleCryptoForAlgorithm(cryptoFactory, randomGenerator); // Set the content encryption key let contentEncryptionKey = this.getContentEncryptionKey(options, false); if (!contentEncryptionKey) { const key = await generator.generateKey(randomGenerator, true, ['encrypt']); const jwk = await generator.exportKey('jwk', key); contentEncryptionKey = base64url_1.default.toBuffer(jwk.k); } // Set the initial vector jweToken.iv = this.getInitialVector(options, false); if (!jweToken.iv) { const key = await generator.generateKey(randomGenerator, true, ['encrypt']); const jwk = await generator.exportKey('jwk', key); jweToken.iv = base64url_1.default.toBuffer(jwk.k); } // Needs to be improved when alg is not provided. // Decide key encryption algorithm based on given JWK. let publicKey = recipients[0]; for (let key of recipients) { if (key.alg === JoseConstants_1.default.RsaOaep256) { publicKey = key; break; } } let keyEncryptionAlgorithm = publicKey.alg; if (!keyEncryptionAlgorithm) { if (publicKey.kty == KeyTypeFactory_1.KeyType.EC) { throw new Error('EC encryption not implemented'); } else { // Default RSA algorithm keyEncryptionAlgorithm = JoseConstants_1.default.RsaOaep256; } } // tslint:disable: no-backbone-get-set-outside-model jweToken.unprotected = this.getUnprotected(options) || new typescript_map_1.TSMap(); jweToken.protected = this.getProtected(options) || new typescript_map_1.TSMap(); jweToken.protected.set(JoseConstants_1.default.Alg, keyEncryptionAlgorithm); jweToken.protected.set(JoseConstants_1.default.Enc, contentEncryptionAlgorithm); if (publicKey.kid) { jweToken.protected.set(JoseConstants_1.default.Kid, publicKey.kid); } encodedProtected = JoseHelpers_1.default.headerHasElements(jweToken.protected) ? JoseHelpers_1.default.encodeHeader(jweToken.protected) : ''; // Set aad as the protected header jweToken.aad = base64url_1.default.toBuffer(encodedProtected); for (let inx = 0; inx < recipients.length; inx++) { // Set the recipients structure const jweRecipient = new JweRecipient_1.default(); jweToken.recipients.push(jweRecipient); // Decide key encryption algorithm based on given JWK. publicKey = recipients[inx]; // Get key encrypter and encrypt the content key jweRecipient.encrypted_key = Buffer.from(await encryptor.encryptByJwk(CryptoHelpers_1.default.jwaToWebCrypto(keyEncryptionAlgorithm), publicKey, contentEncryptionKey)); const header = new typescript_map_1.TSMap([ [JoseConstants_1.default.Kid, publicKey.kid] ]); jweRecipient.header = JweToken.setHeader(header); } // encrypt content const contentEncryptorKey = { k: base64url_1.default.encode(contentEncryptionKey), alg: contentEncryptionAlgorithm, kty: 'oct' }; const encodedAad = base64url_1.default.encode(jweToken.aad); const cipherText = await encryptor.encryptByJwk(CryptoHelpers_1.default.jwaToWebCrypto(contentEncryptionAlgorithm, new Uint8Array(jweToken.iv), new Uint8Array(Buffer.from(encodedAad))), contentEncryptorKey, Buffer.from(payload)); jweToken.tag = Buffer.from(cipherText, cipherText.byteLength - 16); jweToken.ciphertext = Buffer.from(cipherText, 0, cipherText.byteLength - 16); return jweToken; } //#endregion //#region decryption functions /** * Decrypt the content. * * @param decryptionKeyReference Reference to the decryption key. * @param options used for the signature. These options override the options provided in the constructor. * @returns Signed payload in compact Jwe format. */ // tslint:disable-next-line: max-func-body-length async decrypt(decryptionKeyReference, options) { const cryptoFactory = this.getCryptoFactory(options); // Get the encryptor extensions const decryptor = new SubtleCryptoExtension_1.default(cryptoFactory); // get decryption public key let jwk = (await cryptoFactory.keyStore.get(decryptionKeyReference, true)).getKey(); // Get the encrypted key // Check if kid matches let contentEncryptionKey; if (jwk.kid) { for (let inx = 0; inx < this.recipients.length; inx++) { const recipient = this.recipients[inx]; if (recipient.header) { const headerKid = recipient.header.get(JoseConstants_1.default.Kid); if (headerKid && headerKid === jwk.kid) { if (contentEncryptionKey = await this.decryptContentEncryptionKey(recipient, decryptor, decryptionKeyReference)) { break; } } } } } if (!contentEncryptionKey) { // try to decrypt every key for (let inx = 0, length = this.recipients.length; inx < length; inx++) { const recipient = this.recipients[inx]; if (contentEncryptionKey = await this.decryptContentEncryptionKey(recipient, decryptor, decryptionKeyReference)) { break; } } } if (!contentEncryptionKey) { throw new CryptoProtocolError_1.default(JoseConstants_1.default.Jwe, 'Cannot decrypt the content encryption key because of missing key'); } // Decrypt content const contentEncryptionAlgorithm = this.protected.get(JoseConstants_1.default.Enc); const iv = new Uint8Array(this.iv); const encodedAad = base64url_1.default.encode(this.aad); const aad = new Uint8Array(Buffer.from(encodedAad)); const algorithm = CryptoHelpers_1.default.jwaToWebCrypto(contentEncryptionAlgorithm, iv, aad); const contentJwk = { kty: 'oct', alg: contentEncryptionAlgorithm, k: base64url_1.default.encode(contentEncryptionKey) }; const plaintext = await decryptor.decryptByJwk(algorithm, contentJwk, Buffer.concat([this.ciphertext, this.tag])); return Buffer.from(plaintext); } async decryptContentEncryptionKey(recipient, decryptor, decryptionKeyReference) { let keyDecryptionAlgorithm = ''; if (!recipient.header) { keyDecryptionAlgorithm = this.protected.get(JoseConstants_1.default.Alg); } else { keyDecryptionAlgorithm = recipient.header.get(JoseConstants_1.default.Alg) || this.protected.get(JoseConstants_1.default.Alg); } const algorithm = CryptoHelpers_1.default.jwaToWebCrypto(keyDecryptionAlgorithm); return Buffer.from(await decryptor.decryptByKeyStore(algorithm, decryptionKeyReference, recipient.encrypted_key)); } //#endregion /** * Get the default protected header to be used from the options * @param newOptions Options passed in after the constructure * @param mandatory True if property needs to be defined */ getProtected(newOptions, mandatory = false) { return JoseHelpers_1.default.getOptionsProperty('protected', this.options, newOptions, mandatory); } /** * Get the default header to be used from the options * @param newOptions Options passed in after the constructure * @param mandatory True if property needs to be defined */ getUnprotected(newOptions, mandatory = false) { return JoseHelpers_1.default.getOptionsProperty('unprotected', this.options, newOptions, mandatory); } /** * Get the default header to be used from the options * @param newOptions Options passed in after the constructure * @param mandatory True if property needs to be defined */ getHeader(newOptions, mandatory = false) { return JoseHelpers_1.default.getOptionsProperty('header', this.options, newOptions, mandatory); } /** * Convert a @class ICryptoToken into a @class JweToken * @param cryptoToken to convert * @param protectOptions options for the token */ static fromCryptoToken(cryptoToken, protectOptions) { const options = JweToken.fromPayloadProtectionOptions(protectOptions); const token = new JweToken(options); token.protected = cryptoToken.has(JoseConstants_1.default.tokenProtected) ? cryptoToken.get(JoseConstants_1.default.tokenProtected) : undefined; token.unprotected = cryptoToken.has(JoseConstants_1.default.tokenUnprotected) ? cryptoToken.get(JoseConstants_1.default.tokenUnprotected) : undefined; token.format = cryptoToken.get(JoseConstants_1.default.tokenFormat); token.aad = cryptoToken.get(JoseConstants_1.default.tokenAad); token.iv = cryptoToken.get(JoseConstants_1.default.tokenIv); token.ciphertext = cryptoToken.get(JoseConstants_1.default.tokenCiphertext); token.tag = cryptoToken.get(JoseConstants_1.default.tokenTag); token.recipients = cryptoToken.get(JoseConstants_1.default.tokenRecipients); return token; } /** * Convert a @class JweToken into a @class ICryptoToken * @param protocolFormat format of the token * @param jweToken to convert * @param options used for the encryption. These options override the options provided in the constructor. */ static toCryptoToken(protocolFormat, jweToken, options) { const cryptoToken = new JoseToken_1.default(options); if (jweToken.protected) { cryptoToken.set(JoseConstants_1.default.tokenProtected, jweToken.protected); } if (jweToken.unprotected) { cryptoToken.set(JoseConstants_1.default.tokenUnprotected, jweToken.unprotected); } cryptoToken.set(JoseConstants_1.default.tokenFormat, protocolFormat); cryptoToken.set(JoseConstants_1.default.tokenAad, jweToken.aad); cryptoToken.set(JoseConstants_1.default.tokenIv, jweToken.iv); cryptoToken.set(JoseConstants_1.default.tokenCiphertext, jweToken.ciphertext); cryptoToken.set(JoseConstants_1.default.tokenTag, jweToken.tag); cryptoToken.set(JoseConstants_1.default.tokenRecipients, jweToken.recipients); return cryptoToken; } /** * Convert a @class IPayloadProtectionOptions into a @class IJweEncryptionOptions * @param protectOptions to convert */ static fromPayloadProtectionOptions(protectOptions) { return { cryptoFactory: protectOptions.cryptoFactory, protected: protectOptions.options.has(JoseConstants_1.default.optionProtectedHeader) ? protectOptions.options.get(JoseConstants_1.default.optionProtectedHeader) : undefined, header: protectOptions.options.has(JoseConstants_1.default.optionHeader) ? protectOptions.options.get(JoseConstants_1.default.optionHeader) : undefined, kidPrefix: protectOptions.options.has(JoseConstants_1.default.optionKidPrefix) ? protectOptions.options.get(JoseConstants_1.default.optionKidPrefix) : undefined, contentEncryptionAlgorithm: protectOptions.options.get(JoseConstants_1.default.optionContentEncryptionAlgorithm) }; } /** * Convert a @class IPayloadProtectionOptions into a @class IJweEncryptionOptions * @param encryptionOptions to convert */ static toPayloadProtectionOptions(encryptionOptions) { const protectOptions = { cryptoFactory: encryptionOptions.cryptoFactory, payloadProtection: new JoseProtocol_1.default(), options: new typescript_map_1.TSMap(), contentEncryptionAlgorithm: encryptionOptions.contentEncryptionAlgorithm }; if (encryptionOptions.header) { protectOptions.options.set(JoseConstants_1.default.optionHeader, encryptionOptions.header); } if (encryptionOptions.protected) { protectOptions.options.set(JoseConstants_1.default.optionProtectedHeader, encryptionOptions.protected); } if (encryptionOptions.kidPrefix) { protectOptions.options.set(JoseConstants_1.default.optionKidPrefix, encryptionOptions.kidPrefix); } return protectOptions; } /** * Set the header in the recipients object * @param header to set on the JweToken recipients object */ static setHeader(header) { if (typeof header === 'string') { return new typescript_map_1.TSMap().fromJSON(JSON.parse(header)); } return header; } /** * Set the unprotected header * @param unprotectedHeader to set on the JweToken object */ static setUnprotected(unprotectedHeader) { if (typeof unprotectedHeader === 'string') { return new typescript_map_1.TSMap().fromJSON(JSON.parse(unprotectedHeader)); } return unprotectedHeader; } /** * Set the protected header * @param protectedHeader to set on the JweToken object */ static setProtected(protectedHeader) { if (typeof protectedHeader === 'string') { const json = base64url_1.default.decode(protectedHeader); return new typescript_map_1.TSMap().fromJSON(JSON.parse(json)); } return protectedHeader; } } exports.default = JweToken; //# sourceMappingURL=JweToken.js.map