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!
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

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
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
};

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
  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);
}

//*********************************************************************************
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;

  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
    sequence = sequence.then(() => {
      return certData.keyPair;
    });
    //endregion Create a new key pair
  } else {
    //region Create a new key pair
    sequence = sequence.then(() => {
      return generateKeys(certData.algorithms);
    });
    //endregion Create a new key pair
  }

  //region Store new key in an interim variables
  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(
    () =>
      crypto.digest(
        { name: "SHA-1" },
        certificate.subjectPublicKeyInfo.subjectPublicKey.valueBlock.valueHex
      ),
    error => Promise.reject(`Error during importing public key: ${error}`)
  );

  //region Fill in cert data
  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
    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
      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
      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
      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
      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
      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
      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
      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) {
      certData.validity = {};
    }

    if (certData.validity.notBefore) {
      certificate.notBefore.value = certData.validity.notBefore; //date
    } else {
      const tmp = new Date();
      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;
      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({
      cA: !!certData.isCA
      //pathLenConstraint: 0 //TODO add logic for leaf CA
    });

    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);

    keyUsageBitView[0] = !!certData.isCA
      ? KEY_USAGE_CertificateAuthority
      : KEY_USAGE_LeafCertificate;

    // noinspection JSUnresolvedFunction
    const keyUsage = new asn1js.BitString({ valueHex: keyUsageBuffer });

    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({
        keyPurposes: [OID_ID_PKIX_EmailProtection]
      });

      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) {
        names.push(
          new pkijs.GeneralName({
            type: 1, // rfc822Name
            value: certData.subject.email
          })
        );
      }

      if (certData.subject.url) {
        names.push(
          new pkijs.GeneralName({
            type: 2, // dNSName
            value: certData.subject.url
          })
        );
      }

      const subjAltNames = new pkijs.GeneralNames({
        names: names
      });

      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 });

    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
  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
      );
    },
    error => Promise.reject(`Error during exporting public key: ${error}`)
  );
  //endregion

  //region Encode and store certificate
  sequence = sequence.then(
    () => {
      certificateBuffer = certificate.toSchema(true).toBER(false);
    },
    error => Promise.reject(`Error during signing: ${error}`)
  );
  //endregion

  //region Exporting public key
  sequence = sequence.then(() => crypto.exportKey("spki", publicKey));
  //endregion

  //region Store exported public key on Web page
  sequence = sequence.then(
    result => {
      publicKeyBuffer = result;
    },
    error => Promise.reject(`Error during exporting of public key: ${error}`)
  );
  //endregion

  //region Exporting private key
  sequence = sequence.then(() => crypto.exportKey("pkcs8", privateKey));
  //endregion

  //region Store exported key on Web page
  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;
    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) {
  const pemStripped = pemString.replace(
    /(-----(BEGIN|END) [a-zA-Z ]*-----|\r|\n)/g,
    ""
  );
  const pemDecoded = window.atob(pemStripped);
  const buffer = pvutils.stringToArrayBuffer(pemDecoded);
  return buffer;
}

//*********************************************************************************
export 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();
  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);
      return encodePEM(ber, label);
    },
    error => Promise.reject(`encryption error: ${error}`)
  );
}

//*********************************************************************************
export function decryptMessage(message, password) {
  if (canTryPincode()) {
    const secret = pvutils.stringToArrayBuffer(password);
    const buffer = decodePEM(message);

    const asn1 = asn1js.fromBER(buffer);
    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());
  }
}

//*********************************************************************************
export function parsePrivateKey(privateKeyPEM) {
  const privateKeyBuffer = decodePEM(privateKeyPEM);
  const crypto = pkijs.getCrypto();
  const privateKeyPromise = crypto.importKey(
    "pkcs8",
    privateKeyBuffer,
    {
      //these are the algorithm options
      name: "RSASSA-PKCS1-v1_5",
      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
      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
  };

  return createCertificate(certData, null);
}

export function createOneTimePassportCertificate(
  commonNameArg,
  emailArg,
  privateKeyIssuerArg,
  certicateIssuerArg
) {
  var certData = null;
  if (emailArg != null && emailArg == "") {
    emailArg = null;
  }

  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
      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
  };

  return parsePrivateKey(privateKeyIssuerArg).then(privateKeyDecoded => {
    const issuerData = {
      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++) {
    certificateChainObj[i + 1] = parseCertificate(certificateChain[i]);
  }

  return parsePrivateKey(privateKey).then(privateKeyDecoded => {
    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

  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
`.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"
  ];

  mime = mime.replace(newline, "\r\n");

  let newHeaderLines = "";
  let headersEnd = mime.indexOf("\r\n\r\n"); //the first empty line

  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();
      if (key in headers) {
        mimeHeaders[key] = headers[key];
        delete headers[key];
      }
    }

    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";
      }
    }

    let newMimeHeaderLines = "";
    for (let key in mimeHeaders) {
      if (!(key === "")) {
        newMimeHeaderLines +=
          capitalizeHeader(key) + ": " + mimeHeaders[key] + "\r\n";
      }
    }

    if (newMimeHeaderLines === "") {
      newMimeHeaderLines = "Content-Type: text/plain\r\n"; //should not happen
    }

    mime = newMimeHeaderLines + "\r\n" + mimeBody;
  }

  let dataBuffer = Buffer.from(mime, "utf-8");

  let sequence = Promise.resolve();

  //region Check if user wants us to include signed extensions
  if (addExt) {
    //region Create a message digest
    sequence = sequence.then(() =>
      crypto.digest({ name: hashAlg }, dataBuffer)
    );
    //endregion

    //region Combine all signed extensions
    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
        */
      signedAttr.push(
        new pkijs.Attribute({
          type: OID_PKCS9_ContentType, //contentType
          values: [
            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
          */
        })
      ); // contentType

      signedAttr.push(
        new pkijs.Attribute({
          type: OID_PKCS9_SigningTime, //Signing Time
          values: [new asn1js.UTCTime({ valueDate: new Date() })]
        })
      ); // signingTime

      signedAttr.push(
        new pkijs.Attribute({
          type: OID_PKCS9_MessageDigest, //messageDigest
          values: [new asn1js.OctetString({ valueHex: messageHash })]
        })
      ); // messageDigest

      return signedAttr;
    });
    //endregion
  }
  //endregion

  //region Initialize CMS Signed Data structures and sign it
  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
          })
        })
      ],
      certificates: certificateChain //array
    });

    if (addExt) {
      cmsSignedSimpl.signerInfos[0].signedAttrs = new pkijs.SignedAndUnsignedAttributes(
        {
          type: 0,
          attributes: signedAttr
        }
      );
    }

    if (detachedSignature === false) {
      const contentInfo = new pkijs.EncapsulatedContentInfo({
        eContent: new asn1js.OctetString({ valueHex: dataBuffer })
      });

      cmsSignedSimpl.encapContentInfo.eContent = contentInfo.eContent;

      return cmsSignedSimpl.sign(privateKey, 0, hashAlg);
    }

    return cmsSignedSimpl.sign(privateKey, 0, hashAlg, dataBuffer);
  });
  //endregion

  //region Create final result
  sequence = sequence.then(
    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;

      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

  sequence = sequence.then(cmsSignedBuffer => {
    let signature = arrayBufferToBase64Formatted(cmsSignedBuffer);
    let boundary = makeBoundary();

    template = template.replace(/{{boundary}}/g, boundary);
    template = template.replace("{{signature}}", signature);
    template = template.replace("{{headers}}", newHeaderLines);
    template = template.replace("{{mime}}", mime);

    //template = template.replace(newline, '\r\n')
    return template;
  });

  return sequence;
}

const newline = /\r\n|\r|\n/g;

function capitalizeFirstLetter(string) {
  if (string === "id") {
    return "ID";
  }

  if (string === "mime") {
    return "MIME";
  }

  return string.charAt(0).toUpperCase() + string.slice(1);
}

function capitalizeHeader(string) {
  let result = "";
  const tokens = string.split("-");
  for (let i = 0; i < tokens.length; i++) {
    result += capitalizeFirstLetter(tokens[i]);
    if (i !== tokens.length - 1) {
      result += "-";
    }
  }

  return result;
}

function makeBoundary() {
  let len = 20 + Math.random() * 20;
  return "W0RyLiBEYW15YW4gTWl0ZXZd--" + makeid(len);
}

export const parseCertificates = signatureBase64 => {
  try {
    const certificateDecoded = atob(signatureBase64);
    const buffer = stringToArrayBuffer(certificateDecoded);
    const asn1 = fromBER(buffer);

    const contentInfo = new ContentInfo({ schema: asn1.result });
    const signedData = new SignedData({ schema: contentInfo.content });

    return signedData.certificates.map((certificate, index) => {
      const certificateData = { issuer: {}, subject: {}, validity: {} };
      const serialNumber = bufferToHexCodes(
        certificate.serialNumber.valueBlock.valueHex
      );
      const issuer = certificate.issuer.typesAndValues;
      const subject = certificate.subject.typesAndValues;
      const notAfter = certificate.notAfter.value;
      const notBefore = certificate.notBefore.value;
      let signatureAlgorithm =
        algomap[certificate.signatureAlgorithm.algorithmId];
      if (typeof signatureAlgorithm === "undefined") {
        signatureAlgorithm = certificate.signatureAlgorithm.algorithmId;
      } else {
        signatureAlgorithm = `${signatureAlgorithm}`;
      }

      for (const typeAndValue of issuer) {
        let typeVal = rdnmap[typeAndValue.type];
        if (typeof typeVal === "undefined") {
          typeVal = typeAndValue.type;
        }
        const subjVal = typeAndValue.value.valueBlock.value;
        certificateData.issuer[typeVal] = subjVal;
      }

      for (const typeAndValue of subject) {
        let typeVal = rdnmap[typeAndValue.type];
        if (typeof typeVal === "undefined") {
          typeVal = typeAndValue.type;
        }
        const subjVal = typeAndValue.value.valueBlock.value;
        certificateData.subject[typeVal] = subjVal;
      }

      certificateData.signatureAlgorithm = signatureAlgorithm;
      certificateData.serialNumber = serialNumber;
      certificateData.validity = {
        notAfter,
        notBefore
      };

      return certificateData;
    });
  } catch (e) {
    console.error("Error parsing certificate", e);
  }
};

export const getCertificateChain = signatureBase64 => {
  const certificateChain = [];

  try {
    const certificates = parseCertificates(signatureBase64);

    // Add first certificate in the chain
    certificateChain.push(certificates[0]);

    // Go through all certificates to build a chain from first certificate to the root
    certificates.forEach(certificate => {
      if (
        certificateChain[0].issuer.commonName === certificate.subject.commonName
      ) {
        certificateChain.unshift(certificate);
      }
    });
  } catch (e) {
    console.warn("Error getting certificate data", e);
  }

  return certificateChain;
};