@microsoft/useragent-sdk
Version:
SDK for building decentralized identity wallets and enterprise agents.
523 lines • 23.9 kB
JavaScript
"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