UNPKG

@microsoft/useragent-sdk

Version:

SDK for building decentralized identity wallets and enterprise agents.

523 lines 23.9 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 base64url_1 = require("base64url"); const ProtectionFormat_1 = require("../../../keyStore/ProtectionFormat"); const CryptoHelpers_1 = require("../../../utilities/CryptoHelpers"); const SubtleCryptoExtension_1 = require("../../../plugin/SubtleCryptoExtension"); const JwsSignature_1 = require("./JwsSignature"); const typescript_map_1 = require("typescript-map"); const JoseHelpers_1 = require("../JoseHelpers"); const JoseConstants_1 = require("../JoseConstants"); const CryptoProtocolError_1 = require("../../CryptoProtocolError"); const JoseProtocol_1 = require("../JoseProtocol"); const JoseToken_1 = require("../JoseToken"); /** * Class for containing JWS 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 JwsToken { /** * Create an Jws token object * @param options Set of jws token options */ constructor(options) { /** * Payload (base64url encoded) */ this.payload = Buffer.from(''); /** * Signatures on content */ this.signatures = []; /** * Get the request serialization format */ this.format = ProtectionFormat_1.ProtectionFormat.JwsGeneralJson; this.options = options; } //#region serialization /** * Serialize a Jws 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.JwsGeneralJson: return JwsToken.serializeJwsGeneralJson(this); case ProtectionFormat_1.ProtectionFormat.JwsCompactJson: return JwsToken.serializeJwsCompact(this); case ProtectionFormat_1.ProtectionFormat.JwsFlatJson: return JwsToken.serializeJwsFlatJson(this); } throw new CryptoProtocolError_1.default(JoseConstants_1.default.Jws, `The format '${this.format}' is not supported`); } /** * Serialize a Jws token object from a token in General Json format * @param token JWS base object */ static serializeJwsGeneralJson(token) { const jws = { payload: base64url_1.default.encode(token.payload), signatures: [] }; for (let inx = 0; inx < token.signatures.length; inx++) { const tokenSignature = token.signatures[inx]; const jwsSignature = { signature: base64url_1.default.encode(tokenSignature.signature) }; if (JoseHelpers_1.default.headerHasElements(tokenSignature.protected)) { jwsSignature.protected = JoseHelpers_1.default.encodeHeader(tokenSignature.protected); } if (JoseHelpers_1.default.headerHasElements(tokenSignature.header)) { jwsSignature.header = JoseHelpers_1.default.encodeHeader(tokenSignature.header, false); } if (!jwsSignature.protected && !jwsSignature.header) { throw new CryptoProtocolError_1.default(JoseConstants_1.default.Jws, `Signature ${inx} is missing header and protected`); } jws.signatures.push(jwsSignature); } return JSON.stringify(jws); } /** * Serialize a Jws token object from a token in Flat Json format * @param token JWS base object */ static serializeJwsFlatJson(token) { const jws = { payload: base64url_1.default.encode(token.payload) }; if (JoseHelpers_1.default.headerHasElements(token.signatures[0].protected)) { jws.protected = JoseHelpers_1.default.encodeHeader(token.signatures[0].protected); } if (JoseHelpers_1.default.headerHasElements(token.signatures[0].header)) { jws.header = token.signatures[0].header; } jws.signature = base64url_1.default.encode(token.signatures[0].signature); return JSON.stringify(jws); } /** * Serialize a Jws token object from a token in Compact format * @param token JWS base object */ static serializeJwsCompact(token) { let encodedProtected = ''; if (JoseHelpers_1.default.headerHasElements(token.signatures[0].protected)) { encodedProtected = JoseHelpers_1.default.encodeHeader(token.signatures[0].protected); } const encodedpayload = base64url_1.default.encode(token.payload); const encodedSignature = base64url_1.default.encode(token.signatures[0].signature); return `${encodedProtected}.${encodedpayload}.${encodedSignature}`; } //#endregion //#region deserialization /** * Deserialize a Jws token object */ static deserialize(token, options) { const jwsToken = new JwsToken(options); // check for JWS compact format if (typeof token === 'string') { const parts = token.split('.'); if (parts.length === 3) { jwsToken.payload = base64url_1.default.toBuffer(parts[1]); const signature = new JwsSignature_1.default(); signature.protected = jwsToken.setProtected(parts[0]); signature.signature = base64url_1.default.toBuffer(parts[2]); jwsToken.signatures = [signature]; return jwsToken; } } else { throw new CryptoProtocolError_1.default(JoseConstants_1.default.Jws, `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.Jws, `The presented object is not deserializable and is no compact format.`); } // set payload jwsToken.payload = base64url_1.default.toBuffer(jsonObject.payload); // Try to handle token as IJwsGeneralJSon let decodeStatus = jwsToken.setGeneralParts(jsonObject); if (decodeStatus.result) { return jwsToken; } else { console.debug(`Failed parsing as IJwsGeneralJSon. Reason: ${decodeStatus.reason}`); } // Try to handle token as IJwsFlatJson decodeStatus = jwsToken.setFlatParts(jsonObject); if (decodeStatus.result) { return jwsToken; } else { console.debug(`Failed parsing as IJwsFlatJson. Reason: ${decodeStatus.reason}`); } // If this point is reached we have not been passed a usable JWS token. throw new CryptoProtocolError_1.default(JoseConstants_1.default.Jws, 'The provided token is not a valid JWS token.'); } /** * Try to parse the input token and set the properties of this JswToken * @param content Alledged IJwsGeneralJSon token * @returns true if valid token was parsed */ setGeneralParts(content) { if (content) { if (content.payload) { this.payload = base64url_1.default.toBuffer(content.payload); } else { // manadatory field return { result: false, reason: 'missing payload' }; } if (!content.signatures) { // manadatory field return { result: false, reason: 'missing signatures' }; } this.signatures = []; for (let inx = 0; inx < content.signatures.length; inx++) { const jwsSignature = new JwsSignature_1.default(); jwsSignature.signature = base64url_1.default.toBuffer(content.signatures[inx].signature); if (content.signatures[inx].header) { jwsSignature.header = this.setHeader(content.signatures[inx].header); } if (content.signatures[inx].protected) { jwsSignature.protected = this.setProtected(content.signatures[inx].protected); } this.signatures.push(jwsSignature); } 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 IJwsFlatJson token * @returns true if valid token was parsed */ setFlatParts(content) { if (content) { const signature = new JwsSignature_1.default(); if (content.signature) { signature.signature = base64url_1.default.toBuffer(content.signature); } else { // manadatory field return { result: false, reason: 'missing signature' }; } if (JoseHelpers_1.default.headerHasElements(content.protected)) { signature.protected = this.setProtected(content.protected); } if (JoseHelpers_1.default.headerHasElements(content.header)) { signature.header = this.setHeader(JSON.stringify(content.header)); } if (content.payload) { this.payload = base64url_1.default.toBuffer(content.payload); } else { // manadatory field return { result: false, reason: 'missing payload' }; } this.signatures = [signature]; return this.isValidToken(); } return { result: false, reason: 'no content passed' }; } /** * Check if a valid token was found after decoding */ isValidToken() { if (!this.payload) { return { result: false, reason: 'missing payload' }; } if (!this.signatures) { return { result: false, reason: 'missing signatures' }; } const noOfSignatures = this.signatures.length; if (noOfSignatures === 0) { return { result: false, reason: 'signatures array is empty' }; } for (let inx = 0; inx < noOfSignatures; inx++) { const signature = this.signatures[inx]; if (!signature.signature) { return { result: false, reason: `signature ${inx} is missing signature` }; } if (!signature.header && !signature.protected) { return { result: false, reason: `signature ${inx} is missing header and protected` }; } } return { result: true, reason: '' }; } //#endregion /** * Get the keyStore to be used * @param newOptions Options passed in after the constructure * @param mandatory True if property needs to be defined */ getKeyStore(newOptions, mandatory = true) { return this.getCryptoFactory(newOptions, mandatory).keyStore; } /** * Get the CryptoFactory to be used * @param newOptions Options passed in after the constructure * @param mandatory True if property needs to be defined */ getCryptoFactory(newOptions, mandatory = true) { return JoseHelpers_1.default.getOptionsProperty('cryptoFactory', this.options, newOptions, mandatory); } /** * 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 */ getHeader(newOptions, mandatory = false) { return JoseHelpers_1.default.getOptionsProperty('header', this.options, newOptions, mandatory); } /** * Signs contents using the given private key in JWK format. * * @param signingKeyReference Reference to the signing key. * @param payload to sign. * @param format of the final signature. * @param options used for the signature. These options override the options provided in the constructor. * @returns Signed payload in compact JWS format. */ async sign(signingKeyReference, payload, format, options) { const keyStore = this.getKeyStore(options); const cryptoFactory = this.getCryptoFactory(options); // tslint:disable-next-line:no-suspicious-comment // TODO support for multiple signatures const jwsSignature = new JwsSignature_1.default(); // Set payload const jwsToken = new JwsToken(this.options); // Get signing key public key const jwk = (await keyStore.get(signingKeyReference, true)).getKey(); const jwaAlgorithm = jwk.alg || JoseConstants_1.default.DefaultSigningAlgorithm; const algorithm = CryptoHelpers_1.default.jwaToWebCrypto(jwaAlgorithm); // Steps according to RTC7515 5.1 // 2. Compute encoded payload value base64URL(JWS Payload) jwsToken.payload = payload; const encodedContent = base64url_1.default.encode(payload); // 3. Compute the headers. jwsSignature.header = this.getHeader(options) || new typescript_map_1.TSMap(); jwsSignature.protected = this.getProtected(options) || new typescript_map_1.TSMap(); // Check if header specifies certain constants // If defined with no value, the value will be placed in. let algInHeader = jwsSignature.header.has(JoseConstants_1.default.Alg); if (algInHeader) { if (!jwsSignature.header.get(JoseConstants_1.default.Alg)) { jwsSignature.header.set(JoseConstants_1.default.Alg, jwaAlgorithm); } } else { jwsSignature.protected.set(JoseConstants_1.default.Alg, jwaAlgorithm); } let kidInHeader = jwsSignature.header.has(JoseConstants_1.default.Kid); if (kidInHeader) { if (!jwsSignature.header.get(JoseConstants_1.default.Kid)) { if (jwk.kid) { jwsSignature.header.set(JoseConstants_1.default.Kid, jwk.kid); } else { jwsSignature.header.delete(JoseConstants_1.default.Kid); } } } else { if (jwk.kid) { jwsSignature.header.set(JoseConstants_1.default.Kid, jwk.kid); } } algInHeader = jwsSignature.protected.has(JoseConstants_1.default.Alg); if (algInHeader) { if (!jwsSignature.protected.get(JoseConstants_1.default.Alg)) { jwsSignature.protected.set(JoseConstants_1.default.Alg, jwaAlgorithm); } } else { jwsSignature.protected.set(JoseConstants_1.default.Alg, jwaAlgorithm); } kidInHeader = jwsSignature.protected.has(JoseConstants_1.default.Kid); if (kidInHeader) { if (!jwsSignature.protected.get(JoseConstants_1.default.Kid)) { if (jwk.kid) { jwsSignature.protected.set(JoseConstants_1.default.Kid, jwk.kid); } else { jwsSignature.protected.delete(JoseConstants_1.default.Kid); } } } else { if (jwk.kid) { jwsSignature.protected.set(JoseConstants_1.default.Kid, jwk.kid); } } const protectedUsed = JoseHelpers_1.default.headerHasElements(jwsSignature.protected); // 4. Compute BASE64URL(UTF8(JWS Header)) const encodedProtected = !protectedUsed ? '' : JoseHelpers_1.default.encodeHeader(jwsSignature.protected); // 5. Compute the signature using data ASCII(BASE64URL(UTF8(JWS protected Header))) || . || . BASE64URL(JWS Payload) // using the "alg" signature algorithm. const signatureInput = `${encodedProtected}.${encodedContent}`; // call base layer plugable crypto API for signing with a key reference const signer = new SubtleCryptoExtension_1.default(cryptoFactory); const signature = await signer.signByKeyStore(algorithm, signingKeyReference, Buffer.from(signatureInput)); // Compose result jwsSignature.signature = Buffer.from(signature); jwsToken.signatures.push(jwsSignature); jwsToken.format = format; return jwsToken; } /** * Verify the JWS signature. * * @param validationKeys Public JWK key to validate the signature. * @param options used for the signature. These options override the options provided in the constructor. * @returns True if signature validated. */ async verify(validationKeys, options) { const cryptoFactory = this.getCryptoFactory(options); const validator = new SubtleCryptoExtension_1.default(cryptoFactory); // Get the encrypted key // Check if kid matches let success; for (let inx = 0; inx < this.signatures.length; inx++) { const payloadSignature = this.signatures[inx]; // We need to support an array of public keys todo if ((success = await this.validate(payloadSignature, validator, validationKeys[0]))) { if (success) { return true; } } } return false; } /** * Gets the base64 URL decrypted payload. */ getPayload() { return this.payload.toString('utf8'); } /** * Convert a @class ICryptoToken into a @class JwsToken * @param cryptoToken to convert * @param protectOptions options for the token */ static fromCryptoToken(cryptoToken, protectOptions) { const options = JwsToken.fromPayloadProtectionOptions(protectOptions); const jwsToken = new JwsToken(options); jwsToken.payload = cryptoToken.get(JoseConstants_1.default.tokenPayload); jwsToken.format = cryptoToken.get(JoseConstants_1.default.tokenFormat); jwsToken.signatures = cryptoToken.get(JoseConstants_1.default.tokenSignatures); return jwsToken; } /** * Convert a @class JwsToken into a @class ICryptoToken * @param protocolFormat format of the token * @param jwsToken to convert * @param options used for the signature. These options override the options provided in the constructor. */ static toCryptoToken(protocolFormat, jwsToken, options) { const cryptoToken = new JoseToken_1.default(options); cryptoToken.set(JoseConstants_1.default.tokenPayload, jwsToken.payload); cryptoToken.set(JoseConstants_1.default.tokenSignatures, jwsToken.signatures); cryptoToken.set(JoseConstants_1.default.tokenFormat, protocolFormat); return cryptoToken; } /** * Convert a @class IPayloadProtectionOptions into a @class IJwsSigningOptions * @param protectOptions to convert */ static fromPayloadProtectionOptions(protectOptions) { return { cryptoFactory: protectOptions.cryptoFactory, protected: protectOptions.options && protectOptions.options.has(JoseConstants_1.default.optionProtectedHeader) ? protectOptions.options.get(JoseConstants_1.default.optionProtectedHeader) : undefined, header: protectOptions.options && protectOptions.options.has(JoseConstants_1.default.optionHeader) ? protectOptions.options.get(JoseConstants_1.default.optionHeader) : undefined, kidPrefix: protectOptions.options && protectOptions.options.has(JoseConstants_1.default.optionKidPrefix) ? protectOptions.options.get(JoseConstants_1.default.optionKidPrefix) : undefined }; } /** * Convert a @class IPayloadProtectionOptions into a @class IJwsSigningOptions * @param signingOptions to convert */ static toPayloadProtectionOptions(signingOptions) { const protectOptions = { cryptoFactory: signingOptions.cryptoFactory, payloadProtection: new JoseProtocol_1.default(), options: new typescript_map_1.TSMap() }; if (signingOptions.header) { protectOptions.options.set(JoseConstants_1.default.optionHeader, signingOptions.header); } if (signingOptions.protected) { protectOptions.options.set(JoseConstants_1.default.optionProtectedHeader, signingOptions.protected); } if (signingOptions.kidPrefix) { protectOptions.options.set(JoseConstants_1.default.optionKidPrefix, signingOptions.kidPrefix); } return protectOptions; } // Validate the current state for completeness async validate(payloadSignature, validator, validationKey) { let alg; const protectedHeader = payloadSignature.protected; if (protectedHeader) { // tslint:disable-next-line: no-backbone-get-set-outside-model alg = protectedHeader.get(JoseConstants_1.default.Alg); } const header = payloadSignature.header; if (!alg) { if (header) { // tslint:disable-next-line: no-backbone-get-set-outside-model alg = header.get(JoseConstants_1.default.Alg); } } if (!alg) { throw new CryptoProtocolError_1.default(JoseConstants_1.default.Jws, 'Unable to validate signature as no signature algorithm has been specified in the header.'); } const algorithm = CryptoHelpers_1.default.jwaToWebCrypto(alg); const encodedProtected = !protectedHeader ? '' : JoseHelpers_1.default.encodeHeader(protectedHeader); const encodedContent = base64url_1.default.encode(this.payload); const signatureInput = `${encodedProtected}.${encodedContent}`; return validator.verifyByJwk(algorithm, validationKey, payloadSignature.signature, Buffer.from(signatureInput)); } /** * Set the protected header * @param protectedHeader to set on the JwsToken object */ 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; } /** * Set the header for the signature * @param header to set on the JwsToken object */ setHeader(header) { return new typescript_map_1.TSMap().fromJSON(JSON.parse(header)); } } exports.default = JwsToken; //# sourceMappingURL=JwsToken.js.map