Skip to content
Snippets Groups Projects
signingUtilities.js 35.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • Alexey Lunin's avatar
    Alexey Lunin committed
    import {
      canTryPincode,
      failPincodeAttempt,
      getTimeLeftInLocalStorage,
      makeid
    } from "./appUtility";
    import { bufferToHexCodes, stringToArrayBuffer } from "pvutils";
    import { fromBER } from "asn1js";
    import { ContentInfo, SignedData } from "pkijs";
    import { algomap, rdnmap } from "../constants/certificates";
    
    const libmime = require("libmime");
    const pkijs = require("pkijs");
    const asn1js = require("asn1js");
    const pvutils = require("pvutils");
    
    
    //*********************************************************************************
    
    const CERTIFIATE_Version_1 = 0;
    const CERTIFIATE_Version_3 = 2;
    
    //these bit fields are reversed, WTF!
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    const KEY_USAGE_DigitalSignature = 0x80; //01;
    const KEY_USAGE_NonRepudiation = 0x40; //02;
    const KEY_USAGE_KeyEncipherment = 0x20; //04;
    const KEY_USAGE_DataEncipherment = 0x10; //08;
    const KEY_USAGE_KeyAgreement = 0x08; //10;
    const KEY_USAGE_KeyCertSign = 0x04; //20;
    const KEY_USAGE_CRLSign = 0x02; //40;
    
    //const KEY_USAGE_EncipherOnly		= 0x01;//80; // Not used for now. Must be used together with KEY_USAGE_KeyAgreement (maybe should be ORed as a constant directly?)
    //const KEY_USAGE_DecipherOnly		= 0x80;//0100; // If used, modify "KeyUsage" extension array buffer size and appropriate bit operators to accomodate for extra byte
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    const KEY_USAGE_LeafCertificate =
      KEY_USAGE_DigitalSignature |
      KEY_USAGE_NonRepudiation |
      KEY_USAGE_KeyEncipherment |
      KEY_USAGE_DataEncipherment;
    const KEY_USAGE_CertificateAuthority =
      KEY_USAGE_DigitalSignature | KEY_USAGE_KeyCertSign | KEY_USAGE_CRLSign;
    
    const OID_EXT_KEY_USAGE_Any = "2.5.29.37.0";
    const OID_ID_PKIX_ServerAuth = "1.3.6.1.5.5.7.3.1";
    const OID_ID_PKIX_ClientAuth = "1.3.6.1.5.5.7.3.2";
    const OID_ID_PKIX_CodeSigning = "1.3.6.1.5.5.7.3.3";
    const OID_ID_PKIX_EmailProtection = "1.3.6.1.5.5.7.3.4";
    const OID_ID_PKIX_TimeStamping = "1.3.6.1.5.5.7.3.8";
    const OID_ID_PKIX_OCSPSigning = "1.3.6.1.5.5.7.3.9";
    
    // const OID_EXT_KEY_USAGE_MS...	= "1.3.6.1.4.1.311.10.3.1"; // Microsoft Certificate Trust List signing
    // const OID_EXT_KEY_USAGE_MS...	= "1.3.6.1.4.1.311.10.3.4";  // Microsoft Encrypted File System
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    const OID_PKCS7_Data = "1.2.840.113549.1.7.1";
    const OID_PKCS7_SignedData = "1.2.840.113549.1.7.2";
    const OID_PKCS7_EnvelopedData = "1.2.840.113549.1.7.3";
    const OID_PKCS9_EmailAddress = "1.2.840.113549.1.9.1";
    const OID_PKCS9_ContentType = "1.2.840.113549.1.9.3";
    const OID_PKCS9_MessageDigest = "1.2.840.113549.1.9.4";
    const OID_PKCS9_SigningTime = "1.2.840.113549.1.9.5";
    
    
    const defaultAlgorithms = {
      hashAlg: "SHA-256",
      signAlg: "RSASSA-PKCS1-v1_5",
      keyLength: 2048
    };
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    const AES_encryptionVariant_Password = 2;
    
    const encryptionAlgorithm = {
      name: "AES-CBC",
      length: 128
    };
    
    //*********************************************************************************
    // Returns promise, resolved to keyPair object {publicKey, privateKey}
    //*********************************************************************************
    function generateKeys(algorithms) {
      //region Get a "crypto" extension
      const crypto = pkijs.getCrypto();
      if (typeof crypto === "undefined") {
        return Promise.reject("No WebCrypto extension found");
      }
      //endregion Get a "crypto" extension
    
      if (!algorithms) {
        algorithms = defaultAlgorithms;
      } else {
        if (!algorithms.hashAlg) {
          algorithms.hashAlg = defaultAlgorithms.hashAlg;
        }
        if (!algorithms.signAlg) {
          algorithms.signAlg = defaultAlgorithms.signAlg;
        }
        if (!algorithms.keyLength) {
          algorithms.keyLength = defaultAlgorithms.keyLength;
        }
      }
    
      //region Get default algorithm parameters for key generation
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const algorithm = pkijs.getAlgorithmParameters(
        algorithms.signAlg,
        "generatekey"
      );
      if ("hash" in algorithm.algorithm) {
    
        algorithm.algorithm.hash.name = algorithms.hashAlg;
      }
      algorithm.algorithm.modulusLength = algorithms.keyLength;
      //endregion
    
      return crypto.generateKey(algorithm.algorithm, true, algorithm.usages);
    }
    
    //*********************************************************************************
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    function createCertificate(certData, issuerData = null) {
    
      if (typeof certData === "undefined") {
        return Promise.reject("No Certificate data provided");
      }
    
      if (typeof certData.subject === "undefined") {
        return Promise.reject("No Certificate subject data provided");
      }
    
      //region Get a "crypto" extension
      const crypto = pkijs.getCrypto();
    
      if (typeof crypto === "undefined") {
        return Promise.reject("No WebCrypto extension found");
      }
      //endregion Get a "crypto" extension
    
      //region Initial variables
      let sequence = Promise.resolve();
    
      const certificate = new pkijs.Certificate();
      let publicKey;
      let privateKey;
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      let certificateBuffer; // = new ArrayBuffer(0); // ArrayBuffer with loaded or created CERT
      let privateKeyBuffer; // = new ArrayBuffer(0);
      let publicKeyBuffer; // = new ArrayBuffer(0);
    
    
      //endregion Initial variables
    
      if (certData.keyPair) {
        //region Create a new key pair
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        sequence = sequence.then(() => {
    
          return certData.keyPair;
        });
        //endregion Create a new key pair
      } else {
        //region Create a new key pair
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        sequence = sequence.then(() => {
    
          return generateKeys(certData.algorithms);
        });
        //endregion Create a new key pair
      }
    
      //region Store new key in an interim variables
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      sequence = sequence.then(
        keyPair => {
          publicKey = keyPair.publicKey;
          privateKey = keyPair.privateKey;
        },
        error => Promise.reject(`Error during key generation: ${error}`)
      );
    
      //endregion Store new key in an interim variables
    
      //region Exporting public key into "subjectPublicKeyInfo" value of certificate
      sequence = sequence.then(() =>
        certificate.subjectPublicKeyInfo.importKey(publicKey)
      );
      //endregion Exporting public key into "subjectPublicKeyInfo" value of certificate
    
      sequence = sequence.then(
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        () =>
          crypto.digest(
            { name: "SHA-1" },
            certificate.subjectPublicKeyInfo.subjectPublicKey.valueBlock.valueHex
          ),
    
        error => Promise.reject(`Error during importing public key: ${error}`)
      );
    
      //region Fill in cert data
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      sequence = sequence.then(subjKeyIdBuffer => {
    
        //region Put a static values
        certificate.version = CERTIFIATE_Version_3;
    
        const serialNumberBuffer = new ArrayBuffer(20);
        const serialNumberView = new Uint8Array(serialNumberBuffer);
        pkijs.getRandomValues(serialNumberView);
        // noinspection JSUnresolvedFunction
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        certificate.serialNumber = new asn1js.Integer({
          valueHex: serialNumberView
        });
    
        //endregion Put a static values
    
        //region Subject
        // For reference http://oidref.com/2.5.4.3
        if (certData.subject.commonName) {
          // noinspection JSUnresolvedFunction
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          certificate.subject.typesAndValues.push(
            new pkijs.AttributeTypeAndValue({
              type: "2.5.4.3", // Common name
              value: new asn1js.PrintableString({
                value: certData.subject.commonName
              })
            })
          );
    
        }
    
        if (certData.subject.country) {
          // noinspection JSUnresolvedFunction
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          certificate.subject.typesAndValues.push(
            new pkijs.AttributeTypeAndValue({
              type: "2.5.4.6", // Country name
              value: new asn1js.PrintableString({ value: certData.subject.country })
            })
          );
    
        }
    
        if (certData.subject.locality) {
          // noinspection JSUnresolvedFunction
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          certificate.subject.typesAndValues.push(
            new pkijs.AttributeTypeAndValue({
              type: "2.5.4.7", // Locality Name
              value: new asn1js.PrintableString({
                value: certData.subject.locality
              })
            })
          );
    
        }
    
        if (certData.subject.state) {
          // noinspection JSUnresolvedFunction
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          certificate.subject.typesAndValues.push(
            new pkijs.AttributeTypeAndValue({
              type: "2.5.4.8", // State or Province name
              value: new asn1js.PrintableString({ value: certData.subject.state })
            })
          );
    
        }
    
        if (certData.subject.organization) {
          // noinspection JSUnresolvedFunction
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          certificate.subject.typesAndValues.push(
            new pkijs.AttributeTypeAndValue({
              type: "2.5.4.10", // Organization name
              value: new asn1js.PrintableString({
                value: certData.subject.organization
              })
            })
          );
    
        }
    
        if (certData.subject.organizationUnit) {
          // noinspection JSUnresolvedFunction
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          certificate.subject.typesAndValues.push(
            new pkijs.AttributeTypeAndValue({
              type: "2.5.4.11", // Organization unit name
              value: new asn1js.PrintableString({
                value: certData.subject.organizationUnit
              })
            })
          );
    
        }
    
        if (certData.subject.email) {
          // noinspection JSUnresolvedFunction
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          certificate.subject.typesAndValues.push(
            new pkijs.AttributeTypeAndValue({
              type: OID_PKCS9_EmailAddress, // Email, deprecated but still widely used
              value: new asn1js.IA5String({ value: certData.subject.email })
            })
          );
    
        }
        //endregion Subject
    
        //region Issuer
        if (issuerData && issuerData.certificate) {
          certificate.issuer = issuerData.certificate.subject;
        } else {
          certificate.issuer = certificate.subject;
        }
        //endregion Issuer
    
        //region Validity
        if (!certData.validity) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          certData.validity = {};
    
        }
    
        if (certData.validity.notBefore) {
          certificate.notBefore.value = certData.validity.notBefore; //date
        } else {
          const tmp = new Date();
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          certificate.notBefore.value = new Date(
            tmp.getFullYear(),
            tmp.getMonth(),
            tmp.getDate(),
            0,
            0,
            0
          );
    
        }
    
        if (certData.validity.notAfter) {
          certificate.notAfter.value = certData.validity.notAfter; //date
        } else {
          const tmp = certificate.notBefore.value;
          const validYears = certData.validity.validYears || 1;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          certificate.notAfter.value = new Date(
            tmp.getFullYear() + validYears,
            tmp.getMonth(),
            tmp.getDate(),
            23,
            59,
            59
          );
    
        }
        //endregion Validity
    
        //region Extensions
        certificate.extensions = []; // Extensions are not a part of certificate by default, it's an optional array
    
        //region "BasicConstraints" extension
        const basicConstr = new pkijs.BasicConstraints({
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          cA: !!certData.isCA
    
          //pathLenConstraint: 0 //TODO add logic for leaf CA
        });
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        certificate.extensions.push(
          new pkijs.Extension({
            extnID: "2.5.29.19",
            critical: true,
            extnValue: basicConstr.toSchema().toBER(false),
            parsedValue: basicConstr // Parsed value for well-known extensions
          })
        );
    
        //endregion "BasicConstraints" extension
    
        //region "KeyUsage" extension
        const keyUsageBuffer = new ArrayBuffer(1);
        const keyUsageBitView = new Uint8Array(keyUsageBuffer);
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        keyUsageBitView[0] = !!certData.isCA
          ? KEY_USAGE_CertificateAuthority
          : KEY_USAGE_LeafCertificate;
    
    
        // noinspection JSUnresolvedFunction
        const keyUsage = new asn1js.BitString({ valueHex: keyUsageBuffer });
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        certificate.extensions.push(
          new pkijs.Extension({
            extnID: "2.5.29.15",
            critical: true,
            extnValue: keyUsage.toBER(false),
            parsedValue: keyUsage // Parsed value for well-known extensions
          })
        );
    
        //endregion "KeyUsage" extension
    
        //region "ExtKeyUsage" extension
        if (!certData.isCA && certData.subject.email) {
          const extKeyUsage = new pkijs.ExtKeyUsage({
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            keyPurposes: [OID_ID_PKIX_EmailProtection]
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          certificate.extensions.push(
            new pkijs.Extension({
              extnID: "2.5.29.37",
              critical: false,
              extnValue: extKeyUsage.toSchema().toBER(false),
              parsedValue: extKeyUsage // Parsed value for well-known extensions
            })
          );
    
        }
        //endregion "ExtKeyUsage" extension
    
        //region "SubjAltName" extension
        if (certData.subject.email || certData.subject.url) {
          const names = [];
    
          if (certData.subject.email) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            names.push(
              new pkijs.GeneralName({
                type: 1, // rfc822Name
                value: certData.subject.email
              })
            );
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            names.push(
              new pkijs.GeneralName({
                type: 2, // dNSName
                value: certData.subject.url
              })
            );
    
          }
    
          const subjAltNames = new pkijs.GeneralNames({
            names: names
          });
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          certificate.extensions.push(
            new pkijs.Extension({
              extnID: "2.5.29.17",
              critical: false,
              extnValue: subjAltNames.toSchema().toBER(false),
              parsedValue: subjAltNames // Parsed value for well-known extensions
            })
          );
    
        }
        //endregion "SubjAltName" extension
    
        //region "SubjectKeyIdentifier" extension
        const subjKeyId = new asn1js.OctetString({ valueHex: subjKeyIdBuffer });
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        certificate.extensions.push(
          new pkijs.Extension({
            extnID: "2.5.29.14",
            critical: false,
            extnValue: subjKeyId.toBER(false),
            parsedValue: subjKeyId // Parsed value for well-known extensions
          })
        );
    
        //endregion "SubjectKeyIdentifier" extension
    
        /* COULD NOT GET IT WORKING
            //region "AuthorityKeyIdentifier" extension
            if (issuerData && issuerData.certificate) {
    
              let issuerSubjKeyExt = null;
    
              let extLength = issuerData.certificate.extensions.length;
              for (var i = 0; i < extLength; i++) {
                let ext = issuerData.certificate.extensions[i];
                if (ext.extnID == "2.5.29.14") {
                  issuerSubjKeyExt = ext;
                  break;
                }
              }
    
              if (issuerSubjKeyExt) {
    
                const asn1 = asn1js.fromBER(issuerSubjKeyExt.extnValue);
    
                const authKeyIdentifier = new AuthorityKeyIdentifier({
                  keyIdentifier: new asn1js.OctetString({
                    //isHexOnly: true,
                    //valueHex: issuerSubjKeyExt.parsedValue.valueBlock.valueHex
                    value: new asn1js.OctetString({ valueHex: subjKeyIdBuffer })
                  })
                });
                // const authKeyIdentifier = new AuthorityKeyIdentifier({
                // 	//keyIdentifier: new asn1js.OctetString({ valueHex: subjKeyIdBuffer })
    
                // });
    
                certificate.extensions.push(new Extension({
                  extnID: "2.5.29.35",
                  critical: false,
                  extnValue: authKeyIdentifier.toSchema().toBER(false),
                  parsedValue: authKeyIdentifier // Parsed value for well-known extensions
                }));
              }
            }
            //endregion "AuthorityKeyIdentifier" extension
        */
        //endregion Extensions
      });
      //region Fill in cert data
    
      //region Signing final certificate
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      sequence = sequence.then(
        () => {
          let signerKey =
            issuerData && issuerData.privateKey
              ? issuerData.privateKey
              : privateKey;
          return certificate.sign(
            signerKey,
            certData.algorithms && certData.algorithms.hashAlg
              ? certData.algorithms.hashAlg
              : defaultAlgorithms.hashAlg
          );
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        error => Promise.reject(`Error during exporting public key: ${error}`)
      );
    
      //endregion
    
      //region Encode and store certificate
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      sequence = sequence.then(
        () => {
          certificateBuffer = certificate.toSchema(true).toBER(false);
        },
        error => Promise.reject(`Error during signing: ${error}`)
      );
    
      //endregion
    
      //region Exporting public key
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      sequence = sequence.then(() => crypto.exportKey("spki", publicKey));
    
      //endregion
    
      //region Store exported public key on Web page
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      sequence = sequence.then(
        result => {
          publicKeyBuffer = result;
        },
        error => Promise.reject(`Error during exporting of public key: ${error}`)
      );
    
      //endregion
    
      //region Exporting private key
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      sequence = sequence.then(() => crypto.exportKey("pkcs8", privateKey));
    
      //endregion
    
      //region Store exported key on Web page
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      sequence = sequence.then(
        result => {
          privateKeyBuffer = result;
        },
        error => Promise.reject(`Error during exporting of private key: ${error}`)
      );
    
      //endregion
    
      return sequence.then(() => {
        const result = {
          certificate: certificate,
          certificatePEM: encodePEM(certificateBuffer, "CERTIFICATE"),
          publicKey: publicKey,
          publicKeyPEM: encodePEM(publicKeyBuffer, "PUBLIC KEY"),
          privateKey: privateKey,
          privateKeyPEM: encodePEM(privateKeyBuffer, "PRIVATE KEY")
        };
        return result;
      });
    }
    
    function formatPEM(pemString) {
      const lineWidth = 64;
      let resultString = "";
      let start = 0;
      let piece;
      while ((piece = pemString.substring(start, start + lineWidth)).length > 0) {
        start += lineWidth;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        resultString += piece + "\r\n";
    
      }
      return resultString;
    }
    
    function encodePEM(buffer, label) {
      const bufferString = String.fromCharCode.apply(null, new Uint8Array(buffer));
    
      const header = `-----BEGIN ${label}-----\r\n`;
      const base64formatted = formatPEM(window.btoa(bufferString));
      const footer = `-----END ${label}-----\r\n`;
      const resultString = header + base64formatted + footer;
    
      return resultString;
    }
    
    function decodePEM(pemString) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const pemStripped = pemString.replace(
        /(-----(BEGIN|END) [a-zA-Z ]*-----|\r|\n)/g,
        ""
      );
    
      const pemDecoded = window.atob(pemStripped);
      const buffer = pvutils.stringToArrayBuffer(pemDecoded);
      return buffer;
    }
    
    //*********************************************************************************
    function parseCertificate(certificatePEM) {
      const certificateBuffer = decodePEM(certificatePEM);
      const asn1 = asn1js.fromBER(certificateBuffer);
      const certificate = new pkijs.Certificate({ schema: asn1.result });
      return certificate;
    }
    
    //*********************************************************************************
    export function encryptMessage(message, password, label) {
      const buffer = pvutils.stringToArrayBuffer(message);
      const secret = pvutils.stringToArrayBuffer(password);
    
      const enveloped = new pkijs.EnvelopedData();
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      enveloped.addRecipientByPreDefinedData(
        secret,
        {},
        AES_encryptionVariant_Password
      );
      return enveloped.encrypt(encryptionAlgorithm, buffer).then(
    
        () => {
          const content = new pkijs.ContentInfo();
          content.contentType = OID_PKCS7_EnvelopedData;
          content.content = enveloped.toSchema();
          const ber = content.toSchema().toBER(false);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          return encodePEM(ber, label);
    
        },
        error => Promise.reject(`encryption error: ${error}`)
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      );
    
    }
    
    //*********************************************************************************
    export function decryptMessage(message, password) {
      if (canTryPincode()) {
        const secret = pvutils.stringToArrayBuffer(password);
        const buffer = decodePEM(message);
    
        const asn1 = asn1js.fromBER(buffer);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        const content = new pkijs.ContentInfo({ schema: asn1.result });
        const enveloped = new pkijs.EnvelopedData({ schema: content.content });
        return enveloped
          .decrypt(0, { preDefinedData: secret })
          .then(result => {
            return pvutils.arrayBufferToString(result);
          })
          .catch(() => {
            return Promise.reject(failPincodeAttempt(password));
          });
    
      } else {
        return Promise.reject(getTimeLeftInLocalStorage());
      }
    }
    
    //*********************************************************************************
    function parsePrivateKey(privateKeyPEM) {
      const privateKeyBuffer = decodePEM(privateKeyPEM);
      const crypto = pkijs.getCrypto();
      const privateKeyPromise = crypto.importKey(
        "pkcs8",
        privateKeyBuffer,
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        {
          //these are the algorithm options
    
          name: "RSASSA-PKCS1-v1_5",
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          hash: { name: "SHA-256" } //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
    
        },
        true,
        ["sign"]
      );
      return privateKeyPromise;
    }
    
    export function createPassportCertificate(commonNameArg) {
      const certData = {
        algorithms: {
          hashAlg: "SHA-256",
          signAlg: "RSASSA-PKCS1-v1_5",
          keyLength: 2048
        },
        //keyPair: generateKeys(), //optional , if provided must be object or promise that resolves to object {publicKey, prvateKey}. If it is not provided, new ones are generated automatically
        subject: {
          commonName: commonNameArg + "-userdevice", //optional for leaf, recommended for CA
          country: "CH", //optional for leaf, recommended for CA
          locality: "Zug", //optional for leaf, recommended for CA
          state: "Zug", //optional for leaf, recommended for CA
          organization: "Vereign AG", //optional for leaf, recommended for CA
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          organizationUnit: "Business Dep" //optional for leaf, recommended for CA
    
          //email: "damyan.mitev@vereign.com", // added to DN and Subject Alternative Name extension. Optional for CA. Mandatory for leaf certificate, used for email protection
          //url: "www.vereign.com" // optional url, recommended for CA, added to Subject Alternative Name extension
        },
        validity: {
          //notBefore: new Date() // optional, defaults to today at 00:00:00
          //notAfter: new Date()  // optional, defaults to notBefore + validYears at 23:59:59
          validYears: 5 //optional, defaults to 1
        },
        isCA: true // optional flag denoting if this is CA certificate or leaf certificate, defaults to false
      };
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      return createCertificate(certData, null);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    export function createOneTimePassportCertificate(
      commonNameArg,
      emailArg,
      privateKeyIssuerArg,
      certicateIssuerArg
    ) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      if (emailArg != null && emailArg !== "") {
    
        certData = {
          algorithms: {
            hashAlg: "SHA-256",
            signAlg: "RSASSA-PKCS1-v1_5",
            keyLength: 2048
          },
          //keyPair: generateKeys(), //optional , if provided must be object or promise that resolves to object {publicKey, prvateKey}. If it is not provided, new ones are generated automatically
          subject: {
            commonName: commonNameArg + "-onetime", //optional for leaf, recommended for CA
            country: "CH", //optional for leaf, recommended for CA
            locality: "Zug", //optional for leaf, recommended for CA
            state: "Zug", //optional for leaf, recommended for CA
            organization: "Vereign AG", //optional for leaf, recommended for CA
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            organizationUnit: "Business Dep", //optional for leaf, recommended for CA
            email: emailArg // added to DN and Subject Alternative Name extension. Optional for CA. Mandatory for leaf certificate, used for email protection
    
            //url: "www.vereign.com" // optional url, recommended for CA, added to Subject Alternative Name extension
          },
          validity: {
            //notBefore: new Date() // optional, defaults to today at 00:00:00
            //notAfter: new Date()  // optional, defaults to notBefore + validYears at 23:59:59
            validYears: 5 //optional, defaults to 1
          },
          isCA: false // optional flag denoting if this is CA certificate or leaf certificate, defaults to false
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        };
    
      } else {
        certData = {
          algorithms: {
            hashAlg: "SHA-256",
            signAlg: "RSASSA-PKCS1-v1_5",
            keyLength: 2048
          },
          //keyPair: generateKeys(), //optional , if provided must be object or promise that resolves to object {publicKey, prvateKey}. If it is not provided, new ones are generated automatically
          subject: {
            commonName: commonNameArg + "-onetime", //optional for leaf, recommended for CA
            country: "CH", //optional for leaf, recommended for CA
            locality: "Zug", //optional for leaf, recommended for CA
            state: "Zug", //optional for leaf, recommended for CA
            organization: "Vereign AG", //optional for leaf, recommended for CA
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            organizationUnit: "Business Dep" //optional for leaf, recommended for CA
    
            //email: emailArg, // added to DN and Subject Alternative Name extension. Optional for CA. Mandatory for leaf certificate, used for email protection
            //url: "www.vereign.com" // optional url, recommended for CA, added to Subject Alternative Name extension
          },
          validity: {
            //notBefore: new Date() // optional, defaults to today at 00:00:00
            //notAfter: new Date()  // optional, defaults to notBefore + validYears at 23:59:59
            validYears: 5 //optional, defaults to 1
          },
          isCA: false // optional flag denoting if this is CA certificate or leaf certificate, defaults to false
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        };
    
      }
    
      return parsePrivateKey(privateKeyIssuerArg).then(privateKeyDecoded => {
        const issuerData = {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          certificate: parseCertificate(certicateIssuerArg), // vereignCACertPEM),
    
          privateKey: privateKeyDecoded
        };
        return createCertificate(certData, issuerData);
      });
    }
    
    function arrayBufferToBase64Formatted(buffer) {
      const bufferString = String.fromCharCode.apply(null, new Uint8Array(buffer));
      const base64formatted = formatPEM(window.btoa(bufferString));
      return base64formatted;
    }
    
    export function signEmail(mime, signingCert, certificateChain, privateKey) {
      const signingCertObj = parseCertificate(signingCert);
      const certificateChainObj = [];
      certificateChainObj[0] = parseCertificate(signingCert);
      for (let i = 0; i < certificateChain.length; i++) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        certificateChainObj[i + 1] = parseCertificate(certificateChain[i]);
    
      }
    
      return parsePrivateKey(privateKey).then(privateKeyDecoded => {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        return signEmailObjects(
          mime,
          signingCertObj,
          certificateChainObj,
          privateKeyDecoded
        );
    
      });
    }
    
    function signEmailObjects(mime, signingCert, certificateChain, privateKey) {
      //region Get a "crypto" extension
      const crypto = pkijs.getCrypto();
      if (typeof crypto === "undefined") {
        return Promise.reject("No WebCrypto extension found");
      }
      //endregion Get a "crypto" extension
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      let template = `{{headers}}Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="{{boundary}}"
    
    MIME-Version: 1.0
    
    This is a cryptographically signed message in MIME format.
    
    --{{boundary}}
    {{mime}}
    --{{boundary}}
    Content-Type: application/pkcs7-signature; name="smime.p7s"
    Content-Transfer-Encoding: base64
    Content-Disposition: attachment; filename="smime.p7s"
    Content-Description: S/MIME Cryptographic Signature
    
    {{signature}}
    --{{boundary}}--
    
    Vereign - Authentic Communication
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    `.replace(newline, "\r\n");
    
    
      const detachedSignature = true;
      const addExt = true;
      const hashAlg = "SHA-256";
      let cmsSignedSimpl;
    
      var mimeHeadersTitles = [
        "Content-Type",
        "Content-Transfer-Encoding",
        "Content-ID",
        "Content-Description",
        "Content-Disposition",
        "Content-Language",
        "Content-Location"
      ];
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      mime = mime.replace(newline, "\r\n");
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      let headersEnd = mime.indexOf("\r\n\r\n"); //the first empty line
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      if (headersEnd < 0 && mime.startsWith("\r\n")) {
        mime = mime.substring(2); //should not happen
      } else if (headersEnd >= 0) {
    
        let mimeHeaders = {};
        let mimeBody = mime.substring(headersEnd + 4);
    
        let mimeHeadersStr = mime.substring(0, headersEnd);
    
        let headers = libmime.decodeHeaders(mimeHeadersStr);
        for (var i = 0; i < mimeHeadersTitles.length; i++) {
          let key = mimeHeadersTitles[i].toLowerCase();
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          if (key in headers) {
    
            mimeHeaders[key] = headers[key];
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            delete headers[key];
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        for (let key in headers) {
          if (!(key === "" || key === "MIME-Version".toLowerCase())) {
            //we have MIME-Version in the template
            newHeaderLines += capitalizeHeader(key) + ": " + headers[key] + "\r\n";
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        for (let key in mimeHeaders) {
          if (!(key === "")) {
            newMimeHeaderLines +=
              capitalizeHeader(key) + ": " + mimeHeaders[key] + "\r\n";
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          newMimeHeaderLines = "Content-Type: text/plain\r\n"; //should not happen
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        mime = newMimeHeaderLines + "\r\n" + mimeBody;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      let dataBuffer = Buffer.from(mime, "utf-8");
    
    
      let sequence = Promise.resolve();
    
      //region Check if user wants us to include signed extensions
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      if (addExt) {
    
        //region Create a message digest
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        sequence = sequence.then(() =>
          crypto.digest({ name: hashAlg }, dataBuffer)
    
        );
        //endregion
    
        //region Combine all signed extensions
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        sequence = sequence.then(messageHash => {
          const signedAttr = [];
          /*
    
                    1.2.840.113549.1.9.1 - e-mailAddress
                    1.2.840.113549.1.9.2 - PKCS-9 unstructuredName
                    1.2.840.113549.1.9.3 - contentType
                    1.2.840.113549.1.9.4 - messageDigest
                    1.2.840.113549.1.9.5 - Signing Time
                    1.2.840.113549.1.9.6 - counterSignature
            */
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          signedAttr.push(
            new pkijs.Attribute({
    
              type: OID_PKCS9_ContentType, //contentType
              values: [
    
    Alexey Lunin's avatar
    Alexey Lunin committed
                new asn1js.ObjectIdentifier({ value: OID_PKCS7_Data }) //data
    
              ]
              /*
                          1.2.840.113549.1.7.1 - data
                          1.2.840.113549.1.7.2 - signedData
                          1.2.840.113549.1.7.3 - envelopedData
                          1.2.840.113549.1.7.4 - signedAndEnvelopedData
                          1.2.840.113549.1.7.5 - digestedData
                          1.2.840.113549.1.7.6 - encryptedData
              */
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            })
          ); // contentType
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          signedAttr.push(
            new pkijs.Attribute({
    
              type: OID_PKCS9_SigningTime, //Signing Time
    
    Alexey Lunin's avatar
    Alexey Lunin committed
              values: [new asn1js.UTCTime({ valueDate: new Date() })]
            })
          ); // signingTime
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          signedAttr.push(
            new pkijs.Attribute({
    
              type: OID_PKCS9_MessageDigest, //messageDigest
    
    Alexey Lunin's avatar
    Alexey Lunin committed
              values: [new asn1js.OctetString({ valueHex: messageHash })]
            })
          ); // messageDigest
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          return signedAttr;
        });
    
        //endregion
      }
      //endregion
    
      //region Initialize CMS Signed Data structures and sign it
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      sequence = sequence.then(signedAttr => {
        cmsSignedSimpl = new pkijs.SignedData({
          version: 1,
          encapContentInfo: new pkijs.EncapsulatedContentInfo({
            eContentType: OID_PKCS7_Data // "data" content type
          }),
          signerInfos: [
            new pkijs.SignerInfo({
              version: 1,
              sid: new pkijs.IssuerAndSerialNumber({
                issuer: signingCert.issuer,
                serialNumber: signingCert.serialNumber
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            })
          ],
          certificates: certificateChain //array
        });
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        if (addExt) {
          cmsSignedSimpl.signerInfos[0].signedAttrs = new pkijs.SignedAndUnsignedAttributes(
            {
    
              type: 0,
              attributes: signedAttr
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            }
          );
        }
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        if (detachedSignature === false) {
          const contentInfo = new pkijs.EncapsulatedContentInfo({
            eContent: new asn1js.OctetString({ valueHex: dataBuffer })
          });
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          cmsSignedSimpl.encapContentInfo.eContent = contentInfo.eContent;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          return cmsSignedSimpl.sign(privateKey, 0, hashAlg);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    
        return cmsSignedSimpl.sign(privateKey, 0, hashAlg, dataBuffer);
      });
    
      //endregion
    
      //region Create final result
      sequence = sequence.then(
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        result => {
    
          const cmsSignedSchema = cmsSignedSimpl.toSchema(true);
    
          const cmsContentSimp = new pkijs.ContentInfo({
            contentType: OID_PKCS7_SignedData, //signedData
            content: cmsSignedSchema
          });
    
          const _cmsSignedSchema = cmsContentSimp.toSchema();
    
          //region Make length of some elements in "indefinite form"
          _cmsSignedSchema.lenBlock.isIndefiniteForm = true;
    
          const block1 = _cmsSignedSchema.valueBlock.value[1];
          block1.lenBlock.isIndefiniteForm = true;
    
          const block2 = block1.valueBlock.value[0];
          block2.lenBlock.isIndefiniteForm = true;
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          if (detachedSignature === false) {
    
            const block3 = block2.valueBlock.value[2];
            block3.lenBlock.isIndefiniteForm = true;
            block3.valueBlock.value[1].lenBlock.isIndefiniteForm = true;
            block3.valueBlock.value[1].valueBlock.value[0].lenBlock.isIndefiniteForm = true;
          }
          //endregion
    
          const cmsSignedBuffer = _cmsSignedSchema.toBER(false);
          return cmsSignedBuffer;
        },
        error => Promise.reject(`Erorr during signing of CMS Signed Data: ${error}`)
      );
      //endregion
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      sequence = sequence.then(cmsSignedBuffer => {
        let signature = arrayBufferToBase64Formatted(cmsSignedBuffer);
        let boundary = makeBoundary();
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        template = template.replace(/{{boundary}}/g, boundary);
        template = template.replace("{{signature}}", signature);
        template = template.replace("{{headers}}", newHeaderLines);
        template = template.replace("{{mime}}", mime);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        //template = template.replace(newline, '\r\n')
        return template;
      });
    
    
      return sequence;
    }
    
    const newline = /\r\n|\r|\n/g;