UNPKG

@vostokplatform/crypto-gost-js

Version:

Pure Javascript implementation of WebCrypto API interfaces and Public Key Infrastructure for GOST algorithms (Russian Cryptographic Standards)

1,266 lines (1,200 loc) 101 kB
/** * @file Key and Certificate Store methods * @version 1.76 * @copyright 2014-2016, Rudolf Nickolaev. All rights reserved. */ import { gostSecurityInstance } from "./gostSecurity"; import { gostCodingInstance } from "./gostCoding"; import { gostASN1Instance } from "./gostASN1"; import { gostSubtleInstance } from "./gostSubtle"; import { gostCertInstance } from "./gostCert"; import { gostCMSInstance } from "./gostCMS"; /* * Common tools and methods */ // <editor-fold defaultstate="collapsed"> var CryptoOperationData = ArrayBuffer; var coding = gostCodingInstance; var providers = gostSecurityInstance.providers; var asn1 = gostASN1Instance; var subtle = gostSubtleInstance; var cert = gostCertInstance; var cms = gostCMSInstance; // Expand javascript object function expand() { var r = {}; for (var i = 0, n = arguments.length; i < n; i++) { var item = arguments[i]; if (typeof item === 'object') for (var name in item) if (item.hasOwnProperty(name)) r[name] = item[name]; } return r; } function defineProperty(object, name, descriptor, enumerable) { if (typeof descriptor !== 'object') descriptor = { value: descriptor }; if (enumerable !== undefined) descriptor.enumerable = enumerable; Object.defineProperty(object, name, descriptor); } function defineProperties(object, properties, enumerable) { for (var name in properties) defineProperty(object, name, properties[name], enumerable); } // Extend javascript class function extend(Super, Class, propertiesObject, propertiesClass) { // If constructor not defined if (typeof Class !== 'function') { propertiesClass = propertiesObject; propertiesObject = Class; Class = function () { Super.apply(this, arguments); }; } // Create prototype properties Class.prototype = Object.create(Super.prototype, { constructor: { value: Class }, superclass: { value: Super.prototype } }); if (propertiesObject) defineProperties(Class.prototype, propertiesObject, true); // Inherites super class properties if (Super !== Object) for (var name in Super) Class[name] = Super[name]; Class.super = Super; if (propertiesClass) defineProperties(Class, propertiesClass, true); return Class; } // Get random values function getSeed(length) { var seed = new Uint8Array(length); gostCrypto.getRandomValues(seed); return seed.buffer; } // Self resolver function call(callback) { try { callback(); } catch (e) { } } // Get buffer function buffer(d) { if (d instanceof CryptoOperationData) return d; else if (d && d.buffer && d.buffer instanceof CryptoOperationData) return d.byteOffset === 0 && d.byteLength === d.buffer.byteLength ? d.buffer : new Uint8Array(new Uint8Array(d, d.byteOffset, d.byteLength)).buffer; else throw new DataError('CryptoOperationData required'); } // Today date + n days with time function now(n) { var date = new Date(); if (n) date.setDate(date.getDate() + n); return date; } // Today date + n days function today(n) { var date = now(n); date.setHours(0, 0, 0, 0); return date; } // Check the buffers to equal function equalBuffers(r1, r2) { var s1 = new Uint8Array(r1), s2 = new Uint8Array(r2); if (s1.length !== s2.length) return false; for (var i = 0, n = s1.length; i < n; i++) if (s1[i] !== s2[i]) return false; return true; } // Generate new alias function generateUUID() { var r = new Uint8Array(getSeed(16)), s = ''; for (var i = 0; i < 16; i++) s += ('00' + r[i].toString(16)).slice(-2); return s.substr(0, 8) + '-' + s.substr(8, 4) + '-4' + s.substr(13, 3) + '-9' + s.substr(17, 3) + '-' + s.substr(20, 12); } // Return get32 from buffer function get32(buffer, offset) { var r = new Uint8Array(buffer, offset, 4); return (r[3] << 24) | (r[2] << 16) | (r[1] << 8) | r[0]; } function set32(buffer, offset, int) { var r = new Uint8Array(buffer, offset, 4); r[3] = int >>> 24; r[2] = int >>> 16 & 0xff; r[1] = int >>> 8 & 0xff; r[0] = int & 0xff; return r; } // Salt size function saltSize(algorithm) { switch (algorithm.id) { case 'pbeWithSHAAnd40BitRC2-CBC': case 'pbeWithSHAAnd128BitRC2-CBC': return 8; case 'pbeUnknownGost': return 16; case 'sha1': return 20; default: return 32; } } // Password to bytes function passwordData(derivation, password) { if (!password) return new CryptoOperationData(0); if (derivation.name.indexOf('CPKDF') >= 0) { // CryptoPro store password var r = []; for (var i = 0; i < password.length; i++) { var c = password.charCodeAt(i); r.push(c & 0xff); r.push(c >>> 8 & 0xff); r.push(0); r.push(0); } return new Uint8Array(r).buffer; } else if (derivation.name.indexOf('PFXKDF') >= 0) // PKCS#12 unicode password return coding.Chars.decode(password + '\0', 'unicode'); else // PKCS#5 password mode return coding.Chars.decode(password, 'utf8'); } // </editor-fold> /** * Key and Certificate Store methods * * @class GostKeys */ export function GostKeys() { } /** * Key templates * <ul> * <li>providerName - provider name for key encryption, default 'CP-01'</li> * <li>days - validity of the key in days, default 7305</li> * </ul> * * @memberOf GostKeys * @instance */ var options = {// <editor-fold defaultstate="collapsed"> providerName: 'CP-01', days: 7305 // </editor-fold> }; GostKeys.prototype.options = options; /** * A class for private keys in PKCS #8 format * * @class GostKeys.PKCS8 * @extends GostASN1.PrivateKeyInfo * @param {(FormatedData|GostASN1.PrivateKeyInfo)} keyInfo */ function PKCS8(keyInfo) { asn1.PrivateKeyInfo.call(this, keyInfo); } extend(asn1.PrivateKeyInfo, PKCS8, { /** * Get the private key * * @memberOf GostKeys.PKCS8 * @instance * @returns {Promise} Promise to return the {@link Key} */ getPrivateKey: function () // <editor-fold defaultstate="collapsed"> { var keyUsages = (this.privateKeyAlgorithm.id === 'rsaEncryption') ? ['sign'] : ['sign', 'deriveKey', 'deriveBits']; return subtle.importKey('pkcs8', this.encode(), this.privateKeyAlgorithm, 'true', keyUsages); }, // </editor-fold> /** * Set the private key * * @memberOf GostKeys.PKCS8 * @instance * @param {Key} privateKey The Private Key * @returns {Promise} Promise to return the self object after set the key */ setPrivateKey: function (privateKey) // <editor-fold defaultstate="collapsed"> { var self = this; return subtle.exportKey('pkcs8', privateKey).then(function (keyInfo) { asn1.PrivateKeyInfo.call(self, keyInfo); return self; }); }, // </editor-fold> /** * Generate private key and return certification request * * @memberOf GostKeys.PKCS8 * @instance * @param {(FormatedData|GostASN1.CertificationRequest)} req The request templates * @param {(AlgorithmIdentifier|string)} keyAlgorithm The name of provider or algorithm identifier * @returns {Promise} Promise to return the {@link GostCert.Request} after key generation */ generate: function (req, keyAlgorithm) // <editor-fold defaultstate="collapsed"> { var self = this; return new Promise(call).then(function () { if (!(req instanceof cert.Request)) req = new cert.Request(req); // Generate request return req.generate(keyAlgorithm); }).then(function (key) { asn1.PrivateKeyInfo.call(self, key); ; return req; }); } // </editor-fold> }); /** * A class for private keys in PKCS #8 format * * @memberOf GostKeys * @type GostKeys.PKCS8 */ GostKeys.prototype.PKCS8 = PKCS8; /** * A class for PKCS #5 and PKCS #12 password-encrypted private keys in PKCS #8 format * * @class GostKeys.PKCS8Encrypted * @extends GostASN1.EncryptedPrivateKeyInfo * @param {(FormatedData|GostASN1.EncryptedPrivateKeyInfo)} encryptedKey */ function PKCS8Encrypted(encryptedKey) { asn1.EncryptedPrivateKeyInfo.call(this, encryptedKey); } extend(asn1.EncryptedPrivateKeyInfo, PKCS8Encrypted, { /** * Get the private key info * * @memberOf GostKeys.PKCS8Encrypted * @instance * @param {(Key|CryptoOperationData|string)} keyPassword The secret key or password for decryption * @returns {Promise} Promise to return decrypted {@link GostKeys.PKCS8} */ getKey: function (keyPassword) // <editor-fold defaultstate="collapsed"> { var self = this, engine; return new Promise(call).then(function () { engine = new cms.EncryptedDataContentInfo({ contentType: 'encryptedData', version: 0, encryptedContentInfo: { contentType: 'data', contentEncryptionAlgorithm: self.encryptionAlgorithm, encryptedContent: self.encryptedData } }); return engine.getEnclosed(keyPassword); }).then(function (contentInfo) { // Create key object return PKCS8.decode(contentInfo.content); }); }, // </editor-fold> /** * Get the private key * * @memberOf GostKeys.PKCS8Encrypted * @instance * @param {(Key|CryptoOperationData|string)} keyPassword The secret key or password for decryption * @returns {Promise} Promise to return decrypted {@link Key} */ getPrivateKey: function (keyPassword) // <editor-fold defaultstate="collapsed"> { return this.getKey(keyPassword).then(function (keyInfo) { return keyInfo.getPrivateKey(); }); }, // </editor-fold> /** * Sets and encrypt the private key info * * @memberOf GostKeys.PKCS8Encrypted * @instance * @param {(FormatedData|GostKeys.PKCS8)} keyInfo The private key info * @param {(Key|CryptoOperationData|string)} keyPassword The secret key or password for encryption * @param {(AlgorithmIdentifier|string)} encryptionAlgorithm The encryption algorithm or provider name * @returns {Promise} Promise to return self object after set key */ setKey: function (keyInfo, keyPassword, encryptionAlgorithm) // <editor-fold defaultstate="collapsed"> { var self = this, engine; return new Promise(call).then(function () { keyInfo = new PKCS8(keyInfo); engine = new cms.EncryptedDataContentInfo(); return engine.encloseContent(keyInfo.encode(), keyPassword, encryptionAlgorithm || options.providerName); }).then(function () { self.encryptionAlgorithm = engine.encryptedContentInfo.contentEncryptionAlgorithm; self.encryptedData = engine.encryptedContentInfo.encryptedContent; return self; }); }, // </editor-fold> /** * Set the private key * * @memberOf GostKeys.PKCS8Encrypted * @instance * @param {Key} privateKey The private key * @param {(Key|CryptoOperationData|string)} keyPassword The secret key or password for decryption * @param {(AlgorithmIdentifier|string)} encryptionAlgorithm The encryption algorithm or provider name * @returns {Promise} Promise to return self object after set key */ setPrivateKey: function (privateKey, keyPassword, encryptionAlgorithm) // <editor-fold defaultstate="collapsed"> { var self = this; return new PKCS8().setPrivateKey(privateKey).then(function (keyInfo) { return self.setKey(keyInfo, keyPassword, encryptionAlgorithm); }); }, // </editor-fold> /** * Generate private key and return certification request * * @memberOf GostKeys.PKCS8Encrypted * @instance * @param {(FormatedData|GostASN1.CertificationRequest)} req The request templates * @param {(Key|CryptoOperationData|string)} keyPassword The secret key or password for decryption * @param {(AlgorithmIdentifier|string)} keyAlgorithm The name of provider or algorithm * @param {(AlgorithmIdentifier|string)} encryptionAlgorithm The encryption algorithm or provider name * @returns {Promise} Promise to return {@link GostCert.Request} */ generate: function (req, keyPassword, keyAlgorithm, encryptionAlgorithm) // <editor-fold defaultstate="collapsed"> { var self = this; return new Promise(call).then(function () { if (!(req instanceof cert.Request)) req = new cert.Request(req); // Generate request return req.generate(keyAlgorithm); }).then(function (key) { return self.setKey(key, keyPassword, encryptionAlgorithm); }).then(function () { return req; }); } // </editor-fold> }); /** * A class for PKCS #5 and PKCS #12 password-encrypted private keys in PKCS #8 format * * @memberOf GostKeys * @type GostKeys.PKCS8Encrypted */ GostKeys.prototype.PKCS8Encrypted = PKCS8Encrypted; /** * A class for password-encrypted private keys in SignalCom container<br><br> * * The container file list: * <ul> * <li>mk.db3 - master key data</li> * <li>masks.db3 - encrypted or decrypted masks</li> * <li>kek.opq - wrapped key encryption key</li> * <li>rand.opq - wrapped random data</li> * </ul> * * @class GostKeys.SignalComKeyContainer * @param {SignalComKeyContainer} container */ function SignalComKeyContainer(container) // <editor-fold defaultstate="collapsed"> { if (container) { var self = this; ['mk.db3', 'masks.db3', 'kek.opq', 'rand.opq'].forEach(function (name) { self[name] = container[name]; }); } } // </editor-fold> extend(Object, SignalComKeyContainer, { /** * Get password-based encryption key * * @memberOf GostKeys.SignalComKeyContainer * @instance * @param {string} keyPassword * @returns {Promise} Promise to return {@link Key} */ getEncryptionKey: function (keyPassword) // <editor-fold defaultstate="collapsed"> { var self = this, wrapping = providers['SC-01'].wrapping, encryption = providers['SC-01'].encryption, derivation = providers['SC-01'].derivation, masks = self['masks.db3'], mk = self['mk.db3'], kek = self['kek.opq']; // Decrypt key return new Promise(call).then(function () { if ((!masks || !mk || !kek)) throw new Error('Not enougth key container files'); // Check for encrypted key if (masks.byteLength > 32) { if (keyPassword) { // Extract password based encryption mask return subtle.importKey('raw', coding.Chars.decode(keyPassword, 'utf8'), derivation, false, ['deriveKey', 'deriveBits']).then(function (integrityKey) { return subtle.deriveKey(expand(derivation, { salt: new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]) }), integrityKey, encryption, false, ['decrypt']); }).then(function (encryptionKey) { var encrypted = new cms.EncryptedDataContentInfo(masks); return encrypted.getEnclosed(encryptionKey); }).then(function (digested) { return digested.verify(); }).then(function (data) { return data.content; }); } else throw new Error('Key password is required'); } else if (keyPassword) throw new Error('Key password is not required'); return masks; }).then(function (decrypedMasks) { // Combine masks masks = decrypedMasks; var mkm = new Uint8Array(mk.byteLength + masks.byteLength); mkm.set(new Uint8Array(mk), 0); mkm.set(new Uint8Array(masks), mk.byteLength); // Import master key return subtle.importKey('raw', mkm.buffer, wrapping, false, ['unwrapKey']); }).then(function (unwrappingKey) { // Unwrap kek return subtle.unwrapKey('raw', kek, unwrappingKey, wrapping, encryption, false, ['wrapKey', 'unwrapKey']); }); }, // </editor-fold> /** * Generate encryption key and container files * * @memberOf GostKeys.SignalComKeyContainer * @instance * @param {string} keyPassword * @returns {Promise} Promise to return {@link Key} */ generateContainer: function (keyPassword) // <editor-fold defaultstate="collapsed"> { var self = this, wrapping = providers['SC-01'].wrapping, encryption = providers['SC-01'].encryption, derivation = providers['SC-01'].derivation, digest = providers['SC-01'].digest, encryptionKey, wrappingKey; return new Promise(call).then(function () { // Generate wrapping key return subtle.generateKey(wrapping, true, ['wrapKey']); }).then(function (key) { wrappingKey = key; // Split masks var len = wrappingKey.buffer.byteLength; self['mk.db3'] = new Uint8Array(new Uint8Array(wrappingKey.buffer, 0, len - 32)).buffer; var masks = new Uint8Array(new Uint8Array(wrappingKey.buffer, len - 32, 32)).buffer; if (keyPassword) { // Encrypt masks var encrypted = new cms.EncryptedDataContentInfo(), digested = new cms.DigestedDataContentInfo(); // Digest data return digested.encloseContent(masks, digest).then(function () { digested = {// Double wrapping - SignalCom mistake contentType: 'digestedData', content: digested.encode() }; return subtle.importKey('raw', coding.Chars.decode(keyPassword, 'utf8'), derivation, false, ['deriveKey', 'deriveBits']); }).then(function (integrityKey) { return subtle.deriveKey(expand(derivation, { salt: new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]) }), integrityKey, encryption, false, ['encrypt']); }).then(function (encryptionKey) { // Encrypt data with password return encrypted.encloseContent(digested, encryptionKey, encryption); }).then(function () { return encrypted.encode(); }); } return masks; }).then(function (masks) { self['masks.db3'] = masks; // Generate encryption key return subtle.generateKey(encryption, false, ['wrapKey', 'unwrapKey']); }).then(function (key) { encryptionKey = key; // Wrap encryption key return subtle.wrapKey('raw', key, wrappingKey, wrapping); }).then(function (data) { self['kek.opq'] = data; // Generate random seed return subtle.generateKey(encryption, false, ['wrapKey', 'unwrapKey']); }).then(function (key) { // Wrap random seed return subtle.wrapKey('raw', key, wrappingKey, wrapping); }).then(function (data) { self['rand.opq'] = data; return encryptionKey; }); } // </editor-fold> }); /** * A class for password-encrypted private keys in SignalCom container * * @memberOf GostKeys * @type GostKeys.SignalComKeyContainer */ GostKeys.prototype.SignalComKeyContainer = SignalComKeyContainer; /** * A class for password-encrypted SignalCom private keys * * @class GostKeys.SignalComPrivateKeyInfo * @extends GostASN1.GostWrappedPrivateKey * @extends GostKeys.SignalComKeyContainer * @param {GostASN1.PrivateKeyInfo} keyInfo * @param {GostKeys.SignalComKeyContainer} container */ function SignalComPrivateKeyInfo(keyInfo, container) // <editor-fold defaultstate="collapsed"> { asn1.GostWrappedPrivateKey.call(this, keyInfo); SignalComKeyContainer.call(this, container); } // </editor-fold> extend(asn1.GostWrappedPrivateKey, SignalComPrivateKeyInfo, { /** * Get the private key info * * @memberOf GostKeys.SignalComPrivateKeyInfo * @param {string} keyPassword The password for decryption * @returns {Promise} Promise to return {@link GostKeys.PKCS8} */ getKey: function (keyPassword) // <editor-fold defaultstate="collapsed"> { return this.getPrivateKey(keyPassword).then(function (privateKey) { return new PKCS8().setPrivateKey(privateKey); }); }, // </editor-fold> /** * Get the private key * * @memberOf GostKeys.SignalComPrivateKeyInfo * @instance * @param {string} keyPassword The password for decryption * @returns {Promise} Promise to return the {@link Key} */ getPrivateKey: function (keyPassword) // <editor-fold defaultstate="collapsed"> { var self = this, wrapping = providers['SC-01'].wrapping, publicKeyData; // Decrypt key return new Promise(call).then(function () { // Get password key return self.getEncryptionKey(keyPassword, true); }).then(function (encryptionKey) { // Unwrap private key return subtle.unwrapKey('raw', self.privateKeyWrapped, encryptionKey, wrapping, self.privateKeyAlgorithm, true, ['sign', 'deriveKey', 'deriveBits']); }).then(function (privateKey) { publicKeyData = self.attributes && self.attributes['id-sc-gostR3410-2001-publicKey']; // Generate key pair if (publicKeyData) return subtle.generateKey(expand(privateKey.algorithm, { ukm: privateKey.buffer }), privateKey.extractable, privateKey.usages); else return { privateKey: privateKey }; }).then(function (keyPair) { // Compare public key if (publicKeyData && !equalBuffers(keyPair.publicKey.buffer, publicKeyData)) throw new Error('Check public key failed'); return keyPair.privateKey; }); }, // </editor-fold> /** * Sets and encrypt the private key info * * @memberOf GostKeys.SignalComPrivateKeyInfo * @instance * @param {(FormatedData|GostKeys.PKCS8)} keyInfo The private key info * @param {string} keyPassword The password for encryption * @returns {Promise} Promise to return self object after set the key */ setKey: function (keyInfo, keyPassword) // <editor-fold defaultstate="collapsed"> { var self = this; return new PKCS8(keyInfo).getPrivateKey().then(function (privateKey) { return self.setPrivateKey(privateKey, keyPassword); }); }, // </editor-fold> /** * Set the private key * * @memberOf GostKeys.SignalComPrivateKeyInfo * @instance * @param {Key} privateKey The private key * @param {string} keyPassword The secret key encryption * @returns {Promise} Promise to return self object after set the key */ setPrivateKey: function (privateKey, keyPassword) // <editor-fold defaultstate="collapsed"> { var self = this, wrapping = providers['SC-01'].wrapping, wrappedData; return new Promise(call).then(function () { // Get or generate encryption key return self.getEncryptionKey(keyPassword)['catch'](function () { return self.generateContainer(keyPassword); }); }).then(function (encryptionKey) { // Encrypt key buffer return subtle.wrapKey('raw', privateKey, encryptionKey, wrapping); }).then(function (data) { wrappedData = data; // Generate public key return subtle.generateKey(expand(privateKey.algorithm, { ukm: privateKey.buffer }), true, ['sign', 'verify']); }).then(function (keyPair) { self.object = { version: 0, privateKeyAlgorithm: privateKey.algorithm, privateKeyWrapped: wrappedData, attributes: { 'id-sc-gostR3410-2001-publicKey': keyPair.publicKey.buffer } }; return self; }); }, // </editor-fold> /** * Change key password * * @memberOf GostKeys.SignalComPrivateKeyInfo * @instance * @param {string} oldKeyPassword Old key password * @param {string} newKeyPassword New key password * @returns {Promise} Promise to return self object after change password */ changePassword: function (oldKeyPassword, newKeyPassword) // <editor-fold defaultstate="collapsed"> { var self = this; return self.getPrivateKey(oldKeyPassword).then(function (privateKey) { return self.setPrivateKey(privateKey, newKeyPassword); }); }, // </editor-fold> /** * Generate private key, certificate and return certification request * * @memberOf GostKeys.SignalComPrivateKeyInfo * @instance * @param {(FormatedData|GostASN1.CertificationRequest)} req The request templates * @param {(Key|CryptoOperationData|string)} keyPassword The secret key or password for decryption * @param {(AlgorithmIdentifier|string)} keyAlgorithm The name of provider or algorithm * @returns {Promise} Promise to return {@link GostCert.Request} */ generate: function (req, keyPassword, keyAlgorithm) // <editor-fold defaultstate="collapsed"> { var self = this, keyInfo; return new Promise(call).then(function () { if (!(req instanceof cert.Request)) req = new cert.Request(req); // Generate request return req.generate(keyAlgorithm); }).then(function (key) { keyInfo = key; return self.setKey(keyInfo, keyPassword); }).then(function () { return req; }); } // </editor-fold> }); defineProperties(SignalComPrivateKeyInfo.prototype, SignalComKeyContainer.prototype); /** * A class for password-encrypted SignalCom private keys * * @memberOf GostKeys * @type GostKeys.SignalComPrivateKeyInfo */ GostKeys.prototype.SignalComPrivateKeyInfo = SignalComPrivateKeyInfo; /** * A class for password-encrypted private keys in CryptoPro container * * The container file list: * <ul> * <li>header - container header @link GostASN1.GostKeyContainer</li> * <li>name - container name @link GostASN1.GostKeyContainerName</li> * <li>primary - private keys data @link GostASN1.GostPrivateKeys</li> * <li>masks - private key masks @link GostASN1.GostPrivateMasks</li> * <li>primary2 - reserve of private keys data @link GostASN1.GostPrivateKeys</li> * <li>masks2 - reserve of private key masks @link GostASN1.GostPrivateMasks</li> * </ul> * * @class GostKeys.CryptoProKeyContainer * @param {Object} container */ function CryptoProKeyContainer(container) // <editor-fold defaultstate="collapsed"> { if (container) { this.header = asn1.GostKeyContainer.decode(container.header); this.name = asn1.GostKeyContainerName.decode(container.name); this.primary = asn1.GostPrivateKeys.decode(container.primary); this.masks = asn1.GostPrivateMasks.decode(container.masks); if (container.primary2 && container.masks2) { this.primary2 = asn1.GostPrivateKeys.decode(container.primary2); this.masks2 = asn1.GostPrivateMasks.decode(container.masks2); } } } // </editor-fold> extend(Object, CryptoProKeyContainer, (function () { // <editor-fold defaultstate="collapsed"> // True if 512 bit function isKeySize512(algorithm) { return algorithm.name.indexOf('-512') >= 0 || algorithm.length === 512; } // Test version 2012 function isVersion2012(algorithm) { return !((algorithm.name.indexOf('-94') >= 0 || algorithm.name.indexOf('-2001') >= 0 || algorithm.version === 1994 || algorithm.version === 2001)); } // Derive password key function derivePasswordKey(algorithm, password, salt) { var hash = isVersion2012(algorithm) ? 'GOST R 34.11-256' : 'GOST R 34.11-94/' + (algorithm.sBox || 'D-A'), derivation = { name: 'CPKDF', hash: hash, salt: salt, iterations: password ? 2000 : 2 }; // Import password return subtle.importKey('raw', passwordData(derivation, password), derivation, false, ['deriveKey', 'deriveBits']).then(function (baseKey) { // Derive key return subtle.deriveKey(derivation, baseKey, 'GOST 28147', false, ['sign', 'verify', 'encrypt', 'decrypt']); }); } // Compute password MAC function computePasswordMAC(algorithm, password, salt) { var mac = expand({ name: 'GOST 28147-MAC' }, algorithm.encParams); // Derive password return derivePasswordKey(algorithm, password, salt).then(function (macKey) { // Mac for 16 zero bytes return subtle.sign(mac, macKey, new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])); }); } // var lastBuffer; // Compute container MAC function computeContainerMAC(algorithm, content) { var mac = expand({ name: 'GOST 28147-MAC' }, algorithm.encParams), keyData = new Uint8Array([// 32 zero bytes 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); return subtle.importKey('raw', keyData, mac, false, ['sign']).then(function (macKey) { // var buffer = new Uint8Array(content.encode()); // console.log(coding.Hex.encode(buffer)); // if (lastBuffer && lastBuffer.length === buffer.length) { // for (var i = 0; i < buffer.length; i++) // if (lastBuffer[i] !== buffer[i]) // console.log('diff at ' + i); // } else // console.log('diff length'); // lastBuffer = buffer; // Mac for content return subtle.sign(mac, macKey, content.encode()); }); } // Compute mask MAC function computeMaskMAC(algorithm, mask, status) { // Import mask as key for MAC var mac = expand({ name: 'GOST 28147-MAC' }, algorithm.encParams), keyData = mask.byteLength === 64 ? new Uint8Array(new Uint8Array(mask, 32, 32)).buffer : mask; return subtle.importKey('raw', keyData, mac, false, ['sign']).then(function (macKey) { // Verify MAC for maskStatus return subtle.sign(mac, macKey, status); }); } // Generate mask function generateMasks(algorithm) { var wrapAlgorithm = expand(algorithm, { mode: 'MASK' }), mask, status = getSeed(12); wrapAlgorithm.name = wrapAlgorithm.name.replace('-DH', ''); return subtle.generateKey(wrapAlgorithm, true, ['wrapKey', 'unwrapKey']).then(function (key) { return subtle.exportKey('raw', key); }).then(function (data) { mask = data; return computeMaskMAC(algorithm, mask, status); }).then(function (hmac) { return new asn1.GostPrivateMasks({ mask: mask, randomStatus: status, hmacRandom: hmac }); }); } // Compute FP function computeFP(privateKey) { // Generate key pair with predefined ukm for check public key return subtle.generateKey(expand(privateKey.algorithm, { ukm: privateKey.buffer }), true, ['sign', 'verify']).then(function (keyPair) { return new Uint8Array(new Uint8Array(keyPair.publicKey.buffer, 0, 8)).buffer; }); } // Unwrap private key function unwrapKey(algorithm, encryptionKey, key, mask, fp) { var encryption = { name: 'GOST 28147-ECB', sBox: algorithm.encParams && algorithm.encParams.sBox }, unwrapAlgorithm = expand(algorithm, { mode: 'MASK' }), privateKey; unwrapAlgorithm.name = unwrapAlgorithm.name.replace('-DH', ''); var wrappedKey; // Encrypt ukm data for private key return subtle.decrypt(encryption, encryptionKey, key).then(function (data) { wrappedKey = data; // Import mask key return subtle.importKey('raw', mask, unwrapAlgorithm, 'false', ['sign', 'unwrapKey']); }).then(function (unwrappingKey) { // Unwrap private key return subtle.unwrapKey('raw', wrappedKey, unwrappingKey, unwrapAlgorithm, algorithm, 'true', ['sign']); }).then(function (key) { privateKey = key; return computeFP(privateKey); }).then(function (computedFP) { // Check public key buffer if (!equalBuffers(computedFP, fp)) throw new Error('Incorrect fp'); return privateKey; }); } // Wrap private key function wrapKey(algorithm, encryptionKey, privateKey, mask) { var encryption = { name: 'GOST 28147-ECB', sBox: algorithm.encParams && algorithm.encParams.sBox }, wrapAlgorithm = expand(algorithm, { mode: 'MASK' }); wrapAlgorithm.name = wrapAlgorithm.name.replace('-DH', ''); // Import mask key return subtle.importKey('raw', mask, wrapAlgorithm, false, ['sign', 'wrapKey']).then(function (wrappingKey) { // Wrap private key return subtle.wrapKey('raw', privateKey, wrappingKey, wrapAlgorithm); }).then(function (wrappedKey) { // Encrypt key return subtle.encrypt(encryption, encryptionKey, wrappedKey); }); } // Decrypt private key function decryptKey(content, primary, masks, keyPassword, secondary) { var algorithm = content.primaryPrivateKeyParameters.privateKeyAlgorithm; return new Promise(call).then(function () { // Check format if (primary.hmacKey) throw new Error('Old key format'); if (masks.randomStatus.byteLength < 12) throw new Error("Invalid random status length"); // Import mask as key for MAC return computeMaskMAC(algorithm, masks.mask, masks.randomStatus); }).then(function (hmac) { if (!equalBuffers(hmac, masks.hmacRandom)) throw new Error("Imita for mask is invalid"); // Derive key return derivePasswordKey(algorithm, keyPassword, new Uint8Array(masks.randomStatus, 0, 12)); }).then(function (encryptionKey) { // Unwrap keys return secondary && primary.secondaryKey ? unwrapKey(content.secondaryPrivateKeyParameters.privateKeyAlgorithm, encryptionKey, primary.secondaryKey, masks.mask, content.secondaryFP) : unwrapKey(algorithm, encryptionKey, primary.primaryKey, masks.mask, content.primaryFP); }); } // Encrypt private key function encryptKey(algorithm, primary, masks, keyPassword, secondary, privateKey) { // Derive key return derivePasswordKey(algorithm, keyPassword, new Uint8Array(masks.randomStatus, 0, 12)).then(function (encryptionKey) { // Wrap keys return wrapKey(algorithm, encryptionKey, privateKey, masks.mask); }).then(function (encryptedKey) { if (!primary) primary = new asn1.GostPrivateKeys(); if (secondary) { primary.secondaryKey = encryptedKey; } else { primary.primaryKey = encryptedKey; } return primary; }); } // </editor-fold> return { /** * Get the private key info * * @memberOf GostKeys.CryptoProKeyContainer * @instance * @param {string} keyPassword The password for decryption * @param {boolean} secondary True if required secondary key * @returns {Promise} Promise to return {@link GostKeys.PKCS8} */ getKey: function (keyPassword, secondary) // <editor-fold defaultstate="collapsed"> { return this.getPrivateKey(keyPassword, secondary).then(function (privateKey) { return new PKCS8().setPrivateKey(privateKey); }); }, // </editor-fold> /** * Get the private key * * @memberOf GostKeys.CryptoProKeyContainer * @instance * @param {string} keyPassword Rhe password for decryption * @param {boolean} secondary True if required secondary key * @returns {Promise} Promise to return the {@link Key} */ getPrivateKey: function (keyPassword, secondary) // <editor-fold defaultstate="collapsed"> { var self = this, content = self.header.keyContainerContent; // Decrypt key return decryptKey(content, self.primary, self.masks, keyPassword, secondary)['catch'](function (e) { if (self.primary2 && self.masks2) return decryptKey(content, self.primary2, self.masks2, keyPassword, secondary); else throw e; }); }, // </editor-fold> /** * Get the certificate from the key container * * @memberOf GostKeys.CryptoProKeyContainer * @instance * @param {boolean} secondary True for set secondary certificate * @returns {Promise} Promise to return {@link GostCert.X509} */ getCertificate: function (secondary) // <editor-fold defaultstate="collapsed"> { var self = this, content = self.header.keyContainerContent; return new Promise(call).then(function () { if (secondary) return new cert.X509(content.secondaryCertificate); else return new cert.X509(content.primaryCertificate); }); }, // </editor-fold> /** * Get the container name * * @memberOf GostKeys.CryptoProKeyContainer * @instance * @returns {string} Container name */ getContainerName: function () // <editor-fold defaultstate="collapsed"> { return this.name.containerName; }, // </editor-fold> /** * Sets and encrypt the private key info * * @memberOf GostKeys.CryptoProKeyContainer * @instance * @param {(FormatedData|GostKeys.PKCS8)} keyInfo The private key info * @param {string} keyPassword The assword for encryption * @param {boolean} secondary True for set secondary key * @param {number} days Validity days. Default 7305 days (20 years) * @returns {Promise} Promise to return self object after set key */ setKey: function (keyInfo, keyPassword, secondary, days) // <editor-fold defaultstate="collapsed"> { var self = this; return new PKCS8(keyInfo).getPrivateKey().then(function (privateKey) { return self.setPrivateKey(privateKey, keyPassword, secondary, days); }); }, // </editor-fold> /** * Set the private key * * @memberOf GostKeys.CryptoProKeyContainer * @instance * @param {Key} privateKey The private key * @param {string} keyPassword The secret key encryption * @param {boolean} secondary True for set secondary key * @param {number} days Validity days. Default 7305 days (20 years) * @returns {Promise} Promise to return self object after set key */ setPrivateKey: function (privateKey, keyPassword, secondary, days) // <editor-fold defaultstate="collapsed"> { var self = this, content, algorithm; return new Promise(call).then(function () { self.header = self.header || new asn1.GostKeyContainer({ keyContainerContent: { containerAlgoritmIdentifier: { algorithm: 'id-CryptoPro-GostPrivateKeys-V2-Full' }, attributes: ['kccaReservePrimary', 'kccaPrimaryKeyAbsent'], extensions: { keyValidity: { notAfter: now(days || options.days) } } } }); content = self.header.keyContainerContent; // Set private key var keyParameters = secondary ? content.secondaryPrivateKeyParameters : content.primaryPrivateKeyParameters; if (!keyParameters) { algorithm = expand(privateKey.algorithm, { sBox: "D-A", encParams: { block: "CFB", sBox: "E-A", keyMeshing: "CP" } }); keyParameters = { attributes: ["pkaExportable", "pkaExchange", "pkaDhAllowed"], privateKeyAlgorithm: algorithm }; if (secondary) { if (!content.primaryPrivateKeyParameters) throw new Error('Primary key must be defined first'); content.secondaryPrivateKeyParameters = keyParameters; } else { content.primaryPrivateKeyParameters = keyParameters; var absent = content.attributes.indexOf('kccaPrimaryKeyAbsent'); if (absent >= 0) content.attributes.splice(absent, 1); } } else algorithm = keyParameters.privateKeyAlgorithm; // Generate masks var promises = []; [0, 1].forEach(function (i) { var name = 'masks' + (i > 0 ? '2' : ''); if (!self[name]) promises.push(generateMasks(algorithm).then(function (masks) { self[name] = masks; })); }); return Promise.all(promises); }).then(function () { // Encrypt key var promises = []; [0, 1].forEach(function (i) { var name = 'primary' + (i > 0 ? '2' : ''), maskname = 'masks' + (i > 0 ? '2' : ''); promises.push(encryptKey(algorithm, self[name], self[maskname], keyPassword, secondary, privateKey).then(function (primary) { self[name] = primary; })); }); return Promise.all(promises); }).then(function () { // Compute FP for a private key return computeFP(privateKey).then(function (FP) { if (secondary) content.secondaryFP = FP; else content.primaryFP = FP; }); }).then(function () { // Compute password MAC var softPassword = content.attributes.indexOf('kccaSoftPassword'); if (keyPassword) { if (softPassword < 0) content.attributes.push('kccaSoftPassword'); return computePasswordMAC(algorithm, keyPassword, content.primaryFP); } else { if (softPassword >= 0) content.attributes.splice(softPassword, 1); } }).then(function (hmac) { if (hmac) content.hmacPassword = hmac; // Calculate container MAC return computeContainerMAC(algorithm, content); }).then(function (hmac) { self.header.hmacKeyContainerContent = hmac; return self; }); }, // </editor-fold> /** * Set the certificate to the key container * * @memberOf GostKeys.CryptoProKeyContainer * @instance * @param {(FormatedData|GostCert.X509)} certificate The certificate * @param {boolean} secondary True for set secondary certificate * @param {number} days Validity days. Default 7305 days (20 years) * @returns {Promise} Promise to return self object after set certificate */ setCertificate: function (certificate, secondary, days) // <editor-fold defaultstate="collapsed"> { var self = this, content, algorithm; return new Promise(call).then(function () { self.header = self.header || new asn1.GostKeyContainer({ keyContainerContent: { containerAlgoritmIdentifier: { algorithm: 'id-CryptoPro-GostPrivateKeys-V2-Full' }, attributes: ['kccaReservePrimary', 'kccaPrimaryKeyAbsent'], extensions: { keyValidity: { notAfter: now(days || options.days) } } } }); content = self.header.keyContainerContent; certificate = new cert.X509(certificate); algorithm = (content.primaryPrivateKeyParameters && content.primaryPrivateKeyParameters.privateKeyAlgorithm) || expand(certificate.subjectPublicKeyInfo.algorithm, { sBox: "D-A", encParams: { block: "CFB", sBox: "E-A", keyMeshing: "CP" } }); return certificate.getPublicKey(); }).then(function (publicKey) { if (secondary) { if (content.secondaryFP && !equalBuffers(content.secondaryFP, new Uint8Array(publicKey.buffer, 0, content.secondaryFP.byteLength))) throw new Error('The public key of the certificate does not match the private key'); content.secondaryCertificate = certificate; } else { if (content.primaryFP && !equalBuffers(content.primaryFP, new Uint8Array(publicKey.buffer, 0, content.primaryFP.byteLength))) throw new Error('The public key of the certificate does not match the private key'); content.primaryCertificate = certificate; } // Calculate container MAC return computeContainerMAC(algorithm, content); }).then(function (hmac) { self.header.hmacKeyContainerContent = hmac; return self; }); }, // </editor-fold> /** * Set the container name * * @memberOf GostKeys.CryptoProKeyContainer * @instance * @param {string} name Container name */ setContainerName: function (name) // <editor-fold defaultstate="collapsed"> { this.name = new asn1.GostKeyContainerName({ containerName: name }); }, // </editor-fold> /** * Verify key container with password * * @memberOf GostKeys.CryptoProKeyContainer * @instance * @param {string} keyPassword the secret key or password for decryption * @returns {Promise} Promise to return self object after verify */ verify: function (keyPassword) // <editor-fold defaultstate="collapsed"> { var self = this, content, algorithm; return new Promise(call).then(function () { content = self.header.keyContainerContent; algorithm = content.primaryPrivateKeyParameters.privateKeyAlgorithm; // Verify container MAC return computeContainerMAC(algorithm, content); }).then(function (hmac) { if (!equalBuffers(hmac, self.header.hmacKeyContainerContent)) throw new Error("Container is not valid."); // Verify key password MAC var needPassword = content.attributes.indexOf('kccaSoftPassword') >= 0; if (!keyPassword && needPassword)