Newer
Older
}
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 = "";
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++) {
mimeHeaders[key] = headers[key];
if (!(key === "" || key === "MIME-Version".toLowerCase())) {
//we have MIME-Version in the template
newHeaderLines += capitalizeHeader(key) + ": " + headers[key] + "\r\n";
}
}
let newMimeHeaderLines = "";
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
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) {
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() {
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
});
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
} 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);
});
};
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
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
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();