diff --git a/javascript/src/CryptoData.js b/javascript/src/CryptoData.js new file mode 100644 index 0000000000000000000000000000000000000000..9261a2d92c9f12334b2388a9224f90b2e2653b19 --- /dev/null +++ b/javascript/src/CryptoData.js @@ -0,0 +1,58 @@ +function CryptoData() {} + +CryptoData.prototype.set = function(obj) { + for(var member in obj) { + this[member] = JSON.parse(JSON.stringify(obj[member])) + } +}; + +CryptoData.prototype.serialize = function() { + return JSON.stringify(this) +}; + +CryptoData.prototype.deserialize = function(serialized) { + var obj = JSON.parse(serialized); + this.set(obj) +}; + +CryptoData.prototype.setPublicKey = function(publicKey) { + this["publicKey"] = publicKey +}; + +CryptoData.prototype.getPublicKey = function() { + return this["publicKey"] +}; + +CryptoData.prototype.setPrivateKey = function(privateKey) { + this["privateKey"] = privateKey +}; + +CryptoData.prototype.getPrivateKey = function() { + return this["privateKey"] +}; + +CryptoData.prototype.setx509Certificate = function(x509Certificate) { + this["x509Certificate"] = x509Certificate +}; + +CryptoData.prototype.getx509Certificate = function() { + return this["x509Certificate"] +}; + +CryptoData.prototype.setKeyUUID = function(keyUUID) { + this["keyUUID"] = keyUUID +}; + +CryptoData.prototype.getKeyUUID = function() { + return this["keyUUID"] +}; + +CryptoData.prototype.setChain = function(chain) { + this["chain"] = chain +}; + +CryptoData.prototype.getChain = function() { + return this["chain"] +}; + +export default CryptoData; \ No newline at end of file diff --git a/javascript/src/Identity.js b/javascript/src/Identity.js new file mode 100644 index 0000000000000000000000000000000000000000..43a52eafc057f67cf2feb961db87e5392f015f4d --- /dev/null +++ b/javascript/src/Identity.js @@ -0,0 +1,51 @@ +function Identity() { +} + +Identity.prototype.set = function(obj) { + for(var member in obj) { + this[member] = JSON.parse(JSON.stringify(obj[member])) + } +}; + +Identity.prototype.serialize = function() { + return JSON.stringify(this) +}; + +Identity.prototype.deserialize = function(serialized) { + var obj = JSON.parse(serialized); + this.set(obj) +}; + +Identity.prototype.setAuthentication = function(cryptoData) { + this["authentication"] = cryptoData +}; + +Identity.prototype.getAuthentication = function() { + return this["authentication"] +}; + +Identity.prototype.setPinCode = function(pinCode) { + this["pinCode"] = pinCode +}; + +Identity.prototype.getPinCode = function() { + return this["pinCode"] +}; + +Identity.prototype.setPassport = function(passportUUID, cryptoData) { + if(this["passports"] === undefined || this["passports"] === null) { + this["passports"] = {} + } + + this["passports"][passportUUID] = cryptoData +}; + +Identity.prototype.getPassport = function(passportUUID) { + if(this["passports"] === undefined || this["passports"] === null) { + this["passports"] = {} + } + + return this["passports"][passportUUID] +}; + +export default Identity; \ No newline at end of file diff --git a/javascript/src/iframe/viamapi-iframe.js b/javascript/src/iframe/viamapi-iframe.js index e89858ef7ff00eceabaaa383623e49d4c4a36d6e..ff4a071dc6b1c28a3aa696efa98cc9696f1690c0 100644 --- a/javascript/src/iframe/viamapi-iframe.js +++ b/javascript/src/iframe/viamapi-iframe.js @@ -1,1177 +1,27 @@ -import {createDeviceHash} from '../utilities/appUtility'; -import {LOGIN_MODES} from '../constants'; - -const libmime = require('libmime'); const QRCode = require('qrcode'); -const pkijs = require('pkijs'); -const asn1js = require('asn1js'); -const pvutils = require('pvutils'); const Penpal = require('penpal').default; +import { + createDeviceHash, + destroyIdentityFromLocalStorage, + encodeResponse, + listIdentitiesFromLocalStorage, makeid +} from '../utilities/appUtility'; +import {LOGIN_MODES} from '../constants'; +import { + createOneTimePassportCertificate, + createPassportCertificate, + decryptMessage, + encryptMessage, signEmail +} from '../utilities/signingUtilities'; +import CryptoData from '../CryptoData'; +import Identity from '../Identity'; + const penpalMethods = require('../../temp/penpal-methods').default; const WopiAPI = require('./wopiapi-iframe'); const CollaboraAPI = require('./collaboraapi-iframe'); const ViamAPI = require('../../temp/viamapi'); -//********************************************************************************* - -const CERTIFIATE_Version_1 = 0; -const CERTIFIATE_Version_3 = 2; - -//these bit fields are reversed, WTF! -const KEY_USAGE_DigitalSignature = 0x80;//01; -const KEY_USAGE_NonRepudiation = 0x40;//02; -const KEY_USAGE_KeyEncipherment = 0x20;//04; -const KEY_USAGE_DataEncipherment = 0x10;//08; -const KEY_USAGE_KeyAgreement = 0x08;//10; -const KEY_USAGE_KeyCertSign = 0x04;//20; -const KEY_USAGE_CRLSign = 0x02;//40; -//const KEY_USAGE_EncipherOnly = 0x01;//80; // Not used for now. Must be used together with KEY_USAGE_KeyAgreement (maybe should be ORed as a constant directly?) -//const KEY_USAGE_DecipherOnly = 0x80;//0100; // If used, modify "KeyUsage" extension array buffer size and appropriate bit operators to accomodate for extra byte - -const KEY_USAGE_LeafCertificate = KEY_USAGE_DigitalSignature | KEY_USAGE_NonRepudiation | KEY_USAGE_KeyEncipherment | KEY_USAGE_DataEncipherment; -const KEY_USAGE_CertificateAuthority= KEY_USAGE_DigitalSignature | KEY_USAGE_KeyCertSign | KEY_USAGE_CRLSign; - - -const OID_EXT_KEY_USAGE_Any = "2.5.29.37.0"; -const OID_ID_PKIX_ServerAuth = "1.3.6.1.5.5.7.3.1"; -const OID_ID_PKIX_ClientAuth = "1.3.6.1.5.5.7.3.2"; -const OID_ID_PKIX_CodeSigning = "1.3.6.1.5.5.7.3.3"; -const OID_ID_PKIX_EmailProtection = "1.3.6.1.5.5.7.3.4"; -const OID_ID_PKIX_TimeStamping = "1.3.6.1.5.5.7.3.8"; -const OID_ID_PKIX_OCSPSigning = "1.3.6.1.5.5.7.3.9"; -// const OID_EXT_KEY_USAGE_MS... = "1.3.6.1.4.1.311.10.3.1"; // Microsoft Certificate Trust List signing -// const OID_EXT_KEY_USAGE_MS... = "1.3.6.1.4.1.311.10.3.4"; // Microsoft Encrypted File System -const OID_PKCS7_Data = "1.2.840.113549.1.7.1"; -const OID_PKCS7_SignedData = "1.2.840.113549.1.7.2"; -const OID_PKCS7_EnvelopedData = "1.2.840.113549.1.7.3"; -const OID_PKCS9_EmailAddress = "1.2.840.113549.1.9.1"; -const OID_PKCS9_ContentType = "1.2.840.113549.1.9.3"; -const OID_PKCS9_MessageDigest = "1.2.840.113549.1.9.4"; -const OID_PKCS9_SigningTime = "1.2.840.113549.1.9.5"; - -const defaultAlgorithms = { - hashAlg: "SHA-256", - signAlg: "RSASSA-PKCS1-v1_5", - keyLength: 2048 -}; - -const AES_encryptionVariant_Password = 2; -const encryptionAlgorithm = { - name: "AES-CBC", - length: 128 -}; - -//********************************************************************************* -// Returns promise, resolved to keyPair object {publicKey, privateKey} -//********************************************************************************* -function generateKeys(algorithms) { - //region Get a "crypto" extension - const crypto = pkijs.getCrypto(); - if (typeof crypto === "undefined") { - return Promise.reject("No WebCrypto extension found"); - } - //endregion Get a "crypto" extension - - if (!algorithms) { - algorithms = defaultAlgorithms; - } else { - if (!algorithms.hashAlg) { - algorithms.hashAlg = defaultAlgorithms.hashAlg; - } - if (!algorithms.signAlg) { - algorithms.signAlg = defaultAlgorithms.signAlg; - } - if (!algorithms.keyLength) { - algorithms.keyLength = defaultAlgorithms.keyLength; - } - } - - //region Get default algorithm parameters for key generation - const algorithm = pkijs.getAlgorithmParameters(algorithms.signAlg, "generatekey"); - if("hash" in algorithm.algorithm) { - algorithm.algorithm.hash.name = algorithms.hashAlg; - } - algorithm.algorithm.modulusLength = algorithms.keyLength; - //endregion - - return crypto.generateKey(algorithm.algorithm, true, algorithm.usages); -} - -//********************************************************************************* -function createCertificate(certData, issuerData = null) -{ - - if (typeof certData === "undefined") { - return Promise.reject("No Certificate data provided"); - } - - if (typeof certData.subject === "undefined") { - return Promise.reject("No Certificate subject data provided"); - } - - - //region Get a "crypto" extension - const crypto = pkijs.getCrypto(); - - if (typeof crypto === "undefined") { - return Promise.reject("No WebCrypto extension found"); - } - //endregion Get a "crypto" extension - - //region Initial variables - let sequence = Promise.resolve(); - - const certificate = new pkijs.Certificate(); - let publicKey; - let privateKey; - - let certificateBuffer;// = new ArrayBuffer(0); // ArrayBuffer with loaded or created CERT - let privateKeyBuffer;// = new ArrayBuffer(0); - let publicKeyBuffer;// = new ArrayBuffer(0); - - //endregion Initial variables - - if (certData.keyPair) { - //region Create a new key pair - sequence = sequence.then(() => - { - return certData.keyPair; - }); - //endregion Create a new key pair - - } else { - //region Create a new key pair - sequence = sequence.then(() => - { - return generateKeys(certData.algorithms); - }); - //endregion Create a new key pair - } - - //region Store new key in an interim variables - sequence = sequence.then(keyPair => - { - publicKey = keyPair.publicKey; - privateKey = keyPair.privateKey; - }, error => Promise.reject(`Error during key generation: ${error}`)); - //endregion Store new key in an interim variables - - //region Exporting public key into "subjectPublicKeyInfo" value of certificate - sequence = sequence.then(() => - certificate.subjectPublicKeyInfo.importKey(publicKey) - ); - //endregion Exporting public key into "subjectPublicKeyInfo" value of certificate - - sequence = sequence.then( - () => crypto.digest({ name: "SHA-1" }, certificate.subjectPublicKeyInfo.subjectPublicKey.valueBlock.valueHex), - error => Promise.reject(`Error during importing public key: ${error}`) - ); - - //region Fill in cert data - sequence = sequence.then(subjKeyIdBuffer => - { - - //region Put a static values - certificate.version = CERTIFIATE_Version_3; - - const serialNumberBuffer = new ArrayBuffer(20); - const serialNumberView = new Uint8Array(serialNumberBuffer); - pkijs.getRandomValues(serialNumberView); - // noinspection JSUnresolvedFunction - certificate.serialNumber = new asn1js.Integer({ valueHex: serialNumberView }); - //endregion Put a static values - - //region Subject - // For reference http://oidref.com/2.5.4.3 - if (certData.subject.commonName) { - // noinspection JSUnresolvedFunction - certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ - type: "2.5.4.3", // Common name - value: new asn1js.PrintableString({ value: certData.subject.commonName }) - })); - } - - if (certData.subject.country) { - // noinspection JSUnresolvedFunction - certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ - type: "2.5.4.6", // Country name - value: new asn1js.PrintableString({ value: certData.subject.country }) - })); - } - - if (certData.subject.locality) { - // noinspection JSUnresolvedFunction - certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ - type: "2.5.4.7", // Locality Name - value: new asn1js.PrintableString({ value: certData.subject.locality }) - })); - } - - if (certData.subject.state) { - // noinspection JSUnresolvedFunction - certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ - type: "2.5.4.8", // State or Province name - value: new asn1js.PrintableString({ value: certData.subject.state }) - })); - } - - if (certData.subject.organization) { - // noinspection JSUnresolvedFunction - certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ - type: "2.5.4.10", // Organization name - value: new asn1js.PrintableString({ value: certData.subject.organization }) - })); - } - - if (certData.subject.organizationUnit) { - // noinspection JSUnresolvedFunction - certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ - type: "2.5.4.11", // Organization unit name - value: new asn1js.PrintableString({ value: certData.subject.organizationUnit }) - })); - } - - if (certData.subject.email) { - // noinspection JSUnresolvedFunction - certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ - type: OID_PKCS9_EmailAddress, // Email, deprecated but still widely used - value: new asn1js.IA5String({ value: certData.subject.email }) - })); - } - //endregion Subject - - - //region Issuer - if (issuerData && issuerData.certificate) { - certificate.issuer = issuerData.certificate.subject; - } else { - certificate.issuer = certificate.subject; - } - //endregion Issuer - - //region Validity - if (!certData.validity) { - certData.validity = {} - } - - if (certData.validity.notBefore) { - certificate.notBefore.value = certData.validity.notBefore; //date - } else { - const tmp = new Date(); - certificate.notBefore.value = new Date(tmp.getFullYear(), tmp.getMonth(), tmp.getDate(), 0, 0, 0); - } - - if (certData.validity.notAfter) { - certificate.notAfter.value = certData.validity.notAfter; //date - } else { - const tmp = certificate.notBefore.value; - const validYears = certData.validity.validYears || 1; - certificate.notAfter.value = new Date(tmp.getFullYear() + validYears, tmp.getMonth(), tmp.getDate(), 23, 59, 59); - } - //endregion Validity - - //region Extensions - certificate.extensions = []; // Extensions are not a part of certificate by default, it's an optional array - - //region "BasicConstraints" extension - const basicConstr = new pkijs.BasicConstraints({ - cA: !!certData.isCA, - //pathLenConstraint: 0 //TODO add logic for leaf CA - }); - - certificate.extensions.push(new pkijs.Extension({ - extnID: "2.5.29.19", - critical: true, - extnValue: basicConstr.toSchema().toBER(false), - parsedValue: basicConstr // Parsed value for well-known extensions - })); - //endregion "BasicConstraints" extension - - //region "KeyUsage" extension - const keyUsageBuffer = new ArrayBuffer(1); - const keyUsageBitView = new Uint8Array(keyUsageBuffer); - - keyUsageBitView[0] = !!certData.isCA ? KEY_USAGE_CertificateAuthority : KEY_USAGE_LeafCertificate; - - // noinspection JSUnresolvedFunction - const keyUsage = new asn1js.BitString({ valueHex: keyUsageBuffer }); - - certificate.extensions.push(new pkijs.Extension({ - extnID: "2.5.29.15", - critical: true, - extnValue: keyUsage.toBER(false), - parsedValue: keyUsage // Parsed value for well-known extensions - })); - //endregion "KeyUsage" extension - - //region "ExtKeyUsage" extension - if (!certData.isCA && certData.subject.email) { - const extKeyUsage = new pkijs.ExtKeyUsage({ - keyPurposes: [ - OID_ID_PKIX_EmailProtection - ] - }); - - certificate.extensions.push(new pkijs.Extension({ - extnID: "2.5.29.37", - critical: false, - extnValue: extKeyUsage.toSchema().toBER(false), - parsedValue: extKeyUsage // Parsed value for well-known extensions - })); - } - //endregion "ExtKeyUsage" extension - - //region "SubjAltName" extension - if (certData.subject.email || certData.subject.url) { - - const names = []; - - if (certData.subject.email) { - names.push(new pkijs.GeneralName({ - type: 1, // rfc822Name - value: certData.subject.email - })); - } - - if (certData.subject.url) { - names.push(new pkijs.GeneralName({ - type: 2, // dNSName - value: certData.subject.url - })); - } - - const subjAltNames = new pkijs.GeneralNames({ - names: names - }); - - certificate.extensions.push(new pkijs.Extension({ - extnID: "2.5.29.17", - critical: false, - extnValue: subjAltNames.toSchema().toBER(false), - parsedValue: subjAltNames // Parsed value for well-known extensions - })); - } - //endregion "SubjAltName" extension - - - //region "SubjectKeyIdentifier" extension - const subjKeyId = new asn1js.OctetString({ valueHex: subjKeyIdBuffer }); - - certificate.extensions.push(new pkijs.Extension({ - extnID: "2.5.29.14", - critical: false, - extnValue: subjKeyId.toBER(false), - parsedValue: subjKeyId // Parsed value for well-known extensions - })); - //endregion "SubjectKeyIdentifier" extension - - /* COULD NOT GET IT WORKING - //region "AuthorityKeyIdentifier" extension - if (issuerData && issuerData.certificate) { - - let issuerSubjKeyExt = null; - - let extLength = issuerData.certificate.extensions.length; - for (var i = 0; i < extLength; i++) { - let ext = issuerData.certificate.extensions[i]; - if (ext.extnID == "2.5.29.14") { - issuerSubjKeyExt = ext; - break; - } - } - - if (issuerSubjKeyExt) { - - const asn1 = asn1js.fromBER(issuerSubjKeyExt.extnValue); - - const authKeyIdentifier = new AuthorityKeyIdentifier({ - keyIdentifier: new asn1js.OctetString({ - //isHexOnly: true, - //valueHex: issuerSubjKeyExt.parsedValue.valueBlock.valueHex - value: new asn1js.OctetString({ valueHex: subjKeyIdBuffer }) - }) - }); - // const authKeyIdentifier = new AuthorityKeyIdentifier({ - // //keyIdentifier: new asn1js.OctetString({ valueHex: subjKeyIdBuffer }) - - // }); - - certificate.extensions.push(new Extension({ - extnID: "2.5.29.35", - critical: false, - extnValue: authKeyIdentifier.toSchema().toBER(false), - parsedValue: authKeyIdentifier // Parsed value for well-known extensions - })); - } - } - //endregion "AuthorityKeyIdentifier" extension - */ - //endregion Extensions - }); - //region Fill in cert data - - - //region Signing final certificate - sequence = sequence.then(() => { - let signerKey = (issuerData && issuerData.privateKey) ? issuerData.privateKey : privateKey; - return certificate.sign(signerKey, (certData.algorithms && certData.algorithms.hashAlg) ? certData.algorithms.hashAlg : defaultAlgorithms.hashAlg) - }, - error => Promise.reject(`Error during exporting public key: ${error}`)); - //endregion - - //region Encode and store certificate - sequence = sequence.then(() => - { - certificateBuffer = certificate.toSchema(true).toBER(false); - }, error => Promise.reject(`Error during signing: ${error}`)); - //endregion - - //region Exporting public key - sequence = sequence.then(() => - crypto.exportKey("spki", publicKey) - ); - //endregion - - //region Store exported public key on Web page - sequence = sequence.then(result => - { - publicKeyBuffer = result; - }, error => Promise.reject(`Error during exporting of public key: ${error}`)); - //endregion - - //region Exporting private key - sequence = sequence.then(() => - crypto.exportKey("pkcs8", privateKey) - ); - //endregion - - //region Store exported key on Web page - sequence = sequence.then(result => - { - privateKeyBuffer = result; - }, error => Promise.reject(`Error during exporting of private key: ${error}`)); - //endregion - - return sequence.then(() => { - - const result = { - certificate: certificate, - certificatePEM: encodePEM(certificateBuffer, "CERTIFICATE"), - publicKey: publicKey, - publicKeyPEM: encodePEM(publicKeyBuffer, "PUBLIC KEY"), - privateKey: privateKey, - privateKeyPEM: encodePEM(privateKeyBuffer, "PRIVATE KEY") - }; - return result; - }); -} - -function formatPEM(pemString) { - const lineWidth = 64; - let resultString = ""; - let start = 0; - let piece; - while ((piece = pemString.substring(start, start + lineWidth)).length > 0) { - start += lineWidth; - resultString += piece + '\r\n'; - } - return resultString; -} - -function encodePEM(buffer, label) { - const bufferString = String.fromCharCode.apply(null, new Uint8Array(buffer)); - - const header = `-----BEGIN ${label}-----\r\n`; - const base64formatted = formatPEM(window.btoa(bufferString)); - const footer = `-----END ${label}-----\r\n`; - const resultString = header + base64formatted + footer; - - return resultString; -} - -function decodePEM(pemString) { - const pemStripped = pemString.replace(/(-----(BEGIN|END) [a-zA-Z ]*-----|\r|\n)/g, ''); - const pemDecoded = window.atob(pemStripped); - const buffer = pvutils.stringToArrayBuffer(pemDecoded); - return buffer; -} - -//********************************************************************************* -function parseCertificate(certificatePEM) { - const certificateBuffer = decodePEM(certificatePEM); - const asn1 = asn1js.fromBER(certificateBuffer); - const certificate = new pkijs.Certificate({ schema: asn1.result }); - return certificate; -} - -//********************************************************************************* -function parsePublicKey(publicKeyPEM) { - const publicKeyBuffer = decodePEM(publicKeyPEM); - const crypto = pkijs.getCrypto(); - const publicKeyPromise = crypto.importKey( - "spki", - publicKeyBuffer, - { //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, - ["verify"] - ); - return publicKeyPromise; -} - -//********************************************************************************* -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}`) - ) -} - -//********************************************************************************* -function decryptMessage(message, password) { - if (canTryPincode()) { - const secret = pvutils.stringToArrayBuffer(password); - const buffer = decodePEM(message); - - const asn1 = asn1js.fromBER(buffer); - const content = new pkijs.ContentInfo({schema: asn1.result}); - const enveloped = new pkijs.EnvelopedData({schema: content.content}); - return enveloped.decrypt(0, {preDefinedData: secret}).then(result => { - return pvutils.arrayBufferToString(result); - }).catch(() => { - return Promise.reject(failPincodeAttempt(password)); - }); - } else { - return Promise.reject(getTimeLeftInLocalStorage()); - } -} - -//********************************************************************************* -function getBlockFinishTimeInLocalStorage() { - return localStorage.getItem("blockFinishTime") || getCurrentTime(); -} - -function getCurrentTime() { - return Math.floor(new Date().getTime() / 1000); -} - -function getTimeLeftInLocalStorage() { - const blockFinishTime = getBlockFinishTimeInLocalStorage(); - const timeNow = getCurrentTime(); - const seconds = (blockFinishTime - timeNow) % 60; - let minutes = Math.floor((blockFinishTime - timeNow) / 60); - minutes %= 60; - - const left = "Your identity has been locked. Try again in " + minutes + " minutes and " + seconds + " seconds."; - return left; -} - -function failPincodeAttempt(password) { - let message = "Wrong pincode"; - if (password !== '00000000') { - let attempt = localStorage.getItem("attempt") || 1; - attempt = parseInt(attempt); - if (attempt === 9) { - const identitiesTemp = listIdentitiesFromLocalStorage(); - for (let i in identitiesTemp) { - destroyIdentityFromLocalStorage(i); - } - message = "9 failed attempts. Identity is revoked!"; - localStorage.removeItem("attempt"); - } else if (attempt % 3 === 0) { - const timeNow = getCurrentTime(); - const blockFinishTime = timeNow + 300; - localStorage.setItem("blockFinishTime", blockFinishTime); - localStorage.setItem("attempt", attempt + 1); - message = "3 failed attempts. Identity is locked!"; - } else { - localStorage.setItem("attempt", attempt + 1); - } - } - return message; -} - -function canTryPincode() { - const timeNow = getCurrentTime(); - const blockFinishTime = getBlockFinishTimeInLocalStorage(); - if (blockFinishTime <= timeNow) { - localStorage.removeItem("blockFinishTime"); - return true; - } else { - return false; - } -} - -//********************************************************************************* -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; -} - - -function createPassportCertificate(commonNameArg) { - const certData = { - algorithms: { - hashAlg: "SHA-256", - signAlg: "RSASSA-PKCS1-v1_5", - keyLength: 2048 - }, - //keyPair: generateKeys(), //optional , if provided must be object or promise that resolves to object {publicKey, prvateKey}. If it is not provided, new ones are generated automatically - subject: { - commonName: commonNameArg + "-userdevice", //optional for leaf, recommended for CA - country: "CH", //optional for leaf, recommended for CA - locality: "Zug", //optional for leaf, recommended for CA - state: "Zug", //optional for leaf, recommended for CA - organization: "Vereign AG", //optional for leaf, recommended for CA - organizationUnit:"Business Dep", //optional for leaf, recommended for CA - //email: "damyan.mitev@vereign.com", // added to DN and Subject Alternative Name extension. Optional for CA. Mandatory for leaf certificate, used for email protection - //url: "www.vereign.com" // optional url, recommended for CA, added to Subject Alternative Name extension - }, - validity: { - //notBefore: new Date() // optional, defaults to today at 00:00:00 - //notAfter: new Date() // optional, defaults to notBefore + validYears at 23:59:59 - validYears: 5 //optional, defaults to 1 - }, - isCA: true // optional flag denoting if this is CA certificate or leaf certificate, defaults to false - }; - - return createCertificate(certData, null) -} - -function createOneTimePassportCertificate(commonNameArg, emailArg, privateKeyIssuerArg, certicateIssuerArg) { - var certData = null; - if(emailArg != null && emailArg !== "") { - 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 - } - } else { - 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 download(filename, contentType, text) { - var element = document.createElement('a'); - element.setAttribute('href', 'data:' + contentType + ';charset=utf-8,' + encodeURIComponent(text)); - element.setAttribute('download', filename); - - element.style.display = 'none'; - document.body.appendChild(element); - - element.click(); - - document.body.removeChild(element); -} - -function arrayBufferToBase64Formatted(buffer) { - const bufferString = String.fromCharCode.apply(null, new Uint8Array(buffer)); - const base64formatted = formatPEM(window.btoa(bufferString)); - return base64formatted; -} - -function arrayBufferToBase64(buffer) { - const bufferString = String.fromCharCode.apply(null, new Uint8Array(buffer)); - const base64 = window.btoa(bufferString); - return base64; -} - -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 signEmail(mime, signingCert, certificateChain, privateKey) { - const signingCertObj = parseCertificate(signingCert); - const certificateChainObj = []; - certificateChainObj[0] = parseCertificate(signingCert); - for (let i = 0; i < certificateChain.length; i++) { - certificateChainObj[i + 1] = parseCertificate(certificateChain[i]) - } - - return parsePrivateKey(privateKey).then(privateKeyDecoded => { - return signEmailObjects(mime, signingCertObj, certificateChainObj, privateKeyDecoded); - }); -} - -function signEmailObjects(mime, signingCert, certificateChain, privateKey) { - - //region Get a "crypto" extension - const crypto = pkijs.getCrypto(); - if (typeof crypto === "undefined") { - return Promise.reject("No WebCrypto extension found"); - } - //endregion Get a "crypto" extension - - let template = - `{{headers}}Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="{{boundary}}" -MIME-Version: 1.0 - -This is a cryptographically signed message in MIME format. - ---{{boundary}} -{{mime}} ---{{boundary}} -Content-Type: application/pkcs7-signature; name="smime.p7s" -Content-Transfer-Encoding: base64 -Content-Disposition: attachment; filename="smime.p7s" -Content-Description: S/MIME Cryptographic Signature - -{{signature}} ---{{boundary}}-- - -Vereign - Authentic Communication -`.replace(newline, '\r\n'); - - const detachedSignature = true; - const addExt = true; - const hashAlg = "SHA-256"; - let cmsSignedSimpl; - - var mimeHeadersTitles = [ - "Content-Type", - "Content-Transfer-Encoding", - "Content-ID", - "Content-Description", - "Content-Disposition", - "Content-Language", - "Content-Location" - ]; - - mime = mime.replace(newline, '\r\n'); - - let newHeaderLines = ""; - let headersEnd = mime.indexOf('\r\n\r\n'); //the first empty line - - if (headersEnd < 0 && mime.startsWith('\r\n')) { - - mime = mime.substring(2) //should not happen - - } else - if (headersEnd >= 0) { - - let mimeHeaders = {}; - let mimeBody = mime.substring(headersEnd + 4); - - let mimeHeadersStr = mime.substring(0, headersEnd); - - let headers = libmime.decodeHeaders(mimeHeadersStr); - for (var i = 0; i < mimeHeadersTitles.length; i++) { - let key = mimeHeadersTitles[i].toLowerCase(); - if(key in headers) { - mimeHeaders[key] = headers[key]; - delete headers[key] - } - } - - for(let key in headers) { - if(!(key === "" || key === "MIME-Version".toLowerCase())) { //we have MIME-Version in the template - newHeaderLines += capitalizeHeader(key) + ": " + headers[key] + '\r\n'; - } - } - - let newMimeHeaderLines = ""; - for(let key in mimeHeaders) { - if(!(key === "")) { - newMimeHeaderLines += capitalizeHeader(key) + ": " + mimeHeaders[key] + '\r\n'; - } - } - - if (newMimeHeaderLines === "") { - newMimeHeaderLines = 'Content-Type: text/plain\r\n' //should not happen - } - - mime = newMimeHeaderLines + '\r\n' + mimeBody - } - - let dataBuffer = Buffer.from(mime,'utf-8'); - - let sequence = Promise.resolve(); - - //region Check if user wants us to include signed extensions - if(addExt) - { - //region Create a message digest - sequence = sequence.then( - () => crypto.digest({ name: hashAlg }, dataBuffer) - ); - //endregion - - //region Combine all signed extensions - sequence = sequence.then( - messageHash => - { - const signedAttr = []; - /* - 1.2.840.113549.1.9.1 - e-mailAddress - 1.2.840.113549.1.9.2 - PKCS-9 unstructuredName - 1.2.840.113549.1.9.3 - contentType - 1.2.840.113549.1.9.4 - messageDigest - 1.2.840.113549.1.9.5 - Signing Time - 1.2.840.113549.1.9.6 - counterSignature - */ - signedAttr.push(new pkijs.Attribute({ - type: OID_PKCS9_ContentType, //contentType - values: [ - new asn1js.ObjectIdentifier({ value: OID_PKCS7_Data}) //data - ] - /* - 1.2.840.113549.1.7.1 - data - 1.2.840.113549.1.7.2 - signedData - 1.2.840.113549.1.7.3 - envelopedData - 1.2.840.113549.1.7.4 - signedAndEnvelopedData - 1.2.840.113549.1.7.5 - digestedData - 1.2.840.113549.1.7.6 - encryptedData - */ - })); // contentType - - signedAttr.push(new pkijs.Attribute({ - type: OID_PKCS9_SigningTime, //Signing Time - values: [ - new asn1js.UTCTime({ valueDate: new Date() }) - ] - })); // signingTime - - signedAttr.push(new pkijs.Attribute({ - type: OID_PKCS9_MessageDigest, //messageDigest - values: [ - new asn1js.OctetString({ valueHex: messageHash }) - ] - })); // messageDigest - - return signedAttr; - } - ); - //endregion - } - //endregion - - //region Initialize CMS Signed Data structures and sign it - sequence = sequence.then( - signedAttr => - { - cmsSignedSimpl = new pkijs.SignedData({ - version: 1, - encapContentInfo: new pkijs.EncapsulatedContentInfo({ - eContentType: OID_PKCS7_Data // "data" content type - }), - signerInfos: [ - new pkijs.SignerInfo({ - version: 1, - sid: new pkijs.IssuerAndSerialNumber({ - issuer: signingCert.issuer, - serialNumber: signingCert.serialNumber - }) - }) - ], - certificates: certificateChain //array - }); - - if(addExt) - { - cmsSignedSimpl.signerInfos[0].signedAttrs = new pkijs.SignedAndUnsignedAttributes({ - type: 0, - attributes: signedAttr - }); - } - - if(detachedSignature === false) - { - const contentInfo = new pkijs.EncapsulatedContentInfo({ - eContent: new asn1js.OctetString({ valueHex: dataBuffer }) - }); - - cmsSignedSimpl.encapContentInfo.eContent = contentInfo.eContent; - - return cmsSignedSimpl.sign(privateKey, 0, hashAlg); - } - - return cmsSignedSimpl.sign(privateKey, 0, hashAlg, dataBuffer); - } - ); - //endregion - - //region Create final result - sequence = sequence.then( - (result) => - { - const cmsSignedSchema = cmsSignedSimpl.toSchema(true); - - const cmsContentSimp = new pkijs.ContentInfo({ - contentType: OID_PKCS7_SignedData, //signedData - content: cmsSignedSchema - }); - - const _cmsSignedSchema = cmsContentSimp.toSchema(); - - //region Make length of some elements in "indefinite form" - _cmsSignedSchema.lenBlock.isIndefiniteForm = true; - - const block1 = _cmsSignedSchema.valueBlock.value[1]; - block1.lenBlock.isIndefiniteForm = true; - - const block2 = block1.valueBlock.value[0]; - block2.lenBlock.isIndefiniteForm = true; - - if(detachedSignature === false) - { - const block3 = block2.valueBlock.value[2]; - block3.lenBlock.isIndefiniteForm = true; - block3.valueBlock.value[1].lenBlock.isIndefiniteForm = true; - block3.valueBlock.value[1].valueBlock.value[0].lenBlock.isIndefiniteForm = true; - } - //endregion - - const cmsSignedBuffer = _cmsSignedSchema.toBER(false); - return cmsSignedBuffer; - }, - error => Promise.reject(`Erorr during signing of CMS Signed Data: ${error}`) - ); - //endregion - - sequence = sequence.then( - (cmsSignedBuffer) => - { - let signature = arrayBufferToBase64Formatted(cmsSignedBuffer); - let boundary = makeBoundary(); - - template = template.replace(/{{boundary}}/g, boundary); - template = template.replace("{{signature}}", signature); - template = template.replace("{{headers}}", newHeaderLines); - template = template.replace("{{mime}}", mime); - - //template = template.replace(newline, '\r\n') - return template - } - ); - - return sequence; -} - -function makeBoundary() { - let len = 20 + Math.random() * 20; - return 'W0RyLiBEYW15YW4gTWl0ZXZd--' + makeid(len) -} - -function makeid(len) { - if (typeof len === 'undefined') { - len = 10 - } - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for (var i = 0; i < len; i++) - text += possible.charAt(Math.floor(Math.random() * possible.length)); - - return text; -} - -function CryptoData() { -} - -CryptoData.prototype.set = function(obj) { - for(var member in obj) { - this[member] = JSON.parse(JSON.stringify(obj[member])) - } -}; - -CryptoData.prototype.serialize = function() { - return JSON.stringify(this) -}; - -CryptoData.prototype.deserialize = function(serialized) { - var obj = JSON.parse(serialized); - this.set(obj) -}; - -CryptoData.prototype.setPublicKey = function(publicKey) { - this["publicKey"] = publicKey -}; - -CryptoData.prototype.getPublicKey = function() { - return this["publicKey"] -}; - -CryptoData.prototype.setPrivateKey = function(privateKey) { - this["privateKey"] = privateKey -}; - -CryptoData.prototype.getPrivateKey = function() { - return this["privateKey"] -}; - -CryptoData.prototype.setx509Certificate = function(x509Certificate) { - this["x509Certificate"] = x509Certificate -}; - -CryptoData.prototype.getx509Certificate = function() { - return this["x509Certificate"] -}; - -CryptoData.prototype.setKeyUUID = function(keyUUID) { - this["keyUUID"] = keyUUID -}; - -CryptoData.prototype.getKeyUUID = function() { - return this["keyUUID"] -}; - -CryptoData.prototype.setChain = function(chain) { - this["chain"] = chain -}; - -CryptoData.prototype.getChain = function() { - return this["chain"] -}; - -function Identity() { -} - -Identity.prototype.set = function(obj) { - for(var member in obj) { - this[member] = JSON.parse(JSON.stringify(obj[member])) - } -}; - -Identity.prototype.serialize = function() { - return JSON.stringify(this) -}; - -Identity.prototype.deserialize = function(serialized) { - var obj = JSON.parse(serialized); - this.set(obj) -}; - -Identity.prototype.setAuthentication = function(cryptoData) { - this["authentication"] = cryptoData -}; - -Identity.prototype.getAuthentication = function() { - return this["authentication"] -}; - -Identity.prototype.setPinCode = function(pinCode) { - this["pinCode"] = pinCode -}; - -Identity.prototype.getPinCode = function() { - return this["pinCode"] -}; - -Identity.prototype.setPassport = function(passportUUID, cryptoData) { - if(this["passports"] === undefined || this["passports"] === null) { - this["passports"] = {} - } - - this["passports"][passportUUID] = cryptoData -}; - -Identity.prototype.getPassport = function(passportUUID) { - if(this["passports"] === undefined || this["passports"] === null) { - this["passports"] = {} - } - - return this["passports"][passportUUID] -}; - var identityColors = ["#994392", "#cb0767", "#e51d31", "#ec671b", "#fab610"]; function getNextColor() { @@ -1289,22 +139,7 @@ function getIdentityFromLocalStorage(key, pinCode, extendTtl = true) { } -function listIdentitiesFromLocalStorage() { - var serializedIdentitiesList = localStorage.getItem("identities"); - var identities = JSON.parse(serializedIdentitiesList); - var identitiesResult = {}; - - for(var key in identities) { - var profile = JSON.parse(JSON.stringify(localStorage.getItem("profiles/" + key))); - if(profile != null && profile !== "") { - identitiesResult[key] = JSON.parse(profile) - } else { - identitiesResult[key] = {} - } - } - return identitiesResult -} function extendPinCodeTtl(key, pinCode) { if(pinCode == null || pinCode === "") { @@ -1360,22 +195,6 @@ function createEvent(actionId, type, payloads) { } } -function destroyIdentityFromLocalStorage(key) { - localStorage.removeItem(key); - localStorage.removeItem("profiles/" + key); - localStorage.removeItem("colors/" + key); - - var serializedIdentitiesList = localStorage.getItem("identities"); - - var identities = JSON.parse(serializedIdentitiesList); - - identities[key] = null; - - delete identities[key]; - - localStorage.setItem("identities", JSON.stringify(identities)) -} - window.loadedIdentities = {}; window.wopiAPI = new WopiAPI(); window.collaboraApi = new CollaboraAPI(); @@ -2026,80 +845,54 @@ const connection = Penpal.connectToParent({ }) }); }, - signEmail(passportUUID, emailArg, emailMessage) { - return new Penpal.Promise(result => { - const authenticationPublicKey = localStorage.getItem("authenticatedIdentity"); - if (authenticationPublicKey === null) { - return {"data" : "", - "code" : "400", - "status" : "Identity not authenticated" - } - } - if (window.loadedIdentities[authenticationPublicKey] === null) { - return {"data" : "", - "code" : "400", - "status" : "Identity not authenticated" - } - } + signEmail: async (passportUUID, emailArg, emailMessage) => { + const authenticationPublicKey = localStorage.getItem("authenticatedIdentity"); - var success = extendPinCodeTtl(authenticationPublicKey); + if ( + !authenticationPublicKey || + !window.loadedIdentities[authenticationPublicKey] || + !extendPinCodeTtl(authenticationPublicKey) + ) { + return encodeResponse("400", "", "Identity not authenticated"); + } - if(success === false) { - result({"data" : "", - "code" : "400", - "status" : "Identity not authenticated" - }) - } + let response = await getCertificateForPassport(passportUUID, true); - getCertificateForPassport(passportUUID, true).then(certificateResult => { - if(certificateResult.code === "200") { - var passportCertificate = certificateResult.data["x509Certificate"]; - var passportPrivateKey = certificateResult.data["privateKey"]; - var passportChain = certificateResult.data["chain"]; + if (response.code !== "200") { + return encodeResponse("400", "", response.status); + } - createOneTimePassportCertificate(makeid() + "-" + passportUUID, emailArg, passportPrivateKey, passportCertificate).then(function(keys){ - var publicKeyOneTime = keys["publicKeyPEM"]; - var privateKeyOneTime = keys["privateKeyPEM"]; - var certificateOneTime = keys["certificatePEM"]; - //download("certificateOneTime.crt", "text/plain", certificateOneTime) + const { + x509Certificate: passportCertificate, + privateKey: passportPrivateKey, + chain: passportChain + } = response.data; - passportChain.push(passportCertificate); + const keys = + await createOneTimePassportCertificate( + makeid() + "-" + passportUUID, emailArg, passportPrivateKey, passportCertificate); - executeRestfulFunction("private", viamApi, viamApi.passportGetEmailWithHeaderByPassport, - passportUUID, emailMessage).then(executeResult2 => { - var emailWithHeader = executeResult2.data; - //download("withheader.eml", "message/rfc822", emailWithHeader) - var signedEmailPromise = signEmail(emailWithHeader, - certificateOneTime, - passportChain, - privateKeyOneTime); - - signedEmailPromise.then(signedEmail => { - executeRestfulFunction("private", viamApi, viamApi.signResignEmail, - passportUUID, signedEmail).then(executeResult => { - result({"data" : executeResult.data, - "code" : "200", - "status" : "Email signed" - }) - }); - /*result({"data" : signedEmail, - "code" : "200", - "status" : "Email signed" - })*/ - }); - }); - // Prints PEM formatted signed certificate - // -----BEGIN CERTIFICATE-----MIID....7Hyg==-----END CERTIFICATE----- + const { privateKeyPEM: privateKeyOneTime, certificatePEM: certificateOneTime } = keys; - }); - } else { - result({"data" : "", - "code" : "400", - "status" : "Can not sign email" - }) - } - }) - }); + passportChain.push(passportCertificate); + + response = await executeRestfulFunction( + "private", window.viamApi, window.viamApi.passportGetEmailWithHeaderByPassport, passportUUID, emailMessage); + + if (response.code !== "200") { + return encodeResponse("400", "", response.status); + } + + const signedEmail = await signEmail(response.data, certificateOneTime, passportChain, privateKeyOneTime); + + response = await executeRestfulFunction( + "private", window.viamApi, window.viamApi.signResignEmail, passportUUID, signedEmail); + + if (response.code !== "200") { + return encodeResponse("400", "", response.status); + } + + return encodeResponse("200", response.data, "Email signed"); }, hasSession() { return new Penpal.Promise(result => { diff --git a/javascript/src/utilities/appUtility.js b/javascript/src/utilities/appUtility.js index 66cfaf8b37d46c8175b4393675dd008e9ff84b77..c6090f2558c3a71d5edb8ee60efcb79fc49b27a1 100644 --- a/javascript/src/utilities/appUtility.js +++ b/javascript/src/utilities/appUtility.js @@ -11,3 +11,113 @@ export const createDeviceHash = async (publicKey) => { console.warn(error); // eslint-disable-line no-console } }; + +export const encodeResponse = (code, data, status) => { + return { + code, + data, + status + }; +}; + +export function makeid(len) { + if (typeof len === 'undefined') { + len = 10 + } + var text = ""; + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for (var i = 0; i < len; i++) + text += possible.charAt(Math.floor(Math.random() * possible.length)); + + return text; +} + +//********************************************************************************* +function getBlockFinishTimeInLocalStorage() { + return localStorage.getItem("blockFinishTime") || getCurrentTime(); +} + +function getCurrentTime() { + return Math.floor(new Date().getTime() / 1000); +} + +export function getTimeLeftInLocalStorage() { + const blockFinishTime = getBlockFinishTimeInLocalStorage(); + const timeNow = getCurrentTime(); + const seconds = (blockFinishTime - timeNow) % 60; + let minutes = Math.floor((blockFinishTime - timeNow) / 60); + minutes %= 60; + + const left = "Your identity has been locked. Try again in " + minutes + " minutes and " + seconds + " seconds."; + return left; +} + +export function listIdentitiesFromLocalStorage() { + var serializedIdentitiesList = localStorage.getItem("identities"); + var identities = JSON.parse(serializedIdentitiesList); + var identitiesResult = {}; + + for(var key in identities) { + var profile = JSON.parse(JSON.stringify(localStorage.getItem("profiles/" + key))); + if(profile != null && profile !== "") { + identitiesResult[key] = JSON.parse(profile) + } else { + identitiesResult[key] = {} + } + } + + return identitiesResult +} + +export function destroyIdentityFromLocalStorage(key) { + localStorage.removeItem(key); + localStorage.removeItem("profiles/" + key); + localStorage.removeItem("colors/" + key); + + var serializedIdentitiesList = localStorage.getItem("identities"); + + var identities = JSON.parse(serializedIdentitiesList); + + identities[key] = null; + + delete identities[key]; + + localStorage.setItem("identities", JSON.stringify(identities)) +} + +export function failPincodeAttempt(password) { + let message = "Wrong pincode"; + if (password !== '00000000') { + let attempt = localStorage.getItem("attempt") || 1; + attempt = parseInt(attempt); + if (attempt === 9) { + const identitiesTemp = listIdentitiesFromLocalStorage(); + for (let i in identitiesTemp) { + destroyIdentityFromLocalStorage(i); + } + message = "9 failed attempts. Identity is revoked!"; + localStorage.removeItem("attempt"); + } else if (attempt % 3 === 0) { + const timeNow = getCurrentTime(); + const blockFinishTime = timeNow + 300; + localStorage.setItem("blockFinishTime", blockFinishTime); + localStorage.setItem("attempt", attempt + 1); + message = "3 failed attempts. Identity is locked!"; + } else { + localStorage.setItem("attempt", attempt + 1); + } + } + return message; +} + +export function canTryPincode() { + const timeNow = getCurrentTime(); + const blockFinishTime = getBlockFinishTimeInLocalStorage(); + if (blockFinishTime <= timeNow) { + localStorage.removeItem("blockFinishTime"); + return true; + } else { + return false; + } +} \ No newline at end of file diff --git a/javascript/src/utilities/signingUtilities.js b/javascript/src/utilities/signingUtilities.js new file mode 100644 index 0000000000000000000000000000000000000000..dad16e412bdaa725838b6560b4faa7b71fb79a0f --- /dev/null +++ b/javascript/src/utilities/signingUtilities.js @@ -0,0 +1,955 @@ +import {canTryPincode, failPincodeAttempt, getTimeLeftInLocalStorage, makeid} from './appUtility'; + +const libmime = require('libmime'); +const pkijs = require('pkijs'); +const asn1js = require('asn1js'); +const pvutils = require('pvutils'); + +//********************************************************************************* + +const CERTIFIATE_Version_1 = 0; +const CERTIFIATE_Version_3 = 2; + +//these bit fields are reversed, WTF! +const KEY_USAGE_DigitalSignature = 0x80;//01; +const KEY_USAGE_NonRepudiation = 0x40;//02; +const KEY_USAGE_KeyEncipherment = 0x20;//04; +const KEY_USAGE_DataEncipherment = 0x10;//08; +const KEY_USAGE_KeyAgreement = 0x08;//10; +const KEY_USAGE_KeyCertSign = 0x04;//20; +const KEY_USAGE_CRLSign = 0x02;//40; +//const KEY_USAGE_EncipherOnly = 0x01;//80; // Not used for now. Must be used together with KEY_USAGE_KeyAgreement (maybe should be ORed as a constant directly?) +//const KEY_USAGE_DecipherOnly = 0x80;//0100; // If used, modify "KeyUsage" extension array buffer size and appropriate bit operators to accomodate for extra byte + +const KEY_USAGE_LeafCertificate = KEY_USAGE_DigitalSignature | KEY_USAGE_NonRepudiation | KEY_USAGE_KeyEncipherment | KEY_USAGE_DataEncipherment; +const KEY_USAGE_CertificateAuthority= KEY_USAGE_DigitalSignature | KEY_USAGE_KeyCertSign | KEY_USAGE_CRLSign; + + +const OID_EXT_KEY_USAGE_Any = "2.5.29.37.0"; +const OID_ID_PKIX_ServerAuth = "1.3.6.1.5.5.7.3.1"; +const OID_ID_PKIX_ClientAuth = "1.3.6.1.5.5.7.3.2"; +const OID_ID_PKIX_CodeSigning = "1.3.6.1.5.5.7.3.3"; +const OID_ID_PKIX_EmailProtection = "1.3.6.1.5.5.7.3.4"; +const OID_ID_PKIX_TimeStamping = "1.3.6.1.5.5.7.3.8"; +const OID_ID_PKIX_OCSPSigning = "1.3.6.1.5.5.7.3.9"; +// const OID_EXT_KEY_USAGE_MS... = "1.3.6.1.4.1.311.10.3.1"; // Microsoft Certificate Trust List signing +// const OID_EXT_KEY_USAGE_MS... = "1.3.6.1.4.1.311.10.3.4"; // Microsoft Encrypted File System +const OID_PKCS7_Data = "1.2.840.113549.1.7.1"; +const OID_PKCS7_SignedData = "1.2.840.113549.1.7.2"; +const OID_PKCS7_EnvelopedData = "1.2.840.113549.1.7.3"; +const OID_PKCS9_EmailAddress = "1.2.840.113549.1.9.1"; +const OID_PKCS9_ContentType = "1.2.840.113549.1.9.3"; +const OID_PKCS9_MessageDigest = "1.2.840.113549.1.9.4"; +const OID_PKCS9_SigningTime = "1.2.840.113549.1.9.5"; + +const defaultAlgorithms = { + hashAlg: "SHA-256", + signAlg: "RSASSA-PKCS1-v1_5", + keyLength: 2048 +}; + +const AES_encryptionVariant_Password = 2; +const encryptionAlgorithm = { + name: "AES-CBC", + length: 128 +}; + +//********************************************************************************* +// Returns promise, resolved to keyPair object {publicKey, privateKey} +//********************************************************************************* +function generateKeys(algorithms) { + //region Get a "crypto" extension + const crypto = pkijs.getCrypto(); + if (typeof crypto === "undefined") { + return Promise.reject("No WebCrypto extension found"); + } + //endregion Get a "crypto" extension + + if (!algorithms) { + algorithms = defaultAlgorithms; + } else { + if (!algorithms.hashAlg) { + algorithms.hashAlg = defaultAlgorithms.hashAlg; + } + if (!algorithms.signAlg) { + algorithms.signAlg = defaultAlgorithms.signAlg; + } + if (!algorithms.keyLength) { + algorithms.keyLength = defaultAlgorithms.keyLength; + } + } + + //region Get default algorithm parameters for key generation + const algorithm = pkijs.getAlgorithmParameters(algorithms.signAlg, "generatekey"); + if("hash" in algorithm.algorithm) { + algorithm.algorithm.hash.name = algorithms.hashAlg; + } + algorithm.algorithm.modulusLength = algorithms.keyLength; + //endregion + + return crypto.generateKey(algorithm.algorithm, true, algorithm.usages); +} + +//********************************************************************************* +function createCertificate(certData, issuerData = null) +{ + + if (typeof certData === "undefined") { + return Promise.reject("No Certificate data provided"); + } + + if (typeof certData.subject === "undefined") { + return Promise.reject("No Certificate subject data provided"); + } + + + //region Get a "crypto" extension + const crypto = pkijs.getCrypto(); + + if (typeof crypto === "undefined") { + return Promise.reject("No WebCrypto extension found"); + } + //endregion Get a "crypto" extension + + //region Initial variables + let sequence = Promise.resolve(); + + const certificate = new pkijs.Certificate(); + let publicKey; + let privateKey; + + let certificateBuffer;// = new ArrayBuffer(0); // ArrayBuffer with loaded or created CERT + let privateKeyBuffer;// = new ArrayBuffer(0); + let publicKeyBuffer;// = new ArrayBuffer(0); + + //endregion Initial variables + + if (certData.keyPair) { + //region Create a new key pair + sequence = sequence.then(() => + { + return certData.keyPair; + }); + //endregion Create a new key pair + + } else { + //region Create a new key pair + sequence = sequence.then(() => + { + return generateKeys(certData.algorithms); + }); + //endregion Create a new key pair + } + + //region Store new key in an interim variables + sequence = sequence.then(keyPair => + { + publicKey = keyPair.publicKey; + privateKey = keyPair.privateKey; + }, error => Promise.reject(`Error during key generation: ${error}`)); + //endregion Store new key in an interim variables + + //region Exporting public key into "subjectPublicKeyInfo" value of certificate + sequence = sequence.then(() => + certificate.subjectPublicKeyInfo.importKey(publicKey) + ); + //endregion Exporting public key into "subjectPublicKeyInfo" value of certificate + + sequence = sequence.then( + () => crypto.digest({ name: "SHA-1" }, certificate.subjectPublicKeyInfo.subjectPublicKey.valueBlock.valueHex), + error => Promise.reject(`Error during importing public key: ${error}`) + ); + + //region Fill in cert data + sequence = sequence.then(subjKeyIdBuffer => + { + + //region Put a static values + certificate.version = CERTIFIATE_Version_3; + + const serialNumberBuffer = new ArrayBuffer(20); + const serialNumberView = new Uint8Array(serialNumberBuffer); + pkijs.getRandomValues(serialNumberView); + // noinspection JSUnresolvedFunction + certificate.serialNumber = new asn1js.Integer({ valueHex: serialNumberView }); + //endregion Put a static values + + //region Subject + // For reference http://oidref.com/2.5.4.3 + if (certData.subject.commonName) { + // noinspection JSUnresolvedFunction + certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ + type: "2.5.4.3", // Common name + value: new asn1js.PrintableString({ value: certData.subject.commonName }) + })); + } + + if (certData.subject.country) { + // noinspection JSUnresolvedFunction + certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ + type: "2.5.4.6", // Country name + value: new asn1js.PrintableString({ value: certData.subject.country }) + })); + } + + if (certData.subject.locality) { + // noinspection JSUnresolvedFunction + certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ + type: "2.5.4.7", // Locality Name + value: new asn1js.PrintableString({ value: certData.subject.locality }) + })); + } + + if (certData.subject.state) { + // noinspection JSUnresolvedFunction + certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ + type: "2.5.4.8", // State or Province name + value: new asn1js.PrintableString({ value: certData.subject.state }) + })); + } + + if (certData.subject.organization) { + // noinspection JSUnresolvedFunction + certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ + type: "2.5.4.10", // Organization name + value: new asn1js.PrintableString({ value: certData.subject.organization }) + })); + } + + if (certData.subject.organizationUnit) { + // noinspection JSUnresolvedFunction + certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ + type: "2.5.4.11", // Organization unit name + value: new asn1js.PrintableString({ value: certData.subject.organizationUnit }) + })); + } + + if (certData.subject.email) { + // noinspection JSUnresolvedFunction + certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ + type: OID_PKCS9_EmailAddress, // Email, deprecated but still widely used + value: new asn1js.IA5String({ value: certData.subject.email }) + })); + } + //endregion Subject + + + //region Issuer + if (issuerData && issuerData.certificate) { + certificate.issuer = issuerData.certificate.subject; + } else { + certificate.issuer = certificate.subject; + } + //endregion Issuer + + //region Validity + if (!certData.validity) { + certData.validity = {} + } + + if (certData.validity.notBefore) { + certificate.notBefore.value = certData.validity.notBefore; //date + } else { + const tmp = new Date(); + certificate.notBefore.value = new Date(tmp.getFullYear(), tmp.getMonth(), tmp.getDate(), 0, 0, 0); + } + + if (certData.validity.notAfter) { + certificate.notAfter.value = certData.validity.notAfter; //date + } else { + const tmp = certificate.notBefore.value; + const validYears = certData.validity.validYears || 1; + certificate.notAfter.value = new Date(tmp.getFullYear() + validYears, tmp.getMonth(), tmp.getDate(), 23, 59, 59); + } + //endregion Validity + + //region Extensions + certificate.extensions = []; // Extensions are not a part of certificate by default, it's an optional array + + //region "BasicConstraints" extension + const basicConstr = new pkijs.BasicConstraints({ + cA: !!certData.isCA, + //pathLenConstraint: 0 //TODO add logic for leaf CA + }); + + certificate.extensions.push(new pkijs.Extension({ + extnID: "2.5.29.19", + critical: true, + extnValue: basicConstr.toSchema().toBER(false), + parsedValue: basicConstr // Parsed value for well-known extensions + })); + //endregion "BasicConstraints" extension + + //region "KeyUsage" extension + const keyUsageBuffer = new ArrayBuffer(1); + const keyUsageBitView = new Uint8Array(keyUsageBuffer); + + keyUsageBitView[0] = !!certData.isCA ? KEY_USAGE_CertificateAuthority : KEY_USAGE_LeafCertificate; + + // noinspection JSUnresolvedFunction + const keyUsage = new asn1js.BitString({ valueHex: keyUsageBuffer }); + + certificate.extensions.push(new pkijs.Extension({ + extnID: "2.5.29.15", + critical: true, + extnValue: keyUsage.toBER(false), + parsedValue: keyUsage // Parsed value for well-known extensions + })); + //endregion "KeyUsage" extension + + //region "ExtKeyUsage" extension + if (!certData.isCA && certData.subject.email) { + const extKeyUsage = new pkijs.ExtKeyUsage({ + keyPurposes: [ + OID_ID_PKIX_EmailProtection + ] + }); + + certificate.extensions.push(new pkijs.Extension({ + extnID: "2.5.29.37", + critical: false, + extnValue: extKeyUsage.toSchema().toBER(false), + parsedValue: extKeyUsage // Parsed value for well-known extensions + })); + } + //endregion "ExtKeyUsage" extension + + //region "SubjAltName" extension + if (certData.subject.email || certData.subject.url) { + + const names = []; + + if (certData.subject.email) { + names.push(new pkijs.GeneralName({ + type: 1, // rfc822Name + value: certData.subject.email + })); + } + + if (certData.subject.url) { + names.push(new pkijs.GeneralName({ + type: 2, // dNSName + value: certData.subject.url + })); + } + + const subjAltNames = new pkijs.GeneralNames({ + names: names + }); + + certificate.extensions.push(new pkijs.Extension({ + extnID: "2.5.29.17", + critical: false, + extnValue: subjAltNames.toSchema().toBER(false), + parsedValue: subjAltNames // Parsed value for well-known extensions + })); + } + //endregion "SubjAltName" extension + + + //region "SubjectKeyIdentifier" extension + const subjKeyId = new asn1js.OctetString({ valueHex: subjKeyIdBuffer }); + + certificate.extensions.push(new pkijs.Extension({ + extnID: "2.5.29.14", + critical: false, + extnValue: subjKeyId.toBER(false), + parsedValue: subjKeyId // Parsed value for well-known extensions + })); + //endregion "SubjectKeyIdentifier" extension + + /* COULD NOT GET IT WORKING + //region "AuthorityKeyIdentifier" extension + if (issuerData && issuerData.certificate) { + + let issuerSubjKeyExt = null; + + let extLength = issuerData.certificate.extensions.length; + for (var i = 0; i < extLength; i++) { + let ext = issuerData.certificate.extensions[i]; + if (ext.extnID == "2.5.29.14") { + issuerSubjKeyExt = ext; + break; + } + } + + if (issuerSubjKeyExt) { + + const asn1 = asn1js.fromBER(issuerSubjKeyExt.extnValue); + + const authKeyIdentifier = new AuthorityKeyIdentifier({ + keyIdentifier: new asn1js.OctetString({ + //isHexOnly: true, + //valueHex: issuerSubjKeyExt.parsedValue.valueBlock.valueHex + value: new asn1js.OctetString({ valueHex: subjKeyIdBuffer }) + }) + }); + // const authKeyIdentifier = new AuthorityKeyIdentifier({ + // //keyIdentifier: new asn1js.OctetString({ valueHex: subjKeyIdBuffer }) + + // }); + + certificate.extensions.push(new Extension({ + extnID: "2.5.29.35", + critical: false, + extnValue: authKeyIdentifier.toSchema().toBER(false), + parsedValue: authKeyIdentifier // Parsed value for well-known extensions + })); + } + } + //endregion "AuthorityKeyIdentifier" extension + */ + //endregion Extensions + }); + //region Fill in cert data + + + //region Signing final certificate + sequence = sequence.then(() => { + let signerKey = (issuerData && issuerData.privateKey) ? issuerData.privateKey : privateKey; + return certificate.sign(signerKey, (certData.algorithms && certData.algorithms.hashAlg) ? certData.algorithms.hashAlg : defaultAlgorithms.hashAlg) + }, + error => Promise.reject(`Error during exporting public key: ${error}`)); + //endregion + + //region Encode and store certificate + sequence = sequence.then(() => + { + certificateBuffer = certificate.toSchema(true).toBER(false); + }, error => Promise.reject(`Error during signing: ${error}`)); + //endregion + + //region Exporting public key + sequence = sequence.then(() => + crypto.exportKey("spki", publicKey) + ); + //endregion + + //region Store exported public key on Web page + sequence = sequence.then(result => + { + publicKeyBuffer = result; + }, error => Promise.reject(`Error during exporting of public key: ${error}`)); + //endregion + + //region Exporting private key + sequence = sequence.then(() => + crypto.exportKey("pkcs8", privateKey) + ); + //endregion + + //region Store exported key on Web page + sequence = sequence.then(result => + { + privateKeyBuffer = result; + }, error => Promise.reject(`Error during exporting of private key: ${error}`)); + //endregion + + return sequence.then(() => { + + const result = { + certificate: certificate, + certificatePEM: encodePEM(certificateBuffer, "CERTIFICATE"), + publicKey: publicKey, + publicKeyPEM: encodePEM(publicKeyBuffer, "PUBLIC KEY"), + privateKey: privateKey, + privateKeyPEM: encodePEM(privateKeyBuffer, "PRIVATE KEY") + }; + return result; + }); +} + +function formatPEM(pemString) { + const lineWidth = 64; + let resultString = ""; + let start = 0; + let piece; + while ((piece = pemString.substring(start, start + lineWidth)).length > 0) { + start += lineWidth; + resultString += piece + '\r\n'; + } + return resultString; +} + +function encodePEM(buffer, label) { + const bufferString = String.fromCharCode.apply(null, new Uint8Array(buffer)); + + const header = `-----BEGIN ${label}-----\r\n`; + const base64formatted = formatPEM(window.btoa(bufferString)); + const footer = `-----END ${label}-----\r\n`; + const resultString = header + base64formatted + footer; + + return resultString; +} + +function decodePEM(pemString) { + const pemStripped = pemString.replace(/(-----(BEGIN|END) [a-zA-Z ]*-----|\r|\n)/g, ''); + const pemDecoded = window.atob(pemStripped); + const buffer = pvutils.stringToArrayBuffer(pemDecoded); + return buffer; +} + +//********************************************************************************* +function parseCertificate(certificatePEM) { + const certificateBuffer = decodePEM(certificatePEM); + const asn1 = asn1js.fromBER(certificateBuffer); + const certificate = new pkijs.Certificate({ schema: asn1.result }); + return certificate; +} + +//********************************************************************************* +export function encryptMessage(message, password, label) { + const buffer = pvutils.stringToArrayBuffer(message); + const secret = pvutils.stringToArrayBuffer(password); + + const enveloped = new pkijs.EnvelopedData(); + enveloped.addRecipientByPreDefinedData(secret, {}, AES_encryptionVariant_Password); + return enveloped.encrypt(encryptionAlgorithm, buffer). + then( + () => { + const content = new pkijs.ContentInfo(); + content.contentType = OID_PKCS7_EnvelopedData; + content.content = enveloped.toSchema(); + const ber = content.toSchema().toBER(false); + return encodePEM(ber, label) + }, + error => Promise.reject(`encryption error: ${error}`) + ) +} + +//********************************************************************************* +export function decryptMessage(message, password) { + if (canTryPincode()) { + const secret = pvutils.stringToArrayBuffer(password); + const buffer = decodePEM(message); + + const asn1 = asn1js.fromBER(buffer); + const content = new pkijs.ContentInfo({schema: asn1.result}); + const enveloped = new pkijs.EnvelopedData({schema: content.content}); + return enveloped.decrypt(0, {preDefinedData: secret}).then(result => { + return pvutils.arrayBufferToString(result); + }).catch(() => { + return Promise.reject(failPincodeAttempt(password)); + }); + } else { + return Promise.reject(getTimeLeftInLocalStorage()); + } +} + +//********************************************************************************* +function parsePrivateKey(privateKeyPEM) { + const privateKeyBuffer = decodePEM(privateKeyPEM); + const crypto = pkijs.getCrypto(); + const privateKeyPromise = crypto.importKey( + "pkcs8", + privateKeyBuffer, + { //these are the algorithm options + name: "RSASSA-PKCS1-v1_5", + hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512" + }, + true, + ["sign"] + ); + return privateKeyPromise; +} + + +export function createPassportCertificate(commonNameArg) { + const certData = { + algorithms: { + hashAlg: "SHA-256", + signAlg: "RSASSA-PKCS1-v1_5", + keyLength: 2048 + }, + //keyPair: generateKeys(), //optional , if provided must be object or promise that resolves to object {publicKey, prvateKey}. If it is not provided, new ones are generated automatically + subject: { + commonName: commonNameArg + "-userdevice", //optional for leaf, recommended for CA + country: "CH", //optional for leaf, recommended for CA + locality: "Zug", //optional for leaf, recommended for CA + state: "Zug", //optional for leaf, recommended for CA + organization: "Vereign AG", //optional for leaf, recommended for CA + organizationUnit:"Business Dep", //optional for leaf, recommended for CA + //email: "damyan.mitev@vereign.com", // added to DN and Subject Alternative Name extension. Optional for CA. Mandatory for leaf certificate, used for email protection + //url: "www.vereign.com" // optional url, recommended for CA, added to Subject Alternative Name extension + }, + validity: { + //notBefore: new Date() // optional, defaults to today at 00:00:00 + //notAfter: new Date() // optional, defaults to notBefore + validYears at 23:59:59 + validYears: 5 //optional, defaults to 1 + }, + isCA: true // optional flag denoting if this is CA certificate or leaf certificate, defaults to false + }; + + return createCertificate(certData, null) +} + +export function createOneTimePassportCertificate(commonNameArg, emailArg, privateKeyIssuerArg, certicateIssuerArg) { + var certData = null; + if(emailArg != null && emailArg !== "") { + 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 + } + } else { + certData = { + algorithms: { + hashAlg: "SHA-256", + signAlg: "RSASSA-PKCS1-v1_5", + keyLength: 2048 + }, + //keyPair: generateKeys(), //optional , if provided must be object or promise that resolves to object {publicKey, prvateKey}. If it is not provided, new ones are generated automatically + subject: { + commonName: commonNameArg + "-onetime", //optional for leaf, recommended for CA + country: "CH", //optional for leaf, recommended for CA + locality: "Zug", //optional for leaf, recommended for CA + state: "Zug", //optional for leaf, recommended for CA + organization: "Vereign AG", //optional for leaf, recommended for CA + organizationUnit:"Business Dep", //optional for leaf, recommended for CA + //email: emailArg, // added to DN and Subject Alternative Name extension. Optional for CA. Mandatory for leaf certificate, used for email protection + //url: "www.vereign.com" // optional url, recommended for CA, added to Subject Alternative Name extension + }, + validity: { + //notBefore: new Date() // optional, defaults to today at 00:00:00 + //notAfter: new Date() // optional, defaults to notBefore + validYears at 23:59:59 + validYears: 5 //optional, defaults to 1 + }, + isCA: false // optional flag denoting if this is CA certificate or leaf certificate, defaults to false + } + } + + return parsePrivateKey(privateKeyIssuerArg).then(privateKeyDecoded => { + const issuerData = { + certificate: parseCertificate(certicateIssuerArg),// vereignCACertPEM), + privateKey: privateKeyDecoded + }; + return createCertificate(certData, issuerData); + }); +} + +function arrayBufferToBase64Formatted(buffer) { + const bufferString = String.fromCharCode.apply(null, new Uint8Array(buffer)); + const base64formatted = formatPEM(window.btoa(bufferString)); + return base64formatted; +} + +export function signEmail(mime, signingCert, certificateChain, privateKey) { + const signingCertObj = parseCertificate(signingCert); + const certificateChainObj = []; + certificateChainObj[0] = parseCertificate(signingCert); + for (let i = 0; i < certificateChain.length; i++) { + certificateChainObj[i + 1] = parseCertificate(certificateChain[i]) + } + + return parsePrivateKey(privateKey).then(privateKeyDecoded => { + return signEmailObjects(mime, signingCertObj, certificateChainObj, privateKeyDecoded); + }); +} + +function signEmailObjects(mime, signingCert, certificateChain, privateKey) { + + //region Get a "crypto" extension + const crypto = pkijs.getCrypto(); + if (typeof crypto === "undefined") { + return Promise.reject("No WebCrypto extension found"); + } + //endregion Get a "crypto" extension + + let template = + `{{headers}}Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="{{boundary}}" +MIME-Version: 1.0 + +This is a cryptographically signed message in MIME format. + +--{{boundary}} +{{mime}} +--{{boundary}} +Content-Type: application/pkcs7-signature; name="smime.p7s" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7s" +Content-Description: S/MIME Cryptographic Signature + +{{signature}} +--{{boundary}}-- + +Vereign - Authentic Communication +`.replace(newline, '\r\n'); + + const detachedSignature = true; + const addExt = true; + const hashAlg = "SHA-256"; + let cmsSignedSimpl; + + var mimeHeadersTitles = [ + "Content-Type", + "Content-Transfer-Encoding", + "Content-ID", + "Content-Description", + "Content-Disposition", + "Content-Language", + "Content-Location" + ]; + + mime = mime.replace(newline, '\r\n'); + + let newHeaderLines = ""; + let headersEnd = mime.indexOf('\r\n\r\n'); //the first empty line + + if (headersEnd < 0 && mime.startsWith('\r\n')) { + + mime = mime.substring(2) //should not happen + + } else + if (headersEnd >= 0) { + + let mimeHeaders = {}; + let mimeBody = mime.substring(headersEnd + 4); + + let mimeHeadersStr = mime.substring(0, headersEnd); + + let headers = libmime.decodeHeaders(mimeHeadersStr); + for (var i = 0; i < mimeHeadersTitles.length; i++) { + let key = mimeHeadersTitles[i].toLowerCase(); + if(key in headers) { + mimeHeaders[key] = headers[key]; + delete headers[key] + } + } + + for(let key in headers) { + if(!(key === "" || key === "MIME-Version".toLowerCase())) { //we have MIME-Version in the template + newHeaderLines += capitalizeHeader(key) + ": " + headers[key] + '\r\n'; + } + } + + let newMimeHeaderLines = ""; + for(let key in mimeHeaders) { + if(!(key === "")) { + newMimeHeaderLines += capitalizeHeader(key) + ": " + mimeHeaders[key] + '\r\n'; + } + } + + if (newMimeHeaderLines === "") { + newMimeHeaderLines = 'Content-Type: text/plain\r\n' //should not happen + } + + mime = newMimeHeaderLines + '\r\n' + mimeBody + } + + let dataBuffer = Buffer.from(mime,'utf-8'); + + let sequence = Promise.resolve(); + + //region Check if user wants us to include signed extensions + if(addExt) + { + //region Create a message digest + sequence = sequence.then( + () => crypto.digest({ name: hashAlg }, dataBuffer) + ); + //endregion + + //region Combine all signed extensions + sequence = sequence.then( + messageHash => + { + const signedAttr = []; + /* + 1.2.840.113549.1.9.1 - e-mailAddress + 1.2.840.113549.1.9.2 - PKCS-9 unstructuredName + 1.2.840.113549.1.9.3 - contentType + 1.2.840.113549.1.9.4 - messageDigest + 1.2.840.113549.1.9.5 - Signing Time + 1.2.840.113549.1.9.6 - counterSignature + */ + signedAttr.push(new pkijs.Attribute({ + type: OID_PKCS9_ContentType, //contentType + values: [ + new asn1js.ObjectIdentifier({ value: OID_PKCS7_Data}) //data + ] + /* + 1.2.840.113549.1.7.1 - data + 1.2.840.113549.1.7.2 - signedData + 1.2.840.113549.1.7.3 - envelopedData + 1.2.840.113549.1.7.4 - signedAndEnvelopedData + 1.2.840.113549.1.7.5 - digestedData + 1.2.840.113549.1.7.6 - encryptedData + */ + })); // contentType + + signedAttr.push(new pkijs.Attribute({ + type: OID_PKCS9_SigningTime, //Signing Time + values: [ + new asn1js.UTCTime({ valueDate: new Date() }) + ] + })); // signingTime + + signedAttr.push(new pkijs.Attribute({ + type: OID_PKCS9_MessageDigest, //messageDigest + values: [ + new asn1js.OctetString({ valueHex: messageHash }) + ] + })); // messageDigest + + return signedAttr; + } + ); + //endregion + } + //endregion + + //region Initialize CMS Signed Data structures and sign it + sequence = sequence.then( + signedAttr => + { + cmsSignedSimpl = new pkijs.SignedData({ + version: 1, + encapContentInfo: new pkijs.EncapsulatedContentInfo({ + eContentType: OID_PKCS7_Data // "data" content type + }), + signerInfos: [ + new pkijs.SignerInfo({ + version: 1, + sid: new pkijs.IssuerAndSerialNumber({ + issuer: signingCert.issuer, + serialNumber: signingCert.serialNumber + }) + }) + ], + certificates: certificateChain //array + }); + + if(addExt) + { + cmsSignedSimpl.signerInfos[0].signedAttrs = new pkijs.SignedAndUnsignedAttributes({ + type: 0, + attributes: signedAttr + }); + } + + if(detachedSignature === false) + { + const contentInfo = new pkijs.EncapsulatedContentInfo({ + eContent: new asn1js.OctetString({ valueHex: dataBuffer }) + }); + + cmsSignedSimpl.encapContentInfo.eContent = contentInfo.eContent; + + return cmsSignedSimpl.sign(privateKey, 0, hashAlg); + } + + return cmsSignedSimpl.sign(privateKey, 0, hashAlg, dataBuffer); + } + ); + //endregion + + //region Create final result + sequence = sequence.then( + (result) => + { + const cmsSignedSchema = cmsSignedSimpl.toSchema(true); + + const cmsContentSimp = new pkijs.ContentInfo({ + contentType: OID_PKCS7_SignedData, //signedData + content: cmsSignedSchema + }); + + const _cmsSignedSchema = cmsContentSimp.toSchema(); + + //region Make length of some elements in "indefinite form" + _cmsSignedSchema.lenBlock.isIndefiniteForm = true; + + const block1 = _cmsSignedSchema.valueBlock.value[1]; + block1.lenBlock.isIndefiniteForm = true; + + const block2 = block1.valueBlock.value[0]; + block2.lenBlock.isIndefiniteForm = true; + + if(detachedSignature === false) + { + const block3 = block2.valueBlock.value[2]; + block3.lenBlock.isIndefiniteForm = true; + block3.valueBlock.value[1].lenBlock.isIndefiniteForm = true; + block3.valueBlock.value[1].valueBlock.value[0].lenBlock.isIndefiniteForm = true; + } + //endregion + + const cmsSignedBuffer = _cmsSignedSchema.toBER(false); + return cmsSignedBuffer; + }, + error => Promise.reject(`Erorr during signing of CMS Signed Data: ${error}`) + ); + //endregion + + sequence = sequence.then( + (cmsSignedBuffer) => + { + let signature = arrayBufferToBase64Formatted(cmsSignedBuffer); + let boundary = makeBoundary(); + + template = template.replace(/{{boundary}}/g, boundary); + template = template.replace("{{signature}}", signature); + template = template.replace("{{headers}}", newHeaderLines); + template = template.replace("{{mime}}", mime); + + //template = template.replace(newline, '\r\n') + return template + } + ); + + return sequence; +} + + +const newline = /\r\n|\r|\n/g; + +function capitalizeFirstLetter(string) { + if(string === "id") { + return "ID" + } + + if(string === "mime") { + return "MIME"; + } + + return string.charAt(0).toUpperCase() + string.slice(1); +} + +function capitalizeHeader(string) { + let result = ""; + const tokens = string.split("-"); + for (let i = 0; i < tokens.length; i++) { + result += capitalizeFirstLetter(tokens[i]); + if (i !== tokens.length - 1) { + result += "-"; + } + } + + return result; +} + + + +function makeBoundary() { + let len = 20 + Math.random() * 20; + return 'W0RyLiBEYW15YW4gTWl0ZXZd--' + makeid(len) +} \ No newline at end of file