tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }