import * as asn1js from "asn1js"; import { utilConcatBuf } from "pvutils"; import CryptoEngine from "./CryptoEngine.js"; //************************************************************************************** //region Crypto engine related function //************************************************************************************** let engine = { name: "none", crypto: null, subtle: null }; //************************************************************************************** export function setEngine(name, crypto, subtle) { //region We are in Node // noinspection JSUnresolvedVariable if((typeof process !== "undefined") && ("pid" in process) && (typeof global !== "undefined") && (typeof window === "undefined")) { // noinspection ES6ModulesDependencies, JSUnresolvedVariable if(typeof global[process.pid] === "undefined") { // noinspection JSUnresolvedVariable global[process.pid] = {}; } else { // noinspection JSUnresolvedVariable if(typeof global[process.pid] !== "object") { // noinspection JSUnresolvedVariable throw new Error(`Name global.${process.pid} already exists and it is not an object`); } } // noinspection JSUnresolvedVariable if(typeof global[process.pid].pkijs === "undefined") { // noinspection JSUnresolvedVariable global[process.pid].pkijs = {}; } else { // noinspection JSUnresolvedVariable if(typeof global[process.pid].pkijs !== "object") { // noinspection JSUnresolvedVariable throw new Error(`Name global.${process.pid}.pkijs already exists and it is not an object`); } } // noinspection JSUnresolvedVariable global[process.pid].pkijs.engine = { name: name, crypto: crypto, subtle: subtle }; } //endregion //region We are in browser else { engine = { name: name, crypto: crypto, subtle: subtle }; } //endregion } //************************************************************************************** export function getEngine() { //region We are in Node // noinspection JSUnresolvedVariable if((typeof process !== "undefined") && ("pid" in process) && (typeof global !== "undefined") && (typeof window === "undefined")) { let _engine; try { // noinspection JSUnresolvedVariable _engine = global[process.pid].pkijs.engine; } catch(ex) { throw new Error("Please call \"setEngine\" before call to \"getEngine\""); } return _engine; } //endregion return engine; } //************************************************************************************** (function initCryptoEngine() { if(typeof self !== "undefined") { if("crypto" in self) { let engineName = "webcrypto"; /** * Standard crypto object * @type {Object} * @property {Object} [webkitSubtle] Subtle object from Apple */ const cryptoObject = self.crypto; let subtleObject; // Apple Safari support if("webkitSubtle" in self.crypto) { try { subtleObject = self.crypto.webkitSubtle; } catch(ex) { subtleObject = self.crypto.subtle; } engineName = "safari"; } if("subtle" in self.crypto) subtleObject = self.crypto.subtle; if(typeof subtleObject === "undefined") { engine = { name: engineName, crypto: cryptoObject, subtle: null }; } else { engine = { name: engineName, crypto: cryptoObject, subtle: new CryptoEngine({name: engineName, crypto: self.crypto, subtle: subtleObject}) }; } } } setEngine(engine.name, engine.crypto, engine.subtle); })(); //************************************************************************************** //endregion //************************************************************************************** //region Declaration of common functions //************************************************************************************** /** * Get crypto subtle from current "crypto engine" or "undefined" * @returns {({decrypt, deriveKey, digest, encrypt, exportKey, generateKey, importKey, sign, unwrapKey, verify, wrapKey}|null)} */ export function getCrypto() { const _engine = getEngine(); if(_engine.subtle !== null) return _engine.subtle; return undefined; } //************************************************************************************** /** * Initialize input Uint8Array by random values (with help from current "crypto engine") * @param {!Uint8Array} view * @returns {*} */ export function getRandomValues(view) { return getEngine().subtle.getRandomValues(view); } //************************************************************************************** /** * Get OID for each specific algorithm * @param {Object} algorithm * @returns {string} */ export function getOIDByAlgorithm(algorithm) { return getEngine().subtle.getOIDByAlgorithm(algorithm); } //************************************************************************************** /** * Get default algorithm parameters for each kind of operation * @param {string} algorithmName Algorithm name to get common parameters for * @param {string} operation Kind of operation: "sign", "encrypt", "generatekey", "importkey", "exportkey", "verify" * @returns {*} */ export function getAlgorithmParameters(algorithmName, operation) { return getEngine().subtle.getAlgorithmParameters(algorithmName, operation); } //************************************************************************************** /** * Create CMS ECDSA signature from WebCrypto ECDSA signature * @param {ArrayBuffer} signatureBuffer WebCrypto result of "sign" function * @returns {ArrayBuffer} */ export function createCMSECDSASignature(signatureBuffer) { //region Initial check for correct length if((signatureBuffer.byteLength % 2) !== 0) return new ArrayBuffer(0); //endregion //region Initial variables const length = signatureBuffer.byteLength / 2; // There are two equal parts inside incoming ArrayBuffer const rBuffer = new ArrayBuffer(length); const rView = new Uint8Array(rBuffer); rView.set(new Uint8Array(signatureBuffer, 0, length)); const rInteger = new asn1js.Integer({ valueHex: rBuffer }); const sBuffer = new ArrayBuffer(length); const sView = new Uint8Array(sBuffer); sView.set(new Uint8Array(signatureBuffer, length, length)); const sInteger = new asn1js.Integer({ valueHex: sBuffer }); //endregion return (new asn1js.Sequence({ value: [ rInteger.convertToDER(), sInteger.convertToDER() ] })).toBER(false); } //************************************************************************************** /** * String preparation function. In a future here will be realization of algorithm from RFC4518 * @param {string} inputString JavaScript string. As soon as for each ASN.1 string type we have a specific transformation function here we will work with pure JavaScript string * @returns {string} Formated string */ export function stringPrep(inputString) { //region Initial variables let isSpace = false; let cuttedResult = ""; //endregion const result = inputString.trim(); // Trim input string //region Change all sequence of SPACE down to SPACE char for(let i = 0; i < result.length; i++) { if(result.charCodeAt(i) === 32) { if(isSpace === false) isSpace = true; } else { if(isSpace) { cuttedResult += " "; isSpace = false; } cuttedResult += result[i]; } } //endregion return cuttedResult.toLowerCase(); } //************************************************************************************** /** * Create a single ArrayBuffer from CMS ECDSA signature * @param {Sequence} cmsSignature ASN.1 SEQUENCE contains CMS ECDSA signature * @returns {ArrayBuffer} */ export function createECDSASignatureFromCMS(cmsSignature) { //region Check input variables if((cmsSignature instanceof asn1js.Sequence) === false) return new ArrayBuffer(0); if(cmsSignature.valueBlock.value.length !== 2) return new ArrayBuffer(0); if((cmsSignature.valueBlock.value[0] instanceof asn1js.Integer) === false) return new ArrayBuffer(0); if((cmsSignature.valueBlock.value[1] instanceof asn1js.Integer) === false) return new ArrayBuffer(0); //endregion const rValue = cmsSignature.valueBlock.value[0].convertFromDER(); const sValue = cmsSignature.valueBlock.value[1].convertFromDER(); //region Check the lengths of two parts are equal switch(true) { case (rValue.valueBlock.valueHex.byteLength < sValue.valueBlock.valueHex.byteLength): { if((sValue.valueBlock.valueHex.byteLength - rValue.valueBlock.valueHex.byteLength) !== 1) throw new Error("Incorrect DER integer decoding"); const correctedLength = sValue.valueBlock.valueHex.byteLength; const rValueView = new Uint8Array(rValue.valueBlock.valueHex); const rValueBufferCorrected = new ArrayBuffer(correctedLength); const rValueViewCorrected = new Uint8Array(rValueBufferCorrected); rValueViewCorrected.set(rValueView, 1); rValueViewCorrected[0] = 0x00; // In order to be sure we do not have any garbage here return utilConcatBuf(rValueBufferCorrected, sValue.valueBlock.valueHex); } case (rValue.valueBlock.valueHex.byteLength > sValue.valueBlock.valueHex.byteLength): { if((rValue.valueBlock.valueHex.byteLength - sValue.valueBlock.valueHex.byteLength) !== 1) throw new Error("Incorrect DER integer decoding"); const correctedLength = rValue.valueBlock.valueHex.byteLength; const sValueView = new Uint8Array(sValue.valueBlock.valueHex); const sValueBufferCorrected = new ArrayBuffer(correctedLength); const sValueViewCorrected = new Uint8Array(sValueBufferCorrected); sValueViewCorrected.set(sValueView, 1); sValueViewCorrected[0] = 0x00; // In order to be sure we do not have any garbage here return utilConcatBuf(rValue.valueBlock.valueHex, sValueBufferCorrected); } default: { //region In case we have equal length and the length is not even with 2 if(rValue.valueBlock.valueHex.byteLength % 2) { const correctedLength = (rValue.valueBlock.valueHex.byteLength + 1); const rValueView = new Uint8Array(rValue.valueBlock.valueHex); const rValueBufferCorrected = new ArrayBuffer(correctedLength); const rValueViewCorrected = new Uint8Array(rValueBufferCorrected); rValueViewCorrected.set(rValueView, 1); rValueViewCorrected[0] = 0x00; // In order to be sure we do not have any garbage here const sValueView = new Uint8Array(sValue.valueBlock.valueHex); const sValueBufferCorrected = new ArrayBuffer(correctedLength); const sValueViewCorrected = new Uint8Array(sValueBufferCorrected); sValueViewCorrected.set(sValueView, 1); sValueViewCorrected[0] = 0x00; // In order to be sure we do not have any garbage here return utilConcatBuf(rValueBufferCorrected, sValueBufferCorrected); } //endregion } } //endregion return utilConcatBuf(rValue.valueBlock.valueHex, sValue.valueBlock.valueHex); } //************************************************************************************** /** * Get WebCrypto algorithm by wel-known OID * @param {string} oid well-known OID to search for * @returns {Object} */ export function getAlgorithmByOID(oid) { return getEngine().subtle.getAlgorithmByOID(oid); } //************************************************************************************** /** * Getting hash algorithm by signature algorithm * @param {AlgorithmIdentifier} signatureAlgorithm Signature algorithm * @returns {string} */ export function getHashAlgorithm(signatureAlgorithm) { return getEngine().subtle.getHashAlgorithm(signatureAlgorithm); } //************************************************************************************** /** * ANS X9.63 Key Derivation Function having a "Counter" as a parameter * @param {string} hashFunction Used hash function * @param {ArrayBuffer} Zbuffer ArrayBuffer containing ECDH shared secret to derive from * @param {number} Counter * @param {ArrayBuffer} SharedInfo Usually DER encoded "ECC_CMS_SharedInfo" structure */ export function kdfWithCounter(hashFunction, Zbuffer, Counter, SharedInfo) { //region Check of input parameters switch(hashFunction.toUpperCase()) { case "SHA-1": case "SHA-256": case "SHA-384": case "SHA-512": break; default: return Promise.reject(`Unknown hash function: ${hashFunction}`); } if((Zbuffer instanceof ArrayBuffer) === false) return Promise.reject("Please set \"Zbuffer\" as \"ArrayBuffer\""); if(Zbuffer.byteLength === 0) return Promise.reject("\"Zbuffer\" has zero length, error"); if((SharedInfo instanceof ArrayBuffer) === false) return Promise.reject("Please set \"SharedInfo\" as \"ArrayBuffer\""); if(Counter > 255) return Promise.reject("Please set \"Counter\" variable to value less or equal to 255"); //endregion //region Initial variables const counterBuffer = new ArrayBuffer(4); const counterView = new Uint8Array(counterBuffer); counterView[0] = 0x00; counterView[1] = 0x00; counterView[2] = 0x00; counterView[3] = Counter; let combinedBuffer = new ArrayBuffer(0); //endregion //region Get a "crypto" extension const crypto = getCrypto(); if(typeof crypto === "undefined") return Promise.reject("Unable to create WebCrypto object"); //endregion //region Create a combined ArrayBuffer for digesting combinedBuffer = utilConcatBuf(combinedBuffer, Zbuffer); combinedBuffer = utilConcatBuf(combinedBuffer, counterBuffer); combinedBuffer = utilConcatBuf(combinedBuffer, SharedInfo); //endregion //region Return digest of combined ArrayBuffer and information about current counter return crypto.digest({ name: hashFunction }, combinedBuffer) .then(result => ({ counter: Counter, result })); //endregion } //************************************************************************************** /** * ANS X9.63 Key Derivation Function * @param {string} hashFunction Used hash function * @param {ArrayBuffer} Zbuffer ArrayBuffer containing ECDH shared secret to derive from * @param {number} keydatalen Length (!!! in BITS !!!) of used kew derivation function * @param {ArrayBuffer} SharedInfo Usually DER encoded "ECC_CMS_SharedInfo" structure */ export function kdf(hashFunction, Zbuffer, keydatalen, SharedInfo) { //region Initial variables let hashLength = 0; let maxCounter = 1; const kdfArray = []; //endregion //region Check of input parameters switch(hashFunction.toUpperCase()) { case "SHA-1": hashLength = 160; // In bits break; case "SHA-256": hashLength = 256; // In bits break; case "SHA-384": hashLength = 384; // In bits break; case "SHA-512": hashLength = 512; // In bits break; default: return Promise.reject(`Unknown hash function: ${hashFunction}`); } if((Zbuffer instanceof ArrayBuffer) === false) return Promise.reject("Please set \"Zbuffer\" as \"ArrayBuffer\""); if(Zbuffer.byteLength === 0) return Promise.reject("\"Zbuffer\" has zero length, error"); if((SharedInfo instanceof ArrayBuffer) === false) return Promise.reject("Please set \"SharedInfo\" as \"ArrayBuffer\""); //endregion //region Calculated maximum value of "Counter" variable const quotient = keydatalen / hashLength; if(Math.floor(quotient) > 0) { maxCounter = Math.floor(quotient); if((quotient - maxCounter) > 0) maxCounter++; } //endregion //region Create an array of "kdfWithCounter" for(let i = 1; i <= maxCounter; i++) kdfArray.push(kdfWithCounter(hashFunction, Zbuffer, i, SharedInfo)); //endregion //region Return combined digest with specified length return Promise.all(kdfArray).then(incomingResult => { //region Initial variables let combinedBuffer = new ArrayBuffer(0); let currentCounter = 1; let found = true; //endregion //region Combine all buffer together while(found) { found = false; for(const result of incomingResult) { if(result.counter === currentCounter) { combinedBuffer = utilConcatBuf(combinedBuffer, result.result); found = true; break; } } currentCounter++; } //endregion //region Create output buffer with specified length keydatalen >>= 3; // Divide by 8 since "keydatalen" is in bits if(combinedBuffer.byteLength > keydatalen) { const newBuffer = new ArrayBuffer(keydatalen); const newView = new Uint8Array(newBuffer); const combinedView = new Uint8Array(combinedBuffer); for(let i = 0; i < keydatalen; i++) newView[i] = combinedView[i]; return newBuffer; } return combinedBuffer; // Since the situation when "combinedBuffer.byteLength < keydatalen" here we have only "combinedBuffer.byteLength === keydatalen" //endregion }); //endregion } //************************************************************************************** //endregion //**************************************************************************************