-
Zdravko Iliev authoredZdravko Iliev authored
signingUtilities.js 46.92 KiB
import {
canTryPincode,
failPincodeAttempt,
getTimeLeftInLocalStorage,
makeid
} from "./appUtility";
import {
bufferToHexCodes,
stringToArrayBuffer,
isEqualBuffer
} from "pvutils";
import { fromBER } from "asn1js";
import { ContentInfo, SignedData, Certificate } from "pkijs";
import { algomap, rdnmap } from "../constants/certificates";
import {
fixNewLines,
getAttachment,
getAttachments,
getGlobalHeaderValue,
getHeaderValue,
parseMIME
} from "../helpers/mailparser";
import dataUriToBlob from "data-uri-to-blob";
import {
extractHtmlBodyFromString,
getFilenameFromHeaders,
SIGNATURE_CONTENT_TYPE
} from "./emailUtilities";
import {
stringToUtf8ByteArray,
utf8ByteArrayToString,
stringToUtf8Base64,
utf8Base64ToString,
base64ToByteArray,
byteArrayToBase64
} from "../utilities/stringUtilities";
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
};
// Convert a hex string to a byte array
function hexStringToBytes(hex) {
let bytes, c;
if (hex.length % 2 === 1) {
hex = "0" + hex;
}
for (bytes = [], c = 0; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substr(c, 2), 16));
}
return bytes;
}
export class CertificateData {
//**********************************************************************************
/**
* Constructor for SignedData class
* @param {pkijs.Certificate} [certificate]
* @param {Object} [parameters]
*/
constructor(parameters = {}) {
this.serialNumber = null; //string || ArrayBuffer || Uint8Array
this.keyPair = null; // write only; {publicKey, privateKey}
this.signatureAlgorithm = null; //read-only, initialized form pkijs.Certificate object
this.algorithms = null; // write only; {hashAlg: "SHA-256", signAlg: "RSASSA-PKCS1-v1_5", keyLength: 2048};
this.issuer = null; //same as subject
this.subject = {
commonName: null, //string
country: null, //string
locality: null, //string
state: null, //string
organization: null, //string
organizationUnit: null, //string
email: null, //string
url: null //string
};
this.validity = {
notBefore: null, //new Date()
notAfter: null, //new Date()
validYears: null //int
};
this.isCA = false;
if (parameters) {
if (parameters instanceof Certificate) {
this.fromCertificate(parameters);
} else {
this.fromParameters(parameters);
}
}
}
fromCertificate(certificate) {
this.serialNumber = bufferToHexCodes(
certificate.serialNumber.valueBlock.valueHex
);
let signatureAlgorithm =
algomap[certificate.signatureAlgorithm.algorithmId];
if (typeof signatureAlgorithm === "undefined") {
signatureAlgorithm = certificate.signatureAlgorithm.algorithmId;
} else {
signatureAlgorithm = `${signatureAlgorithm}`;
}
this.signatureAlgorithm = signatureAlgorithm;
this.issuer = {};
const issuer = certificate.issuer.typesAndValues;
for (const typeAndValue of issuer) {
let typeVal = rdnmap[typeAndValue.type];
if (typeof typeVal === "undefined") {
typeVal = typeAndValue.type;
}
const subjVal = typeAndValue.value.valueBlock.value;
this.issuer[typeVal] = subjVal;
}
const subject = certificate.subject.typesAndValues;
for (const typeAndValue of subject) {
let typeVal = rdnmap[typeAndValue.type];
if (typeof typeVal === "undefined") {
typeVal = typeAndValue.type;
}
const subjVal = typeAndValue.value.valueBlock.value;
this.subject[typeVal] = subjVal;
}
this.validity.notBefore = certificate.notBefore.value;
this.validity.notAfter = certificate.notAfter.value;
this.isCA = certificate.issuer.isEqual(certificate.subject);
}
fromParameters(parameters) {
if ("serialNumber" in parameters) {
this.serialNumber = parameters.serialNumber;
}
if ("keyPair" in parameters) {
this.keyPair = {};
if ("publicKey" in parameters.keyPair) {
this.keyPair.publicKey = parameters.keyPair.publicKey;
}
if ("privateKey" in parameters.keyPair) {
this.keyPair.privateKey = parameters.keyPair.privateKey;
}
}
if ("signatureAlgorithm" in parameters) {
this.signatureAlgorithm = parameters.signatureAlgorithm;
}
if ("algorithms" in parameters) {
this.algorithms = {};
if ("hashAlg" in parameters.algorithms) {
this.algorithms.hashAlg = parameters.algorithms.hashAlg;
}
if ("signAlg" in parameters.algorithms) {
this.algorithms.signAlg = parameters.algorithms.signAlg;
}
if ("keyLength" in parameters.algorithms) {
this.algorithms.keyLength = parameters.algorithms.keyLength;
}
}
if ("subject" in parameters) {
if ("commonName" in parameters.subject) {
this.subject.commonName = parameters.subject.commonName;
}
if ("country" in parameters.subject) {
this.subject.country = parameters.subject.country;
}
if ("locality" in parameters.subject) {
this.subject.locality = parameters.subject.locality;
}
if ("state" in parameters.subject) {
this.subject.state = parameters.subject.state;
}
if ("organization" in parameters.subject) {
this.subject.organization = parameters.subject.organization;
}
if ("organizationUnit" in parameters.subject) {
this.subject.organizationUnit = parameters.subject.organizationUnit;
}
if ("email" in parameters.subject) {
this.subject.email = parameters.subject.email;
}
if ("url" in parameters.subject) {
this.subject.url = parameters.subject.url;
}
}
if ("validity" in parameters) {
if ("notBefore" in parameters.validity) {
this.validity.notBefore = parameters.validity.notBefore;
}
if ("notAfter" in parameters.validity) {
this.validity.notAfter = parameters.validity.notAfter;
}
if ("validYears" in parameters.validity) {
this.validity.validYears = parameters.validity.validYears;
}
}
if ("isCA" in parameters) {
this.isCA = parameters.isCA;
}
}
}
//*********************************************************************************
// 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 fixPkijsRDN() {
pkijs.RelativeDistinguishedNames.prototype.toSchema = function () {
//region Decode stored TBS value
if (this.valueBeforeDecode.byteLength === 0) // No stored encoded array, create "from scratch"
{
return new asn1js.Sequence({
value: Array.from(this.typesAndValues, element => new asn1js.Set({value: [element.toSchema()]}))
});
}
const asn1 = asn1js.fromBER(this.valueBeforeDecode);
//endregion
//region Construct and return new ASN.1 schema for this object
return asn1.result;
//endregion
};
}
//*********************************************************************************
function createCertificate(certData, issuerData = null) {
if (typeof certData === "undefined" || certData === null) {
return Promise.reject("No Certificate data provided");
}
if (typeof certData.subject === "undefined" || certData.subject === null) {
return Promise.reject("No Certificate subject data provided");
}
if (typeof certData.subject.commonName === "undefined" || certData.subject.commonName === null) {
return Promise.reject("No Certificate common name 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 => {
console.log('generation key',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(
() =>{
console.log('importing public key');
return 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;
certificate.serialNumber = null;
if (certData.serialNumber) {
let serialNumberView = null;
if (certData.serialNumber instanceof Buffer) {
serialNumberView = new Uint8Array(certData.serialNumber);
} else if (certData.serialNumber instanceof ArrayBuffer) {
serialNumberView = new Uint8Array(certData.serialNumber);
} else if (certData.serialNumber instanceof Uint8Array) {
serialNumberView = certData.serialNumber;
} else if (typeof certData.serialNumber === "string") {
try {
serialNumberView = new Uint8Array(hexStringToBytes(certData.serialNumber));
} catch (ignore) {
serialNumberView = null;
}
}
if (serialNumberView !== null) {
certificate.serialNumber = new asn1js.Integer({
valueHex: serialNumberView
});
}
}
if (certificate.serialNumber === null) {
const serialNumberBuffer = new ArrayBuffer(20);
const serialNumberView = new Uint8Array(serialNumberBuffer);
pkijs.getRandomValues(serialNumberView);
serialNumberView[0] &= 0x7f;
while (serialNumberView[0] === 0 && (serialNumberView[1] & 0x80) === 0) {
const firstBytesView = new Uint8Array(serialNumberBuffer, 0, 2);
pkijs.getRandomValues(firstBytesView);
firstBytesView[0] &= 0x7f;
}
// 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) {
const keyPurposes = [];
if (certData.subject.url) {
keyPurposes.push(OID_ID_PKIX_ServerAuth, OID_ID_PKIX_ClientAuth);
}
if (certData.subject.email) {
keyPurposes.push(OID_ID_PKIX_EmailProtection);
}
keyPurposes.push(OID_ID_PKIX_TimeStamping);
const extKeyUsage = new pkijs.ExtKeyUsage({
keyPurposes
});
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
});
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
//TODO add policy
/* COULD NOT GET IT WORKING
//region "AuthorityKeyIdentifier" extension
if (issuerData && issuerData.certificate) {
let issuerSubjKeyExt = null;
let extLength = issuerData.certificate.extensions.length;
for (let 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(
() => {
const signerKey =
issuerData && issuerData.privateKey ?
issuerData.privateKey :
privateKey;
console.log({issuerData});
console.log({signerKey});
console.log({certData});
console.log({defaultAlgorithms});
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(
() => {
console.log('signing',certificate);
certificateBuffer = certificate.toSchema(true).toBER(false);
},
error => Promise.reject(`Error during signing: ${error}`)
);
//endregion
//region Exporting public key
console.log('before export skpi');
sequence = sequence.then(() => crypto.exportKey("spki", publicKey));
//endregion
//region Store exported public key on Web page
sequence = sequence.then(
result => {
console.log('exporting public key',result);
publicKeyBuffer = result;
},
error => Promise.reject(`Error during exporting of public key: ${error}`)
);
//endregion
//region Exporting private key
console.log('before exporting private key');
sequence = sequence.then(() => crypto.exportKey("pkcs8", privateKey));
//endregion
//region Store exported key on Web page
sequence = sequence.then(
result => {
console.log('exporting private key', result);
privateKeyBuffer = result;
},
error => Promise.reject(`Error during exporting of private key: ${error}`)
);
//endregion
return sequence.then(() => {
const result = {
certificate,
certificatePEM: encodePEM(certificateBuffer, "CERTIFICATE"),
publicKey,
publicKeyPEM: encodePEM(publicKeyBuffer, "PUBLIC KEY"),
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 new Promise((resolve, reject) => {
failPincodeAttempt(password).then((message) => {
reject(message);
});
});
});
} 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 certDataParams = {
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
};
const certificateData = new CertificateData(certDataParams);
return createCertificate(certificateData, null);
}
export function createOneTimePassportCertificate(
commonNameArg,
emailArg,
privateKeyIssuerArg,
certicateIssuerArg
) {
let 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;
const mimeHeadersTitles = [
"Content-Type",
"Content-Transfer-Encoding",
"Content-ID",
"Content-Description",
"Content-Disposition",
"Content-Language",
"Content-Location"
];
mime = mime.replace(newline, "\r\n");
let newHeaderLines = "";
const 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) {
const mimeHeaders = {};
const mimeBody = mime.substring(headersEnd + 4);
const mimeHeadersStr = mime.substring(0, headersEnd);
const headers = libmime.decodeHeaders(mimeHeadersStr);
for (let i = 0; i < mimeHeadersTitles.length; i++) {
const key = mimeHeadersTitles[i].toLowerCase();
if (key in headers) {
mimeHeaders[key] = headers[key];
delete headers[key];
}
}
for (const 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 (const 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;
}
const 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 => {
const signature = arrayBufferToBase64Formatted(cmsSignedBuffer);
const 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() {
const len = 20 + Math.random() * 20;
return "W0RyLiBEYW15YW4gTWl0ZXZd--" + makeid(len);
}
export const parseSignedData = 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;
} catch (e) {
console.error("Error parsing signed data:", e);
return null;
}
};
export const parseCertificates = signedData => {
try {
return signedData.certificates.map((certificate) => {
const certificateData = new CertificateData(certificate);
return certificateData;
});
} catch (e) {
console.error("Error parsing certificate", e);
}
};
export const getCertificateChain = signedData => {
const certificateChain = [];
try {
const certificates = parseCertificates(signedData);
// 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;
};
const isVereignSignature = (signerInfo, signerVerificationResult) => {
const signerCert = signerVerificationResult.signerCertificate;
for (const typeAndValue of signerCert.subject.typesAndValues) {
try {
if (typeAndValue.type === "2.5.4.10" &&
typeAndValue.value.valueBlock.value === "Vereign AG"
) {
return true;
}
} catch (ignore) {}
}
return false;
};
export const verifySMIME = (smimeString, rootCaPem) => {
return new Promise(resolve => {
setTimeout(async () => {
const emailString = fixNewLines(smimeString);
const parts = parseMIME(emailString);
let signatureBase64;
let signatureBoundary;
for (const part of parts) {
let contentType = getHeaderValue("content-type", part);
if (!contentType) {
continue;
}
contentType = contentType[0];
if (contentType && contentType.startsWith(SIGNATURE_CONTENT_TYPE)) {
signatureBase64 = getAttachment(emailString, part).base64;
signatureBoundary = part.boundary;
break;
}
}
const verificationResult = {
verified: false,
message: "",
vereignSignatures: 0,
nonVereignSignatures: 0
};
if (!signatureBase64) {
verificationResult.message = "Not a signed MIME";
resolve(verificationResult);
return;
}
const dataPart = parts[0];
if (dataPart.boundary !== signatureBoundary) {
verificationResult.message = "Invalid SMIME format: wrong boundary on first MIME part";
resolve(verificationResult);
return;
}
const data = emailString.slice(
dataPart.indices.from,
dataPart.indices.to
);
const dataBuffer = stringToArrayBuffer(data);
const rootCa = parseCertificate(rootCaPem);
if (rootCa.tbs.byteLength === 0) {
rootCa.tbs = rootCa.encodeTBS();
}
const signedData = parseSignedData(signatureBase64);
if (!signedData) {
verificationResult.message = "Corrupt SMIME signature";
resolve(verificationResult);
return;
}
for (let i = 0; i < signedData.signerInfos.length; i++) {
let signerResult;
try {
signerResult = await signedData.verify({
signer: i,
data: dataBuffer,
trustedCerts: [rootCa],
checkDate: new Date(),
checkChain: true,
extendedMode: true,
passedWhenNotRevValues: false
});
} catch (e) {
verificationResult.message = e.message;
resolve(verificationResult);
return;
}
const signerVerified = !!signerResult.signatureVerified && !!signerResult.signerCertificateVerified;
if (!signerVerified) {
if (signerResult.message) {
verificationResult.message = signerResult.message;
} else {
verificationResult.message = "Message integrity is compromised";
}
resolve(verificationResult);
return;
}
if (isVereignSignature(signedData.signerInfos[i], signerResult)) {
const signerPath = signerResult.certificatePath;
const signerRoot = signerPath[signerPath.length - 1];
if (signerRoot.tbs.byteLength === 0) {
signerRoot.tbs = signerRoot.encodeTBS();
}
if (!isEqualBuffer(signerRoot.tbs, rootCa.tbs)) {
verificationResult.message =
`Vereign signature ${i} has root certificate, different from Vereign root CA`;
resolve(verificationResult);
return;
}
verificationResult.vereignSignatures++;
} else {
verificationResult.nonVereignSignatures++;
}
}
if (signedData.signerInfos.length === 0) {
verificationResult.message = "No signatures found";
} else
if (verificationResult.vereignSignatures === 0) {
verificationResult.message = "Verified succesfully, but no Vereign signatures found";
} else {
verificationResult.message = "Verified succesfully";
}
verificationResult.verified = true;
resolve(verificationResult);
}, 50);
});
};
export class ImageData {
/**
* Constructor for ImageData class
* @param {Object} [parameters] Object in format
* {
* contentType: String,
* content: String -- base64 encoded
* }
*/
constructor(parameters = {}) {
this.contentType = null; //string: the content type
this.content = null; // Uint8Array: decoded content
this.contentBase64 = null; // string: base64 encoded content
if (typeof parameters === "object") {
this.fromParameters(parameters);
}
}
fromParameters(parameters) {
if ("contentType" in parameters) {
this.contentType = parameters.contentType;
}
if ("content" in parameters) {
this.content = parameters.content;
}
if ("contentBase64" in parameters) {
this.contentBase64 = parameters.contentBase64;
}
this.getContent();
this.getContentBase64();
}
//fromDataURL()
//fromContentTypeAndContentAsByteArray()
toDataURL() {
return "data:" + this.contentType + ";base64," + this.getContentBase64();
}
getContent() {
if (!this.content) {
if (this.contentBase64) {
this.content = base64ToByteArray(this.contentBase64);
}
}
return this.content;
}
getContentBase64() {
if (!this.contentBase64) {
if (this.content) {
this.contentBase64 = byteArrayToBase64(this.content);
}
}
return this.contentBase64;
}
toJSON() {
return {
"contentType": this.contentType,
"contentBase64": this.getContentBase64()
};
}
}
//Initialization block
fixPkijsRDN();