tor-browser

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

PFX.ts (17995B)


      1 import * as asn1js from "asn1js";
      2 import * as pvutils from "pvutils";
      3 import * as common from "./common";
      4 import { ContentInfo, ContentInfoJson, ContentInfoSchema } from "./ContentInfo";
      5 import { MacData, MacDataJson, MacDataSchema } from "./MacData";
      6 import { DigestInfo } from "./DigestInfo";
      7 import { AlgorithmIdentifier } from "./AlgorithmIdentifier";
      8 import { SignedData } from "./SignedData";
      9 import { EncapsulatedContentInfo } from "./EncapsulatedContentInfo";
     10 import { Attribute } from "./Attribute";
     11 import { SignerInfo } from "./SignerInfo";
     12 import { IssuerAndSerialNumber } from "./IssuerAndSerialNumber";
     13 import { SignedAndUnsignedAttributes } from "./SignedAndUnsignedAttributes";
     14 import { AuthenticatedSafe } from "./AuthenticatedSafe";
     15 import * as Schema from "./Schema";
     16 import { Certificate } from "./Certificate";
     17 import { ArgumentError, AsnError, ParameterError } from "./errors";
     18 import { PkiObject, PkiObjectParameters } from "./PkiObject";
     19 import { BufferSourceConverter } from "pvtsutils";
     20 import { EMPTY_STRING } from "./constants";
     21 
     22 const VERSION = "version";
     23 const AUTH_SAFE = "authSafe";
     24 const MAC_DATA = "macData";
     25 const PARSED_VALUE = "parsedValue";
     26 const CLERA_PROPS = [
     27  VERSION,
     28  AUTH_SAFE,
     29  MAC_DATA
     30 ];
     31 
     32 export interface IPFX {
     33  version: number;
     34  authSafe: ContentInfo;
     35  macData?: MacData;
     36  parsedValue?: PFXParsedValue;
     37 }
     38 
     39 export interface PFXJson {
     40  version: number;
     41  authSafe: ContentInfoJson;
     42  macData?: MacDataJson;
     43 }
     44 
     45 export type PFXParameters = PkiObjectParameters & Partial<IPFX>;
     46 
     47 export interface PFXParsedValue {
     48  authenticatedSafe?: AuthenticatedSafe;
     49  integrityMode?: number;
     50 }
     51 
     52 export type MakeInternalValuesParams =
     53  object
     54  |
     55  {
     56    iterations: number;
     57    pbkdf2HashAlgorithm: Algorithm;
     58    hmacHashAlgorithm: string;
     59    password: ArrayBuffer;
     60  }
     61  |
     62  {
     63    signingCertificate: Certificate;
     64    privateKey: CryptoKey;
     65    hashAlgorithm: string;
     66  };
     67 
     68 /**
     69 * Represents the PFX structure described in [RFC7292](https://datatracker.ietf.org/doc/html/rfc7292)
     70 */
     71 export class PFX extends PkiObject implements IPFX {
     72 
     73  public static override CLASS_NAME = "PFX";
     74 
     75  public version!: number;
     76  public authSafe!: ContentInfo;
     77  public macData?: MacData;
     78  public parsedValue?: PFXParsedValue;
     79 
     80  /**
     81   * Initializes a new instance of the {@link PFX} class
     82   * @param parameters Initialization parameters
     83   */
     84  constructor(parameters: PFXParameters = {}) {
     85    super();
     86 
     87    this.version = pvutils.getParametersValue(parameters, VERSION, PFX.defaultValues(VERSION));
     88    this.authSafe = pvutils.getParametersValue(parameters, AUTH_SAFE, PFX.defaultValues(AUTH_SAFE));
     89    if (MAC_DATA in parameters) {
     90      this.macData = pvutils.getParametersValue(parameters, MAC_DATA, PFX.defaultValues(MAC_DATA));
     91    }
     92    if (PARSED_VALUE in parameters) {
     93      this.parsedValue = pvutils.getParametersValue(parameters, PARSED_VALUE, PFX.defaultValues(PARSED_VALUE));
     94    }
     95 
     96    if (parameters.schema) {
     97      this.fromSchema(parameters.schema);
     98    }
     99  }
    100 
    101  /**
    102   * Returns default values for all class members
    103   * @param memberName String name for a class member
    104   * @returns Default value
    105   */
    106  public static override defaultValues(memberName: typeof VERSION): number;
    107  public static override defaultValues(memberName: typeof AUTH_SAFE): ContentInfo;
    108  public static override defaultValues(memberName: typeof MAC_DATA): MacData;
    109  public static override defaultValues(memberName: typeof PARSED_VALUE): PFXParsedValue;
    110  public static override defaultValues(memberName: string): any {
    111    switch (memberName) {
    112      case VERSION:
    113        return 3;
    114      case AUTH_SAFE:
    115        return (new ContentInfo());
    116      case MAC_DATA:
    117        return (new MacData());
    118      case PARSED_VALUE:
    119        return {};
    120      default:
    121        return super.defaultValues(memberName);
    122    }
    123  }
    124 
    125  /**
    126   * Compare values with default values for all class members
    127   * @param memberName String name for a class member
    128   * @param memberValue Value to compare with default value
    129   */
    130  public static compareWithDefault(memberName: string, memberValue: any): boolean {
    131    switch (memberName) {
    132      case VERSION:
    133        return (memberValue === PFX.defaultValues(memberName));
    134      case AUTH_SAFE:
    135        return ((ContentInfo.compareWithDefault("contentType", memberValue.contentType)) &&
    136          (ContentInfo.compareWithDefault("content", memberValue.content)));
    137      case MAC_DATA:
    138        return ((MacData.compareWithDefault("mac", memberValue.mac)) &&
    139          (MacData.compareWithDefault("macSalt", memberValue.macSalt)) &&
    140          (MacData.compareWithDefault("iterations", memberValue.iterations)));
    141      case PARSED_VALUE:
    142        return ((memberValue instanceof Object) && (Object.keys(memberValue).length === 0));
    143      default:
    144        return super.defaultValues(memberName);
    145    }
    146  }
    147 
    148  /**
    149   * @inheritdoc
    150   * @asn ASN.1 schema
    151   * ```asn
    152   * PFX ::= SEQUENCE {
    153   *    version     INTEGER {v3(3)}(v3,...),
    154   *    authSafe    ContentInfo,
    155   *    macData     MacData OPTIONAL
    156   * }
    157   *```
    158   */
    159  public static override schema(parameters: Schema.SchemaParameters<{
    160    version?: string;
    161    authSafe?: ContentInfoSchema;
    162    macData?: MacDataSchema;
    163  }> = {}): Schema.SchemaType {
    164    const names = pvutils.getParametersValue<NonNullable<typeof parameters.names>>(parameters, "names", {});
    165 
    166    return (new asn1js.Sequence({
    167      name: (names.blockName || EMPTY_STRING),
    168      value: [
    169        new asn1js.Integer({ name: (names.version || VERSION) }),
    170        ContentInfo.schema(names.authSafe || {
    171          names: {
    172            blockName: AUTH_SAFE
    173          }
    174        }),
    175        MacData.schema(names.macData || {
    176          names: {
    177            blockName: MAC_DATA,
    178            optional: true
    179          }
    180        })
    181      ]
    182    }));
    183  }
    184 
    185  public fromSchema(schema: Schema.SchemaType): void {
    186    // Clear input data first
    187    pvutils.clearProps(schema, CLERA_PROPS);
    188 
    189    // Check the schema is valid
    190    const asn1 = asn1js.compareSchema(schema,
    191      schema,
    192      PFX.schema({
    193        names: {
    194          version: VERSION,
    195          authSafe: {
    196            names: {
    197              blockName: AUTH_SAFE
    198            }
    199          },
    200          macData: {
    201            names: {
    202              blockName: MAC_DATA
    203            }
    204          }
    205        }
    206      })
    207    );
    208    AsnError.assertSchema(asn1, this.className);
    209 
    210    // Get internal properties from parsed schema
    211    this.version = asn1.result.version.valueBlock.valueDec;
    212    this.authSafe = new ContentInfo({ schema: asn1.result.authSafe });
    213    if (MAC_DATA in asn1.result)
    214      this.macData = new MacData({ schema: asn1.result.macData });
    215  }
    216 
    217  public toSchema(): asn1js.Sequence {
    218    //#region Construct and return new ASN.1 schema for this object
    219    const outputArray = [
    220      new asn1js.Integer({ value: this.version }),
    221      this.authSafe.toSchema()
    222    ];
    223 
    224    if (this.macData) {
    225      outputArray.push(this.macData.toSchema());
    226    }
    227 
    228    return (new asn1js.Sequence({
    229      value: outputArray
    230    }));
    231    //#endregion
    232  }
    233 
    234  public toJSON(): PFXJson {
    235    const output: PFXJson = {
    236      version: this.version,
    237      authSafe: this.authSafe.toJSON()
    238    };
    239 
    240    if (this.macData) {
    241      output.macData = this.macData.toJSON();
    242    }
    243 
    244    return output;
    245  }
    246 
    247  /**
    248   * Making ContentInfo from PARSED_VALUE object
    249   * @param parameters Parameters, specific to each "integrity mode"
    250   * @param crypto Crypto engine
    251   */
    252  public async makeInternalValues(parameters: MakeInternalValuesParams = {}, crypto = common.getCrypto(true)) {
    253    //#region Check mandatory parameter
    254    ArgumentError.assert(parameters, "parameters", "object");
    255    if (!this.parsedValue) {
    256      throw new Error("Please call \"parseValues\" function first in order to make \"parsedValue\" data");
    257    }
    258    ParameterError.assertEmpty(this.parsedValue.integrityMode, "integrityMode", "parsedValue");
    259    ParameterError.assertEmpty(this.parsedValue.authenticatedSafe, "authenticatedSafe", "parsedValue");
    260    //#endregion
    261 
    262    //#region Makes values for each particular integrity mode
    263    switch (this.parsedValue.integrityMode) {
    264      //#region HMAC-based integrity
    265      case 0:
    266        {
    267          //#region Check additional mandatory parameters
    268          if (!("iterations" in parameters))
    269            throw new ParameterError("iterations");
    270          ParameterError.assertEmpty(parameters.pbkdf2HashAlgorithm, "pbkdf2HashAlgorithm");
    271          ParameterError.assertEmpty(parameters.hmacHashAlgorithm, "hmacHashAlgorithm");
    272          ParameterError.assertEmpty(parameters.password, "password");
    273          //#endregion
    274 
    275          //#region Initial variables
    276          const saltBuffer = new ArrayBuffer(64);
    277          const saltView = new Uint8Array(saltBuffer);
    278 
    279          crypto.getRandomValues(saltView);
    280 
    281          const data = this.parsedValue.authenticatedSafe.toSchema().toBER(false);
    282 
    283          this.authSafe = new ContentInfo({
    284            contentType: ContentInfo.DATA,
    285            content: new asn1js.OctetString({ valueHex: data })
    286          });
    287          //#endregion
    288 
    289          //#region Call current crypto engine for making HMAC-based data stamp
    290          const result = await crypto.stampDataWithPassword({
    291            password: parameters.password,
    292            hashAlgorithm: parameters.hmacHashAlgorithm,
    293            salt: saltBuffer,
    294            iterationCount: parameters.iterations,
    295            contentToStamp: data
    296          });
    297          //#endregion
    298 
    299          //#region Make MAC_DATA values
    300          this.macData = new MacData({
    301            mac: new DigestInfo({
    302              digestAlgorithm: new AlgorithmIdentifier({
    303                algorithmId: crypto.getOIDByAlgorithm({ name: parameters.hmacHashAlgorithm }, true, "hmacHashAlgorithm"),
    304              }),
    305              digest: new asn1js.OctetString({ valueHex: result })
    306            }),
    307            macSalt: new asn1js.OctetString({ valueHex: saltBuffer }),
    308            iterations: parameters.iterations
    309          });
    310          //#endregion
    311          //#endregion
    312        }
    313        break;
    314      //#endregion
    315      //#region publicKey-based integrity
    316      case 1:
    317        {
    318          //#region Check additional mandatory parameters
    319          if (!("signingCertificate" in parameters)) {
    320            throw new ParameterError("signingCertificate");
    321          }
    322          ParameterError.assertEmpty(parameters.privateKey, "privateKey");
    323          ParameterError.assertEmpty(parameters.hashAlgorithm, "hashAlgorithm");
    324          //#endregion
    325 
    326          //#region Making data to be signed
    327          // NOTE: all internal data for "authenticatedSafe" must be already prepared.
    328          // Thus user must call "makeValues" for all internal "SafeContent" value with appropriate parameters.
    329          // Or user can choose to use values from initial parsing of existing PKCS#12 data.
    330 
    331          const toBeSigned = this.parsedValue.authenticatedSafe.toSchema().toBER(false);
    332          //#endregion
    333 
    334          //#region Initial variables
    335          const cmsSigned = new SignedData({
    336            version: 1,
    337            encapContentInfo: new EncapsulatedContentInfo({
    338              eContentType: "1.2.840.113549.1.7.1", // "data" content type
    339              eContent: new asn1js.OctetString({ valueHex: toBeSigned })
    340            }),
    341            certificates: [parameters.signingCertificate]
    342          });
    343          //#endregion
    344 
    345          //#region Making additional attributes for CMS Signed Data
    346          //#region Create a message digest
    347          const result = await crypto.digest({ name: parameters.hashAlgorithm }, new Uint8Array(toBeSigned));
    348          //#endregion
    349 
    350          //#region Combine all signed extensions
    351          //#region Initial variables
    352          const signedAttr: Attribute[] = [];
    353          //#endregion
    354 
    355          //#region contentType
    356          signedAttr.push(new Attribute({
    357            type: "1.2.840.113549.1.9.3",
    358            values: [
    359              new asn1js.ObjectIdentifier({ value: "1.2.840.113549.1.7.1" })
    360            ]
    361          }));
    362          //#endregion
    363          //#region signingTime
    364          signedAttr.push(new Attribute({
    365            type: "1.2.840.113549.1.9.5",
    366            values: [
    367              new asn1js.UTCTime({ valueDate: new Date() })
    368            ]
    369          }));
    370          //#endregion
    371          //#region messageDigest
    372          signedAttr.push(new Attribute({
    373            type: "1.2.840.113549.1.9.4",
    374            values: [
    375              new asn1js.OctetString({ valueHex: result })
    376            ]
    377          }));
    378          //#endregion
    379 
    380          //#region Making final value for "SignerInfo" type
    381          cmsSigned.signerInfos.push(new SignerInfo({
    382            version: 1,
    383            sid: new IssuerAndSerialNumber({
    384              issuer: parameters.signingCertificate.issuer,
    385              serialNumber: parameters.signingCertificate.serialNumber
    386            }),
    387            signedAttrs: new SignedAndUnsignedAttributes({
    388              type: 0,
    389              attributes: signedAttr
    390            })
    391          }));
    392          //#endregion
    393          //#endregion
    394          //#endregion
    395 
    396          //#region Signing CMS Signed Data
    397          await cmsSigned.sign(parameters.privateKey, 0, parameters.hashAlgorithm, undefined, crypto);
    398          //#endregion
    399 
    400          //#region Making final CMS_CONTENT_INFO type
    401          this.authSafe = new ContentInfo({
    402            contentType: "1.2.840.113549.1.7.2",
    403            content: cmsSigned.toSchema(true)
    404          });
    405          //#endregion
    406        }
    407        break;
    408      //#endregion
    409      //#region default
    410      default:
    411        throw new Error(`Parameter "integrityMode" has unknown value: ${this.parsedValue.integrityMode}`);
    412      //#endregion
    413    }
    414    //#endregion
    415  }
    416 
    417  public async parseInternalValues(parameters: {
    418    checkIntegrity?: boolean;
    419    password?: ArrayBuffer;
    420  }, crypto = common.getCrypto(true)) {
    421    //#region Check input data from "parameters"
    422    ArgumentError.assert(parameters, "parameters", "object");
    423 
    424    if (parameters.checkIntegrity === undefined) {
    425      parameters.checkIntegrity = true;
    426    }
    427    //#endregion
    428 
    429    //#region Create value for "this.parsedValue.authenticatedSafe" and check integrity
    430    this.parsedValue = {};
    431 
    432    switch (this.authSafe.contentType) {
    433      //#region data
    434      case ContentInfo.DATA:
    435        {
    436          //#region Check additional mandatory parameters
    437          ParameterError.assertEmpty(parameters.password, "password");
    438          //#endregion
    439 
    440          //#region Integrity based on HMAC
    441          this.parsedValue.integrityMode = 0;
    442          //#endregion
    443 
    444          //#region Check that we do have OCTETSTRING as "content"
    445          ArgumentError.assert(this.authSafe.content, "authSafe.content", asn1js.OctetString);
    446          //#endregion
    447 
    448          //#region Check we have "constructive encoding" for AuthSafe content
    449          const authSafeContent = this.authSafe.content.getValue();
    450          //#endregion
    451 
    452          //#region Set "authenticatedSafe" value
    453          this.parsedValue.authenticatedSafe = AuthenticatedSafe.fromBER(authSafeContent);
    454          //#endregion
    455 
    456          //#region Check integrity
    457          if (parameters.checkIntegrity) {
    458            //#region Check that MAC_DATA exists
    459            if (!this.macData) {
    460              throw new Error("Absent \"macData\" value, can not check PKCS#12 data integrity");
    461            }
    462            //#endregion
    463 
    464            //#region Initial variables
    465            const hashAlgorithm = crypto.getAlgorithmByOID(this.macData.mac.digestAlgorithm.algorithmId, true, "digestAlgorithm");
    466            //#endregion
    467 
    468            //#region Call current crypto engine for verifying HMAC-based data stamp
    469            const result = await crypto.verifyDataStampedWithPassword({
    470              password: parameters.password,
    471              hashAlgorithm: hashAlgorithm.name,
    472              salt: BufferSourceConverter.toArrayBuffer(this.macData.macSalt.valueBlock.valueHexView),
    473              iterationCount: this.macData.iterations || 1,
    474              contentToVerify: authSafeContent,
    475              signatureToVerify: BufferSourceConverter.toArrayBuffer(this.macData.mac.digest.valueBlock.valueHexView),
    476            });
    477            //#endregion
    478 
    479            //#region Verify HMAC signature
    480            if (!result) {
    481              throw new Error("Integrity for the PKCS#12 data is broken!");
    482            }
    483            //#endregion
    484          }
    485          //#endregion
    486        }
    487        break;
    488      //#endregion
    489      //#region signedData
    490      case ContentInfo.SIGNED_DATA:
    491        {
    492          //#region Integrity based on signature using public key
    493          this.parsedValue.integrityMode = 1;
    494          //#endregion
    495 
    496          //#region Parse CMS Signed Data
    497          const cmsSigned = new SignedData({ schema: this.authSafe.content });
    498          //#endregion
    499 
    500          //#region Check that we do have OCTET STRING as "content"
    501          const eContent = cmsSigned.encapContentInfo.eContent;
    502          ParameterError.assert(eContent, "eContent", "cmsSigned.encapContentInfo");
    503          ArgumentError.assert(eContent, "eContent", asn1js.OctetString);
    504          //#endregion
    505 
    506          //#region Create correct data block for verification
    507          const data = eContent.getValue();
    508          //#endregion
    509 
    510          //#region Set "authenticatedSafe" value
    511          this.parsedValue.authenticatedSafe = AuthenticatedSafe.fromBER(data);
    512          //#endregion
    513 
    514          //#region Check integrity
    515          const ok = await cmsSigned.verify({ signer: 0, checkChain: false }, crypto);
    516          if (!ok) {
    517            throw new Error("Integrity for the PKCS#12 data is broken!");
    518          }
    519          //#endregion
    520        }
    521        break;
    522      //#endregion
    523      //#region default
    524      default:
    525        throw new Error(`Incorrect value for "this.authSafe.contentType": ${this.authSafe.contentType}`);
    526      //#endregion
    527    }
    528    //#endregion
    529  }
    530 
    531 }