UNPKG

ruscryptojs

Version:

Simplified library for Russian GOST crypto providers.

526 lines (499 loc) 16.5 kB
/** * JaCarta GOST simplified library * @author Aleksandr.ru * @link http://aleksandr.ru */ import DN from '../DN'; import errors from './errors'; import { convertDN } from '../helpers'; function JaCarta() { var client, tokenId; /** * Инициализация и проверка наличия требуемых возможностей * @returns {Promise<Object>} версия, информация о токене */ this.init = function(){ return new Promise((resolve, reject) => { if (typeof(JCWebClient) !== 'undefined') { resolve(); } else { getScript('https://localhost:24738/JCWebClient.js', resolve, reject); } }).then(() => { return new Promise(resolve => { try { if(typeof JCWebClient != 'undefined') { // Установлен клиент одной из версий JC-WebClient (2.x или новой) либо оба клиента if(typeof JCWebClient.id == 'undefined') { // Установлен клиент новой версии, работающей через локальный веб-сервер client = JCWebClient(); client.initialize(); } else { // Установлен клиент версии 2.x, работающей через NPAPI и ActiveX throw new Error('JaCarta WebClient 2.x не поддерживается'); } } else { //Не установлен клиент ни старой, ни новой версии JC-WebClient throw new Error('Не установлен клиент ни старой, ни новой версии JC-WebClient'); } if(!client.checkWebBrowserVersion()) { throw new Error('Браузер не поддерживается'); } var aTokens = client.getAllTokens(); if(aTokens && aTokens.length == 1) { // OK 1 токен tokenId = aTokens.shift(); } else if(aTokens && aTokens.length > 1) { throw new Error('Подключено ' + aTokens.length + ' токена(ов)'); } else { throw new Error('Нет подключенных токенов'); } var version = client.getPluginVersion(); var tokenInfo = client.getTokenInfo(tokenId); resolve({ version, serial: tokenInfo[0], // серийный номер электронного ключа. flags: tokenInfo[1], // флаги электронного ключа. label: tokenInfo[2], // метка электронного ключа. type: tokenInfo[3] }); } catch(e) { var err = getError(); throw new Error(e.message || err); } }); }); }; /** * Авторизация на токене с пин-кодом юзера * @param {string} userPin если нет, то предлгает ввести пин через UI плагина * @returns {Promise} */ this.bind = function(userPin) { return new Promise(resolve => { try { var state = client.getLoggedInState().shift(); if(state === 1) { resolve(); } else if(!userPin) { if(client.bindTokenUI(tokenId)) { resolve(); } else { throw new Error('Пользователь отменил ввод PIN-кода'); } } else { client.bindTokenAsync(tokenId, userPin, function(a){ if(a && a[0] == 'Error') { var code = a[1]; var err = getError(code); throw new Error(err); } else { resolve(); } }); } } catch(e) { var err = getError(); throw new Error(err || e.message); } }); }; /** * Отменить предъявление PIN-кода. Необходимо вызывать при завершении сеанса работы * @returns {Promise} */ this.unbind = function() { return new Promise(resolve => { try { var state = client.getLoggedInState().shift(); if(state === 1) { client.unbindToken(); } resolve(); } catch(e) { var err = getError(); throw new Error(err || e.message); } }); }; /** * Очистка токена (удаление всех контейнеров) * @returns {Promise<number>} количество удаленных контейнеров */ this.clean = function(){ return new Promise(resolve => { try { var aContainers = client.getCertificateList(tokenId); for(var i in aContainers) { var containerId = aContainers[i].shift(); client.deleteContainerOrCertificate(containerId); } resolve(i); } catch(e) { var err = getError(); throw new Error(err || e.message); } }); }; /** * Создать запрос на сертификат * @param {DN} dn * @param {string} description описание контейнера * @param {array} ekuOids массив OID Extended Key Usage, по-умолчанию Аутентификация клиента '1.3.6.1.5.5.7.3.2' + Защищенная электронная почта '1.3.6.1.5.5.7.3.4' * @param {string} ecParams параметры эллиптической кривой ключевой пары. Может принимать значения A, B, C, XA, XB. * @returns {Promise<Object>} объект с полями { csr: 'base64 запрос на сертификат', containerId } * @see DN */ this.generateCSR = function(dn, description, ekuOids, ecParams){ if(!ekuOids || !ekuOids.length) { ekuOids = [ '1.3.6.1.5.5.7.3.2', // Аутентификация клиента '1.3.6.1.5.5.7.3.4' // Защищенная электронная почта ]; } if(!ecParams) ecParams = 'XA'; return new Promise(resolve => { try { client.createContainerAsync(ecParams, description, function(a){ if(a && a[0] == 'Error') { var code = a[1]; var err = getError(code); throw new Error(err); } else { var containerId = a; var aDn = []; for(var i in dn) if(dn.hasOwnProperty(i)) { aDn.push(i, dn[i]); } var exts = [ 'certificatePolicies', '1.2.643.100.113.1', 'keyUsage', 'digitalSignature,keyEncipherment,nonRepudiation,dataEncipherment', 'extendedKeyUsage', ekuOids.toString(), '1.2.643.100.111', 'ASN1:FORMAT:UTF8,UTF8:"Криптотокен" (АЛАДДИН Р.Д.)' ]; client.genCSRAsync(containerId, aDn, exts, function(a){ if(a && a[0] == 'Error') { var code = a[1]; var err = getError(code); throw new Error(err); } else { // base64(запрос на сертификат в формате PKCS#10) var csr = btoa(String.fromCharCode.apply(null, new Uint8Array(a))); resolve({ csr: pemSplit(csr), containerId }); } }); } }); } catch(e) { var err = getError(); throw new Error(err || e.message); } }); }; /** * Записать сертификат в контейнер * @param {string} certificate base64(массив байт со значением сертификата в формате DER) * @param {int} идентификатор контейнера куда записывать * @returns {Promise} */ this.writeCertificate = function(certificate, containerId){ return new Promise(resolve => { try { var aCertificate = []; var der = atob(certificate); for(var i=0; i<der.length; i++) { aCertificate[i] = der.charCodeAt(i); } client.writeCertificateAsync(containerId, aCertificate, function(){ resolve(); }); } catch(e) { var err = getError(); throw new Error(err || e.message); } }); }; /** * Получение информации о сертификате. * @param {int} containerId идентификатор контейнера (сертификата) * @returns {Promise<Object>} */ this.certificateInfo = function(containerId){ return new Promise(resolve => { try { var o = client.parseX509CertificateEx(tokenId, containerId); var dn = new DN; for(var i in o.Data.Subject) { var rdn = o.Data.Subject[i].rdn; var val = o.Data.Subject[i].value; dn[rdn] = val; } dn = convertDN(dn); var dnI = new DN; for(var i in o.Data.Issuer) { var rdn = o.Data.Issuer[i].rdn; var val = o.Data.Issuer[i].value; dnI[rdn] = val; } var dt = new Date(); var info = { Name: dn.CN, Issuer: dnI, IssuerName: dnI.CN, Subject: dn, SubjectName: dn.toString(), Version: o.Data.Version, SerialNumber: o.Data['Serial Number'].map(byte2hex).join(''), Thumbprint: o.Signature.map(byte2hex).join(''), ValidFromDate: o.Data.Validity['Not Before'], ValidToDate: o.Data.Validity['Not After'], HasPrivateKey: true, IsValid: dt >= o.Data.Validity['Not Before'] && dt <= o.Data.Validity['Not After'], toString: function(){ return 'Название: ' + this.Name + '\nИздатель: ' + this.IssuerName + '\nСубъект: ' + this.SubjectName + '\nВерсия: ' + this.Version + '\nСерийный №: ' + this.SerialNumber + '\nОтпечаток SHA1: ' + this.Thumbprint + '\nНе действителен до: ' + this.ValidFromDate + '\nНе действителен после: ' + this.ValidToDate + '\nПриватный ключ: ' + (this.HasPrivateKey ? 'Есть' : 'Нет') + '\nВалидный: ' + (this.IsValid ? 'Да' : 'Нет'); } }; resolve(info); } catch(e) { var err = getError(); throw new Error(err || e.message); } }); }; /** * Получение массива доступных сертификатов * @returns {Promise<Array>} [{id, subject}, ...] */ this.listCertificates = function(){ return new Promise(resolve => { try { client.getCertificateListAsync(tokenId, function(a){ if(a && a[0] == 'Error') { var code = a[1]; var err = getError(code); throw new Error(err); } else { var certs = []; for(var i=0; i<a.length; i++) { var id = a[i][0]; var contName = a[i][1]; try { var o = client.parseX509CertificateEx(tokenId, contId); var name = formatCertificateName(o, contName); certs.push({ id, name }); } catch(e) { console.log('Certificate (%s) info error: %s', contId, e.message); } } resolve(certs); } }); } catch(e) { var err = getError(); throw new Error(err || e.message); } }); }; /** * Получить сертификат из контейнера * @param {int} containerId * @returns {Promise<string>} base64(массив байт со значением сертификата в формате DER) */ this.readCertificate = function(containerId){ return new Promise(resolve => { try { var state = client.getLoggedInState().shift(); if(state === 0) { var a = client.readCertificateEx(tokenId, containerId); } else { var a = client.readCertificate(containerId); } if(a && a.length) { // base64(массив байт со значением сертификата в формате DER) var cert = btoa(String.fromCharCode.apply(null, new Uint8Array(a))); resolve(pemSplit(cert)); } else { throw new Error('Нет сертификата в контейнере'); } } catch(e) { var err = getError(); throw new Error(err || e.message); } }); }; /** * Подписать данные. Выдает подпись в формате PKCS#7, опционально закодированную в Base64 * @param {string} data данные (и подпись) закодированы в base64 * @param {int} containerId идентификатор контейнера (сертификата) * @returns {Promise<string>} строка-подпись в формате PKCS#7, закодированная в Base64. */ this.signData = function(dataBase64, containerId){ var attachedSignature = false; return new Promise(resolve => { try { client.signBase64EncodedDataAsync(containerId, dataBase64, attachedSignature, false, function(a){ if(a && a[0] == 'Error') { var code = a[1]; var err = getError(code); throw new Error(err); } else { var sign = pemSplit(a); resolve(sign); } }); } catch(e) { var err = getError(); throw new Error(err || e.message); } }); }; /** * Шифрование данных не поддерживается * @returns {Promise} */ this.encryptData = function() { return new Promise(function () { throw new Error('Encrypt feature is not supported, please upgrade to JaCarta-2 GOST'); }); } /** * Дешифрование данных не поддерживается * @returns {Promise<>} */ this.decryptData = function() { return new Promise(function () { throw new Error('Decrypt feature is not supported, please upgrade to JaCarta-2 GOST'); }); } /** * Получить ошибку по коду * @param {string} mnemo мнемонический код ошибки CKR_* * @returns {string|Boolean} false если нет ошибки (CKR_OK) */ function getError(mnemo) { try { if(!mnemo) { var code = client.getLastError(); mnemo = client.getErrorMessage(code); } if(mnemo == 'CKR_OK') { return false; } return errors[mnemo] || mnemo; } catch(e) { return e.message; } } /** * Получить название сертификата * @param {type} o объект, включающий в себя значения всех полей сертификата. * @param {type} containerName * @returns {string} */ function formatCertificateName(o, containerName) { var dn = new DN; for(var i in o.Data.Subject) { var rdn = o.Data.Subject[i].rdn; var val = o.Data.Subject[i].value; dn[rdn] = val; } dn.toString = function(){ var cn = this['CN'] || this['2.5.4.3']; var snils = this['СНИЛС'] || this['SNILS'] || this['1.2.643.100.3']; var inn = this['ИНН'] || this['INN'] || this['1.2.643.3.131.1.1']; return '' + cn + (inn ? '; ИНН ' + inn : '') + (snils ? '; СНИЛС ' + snils : '') + (containerName ? ' (' + containerName + ')' : ''); }; return dn.toString(); } function byte2hex(byte) { //console.log('byte %d -> %s', byte, byte.toString(16)); return ('0' + byte.toString(16)).slice(-2); } // https://gist.github.com/hendriklammers/5231994 function pemSplit(str) { var re = new RegExp('.{1,64}', 'g'); return str.match(re).join('\n'); } /** * Функция загрузки скрипта. * @param src - адрес расположения скрипта; * @param done - callback-функция, срабатывающая при успешной загрузки скрипта; * @param fail - callback-функция, срабатывающая при неудачной загрузки скрипта. */ function getScript(src, done, fail) { var parent = document.getElementsByTagName('body')[0]; var script = document.createElement('script'); script.type = 'text/javascript'; script.src = src; if (script.readyState) { // IE script.onreadystatechange = function () { if (script.readyState === "loaded" || script.readyState === "complete") { script.onreadystatechange = null; // На некоторых браузерах мы попадаем сюда и в тех случаях когда скрипт не загружен, // поэтому дополнительно проверяем валидность JCWebClient if (typeof (JCWebClient) === 'undefined') { onFail("JCWebClient is invalid"); } else { done(); } } else if (script.readyState !== "loading") { onFail("JCWebClient hasn't been loaded"); } } } else { // Others script.onload = done; script.onerror = function() { onFail("JCWebClient hasn't been loaded"); }; } parent.appendChild(script); function onFail(errorMsg) { parent.removeChild(script); fail(errorMsg); } } } export default JaCarta;