diff --git a/javascript/src/helpers/mailparser.js b/javascript/src/helpers/mailparser.js index a6da68a5fb223932afe77acc67fc3d7149711e4e..5e3d8adfd339968f9f158e1badb76114597db66c 100644 --- a/javascript/src/helpers/mailparser.js +++ b/javascript/src/helpers/mailparser.js @@ -61,7 +61,10 @@ function calculateParts(body, from, to, previousBondary) { let boundary = findFirstBoundary(body, from, to); if (boundary == null) { - return [{ indices: { from: from, to: to }, boundary: previousBondary, leaf: true }]; + return [{ indices: { from, + to }, + boundary: previousBondary, + leaf: true }]; } const realBoundary = boundary; @@ -74,12 +77,16 @@ function calculateParts(body, from, to, previousBondary) { for (let i = 0; i < boundaryIndicesLength; i++) { const startBoundary = boundaryIndices[i]; const endBoundary = body.indexOf("\r\n", startBoundary); - boundaryPairs.push({ start: startBoundary, end: endBoundary }); + boundaryPairs.push({ start: startBoundary, + end: endBoundary }); } let bodies = []; if (previousBondary !== null) { - bodies.push({indices: {from: from, to: to}, boundary: previousBondary, leaf: false}); + bodies.push({indices: {from, + to}, + boundary: previousBondary, + leaf: false}); } for (let i = 0; i < boundaryIndicesLength - 1; i++) { @@ -122,9 +129,34 @@ export function fixNewLines(mime) { return mime.replace(newline, "\r\n"); } +export function extractMessageID(mime) { + if (mime.startsWith("\r\n")) { + mime = mime.substring(2); //should not happen + } + + const headersEndIndex = mime.indexOf("\r\n\r\n"); //the first empty line + if (headersEndIndex < 0) { + return null; + } + const mimeHeaders = mime.substring(0, headersEndIndex); + const headers = libmime.decodeHeaders(mimeHeaders); + + let messageId = headers["message-id"]; + if (Array.isArray(messageId) && messageId.length > 0) { + messageId = messageId[0]; + } + + if (messageId && typeof messageId === "string") { + messageId = messageId.replace(/^</, '').replace(/>$/, ''); + return messageId; + } + + return null; +} + export function parseMIME(mime) { let mimeStart = 0; - let headersEnd = mime.indexOf("\r\n\r\n"); //the first empty line + const headersEnd = mime.indexOf("\r\n\r\n"); //the first empty line let mimeBody = ""; if (headersEnd < 0 && mime.startsWith("\r\n")) { mime = mime.substring(2); //should not happen @@ -135,9 +167,9 @@ export function parseMIME(mime) { mimeStart = headersEnd + 4; } - let headers = libmime.decodeHeaders(mime.substring(0, headersEnd)); + const headers = libmime.decodeHeaders(mime.substring(0, headersEnd)); - let indexOfSMIME = mimeBody.indexOf(SMIMEStart); + const indexOfSMIME = mimeBody.indexOf(SMIMEStart); if (indexOfSMIME >= 0) { mimeBody = mimeBody.substring(indexOfSMIME + SMIMEStart.length); @@ -156,7 +188,9 @@ export function parseMIME(mime) { } parts.push({ - indices: { from: 0, to: mime.length, headersEnd: headersEnd }, + indices: { from: 0, + to: mime.length, + headersEnd }, headers, boundary: "mimemessage", leaf: false @@ -213,7 +247,7 @@ export function decodeMimeBody(descriptor, mimeString) { let contentType = getHeaderValue("content-type", descriptor); if (contentType) { contentType = contentType[0]; - let parsedContentType = libmime.parseHeaderValue(contentType); + const parsedContentType = libmime.parseHeaderValue(contentType); if ( parsedContentType && parsedContentType.params && @@ -230,10 +264,10 @@ export function decodeMimeBody(descriptor, mimeString) { } if (contentTransferEncoding.toLowerCase() === "quoted-printable") { - let buff = libqp.decode(mimeBody); + const buff = libqp.decode(mimeBody); return buff.toString(charset); } else if (contentTransferEncoding.toLowerCase() === "base64") { - let buff = Buffer.from(mimeBody, "base64"); + const buff = Buffer.from(mimeBody, "base64"); return buff.toString(charset); } @@ -351,5 +385,6 @@ export function getAttachment(mime, part) { base64 = window.btoa(body); } - return { contentType, base64 }; + return { contentType, + base64 }; } diff --git a/javascript/src/iframe/viamapi-iframe.js b/javascript/src/iframe/viamapi-iframe.js index 1df7b53b1e79009ecb58efda7ad7f7f4ca658f41..370ce07850a582ae24d93a45c646a8e8a512fdb1 100644 --- a/javascript/src/iframe/viamapi-iframe.js +++ b/javascript/src/iframe/viamapi-iframe.js @@ -7,6 +7,7 @@ import { base64ToByteArray, byteArrayToBase64 } from "../utilities/stringUtilities"; +import { extractMessageID } from "../helpers/mailparser"; const QRCode = require("qrcode"); const Penpal = require("penpal").default; @@ -21,6 +22,7 @@ import { import { LOGIN_MODES } from "../constants/authentication"; import { CertificateData, + ImageData, createOneTimePassportCertificate, createPassportCertificate, decryptMessage, @@ -1403,6 +1405,148 @@ const connection = Penpal.connectToParent({ return encodeResponse("200", "", "Document signed"); }, + // passportUUID - passport to sign the vCard + // text, html - the text and html part of the email + // related, attachments - array of objects, containing hashes of images/objects, related to the html in format: + // { + // headers: { + // "Content-Type": "application/hash; algorithm=SHA-256", + // "Original-Content-Type": "image/jpeg", //original content type + // "Content-Disposition": "inline" or "attachment", + // ... //other headers + // }, + // body: "base64 encoded hash" + // } + signVCard: async (passportUUID, text, html, parts = null) => { + const authenticationPublicKey = localStorage.getItem( + "authenticatedIdentity" + ); + + if ( + !authenticationPublicKey || + !window.loadedIdentities[authenticationPublicKey] || + !extendPinCodeTtl(authenticationPublicKey) + ) { + return encodeResponse("400", "", "Identity not authenticated"); + } + + let vCardImageData; + let vCardImageClaimValue; + + const vCardImageClaimName = "vCardImage"; + const defaultTagName = "notag"; + + const vCardClaimResponse = await executeRestfulFunction( + "private", + window.viamApi, + window.viamApi.entityGetClaim, + null, + vCardImageClaimName, + defaultTagName, + passportUUID + ); + // if (vCardClaimResponse.code !== "200") { + // return encodeResponse("400", "", vCardClaimResponse.status); + // } + + if (vCardClaimResponse.code === "200") { + vCardImageClaimValue = vCardClaimResponse.data; + } + + if (vCardImageClaimValue && "state" in vCardImageClaimValue && vCardImageClaimValue.state === "disabled") { + vCardImageData = new ImageData({ + contentType: "image/png", + contentBase64: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" //1x1px transparent pixel + }); + } else { + const vCardImageResponse = await executeRestfulFunction( + "private", + window.viamApi, + window.viamApi.passportGetVCardImage, + null, + passportUUID + ); + if (vCardImageResponse.code !== "200") { + return encodeResponse("400", "", vCardImageResponse.status); + } + vCardImageData = new ImageData(vCardImageResponse.data); + if (vCardImageData.contentType !== "image/png") { + return encodeResponse("400", "", "Content type of vCard mmust be 'image/png'"); + } + } + + if (!parts) { + parts = []; + } + + if (html) { + const htmlPart = { + headers: { + "Content-Type": "text/html" + }, + body: stringToUtf8Base64(html) + }; + parts.unshift(htmlPart); + } + + if (text) { + const textPart = { + headers: { + "Content-Type": "text/plain" + }, + body: stringToUtf8Base64(text) + }; + parts.unshift(textPart); + } + + const certResponse = await getCertificateForPassport(passportUUID, true); + + if (certResponse.code !== "200") { + return encodeResponse("400", "", certResponse.status); + } + + const { + x509Certificate: passportCertificate, + privateKey: passportPrivateKey, + chain: passportChain + } = certResponse.data; + + const keys = await createOneTimePassportCertificate( + makeid() + "-" + passportUUID, + null, + passportPrivateKey, + passportCertificate + ); + + const { + privateKeyPEM: privateKeyOneTime, + certificatePEM: certificateOneTime + } = keys; + + passportChain.reverse(); + + passportChain.push(passportCertificate); + passportChain.push(certificateOneTime); + + passportChain.reverse(); + + const signVCardResponse = await executeRestfulFunction( + "private", + window.viamApi, + window.viamApi.signSignVCardForChain, + null, + vCardImageData, + privateKeyOneTime, + passportChain, + parts + ); + if (signVCardResponse.code !== "200") { + return encodeResponse("400", "", signVCardResponse.status); + } + + const signedVCardImageData = new ImageData(signVCardResponse.data); + return encodeResponse("200", signedVCardImageData, "vCard signed"); + }, documentCreateDocument: async (passportUUID, path, contentType, title) => { const authenticationPublicKey = localStorage.getItem( "authenticatedIdentity" @@ -1597,6 +1741,11 @@ const connection = Penpal.connectToParent({ "Currently authenticated identity" ); }, + extractMessageID(mime) { + return new Penpal.Promise(result => { + result(extractMessageID(mime)); + }); + }, stringToUtf8ByteArray(str) { return new Penpal.Promise(result => { result(stringToUtf8ByteArray(str)); diff --git a/javascript/src/utilities/signingUtilities.js b/javascript/src/utilities/signingUtilities.js index 3fad4cbc4c74e05a39c3eb93f39a5c16ec5d406e..00d08544476fa4765696064f42de291a01811ad2 100644 --- a/javascript/src/utilities/signingUtilities.js +++ b/javascript/src/utilities/signingUtilities.js @@ -26,7 +26,14 @@ import { 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"); @@ -428,6 +435,11 @@ function createCertificate(certData, issuerData = null) { 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 @@ -923,7 +935,7 @@ export function createOneTimePassportCertificate( certicateIssuerArg ) { let certData = null; - if (emailArg != null && emailArg == "") { + if (emailArg !== null && emailArg === "") { emailArg = null; } @@ -1446,5 +1458,75 @@ export const verifySMIME = (smimeString, rootCaPem) => { }); }; +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();