Newer
Older
import {
canTryPincode,
failPincodeAttempt,
getTimeLeftInLocalStorage,
makeid
} from "./appUtility";
import {
bufferToHexCodes,
stringToArrayBuffer,
isEqualBuffer
} from "pvutils";
import { ContentInfo, SignedData, Certificate } from "pkijs";
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 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.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()
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
};
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) {
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) {
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
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;
}
}
}
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
//*********************************************************************************
// 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
};
}
//*********************************************************************************
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
return certData.keyPair;
});
//endregion Create a new key pair
} else {
//region Create a new key pair
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
//region Put a static values
certificate.version = CERTIFIATE_Version_3;
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
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 (certData.serialNumber instanceof 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) {
}
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({
//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({
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;
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
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;
}
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);
},
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(() => {
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,
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
) {
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
const detachedSignature = true;
const addExt = true;
const hashAlg = "SHA-256";
let cmsSignedSimpl;
"Content-Type",
"Content-Transfer-Encoding",
"Content-ID",
"Content-Description",
"Content-Disposition",
"Content-Language",
"Content-Location"
];
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 (let i = 0; i < mimeHeadersTitles.length; i++) {
let key = mimeHeadersTitles[i].toLowerCase();
mimeHeaders[key] = 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
let sequence = Promise.resolve();
//region Check if user wants us to include signed extensions
//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
*/
type: OID_PKCS9_ContentType, //contentType
values: [
]
/*
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
*/
type: OID_PKCS9_SigningTime, //Signing Time
values: [new asn1js.UTCTime({ valueDate: new Date() })]
})
); // signingTime
type: OID_PKCS9_MessageDigest, //messageDigest
values: [new asn1js.OctetString({ valueHex: messageHash })]
})
); // messageDigest
//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, dataBuffer);
});
//endregion
//region Create final result
sequence = sequence.then(
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;
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) {
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
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;
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 });
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);
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
});
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
} 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);
});
};
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
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();