AuthenticatedSafe.ts (13577B)
1 import * as asn1js from "asn1js"; 2 import * as pvutils from "pvutils"; 3 import { ContentInfo, ContentInfoJson } from "./ContentInfo"; 4 import { SafeContents } from "./SafeContents"; 5 import { EnvelopedData } from "./EnvelopedData"; 6 import { EncryptedData } from "./EncryptedData"; 7 import * as Schema from "./Schema"; 8 import { id_ContentType_Data, id_ContentType_EncryptedData, id_ContentType_EnvelopedData } from "./ObjectIdentifiers"; 9 import { ArgumentError, AsnError, ParameterError } from "./errors"; 10 import { PkiObject, PkiObjectParameters } from "./PkiObject"; 11 import { EMPTY_STRING } from "./constants"; 12 import * as common from "./common"; 13 14 const SAFE_CONTENTS = "safeContents"; 15 const PARSED_VALUE = "parsedValue"; 16 const CONTENT_INFOS = "contentInfos"; 17 18 export interface IAuthenticatedSafe { 19 safeContents: ContentInfo[]; 20 parsedValue: any; 21 } 22 23 export type AuthenticatedSafeParameters = PkiObjectParameters & Partial<IAuthenticatedSafe>; 24 25 export interface AuthenticatedSafeJson { 26 safeContents: ContentInfoJson[]; 27 } 28 29 export type SafeContent = ContentInfo | EncryptedData | EnvelopedData | object; 30 31 /** 32 * Represents the AuthenticatedSafe structure described in [RFC7292](https://datatracker.ietf.org/doc/html/rfc7292) 33 */ 34 export class AuthenticatedSafe extends PkiObject implements IAuthenticatedSafe { 35 36 public static override CLASS_NAME = "AuthenticatedSafe"; 37 38 public safeContents!: ContentInfo[]; 39 public parsedValue: any; 40 41 /** 42 * Initializes a new instance of the {@link AuthenticatedSafe} class 43 * @param parameters Initialization parameters 44 */ 45 constructor(parameters: AuthenticatedSafeParameters = {}) { 46 super(); 47 48 this.safeContents = pvutils.getParametersValue(parameters, SAFE_CONTENTS, AuthenticatedSafe.defaultValues(SAFE_CONTENTS)); 49 if (PARSED_VALUE in parameters) { 50 this.parsedValue = pvutils.getParametersValue(parameters, PARSED_VALUE, AuthenticatedSafe.defaultValues(PARSED_VALUE)); 51 } 52 53 if (parameters.schema) { 54 this.fromSchema(parameters.schema); 55 } 56 } 57 58 /** 59 * Returns default values for all class members 60 * @param memberName String name for a class member 61 * @returns Default value 62 */ 63 public static override defaultValues(memberName: typeof SAFE_CONTENTS): ContentInfo[]; 64 public static override defaultValues(memberName: typeof PARSED_VALUE): any; 65 public static override defaultValues(memberName: string): any { 66 switch (memberName) { 67 case SAFE_CONTENTS: 68 return []; 69 case PARSED_VALUE: 70 return {}; 71 default: 72 return super.defaultValues(memberName); 73 } 74 } 75 76 /** 77 * Compare values with default values for all class members 78 * @param memberName String name for a class member 79 * @param memberValue Value to compare with default value 80 */ 81 public static compareWithDefault(memberName: string, memberValue: any): boolean { 82 switch (memberName) { 83 case SAFE_CONTENTS: 84 return (memberValue.length === 0); 85 case PARSED_VALUE: 86 return ((memberValue instanceof Object) && (Object.keys(memberValue).length === 0)); 87 default: 88 return super.defaultValues(memberName); 89 } 90 } 91 92 /** 93 * @inheritdoc 94 * @asn ASN.1 schema 95 * ```asn 96 * AuthenticatedSafe ::= SEQUENCE OF ContentInfo 97 * -- Data if unencrypted 98 * -- EncryptedData if password-encrypted 99 * -- EnvelopedData if public key-encrypted 100 *``` 101 */ 102 public static override schema(parameters: Schema.SchemaParameters<{ 103 contentInfos?: string; 104 }> = {}): Schema.SchemaType { 105 const names = pvutils.getParametersValue<NonNullable<typeof parameters.names>>(parameters, "names", {}); 106 107 return (new asn1js.Sequence({ 108 name: (names.blockName || EMPTY_STRING), 109 value: [ 110 new asn1js.Repeated({ 111 name: (names.contentInfos || EMPTY_STRING), 112 value: ContentInfo.schema() 113 }) 114 ] 115 })); 116 } 117 118 public fromSchema(schema: Schema.SchemaType): void { 119 // Clear input data first 120 pvutils.clearProps(schema, [ 121 CONTENT_INFOS 122 ]); 123 124 // Check the schema is valid 125 const asn1 = asn1js.compareSchema(schema, 126 schema, 127 AuthenticatedSafe.schema({ 128 names: { 129 contentInfos: CONTENT_INFOS 130 } 131 }) 132 ); 133 AsnError.assertSchema(asn1, this.className); 134 135 // Get internal properties from parsed schema 136 this.safeContents = Array.from(asn1.result.contentInfos, element => new ContentInfo({ schema: element })); 137 } 138 139 public toSchema(): asn1js.Sequence { 140 return (new asn1js.Sequence({ 141 value: Array.from(this.safeContents, o => o.toSchema()) 142 })); 143 } 144 145 public toJSON(): AuthenticatedSafeJson { 146 return { 147 safeContents: Array.from(this.safeContents, o => o.toJSON()) 148 }; 149 } 150 151 public async parseInternalValues(parameters: { safeContents: SafeContent[]; }, crypto = common.getCrypto(true)): Promise<void> { 152 //#region Check input data from "parameters" 153 ParameterError.assert(parameters, SAFE_CONTENTS); 154 ArgumentError.assert(parameters.safeContents, SAFE_CONTENTS, "Array"); 155 if (parameters.safeContents.length !== this.safeContents.length) { 156 throw new ArgumentError("Length of \"parameters.safeContents\" must be equal to \"this.safeContents.length\""); 157 } 158 //#endregion 159 160 //#region Create value for "this.parsedValue.authenticatedSafe" 161 this.parsedValue = { 162 safeContents: [] as any[], 163 }; 164 165 for (const [index, content] of this.safeContents.entries()) { 166 const safeContent = parameters.safeContents[index]; 167 const errorTarget = `parameters.safeContents[${index}]`; 168 switch (content.contentType) { 169 //#region data 170 case id_ContentType_Data: 171 { 172 // Check that we do have OCTET STRING as "content" 173 ArgumentError.assert(content.content, "this.safeContents[j].content", asn1js.OctetString); 174 175 //#region Check we have "constructive encoding" for AuthSafe content 176 const authSafeContent = content.content.getValue(); 177 //#endregion 178 179 //#region Finally initialize initial values of SAFE_CONTENTS type 180 this.parsedValue.safeContents.push({ 181 privacyMode: 0, // No privacy, clear data 182 value: SafeContents.fromBER(authSafeContent) 183 }); 184 //#endregion 185 } 186 break; 187 //#endregion 188 //#region envelopedData 189 case id_ContentType_EnvelopedData: 190 { 191 //#region Initial variables 192 const cmsEnveloped = new EnvelopedData({ schema: content.content }); 193 //#endregion 194 195 //#region Check mandatory parameters 196 ParameterError.assert(errorTarget, safeContent, "recipientCertificate", "recipientKey"); 197 const envelopedData = safeContent as any; 198 const recipientCertificate = envelopedData.recipientCertificate; 199 const recipientKey = envelopedData.recipientKey; 200 //#endregion 201 202 //#region Decrypt CMS EnvelopedData using first recipient information 203 const decrypted = await cmsEnveloped.decrypt(0, { 204 recipientCertificate, 205 recipientPrivateKey: recipientKey 206 }, crypto); 207 208 this.parsedValue.safeContents.push({ 209 privacyMode: 2, // Public-key privacy mode 210 value: SafeContents.fromBER(decrypted), 211 }); 212 //#endregion 213 } 214 break; 215 //#endregion 216 //#region encryptedData 217 case id_ContentType_EncryptedData: 218 { 219 //#region Initial variables 220 const cmsEncrypted = new EncryptedData({ schema: content.content }); 221 //#endregion 222 223 //#region Check mandatory parameters 224 ParameterError.assert(errorTarget, safeContent, "password"); 225 226 const password = (safeContent as any).password; 227 //#endregion 228 229 //#region Decrypt CMS EncryptedData using password 230 const decrypted = await cmsEncrypted.decrypt({ 231 password 232 }, crypto); 233 //#endregion 234 235 //#region Initialize internal data 236 this.parsedValue.safeContents.push({ 237 privacyMode: 1, // Password-based privacy mode 238 value: SafeContents.fromBER(decrypted), 239 }); 240 //#endregion 241 } 242 break; 243 //#endregion 244 //#region default 245 default: 246 throw new Error(`Unknown "contentType" for AuthenticatedSafe: " ${content.contentType}`); 247 //#endregion 248 } 249 } 250 //#endregion 251 } 252 public async makeInternalValues(parameters: { 253 safeContents: any[]; 254 }, crypto = common.getCrypto(true)): Promise<this> { 255 //#region Check data in PARSED_VALUE 256 if (!(this.parsedValue)) { 257 throw new Error("Please run \"parseValues\" first or add \"parsedValue\" manually"); 258 } 259 ArgumentError.assert(this.parsedValue, "this.parsedValue", "object"); 260 ArgumentError.assert(this.parsedValue.safeContents, "this.parsedValue.safeContents", "Array"); 261 262 //#region Check input data from "parameters" 263 ArgumentError.assert(parameters, "parameters", "object"); 264 ParameterError.assert(parameters, "safeContents"); 265 ArgumentError.assert(parameters.safeContents, "parameters.safeContents", "Array"); 266 if (parameters.safeContents.length !== this.parsedValue.safeContents.length) { 267 throw new ArgumentError("Length of \"parameters.safeContents\" must be equal to \"this.parsedValue.safeContents\""); 268 } 269 //#endregion 270 271 //#region Create internal values from already parsed values 272 this.safeContents = []; 273 274 for (const [index, content] of this.parsedValue.safeContents.entries()) { 275 //#region Check current "content" value 276 ParameterError.assert("content", content, "privacyMode", "value"); 277 ArgumentError.assert(content.value, "content.value", SafeContents); 278 //#endregion 279 280 switch (content.privacyMode) { 281 //#region No privacy 282 case 0: 283 { 284 const contentBuffer = content.value.toSchema().toBER(false); 285 286 this.safeContents.push(new ContentInfo({ 287 contentType: "1.2.840.113549.1.7.1", 288 content: new asn1js.OctetString({ valueHex: contentBuffer }) 289 })); 290 } 291 break; 292 //#endregion 293 //#region Privacy with password 294 case 1: 295 { 296 //#region Initial variables 297 const cmsEncrypted = new EncryptedData(); 298 299 const currentParameters = parameters.safeContents[index]; 300 currentParameters.contentToEncrypt = content.value.toSchema().toBER(false); 301 //#endregion 302 303 //#region Encrypt CMS EncryptedData using password 304 await cmsEncrypted.encrypt(currentParameters, crypto); 305 //#endregion 306 307 //#region Store result content in CMS_CONTENT_INFO type 308 this.safeContents.push(new ContentInfo({ 309 contentType: "1.2.840.113549.1.7.6", 310 content: cmsEncrypted.toSchema() 311 })); 312 //#endregion 313 } 314 break; 315 //#endregion 316 //#region Privacy with public key 317 case 2: 318 { 319 //#region Initial variables 320 const cmsEnveloped = new EnvelopedData(); 321 const contentToEncrypt = content.value.toSchema().toBER(false); 322 const safeContent = parameters.safeContents[index]; 323 //#endregion 324 325 //#region Check mandatory parameters 326 ParameterError.assert(`parameters.safeContents[${index}]`, safeContent, "encryptingCertificate", "encryptionAlgorithm"); 327 328 switch (true) { 329 case (safeContent.encryptionAlgorithm.name.toLowerCase() === "aes-cbc"): 330 case (safeContent.encryptionAlgorithm.name.toLowerCase() === "aes-gcm"): 331 break; 332 default: 333 throw new Error(`Incorrect parameter "encryptionAlgorithm" in "parameters.safeContents[i]": ${safeContent.encryptionAlgorithm}`); 334 } 335 336 switch (true) { 337 case (safeContent.encryptionAlgorithm.length === 128): 338 case (safeContent.encryptionAlgorithm.length === 192): 339 case (safeContent.encryptionAlgorithm.length === 256): 340 break; 341 default: 342 throw new Error(`Incorrect parameter "encryptionAlgorithm.length" in "parameters.safeContents[i]": ${safeContent.encryptionAlgorithm.length}`); 343 } 344 //#endregion 345 346 //#region Making correct "encryptionAlgorithm" variable 347 const encryptionAlgorithm = safeContent.encryptionAlgorithm; 348 //#endregion 349 350 //#region Append recipient for enveloped data 351 cmsEnveloped.addRecipientByCertificate(safeContent.encryptingCertificate, {}, undefined, crypto); 352 //#endregion 353 354 //#region Making encryption 355 await cmsEnveloped.encrypt(encryptionAlgorithm, contentToEncrypt, crypto); 356 357 this.safeContents.push(new ContentInfo({ 358 contentType: "1.2.840.113549.1.7.3", 359 content: cmsEnveloped.toSchema() 360 })); 361 //#endregion 362 } 363 break; 364 //#endregion 365 //#region default 366 default: 367 throw new Error(`Incorrect value for "content.privacyMode": ${content.privacyMode}`); 368 //#endregion 369 } 370 } 371 //#endregion 372 373 //#region Return result of the function 374 return this; 375 //#endregion 376 } 377 378 }