import * as asn1js from "asn1js"; import * as pvutils from "pvutils"; import { ContentInfo, ContentInfoJson } from "./ContentInfo"; import { SafeContents } from "./SafeContents"; import { EnvelopedData } from "./EnvelopedData"; import { EncryptedData } from "./EncryptedData"; import * as Schema from "./Schema"; import { id_ContentType_Data, id_ContentType_EncryptedData, id_ContentType_EnvelopedData } from "./ObjectIdentifiers"; import { ArgumentError, AsnError, ParameterError } from "./errors"; import { PkiObject, PkiObjectParameters } from "./PkiObject"; import { EMPTY_STRING } from "./constants"; import * as common from "./common"; const SAFE_CONTENTS = "safeContents"; const PARSED_VALUE = "parsedValue"; const CONTENT_INFOS = "contentInfos"; export interface IAuthenticatedSafe { safeContents: ContentInfo[]; parsedValue: any; } export type AuthenticatedSafeParameters = PkiObjectParameters & Partial; export interface AuthenticatedSafeJson { safeContents: ContentInfoJson[]; } export type SafeContent = ContentInfo | EncryptedData | EnvelopedData | object; /** * Represents the AuthenticatedSafe structure described in [RFC7292](https://datatracker.ietf.org/doc/html/rfc7292) */ export class AuthenticatedSafe extends PkiObject implements IAuthenticatedSafe { public static override CLASS_NAME = "AuthenticatedSafe"; public safeContents!: ContentInfo[]; public parsedValue: any; /** * Initializes a new instance of the {@link AuthenticatedSafe} class * @param parameters Initialization parameters */ constructor(parameters: AuthenticatedSafeParameters = {}) { super(); this.safeContents = pvutils.getParametersValue(parameters, SAFE_CONTENTS, AuthenticatedSafe.defaultValues(SAFE_CONTENTS)); if (PARSED_VALUE in parameters) { this.parsedValue = pvutils.getParametersValue(parameters, PARSED_VALUE, AuthenticatedSafe.defaultValues(PARSED_VALUE)); } if (parameters.schema) { this.fromSchema(parameters.schema); } } /** * Returns default values for all class members * @param memberName String name for a class member * @returns Default value */ public static override defaultValues(memberName: typeof SAFE_CONTENTS): ContentInfo[]; public static override defaultValues(memberName: typeof PARSED_VALUE): any; public static override defaultValues(memberName: string): any { switch (memberName) { case SAFE_CONTENTS: return []; case PARSED_VALUE: return {}; default: return super.defaultValues(memberName); } } /** * Compare values with default values for all class members * @param memberName String name for a class member * @param memberValue Value to compare with default value */ public static compareWithDefault(memberName: string, memberValue: any): boolean { switch (memberName) { case SAFE_CONTENTS: return (memberValue.length === 0); case PARSED_VALUE: return ((memberValue instanceof Object) && (Object.keys(memberValue).length === 0)); default: return super.defaultValues(memberName); } } /** * @inheritdoc * @asn ASN.1 schema * ```asn * AuthenticatedSafe ::= SEQUENCE OF ContentInfo * -- Data if unencrypted * -- EncryptedData if password-encrypted * -- EnvelopedData if public key-encrypted *``` */ public static override schema(parameters: Schema.SchemaParameters<{ contentInfos?: string; }> = {}): Schema.SchemaType { const names = pvutils.getParametersValue>(parameters, "names", {}); return (new asn1js.Sequence({ name: (names.blockName || EMPTY_STRING), value: [ new asn1js.Repeated({ name: (names.contentInfos || EMPTY_STRING), value: ContentInfo.schema() }) ] })); } public fromSchema(schema: Schema.SchemaType): void { // Clear input data first pvutils.clearProps(schema, [ CONTENT_INFOS ]); // Check the schema is valid const asn1 = asn1js.compareSchema(schema, schema, AuthenticatedSafe.schema({ names: { contentInfos: CONTENT_INFOS } }) ); AsnError.assertSchema(asn1, this.className); // Get internal properties from parsed schema this.safeContents = Array.from(asn1.result.contentInfos, element => new ContentInfo({ schema: element })); } public toSchema(): asn1js.Sequence { return (new asn1js.Sequence({ value: Array.from(this.safeContents, o => o.toSchema()) })); } public toJSON(): AuthenticatedSafeJson { return { safeContents: Array.from(this.safeContents, o => o.toJSON()) }; } public async parseInternalValues(parameters: { safeContents: SafeContent[]; }, crypto = common.getCrypto(true)): Promise { //#region Check input data from "parameters" ParameterError.assert(parameters, SAFE_CONTENTS); ArgumentError.assert(parameters.safeContents, SAFE_CONTENTS, "Array"); if (parameters.safeContents.length !== this.safeContents.length) { throw new ArgumentError("Length of \"parameters.safeContents\" must be equal to \"this.safeContents.length\""); } //#endregion //#region Create value for "this.parsedValue.authenticatedSafe" this.parsedValue = { safeContents: [] as any[], }; for (const [index, content] of this.safeContents.entries()) { const safeContent = parameters.safeContents[index]; const errorTarget = `parameters.safeContents[${index}]`; switch (content.contentType) { //#region data case id_ContentType_Data: { // Check that we do have OCTET STRING as "content" ArgumentError.assert(content.content, "this.safeContents[j].content", asn1js.OctetString); //#region Check we have "constructive encoding" for AuthSafe content const authSafeContent = content.content.getValue(); //#endregion //#region Finally initialize initial values of SAFE_CONTENTS type this.parsedValue.safeContents.push({ privacyMode: 0, // No privacy, clear data value: SafeContents.fromBER(authSafeContent) }); //#endregion } break; //#endregion //#region envelopedData case id_ContentType_EnvelopedData: { //#region Initial variables const cmsEnveloped = new EnvelopedData({ schema: content.content }); //#endregion //#region Check mandatory parameters ParameterError.assert(errorTarget, safeContent, "recipientCertificate", "recipientKey"); const envelopedData = safeContent as any; const recipientCertificate = envelopedData.recipientCertificate; const recipientKey = envelopedData.recipientKey; //#endregion //#region Decrypt CMS EnvelopedData using first recipient information const decrypted = await cmsEnveloped.decrypt(0, { recipientCertificate, recipientPrivateKey: recipientKey }, crypto); this.parsedValue.safeContents.push({ privacyMode: 2, // Public-key privacy mode value: SafeContents.fromBER(decrypted), }); //#endregion } break; //#endregion //#region encryptedData case id_ContentType_EncryptedData: { //#region Initial variables const cmsEncrypted = new EncryptedData({ schema: content.content }); //#endregion //#region Check mandatory parameters ParameterError.assert(errorTarget, safeContent, "password"); const password = (safeContent as any).password; //#endregion //#region Decrypt CMS EncryptedData using password const decrypted = await cmsEncrypted.decrypt({ password }, crypto); //#endregion //#region Initialize internal data this.parsedValue.safeContents.push({ privacyMode: 1, // Password-based privacy mode value: SafeContents.fromBER(decrypted), }); //#endregion } break; //#endregion //#region default default: throw new Error(`Unknown "contentType" for AuthenticatedSafe: " ${content.contentType}`); //#endregion } } //#endregion } public async makeInternalValues(parameters: { safeContents: any[]; }, crypto = common.getCrypto(true)): Promise { //#region Check data in PARSED_VALUE if (!(this.parsedValue)) { throw new Error("Please run \"parseValues\" first or add \"parsedValue\" manually"); } ArgumentError.assert(this.parsedValue, "this.parsedValue", "object"); ArgumentError.assert(this.parsedValue.safeContents, "this.parsedValue.safeContents", "Array"); //#region Check input data from "parameters" ArgumentError.assert(parameters, "parameters", "object"); ParameterError.assert(parameters, "safeContents"); ArgumentError.assert(parameters.safeContents, "parameters.safeContents", "Array"); if (parameters.safeContents.length !== this.parsedValue.safeContents.length) { throw new ArgumentError("Length of \"parameters.safeContents\" must be equal to \"this.parsedValue.safeContents\""); } //#endregion //#region Create internal values from already parsed values this.safeContents = []; for (const [index, content] of this.parsedValue.safeContents.entries()) { //#region Check current "content" value ParameterError.assert("content", content, "privacyMode", "value"); ArgumentError.assert(content.value, "content.value", SafeContents); //#endregion switch (content.privacyMode) { //#region No privacy case 0: { const contentBuffer = content.value.toSchema().toBER(false); this.safeContents.push(new ContentInfo({ contentType: "1.2.840.113549.1.7.1", content: new asn1js.OctetString({ valueHex: contentBuffer }) })); } break; //#endregion //#region Privacy with password case 1: { //#region Initial variables const cmsEncrypted = new EncryptedData(); const currentParameters = parameters.safeContents[index]; currentParameters.contentToEncrypt = content.value.toSchema().toBER(false); //#endregion //#region Encrypt CMS EncryptedData using password await cmsEncrypted.encrypt(currentParameters); //#endregion //#region Store result content in CMS_CONTENT_INFO type this.safeContents.push(new ContentInfo({ contentType: "1.2.840.113549.1.7.6", content: cmsEncrypted.toSchema() })); //#endregion } break; //#endregion //#region Privacy with public key case 2: { //#region Initial variables const cmsEnveloped = new EnvelopedData(); const contentToEncrypt = content.value.toSchema().toBER(false); const safeContent = parameters.safeContents[index]; //#endregion //#region Check mandatory parameters ParameterError.assert(`parameters.safeContents[${index}]`, safeContent, "encryptingCertificate", "encryptionAlgorithm"); switch (true) { case (safeContent.encryptionAlgorithm.name.toLowerCase() === "aes-cbc"): case (safeContent.encryptionAlgorithm.name.toLowerCase() === "aes-gcm"): break; default: throw new Error(`Incorrect parameter "encryptionAlgorithm" in "parameters.safeContents[i]": ${safeContent.encryptionAlgorithm}`); } switch (true) { case (safeContent.encryptionAlgorithm.length === 128): case (safeContent.encryptionAlgorithm.length === 192): case (safeContent.encryptionAlgorithm.length === 256): break; default: throw new Error(`Incorrect parameter "encryptionAlgorithm.length" in "parameters.safeContents[i]": ${safeContent.encryptionAlgorithm.length}`); } //#endregion //#region Making correct "encryptionAlgorithm" variable const encryptionAlgorithm = safeContent.encryptionAlgorithm; //#endregion //#region Append recipient for enveloped data cmsEnveloped.addRecipientByCertificate(safeContent.encryptingCertificate, {}, undefined, crypto); //#endregion //#region Making encryption await cmsEnveloped.encrypt(encryptionAlgorithm, contentToEncrypt, crypto); this.safeContents.push(new ContentInfo({ contentType: "1.2.840.113549.1.7.3", content: cmsEnveloped.toSchema() })); //#endregion } break; //#endregion //#region default default: throw new Error(`Incorrect value for "content.privacyMode": ${content.privacyMode}`); //#endregion } } //#endregion //#region Return result of the function return this; //#endregion } }