import * as asn1js from "asn1js"; import { getParametersValue, stringToArrayBuffer, arrayBufferToString, utilConcatBuf } from "pvutils"; import { createCMSECDSASignature, createECDSASignatureFromCMS } from "./common.js"; import PublicKeyInfo from "./PublicKeyInfo.js"; import PrivateKeyInfo from "./PrivateKeyInfo.js"; import AlgorithmIdentifier from "./AlgorithmIdentifier.js"; import EncryptedContentInfo from "./EncryptedContentInfo.js"; import RSASSAPSSParams from "./RSASSAPSSParams.js"; import PBKDF2Params from "./PBKDF2Params.js"; import PBES2Params from "./PBES2Params.js"; //************************************************************************************** /** * Making MAC key using algorithm described in B.2 of PKCS#12 standard. */ function makePKCS12B2Key(cryptoEngine, hashAlgorithm, keyLength, password, salt, iterationCount) { //region Initial variables let u; let v; const result = []; //endregion //region Get "u" and "v" values switch(hashAlgorithm.toUpperCase()) { case "SHA-1": u = 20; // 160 v = 64; // 512 break; case "SHA-256": u = 32; // 256 v = 64; // 512 break; case "SHA-384": u = 48; // 384 v = 128; // 1024 break; case "SHA-512": u = 64; // 512 v = 128; // 1024 break; default: throw new Error("Unsupported hashing algorithm"); } //endregion //region Main algorithm making key //region Transform password to UTF-8 like string const passwordViewInitial = new Uint8Array(password); const passwordTransformed = new ArrayBuffer((password.byteLength * 2) + 2); const passwordTransformedView = new Uint8Array(passwordTransformed); for(let i = 0; i < passwordViewInitial.length; i++) { passwordTransformedView[i * 2] = 0x00; passwordTransformedView[i * 2 + 1] = passwordViewInitial[i]; } passwordTransformedView[passwordTransformedView.length - 2] = 0x00; passwordTransformedView[passwordTransformedView.length - 1] = 0x00; password = passwordTransformed.slice(0); //endregion //region Construct a string D (the "diversifier") by concatenating v/8 copies of ID const D = new ArrayBuffer(v); const dView = new Uint8Array(D); for(let i = 0; i < D.byteLength; i++) dView[i] = 3; // The ID value equal to "3" for MACing (see B.3 of standard) //endregion //region Concatenate copies of the salt together to create a string S of length v * ceil(s / v) bytes (the final copy of the salt may be trunacted to create S) const saltLength = salt.byteLength; const sLen = v * Math.ceil(saltLength / v); const S = new ArrayBuffer(sLen); const sView = new Uint8Array(S); const saltView = new Uint8Array(salt); for(let i = 0; i < sLen; i++) sView[i] = saltView[i % saltLength]; //endregion //region Concatenate copies of the password together to create a string P of length v * ceil(p / v) bytes (the final copy of the password may be truncated to create P) const passwordLength = password.byteLength; const pLen = v * Math.ceil(passwordLength / v); const P = new ArrayBuffer(pLen); const pView = new Uint8Array(P); const passwordView = new Uint8Array(password); for(let i = 0; i < pLen; i++) pView[i] = passwordView[i % passwordLength]; //endregion //region Set I=S||P to be the concatenation of S and P const sPlusPLength = S.byteLength + P.byteLength; let I = new ArrayBuffer(sPlusPLength); let iView = new Uint8Array(I); iView.set(sView); iView.set(pView, sView.length); //endregion //region Set c=ceil(n / u) const c = Math.ceil((keyLength >> 3) / u); //endregion //region Initial variables let internalSequence = Promise.resolve(I); //endregion //region For i=1, 2, ..., c, do the following: for(let i = 0; i <= c; i++) { internalSequence = internalSequence.then(_I => { //region Create contecanetion of D and I const dAndI = new ArrayBuffer(D.byteLength + _I.byteLength); const dAndIView = new Uint8Array(dAndI); dAndIView.set(dView); dAndIView.set(iView, dView.length); //endregion return dAndI; }); //region Make "iterationCount" rounds of hashing for(let j = 0; j < iterationCount; j++) internalSequence = internalSequence.then(roundBuffer => cryptoEngine.digest({ name: hashAlgorithm }, new Uint8Array(roundBuffer))); //endregion internalSequence = internalSequence.then(roundBuffer => { //region Concatenate copies of Ai to create a string B of length v bits (the final copy of Ai may be truncated to create B) const B = new ArrayBuffer(v); const bView = new Uint8Array(B); for(let j = 0; j < B.byteLength; j++) bView[j] = roundBuffer[j % roundBuffer.length]; //endregion //region Make new I value const k = Math.ceil(saltLength / v) + Math.ceil(passwordLength / v); const iRound = []; let sliceStart = 0; let sliceLength = v; for(let j = 0; j < k; j++) { const chunk = Array.from(new Uint8Array(I.slice(sliceStart, sliceStart + sliceLength))); sliceStart += v; if((sliceStart + v) > I.byteLength) sliceLength = I.byteLength - sliceStart; let x = 0x1ff; for(let l = (B.byteLength - 1); l >= 0; l--) { x >>= 8; x += bView[l] + chunk[l]; chunk[l] = (x & 0xff); } iRound.push(...chunk); } I = new ArrayBuffer(iRound.length); iView = new Uint8Array(I); iView.set(iRound); //endregion result.push(...(new Uint8Array(roundBuffer))); return I; }); } //endregion //region Initialize final key internalSequence = internalSequence.then(() => { const resultBuffer = new ArrayBuffer(keyLength >> 3); const resultView = new Uint8Array(resultBuffer); resultView.set((new Uint8Array(result)).slice(0, keyLength >> 3)); return resultBuffer; }); //endregion //endregion return internalSequence; } //************************************************************************************** /** * Default cryptographic engine for Web Cryptography API */ export default class CryptoEngine { //********************************************************************************** /** * Constructor for CryptoEngine class * @param {Object} [parameters={}] * @param {Object} [parameters.schema] asn1js parsed value to initialize the class from */ constructor(parameters = {}) { //region Internal properties of the object /** * @type {Object} * @desc Usually here we are expecting "window.crypto" or an equivalent from custom "crypto engine" */ this.crypto = getParametersValue(parameters, "crypto", {}); /** * @type {Object} * @desc Usually here we are expecting "window.crypto.subtle" or an equivalent from custom "crypto engine" */ this.subtle = getParametersValue(parameters, "subtle", {}); /** * @type {string} * @desc Name of the "crypto engine" */ this.name = getParametersValue(parameters, "name", ""); //endregion } //********************************************************************************** /** * Import WebCrypto keys from different formats * @param {string} format * @param {ArrayBuffer|Uint8Array} keyData * @param {Object} algorithm * @param {boolean} extractable * @param {Array} keyUsages * @returns {Promise} */ importKey(format, keyData, algorithm, extractable, keyUsages) { //region Initial variables let jwk = {}; //endregion //region Change "keyData" type if needed if(keyData instanceof Uint8Array) keyData = keyData.buffer; //endregion switch(format.toLowerCase()) { case "raw": return this.subtle.importKey("raw", keyData, algorithm, extractable, keyUsages); case "spki": { const asn1 = asn1js.fromBER(keyData); if(asn1.offset === (-1)) return Promise.reject("Incorrect keyData"); const publicKeyInfo = new PublicKeyInfo(); try { publicKeyInfo.fromSchema(asn1.result); } catch(ex) { return Promise.reject("Incorrect keyData"); } // noinspection FallThroughInSwitchStatementJS switch(algorithm.name.toUpperCase()) { case "RSA-PSS": { //region Get information about used hash function switch(algorithm.hash.name.toUpperCase()) { case "SHA-1": jwk.alg = "PS1"; break; case "SHA-256": jwk.alg = "PS256"; break; case "SHA-384": jwk.alg = "PS384"; break; case "SHA-512": jwk.alg = "PS512"; break; default: return Promise.reject(`Incorrect hash algorithm: ${algorithm.hash.name.toUpperCase()}`); } //endregion } // break omitted case "RSASSA-PKCS1-V1_5": { keyUsages = ["verify"]; // Override existing keyUsages value since the key is a public key jwk.kty = "RSA"; jwk.ext = extractable; jwk.key_ops = keyUsages; if(publicKeyInfo.algorithm.algorithmId !== "1.2.840.113549.1.1.1") return Promise.reject(`Incorrect public key algorithm: ${publicKeyInfo.algorithm.algorithmId}`); //region Get information about used hash function if(("alg" in jwk) === false) { switch(algorithm.hash.name.toUpperCase()) { case "SHA-1": jwk.alg = "RS1"; break; case "SHA-256": jwk.alg = "RS256"; break; case "SHA-384": jwk.alg = "RS384"; break; case "SHA-512": jwk.alg = "RS512"; break; default: return Promise.reject(`Incorrect hash algorithm: ${algorithm.hash.name.toUpperCase()}`); } } //endregion //region Create RSA Public Key elements const publicKeyJSON = publicKeyInfo.toJSON(); for(const key of Object.keys(publicKeyJSON)) jwk[key] = publicKeyJSON[key]; //endregion } break; case "ECDSA": keyUsages = ["verify"]; // Override existing keyUsages value since the key is a public key // break omitted case "ECDH": { //region Initial variables jwk = { kty: "EC", ext: extractable, key_ops: keyUsages }; //endregion //region Get information about algorithm if(publicKeyInfo.algorithm.algorithmId !== "1.2.840.10045.2.1") return Promise.reject(`Incorrect public key algorithm: ${publicKeyInfo.algorithm.algorithmId}`); //endregion //region Create ECDSA Public Key elements const publicKeyJSON = publicKeyInfo.toJSON(); for(const key of Object.keys(publicKeyJSON)) jwk[key] = publicKeyJSON[key]; //endregion } break; case "RSA-OAEP": { jwk.kty = "RSA"; jwk.ext = extractable; jwk.key_ops = keyUsages; if(this.name.toLowerCase() === "safari") jwk.alg = "RSA-OAEP"; else { switch(algorithm.hash.name.toUpperCase()) { case "SHA-1": jwk.alg = "RSA-OAEP"; break; case "SHA-256": jwk.alg = "RSA-OAEP-256"; break; case "SHA-384": jwk.alg = "RSA-OAEP-384"; break; case "SHA-512": jwk.alg = "RSA-OAEP-512"; break; default: return Promise.reject(`Incorrect hash algorithm: ${algorithm.hash.name.toUpperCase()}`); } } //region Create ECDSA Public Key elements const publicKeyJSON = publicKeyInfo.toJSON(); for(const key of Object.keys(publicKeyJSON)) jwk[key] = publicKeyJSON[key]; //endregion } break; default: return Promise.reject(`Incorrect algorithm name: ${algorithm.name.toUpperCase()}`); } } break; case "pkcs8": { const privateKeyInfo = new PrivateKeyInfo(); //region Parse "PrivateKeyInfo" object const asn1 = asn1js.fromBER(keyData); if(asn1.offset === (-1)) return Promise.reject("Incorrect keyData"); try { privateKeyInfo.fromSchema(asn1.result); } catch(ex) { return Promise.reject("Incorrect keyData"); } if(("parsedKey" in privateKeyInfo) === false) return Promise.reject("Incorrect keyData"); //endregion // noinspection FallThroughInSwitchStatementJS // noinspection FallThroughInSwitchStatementJS switch(algorithm.name.toUpperCase()) { case "RSA-PSS": { //region Get information about used hash function switch(algorithm.hash.name.toUpperCase()) { case "SHA-1": jwk.alg = "PS1"; break; case "SHA-256": jwk.alg = "PS256"; break; case "SHA-384": jwk.alg = "PS384"; break; case "SHA-512": jwk.alg = "PS512"; break; default: return Promise.reject(`Incorrect hash algorithm: ${algorithm.hash.name.toUpperCase()}`); } //endregion } // break omitted case "RSASSA-PKCS1-V1_5": { keyUsages = ["sign"]; // Override existing keyUsages value since the key is a private key jwk.kty = "RSA"; jwk.ext = extractable; jwk.key_ops = keyUsages; //region Get information about used hash function if(privateKeyInfo.privateKeyAlgorithm.algorithmId !== "1.2.840.113549.1.1.1") return Promise.reject(`Incorrect private key algorithm: ${privateKeyInfo.privateKeyAlgorithm.algorithmId}`); //endregion //region Get information about used hash function if(("alg" in jwk) === false) { switch(algorithm.hash.name.toUpperCase()) { case "SHA-1": jwk.alg = "RS1"; break; case "SHA-256": jwk.alg = "RS256"; break; case "SHA-384": jwk.alg = "RS384"; break; case "SHA-512": jwk.alg = "RS512"; break; default: return Promise.reject(`Incorrect hash algorithm: ${algorithm.hash.name.toUpperCase()}`); } } //endregion //region Create RSA Private Key elements const privateKeyJSON = privateKeyInfo.toJSON(); for(const key of Object.keys(privateKeyJSON)) jwk[key] = privateKeyJSON[key]; //endregion } break; case "ECDSA": keyUsages = ["sign"]; // Override existing keyUsages value since the key is a private key // break omitted case "ECDH": { //region Initial variables jwk = { kty: "EC", ext: extractable, key_ops: keyUsages }; //endregion //region Get information about used hash function if(privateKeyInfo.privateKeyAlgorithm.algorithmId !== "1.2.840.10045.2.1") return Promise.reject(`Incorrect algorithm: ${privateKeyInfo.privateKeyAlgorithm.algorithmId}`); //endregion //region Create ECDSA Private Key elements const privateKeyJSON = privateKeyInfo.toJSON(); for(const key of Object.keys(privateKeyJSON)) jwk[key] = privateKeyJSON[key]; //endregion } break; case "RSA-OAEP": { jwk.kty = "RSA"; jwk.ext = extractable; jwk.key_ops = keyUsages; //region Get information about used hash function if(this.name.toLowerCase() === "safari") jwk.alg = "RSA-OAEP"; else { switch(algorithm.hash.name.toUpperCase()) { case "SHA-1": jwk.alg = "RSA-OAEP"; break; case "SHA-256": jwk.alg = "RSA-OAEP-256"; break; case "SHA-384": jwk.alg = "RSA-OAEP-384"; break; case "SHA-512": jwk.alg = "RSA-OAEP-512"; break; default: return Promise.reject(`Incorrect hash algorithm: ${algorithm.hash.name.toUpperCase()}`); } } //endregion //region Create RSA Private Key elements const privateKeyJSON = privateKeyInfo.toJSON(); for(const key of Object.keys(privateKeyJSON)) jwk[key] = privateKeyJSON[key]; //endregion } break; default: return Promise.reject(`Incorrect algorithm name: ${algorithm.name.toUpperCase()}`); } } break; case "jwk": jwk = keyData; break; default: return Promise.reject(`Incorrect format: ${format}`); } //region Special case for Safari browser (since its acting not as WebCrypto standard describes) if(this.name.toLowerCase() === "safari") { // Try to use both ways - import using ArrayBuffer and pure JWK (for Safari Technology Preview) return Promise.resolve().then(() => this.subtle.importKey("jwk", stringToArrayBuffer(JSON.stringify(jwk)), algorithm, extractable, keyUsages)) .then(result => result, () => this.subtle.importKey("jwk", jwk, algorithm, extractable, keyUsages)); } //endregion return this.subtle.importKey("jwk", jwk, algorithm, extractable, keyUsages); } //********************************************************************************** /** * Export WebCrypto keys to different formats * @param {string} format * @param {Object} key * @returns {Promise} */ exportKey(format, key) { let sequence = this.subtle.exportKey("jwk", key); //region Currently Safari returns ArrayBuffer as JWK thus we need an additional transformation if(this.name.toLowerCase() === "safari") { sequence = sequence.then(result => { // Some additional checks for Safari Technology Preview if(result instanceof ArrayBuffer) return JSON.parse(arrayBufferToString(result)); return result; }); } //endregion switch(format.toLowerCase()) { case "raw": return this.subtle.exportKey("raw", key); case "spki": sequence = sequence.then(result => { const publicKeyInfo = new PublicKeyInfo(); try { publicKeyInfo.fromJSON(result); } catch(ex) { return Promise.reject("Incorrect key data"); } return publicKeyInfo.toSchema().toBER(false); }); break; case "pkcs8": sequence = sequence.then(result => { const privateKeyInfo = new PrivateKeyInfo(); try { privateKeyInfo.fromJSON(result); } catch(ex) { return Promise.reject("Incorrect key data"); } return privateKeyInfo.toSchema().toBER(false); }); break; case "jwk": break; default: return Promise.reject(`Incorrect format: ${format}`); } return sequence; } //********************************************************************************** /** * Convert WebCrypto keys between different export formats * @param {string} inputFormat * @param {string} outputFormat * @param {ArrayBuffer|Object} keyData * @param {Object} algorithm * @param {boolean} extractable * @param {Array} keyUsages * @returns {Promise} */ convert(inputFormat, outputFormat, keyData, algorithm, extractable, keyUsages) { switch(inputFormat.toLowerCase()) { case "raw": switch(outputFormat.toLowerCase()) { case "raw": return Promise.resolve(keyData); case "spki": return Promise.resolve() .then(() => this.importKey("raw", keyData, algorithm, extractable, keyUsages)) .then(result => this.exportKey("spki", result)); case "pkcs8": return Promise.resolve() .then(() => this.importKey("raw", keyData, algorithm, extractable, keyUsages)) .then(result => this.exportKey("pkcs8", result)); case "jwk": return Promise.resolve() .then(() => this.importKey("raw", keyData, algorithm, extractable, keyUsages)) .then(result => this.exportKey("jwk", result)); default: return Promise.reject(`Incorrect outputFormat: ${outputFormat}`); } case "spki": switch(outputFormat.toLowerCase()) { case "raw": return Promise.resolve() .then(() => this.importKey("spki", keyData, algorithm, extractable, keyUsages)) .then(result => this.exportKey("raw", result)); case "spki": return Promise.resolve(keyData); case "pkcs8": return Promise.reject("Impossible to convert between SPKI/PKCS8"); case "jwk": return Promise.resolve() .then(() => this.importKey("spki", keyData, algorithm, extractable, keyUsages)) .then(result => this.exportKey("jwk", result)); default: return Promise.reject(`Incorrect outputFormat: ${outputFormat}`); } case "pkcs8": switch(outputFormat.toLowerCase()) { case "raw": return Promise.resolve() .then(() => this.importKey("pkcs8", keyData, algorithm, extractable, keyUsages)) .then(result => this.exportKey("raw", result)); case "spki": return Promise.reject("Impossible to convert between SPKI/PKCS8"); case "pkcs8": return Promise.resolve(keyData); case "jwk": return Promise.resolve() .then(() => this.importKey("pkcs8", keyData, algorithm, extractable, keyUsages)) .then(result => this.exportKey("jwk", result)); default: return Promise.reject(`Incorrect outputFormat: ${outputFormat}`); } case "jwk": switch(outputFormat.toLowerCase()) { case "raw": return Promise.resolve() .then(() => this.importKey("jwk", keyData, algorithm, extractable, keyUsages)) .then(result => this.exportKey("raw", result)); case "spki": return Promise.resolve() .then(() => this.importKey("jwk", keyData, algorithm, extractable, keyUsages)) .then(result => this.exportKey("spki", result)); case "pkcs8": return Promise.resolve() .then(() => this.importKey("jwk", keyData, algorithm, extractable, keyUsages)) .then(result => this.exportKey("pkcs8", result)); case "jwk": return Promise.resolve(keyData); default: return Promise.reject(`Incorrect outputFormat: ${outputFormat}`); } default: return Promise.reject(`Incorrect inputFormat: ${inputFormat}`); } } //********************************************************************************** /** * Wrapper for standard function "encrypt" * @param args * @returns {Promise} */ encrypt(...args) { return this.subtle.encrypt(...args); } //********************************************************************************** /** * Wrapper for standard function "decrypt" * @param args * @returns {Promise} */ decrypt(...args) { return this.subtle.decrypt(...args); } //********************************************************************************** /** * Wrapper for standard function "sign" * @param args * @returns {Promise} */ sign(...args) { return this.subtle.sign(...args); } //********************************************************************************** /** * Wrapper for standard function "verify" * @param args * @returns {Promise} */ verify(...args) { return this.subtle.verify(...args); } //********************************************************************************** /** * Wrapper for standard function "digest" * @param args * @returns {Promise} */ digest(...args) { return this.subtle.digest(...args); } //********************************************************************************** /** * Wrapper for standard function "generateKey" * @param args * @returns {Promise} */ generateKey(...args) { return this.subtle.generateKey(...args); } //********************************************************************************** /** * Wrapper for standard function "deriveKey" * @param args * @returns {Promise} */ deriveKey(...args) { return this.subtle.deriveKey(...args); } //********************************************************************************** /** * Wrapper for standard function "deriveBits" * @param args * @returns {Promise} */ deriveBits(...args) { return this.subtle.deriveBits(...args); } //********************************************************************************** /** * Wrapper for standard function "wrapKey" * @param args * @returns {Promise} */ wrapKey(...args) { return this.subtle.wrapKey(...args); } //********************************************************************************** /** * Wrapper for standard function "unwrapKey" * @param args * @returns {Promise} */ unwrapKey(...args) { return this.subtle.unwrapKey(...args); } //********************************************************************************** /** * Initialize input Uint8Array by random values (with help from current "crypto engine") * @param {!Uint8Array} view * @returns {*} */ getRandomValues(view) { if(("getRandomValues" in this.crypto) === false) throw new Error("No support for getRandomValues"); return this.crypto.getRandomValues(view); } //********************************************************************************** /** * Get WebCrypto algorithm by wel-known OID * @param {string} oid well-known OID to search for * @returns {Object} */ getAlgorithmByOID(oid) { switch(oid) { case "1.2.840.113549.1.1.1": case "1.2.840.113549.1.1.5": return { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-1" } }; case "1.2.840.113549.1.1.11": return { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } }; case "1.2.840.113549.1.1.12": return { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-384" } }; case "1.2.840.113549.1.1.13": return { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-512" } }; case "1.2.840.113549.1.1.10": return { name: "RSA-PSS" }; case "1.2.840.113549.1.1.7": return { name: "RSA-OAEP" }; case "1.2.840.10045.2.1": case "1.2.840.10045.4.1": return { name: "ECDSA", hash: { name: "SHA-1" } }; case "1.2.840.10045.4.3.2": return { name: "ECDSA", hash: { name: "SHA-256" } }; case "1.2.840.10045.4.3.3": return { name: "ECDSA", hash: { name: "SHA-384" } }; case "1.2.840.10045.4.3.4": return { name: "ECDSA", hash: { name: "SHA-512" } }; case "1.3.133.16.840.63.0.2": return { name: "ECDH", kdf: "SHA-1" }; case "1.3.132.1.11.1": return { name: "ECDH", kdf: "SHA-256" }; case "1.3.132.1.11.2": return { name: "ECDH", kdf: "SHA-384" }; case "1.3.132.1.11.3": return { name: "ECDH", kdf: "SHA-512" }; case "2.16.840.1.101.3.4.1.2": return { name: "AES-CBC", length: 128 }; case "2.16.840.1.101.3.4.1.22": return { name: "AES-CBC", length: 192 }; case "2.16.840.1.101.3.4.1.42": return { name: "AES-CBC", length: 256 }; case "2.16.840.1.101.3.4.1.6": return { name: "AES-GCM", length: 128 }; case "2.16.840.1.101.3.4.1.26": return { name: "AES-GCM", length: 192 }; case "2.16.840.1.101.3.4.1.46": return { name: "AES-GCM", length: 256 }; case "2.16.840.1.101.3.4.1.4": return { name: "AES-CFB", length: 128 }; case "2.16.840.1.101.3.4.1.24": return { name: "AES-CFB", length: 192 }; case "2.16.840.1.101.3.4.1.44": return { name: "AES-CFB", length: 256 }; case "2.16.840.1.101.3.4.1.5": return { name: "AES-KW", length: 128 }; case "2.16.840.1.101.3.4.1.25": return { name: "AES-KW", length: 192 }; case "2.16.840.1.101.3.4.1.45": return { name: "AES-KW", length: 256 }; case "1.2.840.113549.2.7": return { name: "HMAC", hash: { name: "SHA-1" } }; case "1.2.840.113549.2.9": return { name: "HMAC", hash: { name: "SHA-256" } }; case "1.2.840.113549.2.10": return { name: "HMAC", hash: { name: "SHA-384" } }; case "1.2.840.113549.2.11": return { name: "HMAC", hash: { name: "SHA-512" } }; case "1.2.840.113549.1.9.16.3.5": return { name: "DH" }; case "1.3.14.3.2.26": return { name: "SHA-1" }; case "2.16.840.1.101.3.4.2.1": return { name: "SHA-256" }; case "2.16.840.1.101.3.4.2.2": return { name: "SHA-384" }; case "2.16.840.1.101.3.4.2.3": return { name: "SHA-512" }; case "1.2.840.113549.1.5.12": return { name: "PBKDF2" }; //region Special case - OIDs for ECC curves case "1.2.840.10045.3.1.7": return { name: "P-256" }; case "1.3.132.0.34": return { name: "P-384" }; case "1.3.132.0.35": return { name: "P-521" }; //endregion default: } return {}; } //********************************************************************************** /** * Get OID for each specific algorithm * @param {Object} algorithm * @returns {string} */ getOIDByAlgorithm(algorithm) { let result = ""; switch(algorithm.name.toUpperCase()) { case "RSASSA-PKCS1-V1_5": switch(algorithm.hash.name.toUpperCase()) { case "SHA-1": result = "1.2.840.113549.1.1.5"; break; case "SHA-256": result = "1.2.840.113549.1.1.11"; break; case "SHA-384": result = "1.2.840.113549.1.1.12"; break; case "SHA-512": result = "1.2.840.113549.1.1.13"; break; default: } break; case "RSA-PSS": result = "1.2.840.113549.1.1.10"; break; case "RSA-OAEP": result = "1.2.840.113549.1.1.7"; break; case "ECDSA": switch(algorithm.hash.name.toUpperCase()) { case "SHA-1": result = "1.2.840.10045.4.1"; break; case "SHA-256": result = "1.2.840.10045.4.3.2"; break; case "SHA-384": result = "1.2.840.10045.4.3.3"; break; case "SHA-512": result = "1.2.840.10045.4.3.4"; break; default: } break; case "ECDH": switch(algorithm.kdf.toUpperCase()) // Non-standard addition - hash algorithm of KDF function { case "SHA-1": result = "1.3.133.16.840.63.0.2"; // dhSinglePass-stdDH-sha1kdf-scheme break; case "SHA-256": result = "1.3.132.1.11.1"; // dhSinglePass-stdDH-sha256kdf-scheme break; case "SHA-384": result = "1.3.132.1.11.2"; // dhSinglePass-stdDH-sha384kdf-scheme break; case "SHA-512": result = "1.3.132.1.11.3"; // dhSinglePass-stdDH-sha512kdf-scheme break; default: } break; case "AES-CTR": break; case "AES-CBC": switch(algorithm.length) { case 128: result = "2.16.840.1.101.3.4.1.2"; break; case 192: result = "2.16.840.1.101.3.4.1.22"; break; case 256: result = "2.16.840.1.101.3.4.1.42"; break; default: } break; case "AES-CMAC": break; case "AES-GCM": switch(algorithm.length) { case 128: result = "2.16.840.1.101.3.4.1.6"; break; case 192: result = "2.16.840.1.101.3.4.1.26"; break; case 256: result = "2.16.840.1.101.3.4.1.46"; break; default: } break; case "AES-CFB": switch(algorithm.length) { case 128: result = "2.16.840.1.101.3.4.1.4"; break; case 192: result = "2.16.840.1.101.3.4.1.24"; break; case 256: result = "2.16.840.1.101.3.4.1.44"; break; default: } break; case "AES-KW": switch(algorithm.length) { case 128: result = "2.16.840.1.101.3.4.1.5"; break; case 192: result = "2.16.840.1.101.3.4.1.25"; break; case 256: result = "2.16.840.1.101.3.4.1.45"; break; default: } break; case "HMAC": switch(algorithm.hash.name.toUpperCase()) { case "SHA-1": result = "1.2.840.113549.2.7"; break; case "SHA-256": result = "1.2.840.113549.2.9"; break; case "SHA-384": result = "1.2.840.113549.2.10"; break; case "SHA-512": result = "1.2.840.113549.2.11"; break; default: } break; case "DH": result = "1.2.840.113549.1.9.16.3.5"; break; case "SHA-1": result = "1.3.14.3.2.26"; break; case "SHA-256": result = "2.16.840.1.101.3.4.2.1"; break; case "SHA-384": result = "2.16.840.1.101.3.4.2.2"; break; case "SHA-512": result = "2.16.840.1.101.3.4.2.3"; break; case "CONCAT": break; case "HKDF": break; case "PBKDF2": result = "1.2.840.113549.1.5.12"; break; //region Special case - OIDs for ECC curves case "P-256": result = "1.2.840.10045.3.1.7"; break; case "P-384": result = "1.3.132.0.34"; break; case "P-521": result = "1.3.132.0.35"; break; //endregion default: } return result; } //********************************************************************************** /** * 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 {*} */ getAlgorithmParameters(algorithmName, operation) { let result = { algorithm: {}, usages: [] }; switch(algorithmName.toUpperCase()) { case "RSASSA-PKCS1-V1_5": switch(operation.toLowerCase()) { case "generatekey": result = { algorithm: { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: { name: "SHA-256" } }, usages: ["sign", "verify"] }; break; case "verify": case "sign": case "importkey": result = { algorithm: { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } }, usages: ["verify"] // For importKey("pkcs8") usage must be "sign" only }; break; case "exportkey": default: return { algorithm: { name: "RSASSA-PKCS1-v1_5" }, usages: [] }; } break; case "RSA-PSS": switch(operation.toLowerCase()) { case "sign": case "verify": result = { algorithm: { name: "RSA-PSS", hash: { name: "SHA-1" }, saltLength: 20 }, usages: ["sign", "verify"] }; break; case "generatekey": result = { algorithm: { name: "RSA-PSS", modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: { name: "SHA-1" } }, usages: ["sign", "verify"] }; break; case "importkey": result = { algorithm: { name: "RSA-PSS", hash: { name: "SHA-1" } }, usages: ["verify"] // For importKey("pkcs8") usage must be "sign" only }; break; case "exportkey": default: return { algorithm: { name: "RSA-PSS" }, usages: [] }; } break; case "RSA-OAEP": switch(operation.toLowerCase()) { case "encrypt": case "decrypt": result = { algorithm: { name: "RSA-OAEP" }, usages: ["encrypt", "decrypt"] }; break; case "generatekey": result = { algorithm: { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: { name: "SHA-256" } }, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"] }; break; case "importkey": result = { algorithm: { name: "RSA-OAEP", hash: { name: "SHA-256" } }, usages: ["encrypt"] // encrypt for "spki" and decrypt for "pkcs8" }; break; case "exportkey": default: return { algorithm: { name: "RSA-OAEP" }, usages: [] }; } break; case "ECDSA": switch(operation.toLowerCase()) { case "generatekey": result = { algorithm: { name: "ECDSA", namedCurve: "P-256" }, usages: ["sign", "verify"] }; break; case "importkey": result = { algorithm: { name: "ECDSA", namedCurve: "P-256" }, usages: ["verify"] // "sign" for "pkcs8" }; break; case "verify": case "sign": result = { algorithm: { name: "ECDSA", hash: { name: "SHA-256" } }, usages: ["sign"] }; break; default: return { algorithm: { name: "ECDSA" }, usages: [] }; } break; case "ECDH": switch(operation.toLowerCase()) { case "exportkey": case "importkey": case "generatekey": result = { algorithm: { name: "ECDH", namedCurve: "P-256" }, usages: ["deriveKey", "deriveBits"] }; break; case "derivekey": case "derivebits": result = { algorithm: { name: "ECDH", namedCurve: "P-256", public: [] // Must be a "publicKey" }, usages: ["encrypt", "decrypt"] }; break; default: return { algorithm: { name: "ECDH" }, usages: [] }; } break; case "AES-CTR": switch(operation.toLowerCase()) { case "importkey": case "exportkey": case "generatekey": result = { algorithm: { name: "AES-CTR", length: 256 }, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"] }; break; case "decrypt": case "encrypt": result = { algorithm: { name: "AES-CTR", counter: new Uint8Array(16), length: 10 }, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"] }; break; default: return { algorithm: { name: "AES-CTR" }, usages: [] }; } break; case "AES-CBC": switch(operation.toLowerCase()) { case "importkey": case "exportkey": case "generatekey": result = { algorithm: { name: "AES-CBC", length: 256 }, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"] }; break; case "decrypt": case "encrypt": result = { algorithm: { name: "AES-CBC", iv: this.getRandomValues(new Uint8Array(16)) // For "decrypt" the value should be replaced with value got on "encrypt" step }, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"] }; break; default: return { algorithm: { name: "AES-CBC" }, usages: [] }; } break; case "AES-GCM": switch(operation.toLowerCase()) { case "importkey": case "exportkey": case "generatekey": result = { algorithm: { name: "AES-GCM", length: 256 }, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"] }; break; case "decrypt": case "encrypt": result = { algorithm: { name: "AES-GCM", iv: this.getRandomValues(new Uint8Array(16)) // For "decrypt" the value should be replaced with value got on "encrypt" step }, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"] }; break; default: return { algorithm: { name: "AES-GCM" }, usages: [] }; } break; case "AES-KW": switch(operation.toLowerCase()) { case "importkey": case "exportkey": case "generatekey": case "wrapkey": case "unwrapkey": result = { algorithm: { name: "AES-KW", length: 256 }, usages: ["wrapKey", "unwrapKey"] }; break; default: return { algorithm: { name: "AES-KW" }, usages: [] }; } break; case "HMAC": switch(operation.toLowerCase()) { case "sign": case "verify": result = { algorithm: { name: "HMAC" }, usages: ["sign", "verify"] }; break; case "importkey": case "exportkey": case "generatekey": result = { algorithm: { name: "HMAC", length: 32, hash: { name: "SHA-256" } }, usages: ["sign", "verify"] }; break; default: return { algorithm: { name: "HMAC" }, usages: [] }; } break; case "HKDF": switch(operation.toLowerCase()) { case "derivekey": result = { algorithm: { name: "HKDF", hash: "SHA-256", salt: new Uint8Array([]), info: new Uint8Array([]) }, usages: ["encrypt", "decrypt"] }; break; default: return { algorithm: { name: "HKDF" }, usages: [] }; } break; case "PBKDF2": switch(operation.toLowerCase()) { case "derivekey": result = { algorithm: { name: "PBKDF2", hash: { name: "SHA-256" }, salt: new Uint8Array([]), iterations: 10000 }, usages: ["encrypt", "decrypt"] }; break; default: return { algorithm: { name: "PBKDF2" }, usages: [] }; } break; default: } return result; } //********************************************************************************** /** * Getting hash algorithm by signature algorithm * @param {AlgorithmIdentifier} signatureAlgorithm Signature algorithm * @returns {string} */ getHashAlgorithm(signatureAlgorithm) { let result = ""; switch(signatureAlgorithm.algorithmId) { case "1.2.840.10045.4.1": // ecdsa-with-SHA1 case "1.2.840.113549.1.1.5": result = "SHA-1"; break; case "1.2.840.10045.4.3.2": // ecdsa-with-SHA256 case "1.2.840.113549.1.1.11": result = "SHA-256"; break; case "1.2.840.10045.4.3.3": // ecdsa-with-SHA384 case "1.2.840.113549.1.1.12": result = "SHA-384"; break; case "1.2.840.10045.4.3.4": // ecdsa-with-SHA512 case "1.2.840.113549.1.1.13": result = "SHA-512"; break; case "1.2.840.113549.1.1.10": // RSA-PSS { try { const params = new RSASSAPSSParams({ schema: signatureAlgorithm.algorithmParams }); if("hashAlgorithm" in params) { const algorithm = this.getAlgorithmByOID(params.hashAlgorithm.algorithmId); if(("name" in algorithm) === false) return ""; result = algorithm.name; } else result = "SHA-1"; } catch(ex) { } } break; default: } return result; } //********************************************************************************** /** * Specialized function encrypting "EncryptedContentInfo" object using parameters * @param {Object} parameters * @returns {Promise} */ encryptEncryptedContentInfo(parameters) { //region Check for input parameters if((parameters instanceof Object) === false) return Promise.reject("Parameters must have type \"Object\""); if(("password" in parameters) === false) return Promise.reject("Absent mandatory parameter \"password\""); if(("contentEncryptionAlgorithm" in parameters) === false) return Promise.reject("Absent mandatory parameter \"contentEncryptionAlgorithm\""); if(("hmacHashAlgorithm" in parameters) === false) return Promise.reject("Absent mandatory parameter \"hmacHashAlgorithm\""); if(("iterationCount" in parameters) === false) return Promise.reject("Absent mandatory parameter \"iterationCount\""); if(("contentToEncrypt" in parameters) === false) return Promise.reject("Absent mandatory parameter \"contentToEncrypt\""); if(("contentType" in parameters) === false) return Promise.reject("Absent mandatory parameter \"contentType\""); const contentEncryptionOID = this.getOIDByAlgorithm(parameters.contentEncryptionAlgorithm); if(contentEncryptionOID === "") return Promise.reject("Wrong \"contentEncryptionAlgorithm\" value"); const pbkdf2OID = this.getOIDByAlgorithm({ name: "PBKDF2" }); if(pbkdf2OID === "") return Promise.reject("Can not find OID for PBKDF2"); const hmacOID = this.getOIDByAlgorithm({ name: "HMAC", hash: { name: parameters.hmacHashAlgorithm } }); if(hmacOID === "") return Promise.reject(`Incorrect value for "hmacHashAlgorithm": ${parameters.hmacHashAlgorithm}`); //endregion //region Initial variables let sequence = Promise.resolve(); const ivBuffer = new ArrayBuffer(16); // For AES we need IV 16 bytes long const ivView = new Uint8Array(ivBuffer); this.getRandomValues(ivView); const saltBuffer = new ArrayBuffer(64); const saltView = new Uint8Array(saltBuffer); this.getRandomValues(saltView); const contentView = new Uint8Array(parameters.contentToEncrypt); const pbkdf2Params = new PBKDF2Params({ salt: new asn1js.OctetString({ valueHex: saltBuffer }), iterationCount: parameters.iterationCount, prf: new AlgorithmIdentifier({ algorithmId: hmacOID, algorithmParams: new asn1js.Null() }) }); //endregion //region Derive PBKDF2 key from "password" buffer sequence = sequence.then(() => { const passwordView = new Uint8Array(parameters.password); return this.importKey("raw", passwordView, "PBKDF2", false, ["deriveKey"]); }, error => Promise.reject(error) ); //endregion //region Derive key for "contentEncryptionAlgorithm" sequence = sequence.then(result => this.deriveKey({ name: "PBKDF2", hash: { name: parameters.hmacHashAlgorithm }, salt: saltView, iterations: parameters.iterationCount }, result, parameters.contentEncryptionAlgorithm, false, ["encrypt"]), error => Promise.reject(error) ); //endregion //region Encrypt content sequence = sequence.then(result => this.encrypt({ name: parameters.contentEncryptionAlgorithm.name, iv: ivView }, result, contentView), error => Promise.reject(error) ); //endregion //region Store all parameters in EncryptedData object sequence = sequence.then(result => { const pbes2Parameters = new PBES2Params({ keyDerivationFunc: new AlgorithmIdentifier({ algorithmId: pbkdf2OID, algorithmParams: pbkdf2Params.toSchema() }), encryptionScheme: new AlgorithmIdentifier({ algorithmId: contentEncryptionOID, algorithmParams: new asn1js.OctetString({ valueHex: ivBuffer }) }) }); return new EncryptedContentInfo({ contentType: parameters.contentType, contentEncryptionAlgorithm: new AlgorithmIdentifier({ algorithmId: "1.2.840.113549.1.5.13", // pkcs5PBES2 algorithmParams: pbes2Parameters.toSchema() }), encryptedContent: new asn1js.OctetString({ valueHex: result }) }); }, error => Promise.reject(error) ); //endregion return sequence; } //********************************************************************************** /** * Decrypt data stored in "EncryptedContentInfo" object using parameters * @param parameters * @return {Promise} */ decryptEncryptedContentInfo(parameters) { //region Check for input parameters if((parameters instanceof Object) === false) return Promise.reject("Parameters must have type \"Object\""); if(("password" in parameters) === false) return Promise.reject("Absent mandatory parameter \"password\""); if(("encryptedContentInfo" in parameters) === false) return Promise.reject("Absent mandatory parameter \"encryptedContentInfo\""); if(parameters.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId !== "1.2.840.113549.1.5.13") // pkcs5PBES2 return Promise.reject(`Unknown "contentEncryptionAlgorithm": ${parameters.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId}`); //endregion //region Initial variables let sequence = Promise.resolve(); let pbes2Parameters; try { pbes2Parameters = new PBES2Params({ schema: parameters.encryptedContentInfo.contentEncryptionAlgorithm.algorithmParams }); } catch(ex) { return Promise.reject("Incorrectly encoded \"pbes2Parameters\""); } let pbkdf2Params; try { pbkdf2Params = new PBKDF2Params({ schema: pbes2Parameters.keyDerivationFunc.algorithmParams }); } catch(ex) { return Promise.reject("Incorrectly encoded \"pbkdf2Params\""); } const contentEncryptionAlgorithm = this.getAlgorithmByOID(pbes2Parameters.encryptionScheme.algorithmId); if(("name" in contentEncryptionAlgorithm) === false) return Promise.reject(`Incorrect OID for "contentEncryptionAlgorithm": ${pbes2Parameters.encryptionScheme.algorithmId}`); const ivBuffer = pbes2Parameters.encryptionScheme.algorithmParams.valueBlock.valueHex; const ivView = new Uint8Array(ivBuffer); const saltBuffer = pbkdf2Params.salt.valueBlock.valueHex; const saltView = new Uint8Array(saltBuffer); const iterationCount = pbkdf2Params.iterationCount; let hmacHashAlgorithm = "SHA-1"; if("prf" in pbkdf2Params) { const algorithm = this.getAlgorithmByOID(pbkdf2Params.prf.algorithmId); if(("name" in algorithm) === false) return Promise.reject("Incorrect OID for HMAC hash algorithm"); hmacHashAlgorithm = algorithm.hash.name; } //endregion //region Derive PBKDF2 key from "password" buffer sequence = sequence.then(() => this.importKey("raw", parameters.password, "PBKDF2", false, ["deriveKey"]), error => Promise.reject(error) ); //endregion //region Derive key for "contentEncryptionAlgorithm" sequence = sequence.then(result => this.deriveKey({ name: "PBKDF2", hash: { name: hmacHashAlgorithm }, salt: saltView, iterations: iterationCount }, result, contentEncryptionAlgorithm, false, ["decrypt"]), error => Promise.reject(error) ); //endregion //region Decrypt internal content using derived key sequence = sequence.then(result => { //region Create correct data block for decryption let dataBuffer = new ArrayBuffer(0); if(parameters.encryptedContentInfo.encryptedContent.idBlock.isConstructed === false) dataBuffer = parameters.encryptedContentInfo.encryptedContent.valueBlock.valueHex; else { for(const content of parameters.encryptedContentInfo.encryptedContent.valueBlock.value) dataBuffer = utilConcatBuf(dataBuffer, content.valueBlock.valueHex); } //endregion return this.decrypt({ name: contentEncryptionAlgorithm.name, iv: ivView }, result, dataBuffer); }, error => Promise.reject(error) ); //endregion return sequence; } //********************************************************************************** /** * Stamping (signing) data using algorithm simular to HMAC * @param {Object} parameters * @return {Promise.|Promise} */ stampDataWithPassword(parameters) { //region Check for input parameters if((parameters instanceof Object) === false) return Promise.reject("Parameters must have type \"Object\""); if(("password" in parameters) === false) return Promise.reject("Absent mandatory parameter \"password\""); if(("hashAlgorithm" in parameters) === false) return Promise.reject("Absent mandatory parameter \"hashAlgorithm\""); if(("salt" in parameters) === false) return Promise.reject("Absent mandatory parameter \"iterationCount\""); if(("iterationCount" in parameters) === false) return Promise.reject("Absent mandatory parameter \"salt\""); if(("contentToStamp" in parameters) === false) return Promise.reject("Absent mandatory parameter \"contentToStamp\""); //endregion //region Choose correct length for HMAC key let length; switch(parameters.hashAlgorithm.toLowerCase()) { case "sha-1": length = 160; break; case "sha-256": length = 256; break; case "sha-384": length = 384; break; case "sha-512": length = 512; break; default: return Promise.reject(`Incorrect "parameters.hashAlgorithm" parameter: ${parameters.hashAlgorithm}`); } //endregion //region Initial variables let sequence = Promise.resolve(); const hmacAlgorithm = { name: "HMAC", length, hash: { name: parameters.hashAlgorithm } }; //endregion //region Create PKCS#12 key for integrity checking sequence = sequence.then(() => makePKCS12B2Key(this, parameters.hashAlgorithm, length, parameters.password, parameters.salt, parameters.iterationCount)); //endregion //region Import HMAC key // noinspection JSCheckFunctionSignatures sequence = sequence.then( result => this.importKey("raw", new Uint8Array(result), hmacAlgorithm, false, ["sign"]) ); //endregion //region Make signed HMAC value sequence = sequence.then( result => this.sign(hmacAlgorithm, result, new Uint8Array(parameters.contentToStamp)), error => Promise.reject(error) ); //endregion return sequence; } //********************************************************************************** verifyDataStampedWithPassword(parameters) { //region Check for input parameters if((parameters instanceof Object) === false) return Promise.reject("Parameters must have type \"Object\""); if(("password" in parameters) === false) return Promise.reject("Absent mandatory parameter \"password\""); if(("hashAlgorithm" in parameters) === false) return Promise.reject("Absent mandatory parameter \"hashAlgorithm\""); if(("salt" in parameters) === false) return Promise.reject("Absent mandatory parameter \"iterationCount\""); if(("iterationCount" in parameters) === false) return Promise.reject("Absent mandatory parameter \"salt\""); if(("contentToVerify" in parameters) === false) return Promise.reject("Absent mandatory parameter \"contentToVerify\""); if(("signatureToVerify" in parameters) === false) return Promise.reject("Absent mandatory parameter \"signatureToVerify\""); //endregion //region Choose correct length for HMAC key let length; switch(parameters.hashAlgorithm.toLowerCase()) { case "sha-1": length = 160; break; case "sha-256": length = 256; break; case "sha-384": length = 384; break; case "sha-512": length = 512; break; default: return Promise.reject(`Incorrect "parameters.hashAlgorithm" parameter: ${parameters.hashAlgorithm}`); } //endregion //region Initial variables let sequence = Promise.resolve(); const hmacAlgorithm = { name: "HMAC", length, hash: { name: parameters.hashAlgorithm } }; //endregion //region Create PKCS#12 key for integrity checking sequence = sequence.then(() => makePKCS12B2Key(this, parameters.hashAlgorithm, length, parameters.password, parameters.salt, parameters.iterationCount)); //endregion //region Import HMAC key // noinspection JSCheckFunctionSignatures sequence = sequence.then(result => this.importKey("raw", new Uint8Array(result), hmacAlgorithm, false, ["verify"]) ); //endregion //region Make signed HMAC value sequence = sequence.then( result => this.verify(hmacAlgorithm, result, new Uint8Array(parameters.signatureToVerify), new Uint8Array(parameters.contentToVerify)), error => Promise.reject(error) ); //endregion return sequence; } //********************************************************************************** /** * Get signature parameters by analyzing private key algorithm * @param {Object} privateKey The private key user would like to use * @param {string} [hashAlgorithm="SHA-1"] Hash algorithm user would like to use * @return {Promise.|Promise} */ getSignatureParameters(privateKey, hashAlgorithm = "SHA-1") { //region Check hashing algorithm const oid = this.getOIDByAlgorithm({ name: hashAlgorithm }); if(oid === "") return Promise.reject(`Unsupported hash algorithm: ${hashAlgorithm}`); //endregion //region Initial variables const signatureAlgorithm = new AlgorithmIdentifier(); //endregion //region Get a "default parameters" for current algorithm const parameters = this.getAlgorithmParameters(privateKey.algorithm.name, "sign"); parameters.algorithm.hash.name = hashAlgorithm; //endregion //region Fill internal structures base on "privateKey" and "hashAlgorithm" switch(privateKey.algorithm.name.toUpperCase()) { case "RSASSA-PKCS1-V1_5": case "ECDSA": signatureAlgorithm.algorithmId = this.getOIDByAlgorithm(parameters.algorithm); break; case "RSA-PSS": { //region Set "saltLength" as a length (in octets) of hash function result switch(hashAlgorithm.toUpperCase()) { case "SHA-256": parameters.algorithm.saltLength = 32; break; case "SHA-384": parameters.algorithm.saltLength = 48; break; case "SHA-512": parameters.algorithm.saltLength = 64; break; default: } //endregion //region Fill "RSASSA_PSS_params" object const paramsObject = {}; if(hashAlgorithm.toUpperCase() !== "SHA-1") { const hashAlgorithmOID = this.getOIDByAlgorithm({ name: hashAlgorithm }); if(hashAlgorithmOID === "") return Promise.reject(`Unsupported hash algorithm: ${hashAlgorithm}`); paramsObject.hashAlgorithm = new AlgorithmIdentifier({ algorithmId: hashAlgorithmOID, algorithmParams: new asn1js.Null() }); paramsObject.maskGenAlgorithm = new AlgorithmIdentifier({ algorithmId: "1.2.840.113549.1.1.8", // MGF1 algorithmParams: paramsObject.hashAlgorithm.toSchema() }); } if(parameters.algorithm.saltLength !== 20) paramsObject.saltLength = parameters.algorithm.saltLength; const pssParameters = new RSASSAPSSParams(paramsObject); //endregion //region Automatically set signature algorithm signatureAlgorithm.algorithmId = "1.2.840.113549.1.1.10"; signatureAlgorithm.algorithmParams = pssParameters.toSchema(); //endregion } break; default: return Promise.reject(`Unsupported signature algorithm: ${privateKey.algorithm.name}`); } //endregion return Promise.resolve().then(() => ({ signatureAlgorithm, parameters })); } //********************************************************************************** /** * Sign data with pre-defined private key * @param {ArrayBuffer} data Data to be signed * @param {Object} privateKey Private key to use * @param {Object} parameters Parameters for used algorithm * @return {Promise.|Promise} */ signWithPrivateKey(data, privateKey, parameters) { return this.sign(parameters.algorithm, privateKey, new Uint8Array(data)) .then(result => { //region Special case for ECDSA algorithm if(parameters.algorithm.name === "ECDSA") result = createCMSECDSASignature(result); //endregion return result; }, error => Promise.reject(`Signing error: ${error}`) ); } //********************************************************************************** fillPublicKeyParameters(publicKeyInfo, signatureAlgorithm) { const parameters = {}; //region Find signer's hashing algorithm const shaAlgorithm = this.getHashAlgorithm(signatureAlgorithm); if(shaAlgorithm === "") return Promise.reject(`Unsupported signature algorithm: ${signatureAlgorithm.algorithmId}`); //endregion //region Get information about public key algorithm and default parameters for import let algorithmId; if(signatureAlgorithm.algorithmId === "1.2.840.113549.1.1.10") algorithmId = signatureAlgorithm.algorithmId; else algorithmId = publicKeyInfo.algorithm.algorithmId; const algorithmObject = this.getAlgorithmByOID(algorithmId); if(("name" in algorithmObject) === "") return Promise.reject(`Unsupported public key algorithm: ${signatureAlgorithm.algorithmId}`); parameters.algorithm = this.getAlgorithmParameters(algorithmObject.name, "importkey"); if("hash" in parameters.algorithm.algorithm) parameters.algorithm.algorithm.hash.name = shaAlgorithm; //region Special case for ECDSA if(algorithmObject.name === "ECDSA") { //region Get information about named curve let algorithmParamsChecked = false; if(("algorithmParams" in publicKeyInfo.algorithm) === true) { if("idBlock" in publicKeyInfo.algorithm.algorithmParams) { if((publicKeyInfo.algorithm.algorithmParams.idBlock.tagClass === 1) && (publicKeyInfo.algorithm.algorithmParams.idBlock.tagNumber === 6)) algorithmParamsChecked = true; } } if(algorithmParamsChecked === false) return Promise.reject("Incorrect type for ECDSA public key parameters"); const curveObject = this.getAlgorithmByOID(publicKeyInfo.algorithm.algorithmParams.valueBlock.toString()); if(("name" in curveObject) === false) return Promise.reject(`Unsupported named curve algorithm: ${publicKeyInfo.algorithm.algorithmParams.valueBlock.toString()}`); //endregion parameters.algorithm.algorithm.namedCurve = curveObject.name; } //endregion //endregion return parameters; } //********************************************************************************** getPublicKey(publicKeyInfo, signatureAlgorithm, parameters = null) { if(parameters === null) parameters = this.fillPublicKeyParameters(publicKeyInfo, signatureAlgorithm); const publicKeyInfoSchema = publicKeyInfo.toSchema(); const publicKeyInfoBuffer = publicKeyInfoSchema.toBER(false); const publicKeyInfoView = new Uint8Array(publicKeyInfoBuffer); return this.importKey("spki", publicKeyInfoView, parameters.algorithm.algorithm, true, parameters.algorithm.usages ); } //********************************************************************************** verifyWithPublicKey(data, signature, publicKeyInfo, signatureAlgorithm, shaAlgorithm = null) { //region Initial variables let sequence = Promise.resolve(); //endregion //region Find signer's hashing algorithm if(shaAlgorithm === null) { shaAlgorithm = this.getHashAlgorithm(signatureAlgorithm); if(shaAlgorithm === "") return Promise.reject(`Unsupported signature algorithm: ${signatureAlgorithm.algorithmId}`); //region Import public key sequence = sequence.then(() => this.getPublicKey(publicKeyInfo, signatureAlgorithm)); //endregion } else { const parameters = {}; //region Get information about public key algorithm and default parameters for import let algorithmId; if(signatureAlgorithm.algorithmId === "1.2.840.113549.1.1.10") algorithmId = signatureAlgorithm.algorithmId; else algorithmId = publicKeyInfo.algorithm.algorithmId; const algorithmObject = this.getAlgorithmByOID(algorithmId); if(("name" in algorithmObject) === "") return Promise.reject(`Unsupported public key algorithm: ${signatureAlgorithm.algorithmId}`); parameters.algorithm = this.getAlgorithmParameters(algorithmObject.name, "importkey"); if("hash" in parameters.algorithm.algorithm) parameters.algorithm.algorithm.hash.name = shaAlgorithm; //region Special case for ECDSA if(algorithmObject.name === "ECDSA") { //region Get information about named curve let algorithmParamsChecked = false; if(("algorithmParams" in publicKeyInfo.algorithm) === true) { if("idBlock" in publicKeyInfo.algorithm.algorithmParams) { if((publicKeyInfo.algorithm.algorithmParams.idBlock.tagClass === 1) && (publicKeyInfo.algorithm.algorithmParams.idBlock.tagNumber === 6)) algorithmParamsChecked = true; } } if(algorithmParamsChecked === false) return Promise.reject("Incorrect type for ECDSA public key parameters"); const curveObject = this.getAlgorithmByOID(publicKeyInfo.algorithm.algorithmParams.valueBlock.toString()); if(("name" in curveObject) === false) return Promise.reject(`Unsupported named curve algorithm: ${publicKeyInfo.algorithm.algorithmParams.valueBlock.toString()}`); //endregion parameters.algorithm.algorithm.namedCurve = curveObject.name; } //endregion //endregion //region Import public key sequence = sequence.then(() => this.getPublicKey(publicKeyInfo, null, parameters)); //endregion } //endregion //region Verify signature sequence = sequence.then(publicKey => { //region Get default algorithm parameters for verification const algorithm = this.getAlgorithmParameters(publicKey.algorithm.name, "verify"); if("hash" in algorithm.algorithm) algorithm.algorithm.hash.name = shaAlgorithm; //endregion //region Special case for ECDSA signatures let signatureValue = signature.valueBlock.valueHex; if(publicKey.algorithm.name === "ECDSA") { const asn1 = asn1js.fromBER(signatureValue); // noinspection JSCheckFunctionSignatures signatureValue = createECDSASignatureFromCMS(asn1.result); } //endregion //region Special case for RSA-PSS if(publicKey.algorithm.name === "RSA-PSS") { let pssParameters; try { pssParameters = new RSASSAPSSParams({ schema: signatureAlgorithm.algorithmParams }); } catch(ex) { return Promise.reject(ex); } if("saltLength" in pssParameters) algorithm.algorithm.saltLength = pssParameters.saltLength; else algorithm.algorithm.saltLength = 20; let hashAlgo = "SHA-1"; if("hashAlgorithm" in pssParameters) { const hashAlgorithm = this.getAlgorithmByOID(pssParameters.hashAlgorithm.algorithmId); if(("name" in hashAlgorithm) === false) return Promise.reject(`Unrecognized hash algorithm: ${pssParameters.hashAlgorithm.algorithmId}`); hashAlgo = hashAlgorithm.name; } algorithm.algorithm.hash.name = hashAlgo; } //endregion return this.verify(algorithm.algorithm, publicKey, new Uint8Array(signatureValue), new Uint8Array(data) ); }); //endregion return sequence; } //********************************************************************************** } //**************************************************************************************