UNPKG

ebclient.js

Version:

Client library for using EnigmaBridge crypto services

1,685 lines (1,482 loc) 185 kB
/*! * EnigmaBridge core * @author Dusan Klinec (ph4r05) * @license MIT. */ /*jshint globalstrict: true*/ /*jshint node: true */ 'use strict'; var sprintf = require('./eb-util-sprintf'); var ebextend = require('./eb-util-extend'); var inherit = require('./eb-util-inherit'); var RetryHandler = require('./eb-util-retry'); var sjcl = require('./built/sjcl/sjcl'); var BigInteger = require('jsbn').BigInteger; var superagent = require('superagent'); var modurl = require('url'); var Promise = require("bluebird"); //var superagentNoCache = require('superagent-no-cache'); /** * Monkey-patching for prototype inheritance. * * @param parentClassOrObject * @param newPrototype * @returns {Function} */ Function.prototype.inheritsFrom = function( parentClassOrObject, newPrototype ){ if ( parentClassOrObject.constructor === Function ) { //Normal Inheritance this.prototype = new parentClassOrObject(); this.prototype.constructor = this; this.prototype.parent = parentClassOrObject.prototype; // Better for calling super methods. Avoids looping. this.superclass = parentClassOrObject.prototype; this.prototype = ebextend(this.prototype, newPrototype); // If we have inheritance chain A->B->C, A = root, A defines method x() // B also defines x = function() { this.parent.x.call(this); }, C does not defines x, // then calling x on C will cause infinite loop because this references to C in B.x() and this.parent is B in B.x() // not A as desired. } else { //Pure Virtual Inheritance this.prototype = parentClassOrObject; this.prototype.constructor = this; this.prototype.parent = parentClassOrObject; this.superclass = parentClassOrObject; } return this; }; /** * Base EB package. * @type {{name: string}} */ var eb = { name: "EB", exception: {}, codec: {}, padding: {}, math: {}, comm: {}, client: {} }; /** @namespace Exceptions. */ eb.exception = { /** @constructor Ciphertext is corrupt. */ corrupt: function (message) { this.toString = function () { return "CORRUPT: " + this.message; }; this.message = message; }, /** @constructor Invalid input. */ invalid: function (message) { this.toString = function () { return "INVALID: " + this.message; }; this.message = message; } }; /** * EB misc wrapper. * @type {{name: string, genNonce: eb.misc.genNonce, genHexNonce: eb.misc.genHexNonce, genAlphaNonce: eb.misc.genAlphaNonce, xor: eb.misc.xor}} */ eb.misc = { name: "misc", MAX_SAFE_INTEGER: Math.pow(2, 53) - 1, MIN_SAFE_INTEGER: -(Math.pow(2, 53) - 1), EPSILON: 2.2204460492503130808472633361816E-16, // Exporting used components to the EB namespace. sprintf: sprintf, extend: ebextend, inherit: inherit, RetryHandler: RetryHandler, sjcl: sjcl, BigInteger: BigInteger, superagent: superagent, url: modurl, Promise: Promise, /** * Generates random nonce of given length in characters from the given alphabet. * * @param {Number} length length of the nonce to generate * @param {String} alphabet alphabet of characters to use * @returns {String} nonce */ genNonce: function(length, alphabet){ var nonce = ""; var alphabetLen = alphabet.length; var i = 0; for(i = 0; i < length; i++){ nonce += alphabet.charAt(((sjcl.random.randomWords(1)[0]) & 0xffff) % alphabetLen); } return nonce; }, /** * Generates nonce of the given length using hexadecimal alphabet [0-9a-f]. * * @param {Number} length length of the nonce to generate in characters * @returns {String} nonce */ genHexNonce: function(length){ return this.genNonce(length, "0123456789abcdef"); }, /** * Generates nonce of the given length using alphanumerical alphabet [0-9a-z]. * * @param {Number} length length of the nonce to generate in characters * @returns {String} nonce */ genAlphaNonce: function (length){ return this.genNonce(length, "0123456789abcdefghijklmnopqrstuvwxyz"); }, /** * Returns a new bitArray of length 128 bits, result of x XOR y. * * @param {bitArray|Array} x * @param {bitArray|Array} y * @returns {bitArray|Array} xor result. */ xor: function(x, y){ return [x[0]^y[0], x[1]^y[1], x[2]^y[2], x[3]^y[3]]; }, /** * Returns a new bitArray of length 256 bits, result of x XOR y. * * @param {bitArray|Array} x * @param {bitArray|Array} y * @returns {bitArray|Array} xor result. */ xor8: function(x, y){ return [x[0]^y[0], x[1]^y[1], x[2]^y[2], x[3]^y[3], x[4]^y[4], x[5]^y[5], x[6]^y[6], x[7]^y[7]]; }, absorb: function(dst, src){ if (src === undefined){ return dst; } for(var key in src) { if (src.hasOwnProperty(key)) { dst[key] = src[key]; } } return dst; }, absorbKey: function(dst, src, key){ if (src !== undefined && key in src){ dst[key] = src[key]; } return dst; }, absorbKeyEx: function(dst, dstKey, src, srcKey){ if (src !== undefined && srcKey in src){ dst[dstKey] = src[srcKey]; } return dst; }, absorbKeyIfNotSet: function(dst, dstKey, src, srcKey){ if (src !== undefined && srcKey in src && src[srcKey] !== undefined && dst !== undefined && (!(dstKey in dst) || (dst[dstKey] === undefined))) { dst[dstKey] = src[srcKey]; } return dst; }, absorbValue: function(dst, value, valueKey, defaultValue){ if (value !== undefined){ dst[valueKey] = value; } else if (defaultValue !== undefined){ dst[valueKey] = defaultValue; } }, /** * Converts argument to the SJCL bitArray. * @param x * if x is a number, it is converted to SJCL bitArray. Warning, 32bit numbers are supported only. * if x is a string, it is considered as hex coded string. * if x is an array it is considered as SJCL bitArray. * @returns {*} */ inputToBits: function(x){ if (typeof x === 'number'){ return sjcl.codec.hex.toBits(sprintf("%02x", x)); } else if (typeof x === 'string') { x = x.trim().replace(/^0x/, ''); if (!(x.match(/^[0-9A-Fa-f]+$/))){ throw new eb.exception.invalid("Invalid hex coded number"); } return sjcl.codec.hex.toBits(x); } else { return x; } }, /** * Converts argument to the hexcoded string. * @param x - * if x is a number, will be converted to a hex string. Warning, 32bit numbers are supported only. * if x is a string, it is considered as hex coded string. * if x is an array it is considered as SJCL bitArray. */ inputToHex: function(x){ if (typeof x === 'number'){ return sprintf("%x", x); } else if (typeof x === 'string') { x = x.trim().replace(/^0x/, ''); if (!(x.match(/^[0-9A-Fa-f]+$/))){ throw new eb.exception.invalid("Invalid hex coded number"); } return x; } else { return sjcl.codec.hex.fromBits(x); } }, /** * Converts argument to the integer. If string is passed, it is considered as hex-coded integer. * @param x * @param noThrow */ inputToHexNum: function(x, noThrow){ if (typeof x === 'number'){ return x; } else if (typeof x === 'string') { x = x.trim().replace(/^0x/, ''); if (!(x.match(/^[0-9A-Fa-f]+$/))){ throw new eb.exception.invalid("Invalid hex coded number"); } return parseInt(x, 16); } else if (noThrow === undefined || !noThrow) { throw new eb.exception.invalid("Invalid argument - not a number or string"); } else { return x; } }, /** * Function generates a zero bit vector of given size. * @param bitLength */ getZeroBits: function(bitLength){ if (bitLength <= 0) { return []; } var bs = [0, 0, 0, 0, 0, 0, 0, 0], i; for(i = 256; i < bitLength; i += 32){ bs.push(0); } return sjcl.bitArray.bitSlice(bs, 0, bitLength); }, /** * Function generates random bit vector of given length. * @param bitLength */ getRandomBits: function(bitLength){ return sjcl.bitArray.clamp(sjcl.random.randomWords(Math.ceil(bitLength/32)), bitLength); }, /** * Converts given number to the bitArray representation. * * @param num * @param bitSize */ numberToBits: function(num, bitSize){ if (bitSize > 32){ throw new eb.exception.invalid("num can be maximally 32bit wide"); } if (bitSize == 32){ return [num]; } return sjcl.bitArray.bitSlice([num], 32 - bitSize, 32); }, /** * Replaces part in the given buffer with the provided replacement * * @param {bitArray|Array} buffer * @param {Number} offsetStartBit * @param {Number} offsetEndBit * @param {bitArray|Array} replacement */ replacePart: function(buffer, offsetStartBit, offsetEndBit, replacement){ var w = sjcl.bitArray; var ba = w.concat(w.bitSlice(buffer, 0, offsetStartBit), replacement); // before + transform ba = w.concat(ba, w.bitSlice(buffer, offsetEndBit)); // after return ba; }, /** * Function transforms given slice of the array by the function provided and replaces * original portion with the result of function call. * * @param {bitArray|Array} buffer * @param {Number} offsetStartBit * @param {Number} offsetEndBit * @param {Function} fction */ transformPart: function(buffer, offsetStartBit, offsetEndBit, fction){ var w = sjcl.bitArray; var slice = w.bitSlice(buffer, offsetStartBit, offsetEndBit); var ba = w.concat(w.bitSlice(buffer, 0, offsetStartBit), fction(slice)); // before + transform ba = w.concat(ba, w.bitSlice(buffer, offsetEndBit)); // after return ba; }, /** * Serializes 64bit number to a bitArray. * @param {Number} num * @returns {bitArray|Array} */ serialize64bit: function(num){ return [Math.floor(num/0x100000000), (num|0)]; }, /** * Deserializes 64bit number from bitArray * @param {bitArray} arr * @param {number} [offset=0] Bit offset. */ deserialize64bit: function(arr, offset){ offset = offset || 0; var w = sjcl.bitArray; var hi = w.extract32(arr, offset); var lo = w.extract32(arr, offset+32); return (hi*0x100000000 + (lo) + (lo < 0 ? 0x100000000 : 0)); }, /** * Left zero padding to the even number of hexcoded digits. * @param x * @returns {*} */ padHexToEven: function(x){ x = x.trim().replace(/[\s]+/g, '').replace(/^0x/, ''); return ((x.length & 1) == 1) ? ('0'+x) : x; }, /** * Left zero padding for hex string to the given size. * @param x * @param size * @returns {*} */ padHexToSize: function(x, size){ x = x.trim().replace(/[\s]+/g, '').replace(/^0x/, ''); return (x.length<size) ? (('0'.repeat(size-x.length))+x) : x; }, /** * Pads number x to full block size. * Useful when computing total size after padding added. * If x is multiple of bs, another block is added (pkcs7 works in this way). * * @param x number of units * @param bs block size - same units as x */ padToBlockSize: function(x, bs){ return x + (bs - (x % bs)); }, /** * Returns the byte length of an utf8 string. * @param str * @returns {*} */ strByteLength: function(str) { var s = str.length; for (var i=str.length - 1; i >= 0; i--) { var code = str.charCodeAt(i); if (code > 0x7f && code <= 0x7ff) { s++; } else if (code > 0x7ff && code <= 0xffff) { s+=2; } if (code >= 0xDC00 && code <= 0xDFFF) { i--; //trail surrogate } } return s; }, /** * Returns true if src is defined and src.key is defined. * @param src * @param key * @returns {boolean} */ isDefined: function(src, key){ return src !== undefined && key in src && src[key] !== undefined; }, /** * Generates checksum value from the input. * @param x hexcoded string or bitArray. If you want to checksum arbitrary string, hash it first. * @param size */ genChecksumValue: function(x, size){ var inputBits = eb.misc.inputToBits(x); // As we are reducing information from x to base32*size bits, we are performing // two hash rounds to make sure the dependency is non-trivial. var toHash = sjcl.codec.hex.fromBits(inputBits) + ',' + size + ',' + sjcl.bitArray.bitLength(inputBits); var inputHashBits = sjcl.hash.sha256.hash(toHash); var inputHashBits2 = sjcl.hash.sha256.hash(sjcl.codec.hex.fromBits(inputHashBits) + toHash); var hashOut = [], i; for(i=0; i<256/32; i++){ hashOut[i] = inputHashBits[i] ^ inputHashBits2[i]; } // Base 32, size first characters var base32string = sjcl.codec.base32.fromBits(hashOut); return base32string.substring(0, size); }, /** * Generates checksum value from the input. * @param x an arbitraty string * @param size */ genChecksumValueFromString: function(x, size){ return eb.misc.genChecksumValue(sjcl.hash.sha256.hash(x), size); }, /** * Asserts the condition. * @param condition * @param message */ assert: function(condition, message) { if (!condition) { message = message || "Assertion failed"; if (typeof Error !== "undefined") { throw new Error(message); } throw message; // Fallback } }, /** * Parses url to the components. * https://nodejs.org/docs/latest/api/url.html * https://www.npmjs.com/package/url * * @param {String} url * @param {Boolean} [parseQuery=false] * @returns {{scheme: String, protocol: String, hostname: String, port: Integer}} */ parseUrl: function(url, parseQuery) { parseQuery = parseQuery || false; var p = eb.misc.url.parse(url, parseQuery); if (typeof p.protocol !== 'undefined'){ var proto = p.protocol; p.scheme = proto.slice(-1) === ':' ? proto.substring(0, proto.length - 1) : proto; } return p; }, /** * Generates communication keys from the input. * Used to generate 2x 256bit comm keys from lower entropy key. * @param {bitArray|String} input * @returns {{enc, mac}} */ regenerateCommKeys: function(input){ var w = sjcl.bitArray; var baInput = eb.misc.inputToBits(input); var baEnc = sjcl.hash.sha256.hash(w.concat(baInput, [0x01])); var baMac = sjcl.hash.sha256.hash(w.concat(baInput, [0x02])); return {enc:baEnc, mac:baMac}; }, /** * If val is undefined, def is returned, val otherwise. * @param val * @param def * @returns {*} */ def: function(val, def){ return typeof val === 'undefined' ? def : val; } }; /** * Fault tolerant utf8 codec for user entries. * When converting from hexcoded string to raw data, data may contain both UTF8 characters and hex-coded characters. * Parsing result finds utf8 characters in the hexbytes. If byte sequence does not form valid utf8 character, it is * parsed as ordinary hex sequence. * * When converting from raw data to hexdata, utf8 characters are allowed. Moreover it supports individual byte coding * \x[A-Fa-f0-9]{2} and backslash escaping \\. Single individual backslash is ignored. * @type {{}} */ eb.codec.utf8 = { toHex: function(x, options) { var i, ln = x.length; var out = ""; for (i = 0; i < ln; i++) { var cChar = x.charAt(i); var remChars = (ln - i - 1); if (cChar === '\\') { // Byte coding \xFF ? if (remChars >= 3) { var hCode = x.substring(i, i + 4); var hRegex = /\\x([a-fA-F0-9]{2})/g; var match = hRegex.exec(hCode); if (match) { out += match[1]; i += 3; continue; } } // Escaping \\ ? if (remChars >= 1) { var nChar = x.substring(i + 1, i + 2); if (nChar === '\\') { out += Number('\\'.charCodeAt(0)).toString(16); i += 1; continue; } } // Invalid escaping, ignore this backslash. continue; } // Get UTF8 hex representation. var cc = unescape(encodeURIComponent(cChar)); var jj, llen; for (jj = 0, llen = cc.length; jj < llen; jj++) { var chNum = (Number(cc.charCodeAt(jj))).toString(16); if ((chNum.length & 1) == 1) { chNum = "0" + chNum; } out += chNum; } } return out; }, /** * Converts hexcoded string to raw data. * @param x * @param options * @returns {string} */ fromHex: function(x, options) { var parsed = eb.codec.utf8.fromHexParse(x, options); var str=""; var cur, i, len; for(i=0, len=parsed.parsed.length; i<len; i++){ cur=parsed.parsed[i]; str += cur.utf8 ? cur.rep : cur.enc; } return str; }, /** * Parses hex coded string, can accept utf8 characters. * @param x * @param options, * - if acceptUtf8==false, UTF8 characters are not recognized, each character has 1 byte encoding. Default = true, * thus UTF8 characters are recognized and parsed. * - if acceptOnlyUtf8==true, non-UTF8 characters are skipped, otherwise they are parsed as hexcoded. * * @returns {{nonUtf8Chars: number, parsed: Array}} */ fromHexParse: function(x, options) { var defaults = { 'acceptUtf8': true, 'acceptOnlyUtf8': false }; options = ebextend(defaults, options || {}); var h = sjcl.codec.hex; var acceptUtf8 = options && options.acceptUtf8; var acceptOnlyUtf8 = options && options.acceptOnlyUtf8; // Process only even lengths. var ln = x.length; if ((ln & 1) == 1) { ln-=1; } var nonUtf8Chars = 0; var i, cByte, cBits, cNum; var out = []; // UTF8 encoding table //7 U+0000 U+007F 1 0xxxxxxx //11 U+0080 U+07FF 2 110xxxxx 10xxxxxx //16 U+0800 U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx //21 U+10000 U+1FFFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx //26 U+200000 U+3FFFFFF 5 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx //31 U+4000000 U+7FFFFFFF 6 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx for(i = 0; i < ln; i += 2){ cByte = (x[i] + x[i+1]).toUpperCase(); cBits = h.toBits(cByte); cNum = sjcl.bitArray.extract(cBits, 0, 8); // 1byte char representation. ASCII. if (!acceptUtf8 || (cNum & 0x80) === 0){ var tmpChar = String.fromCharCode(cNum); if (tmpChar === "\\"){ tmpChar = "\\\\"; } out.push({ 'b':1, 'utf8':true, 'hex':cByte, 'enc':String.fromCharCode(cNum), 'rep':cNum < 32 || cNum >= 127 ? "\\x" + cByte : tmpChar}); continue; } // Look for utf8 character. var remBytes = (ln-i-2)/2; var valid = false; var j = 0; for(j=2; j<=6; j++){ // Create first UTF8 byte mask signature, j = number of bytes character occupies. var signature = (Math.pow(2, j)-1)<<1; var byteLow = cNum >> (8-j-1); if (signature !== byteLow){ continue; } // Signature matched, check if there is enough number of bytes in the buffer if (remBytes < (j-1)){ break; } // Start building \uxxxx representation. var utfOut = h.toBits(sprintf("0000%x", cNum & ((1<<(8-j-1))-1) ) ); var utfOutLen = sjcl.bitArray.bitLength(utfOut); if (utfOutLen > (8-j-1)){ utfOut = sjcl.bitArray.bitSlice(utfOut, utfOutLen-(8-j-1)); } // Check if each next byte has 10xxxxxx format. var k = 0; var byteValid = true; for(k=0; k<j-1; k++){ var nByte = eb.codec.utf8.getByte(x, i+2+2*k); if ((nByte >>> 6) != 2){ byteValid = false; break; } var cBitArray = h.toBits(sprintf("0000%x", nByte & ((1<<6)-1) ) ); var cBitLen = sjcl.bitArray.bitLength(cBitArray); if (cBitLen >= 7){ cBitArray = sjcl.bitArray.bitSlice(cBitArray, cBitLen-6); } utfOut = sjcl.bitArray.concat(utfOut, cBitArray); } // Successing were not in the 10xxxxxx format. if(!byteValid){ break; } // utfOut needs to be left padded with zeros to be correctly interpreted. utfOutLen = sjcl.bitArray.bitLength(utfOut); if ((utfOutLen & 7) !== 0){ var toPadLen = 8-(utfOutLen & 7); utfOut = sjcl.bitArray.concat(sjcl.bitArray.bitSlice([0, 0, 0, 0], 0, toPadLen), utfOut); } valid=true; out.push({ 'b':j, 'utf8':true, 'hex':cByte + x.substring(i+2, i+2+(j-1)*2), 'enc':"\\u" + h.fromBits(utfOut), 'rep':String.fromCharCode(parseInt(h.fromBits(utfOut), 16)) }); i+=2*(j-1); break; } if (valid || acceptOnlyUtf8){ continue; } out.push({ 'b':1, 'utf8':false, 'hex':cByte, 'enc':"\\x" + cByte, 'rep':"\\x" + cByte}); nonUtf8Chars+=1; } return {'nonUtf8Chars':nonUtf8Chars, 'parsed':out}; }, getByte: function (str, offset){ var cByte = str[offset] + str[offset+1]; var cBits = sjcl.codec.hex.toBits(cByte); return sjcl.bitArray.extract(cBits, 0, 8); } }; /** * EB padding schemes wrapper. * @type {{name: string}} */ eb.padding = { name: "padding" }; /** * Padding - identity function. * @type {{name: string, pad: eb.padding.empty.pad, unpad: eb.padding.empty.unpad}} */ eb.padding.empty = { name: "empty", pad: function(a, blocklen){ return a; }, unpad: function(a, blocklen){ return a; } }; /** * PKCS7 padding. * @type {{name: string, pad: eb.padding.pkcs7.pad, unpad: eb.padding.pkcs7.unpad}} */ eb.padding.pkcs7 = { name: "pkcs7", pad: function(a, blocklen){ blocklen = blocklen || 16; if (!blocklen || (blocklen & (blocklen - 1))){ throw new sjcl.exception.corrupt("blocklength has to be power of 2"); } if (blocklen != 16){ throw new sjcl.exception.corrupt("blocklength different than 16 is not implemented yet"); // TODO: implement multiple block sizes. } var bl = sjcl.bitArray.bitLength(a); var padLen = (16 - ((bl >> 3) & 15)); var padFill = padLen * 0x1010101; return sjcl.bitArray.concat(a, [padFill, padFill, padFill, padFill]).slice(0, ((bl >> 3) + padLen) >> 2); }, unpad: function(a, blocklen){ blocklen = blocklen || 16; if (!blocklen || (blocklen & (blocklen - 1))){ throw new sjcl.exception.corrupt("blocklength has to be power of 2"); } if (blocklen != 16){ throw new sjcl.exception.corrupt("blocklength different than 16 is not implemented yet"); // TODO: implement multiple block sizes. } var w = sjcl.bitArray; var bl = w.bitLength(a); if (bl & 127 || !a.length) { throw new sjcl.exception.corrupt("input must be a positive multiple of the block size"); } var bi = a[((bl>>3)>>2) - 1] & 255; if (bi === 0 || bi > 16) { throw new sjcl.exception.corrupt("pkcs#5 padding corrupt"); } var bo = bi * 0x1010101; if (!w.equal(w.bitSlice([bo, bo, bo, bo], 0, bi << 3), w.bitSlice(a, (a.length << 5) - (bi << 3), a.length << 5))) { throw new sjcl.exception.corrupt("pkcs#5 padding corrupt"); } return w.bitSlice(a, 0, (a.length << 5) - (bi << 3)); } }; /** * PKCS 1.5 padding for RSA operation. * * EB = 00 || BT || PS || 00 || D * .. EB = encryption block * .. 00 prefix so EB is not bigger than modulus. * .. BT = 1B block type {00, 01} for private key operation, {02} for public key operation. * .. PS = padding string. Has length k - 3 - len(D). * if BT == 0, then padding consists of 0x0, but we need to know size of data in order to remove padding unambiguously. * if BT == 1, then padding consists of 0xFF. * if BT == 2, then padding consists of randomly generated bytes, does not contain 0x00 byte. * .. D = data * [https://tools.ietf.org/html/rfc2313 PKCS#1 1.5] * * @type {{name: string, unpad: eb.padding.pkcs15.unpad, const: *, char: *}} */ eb.padding.pkcs15 = { name: "pkcs1.5", pad: function(a, blockLength, bt){ var w = sjcl.bitArray; var bl = w.bitLength(a); var blb = bl / 8; if (bt === undefined){ bt = 0; } if (bl & 7 || !a.length) { throw new sjcl.exception.corrupt("input type has to have be byte padded, bl="+bl); } if (bt !== 0 && bt !== 1 && bt !== 2){ throw new sjcl.exception.corrupt("invalid BT size"); } if (blb+3 > blockLength){ throw new sjcl.exception.corrupt("data to pad is too big for the padding block length"); } var psLen = blockLength - 3 - blb; var ps = [], i, tmp=0; for (i=0; i<psLen; i++) { var curByte = 0; if (bt == 1){ curByte = 0xff; } else if (bt == 2){ do { curByte = (sjcl.random.randomWords(1)[0]) & 0xff; }while(curByte === 0); } tmp = tmp << 8 | curByte; if ((i&3) === 3) { ps.push(tmp); tmp = 0; } } if (i&3) { ps.push(sjcl.bitArray.partial(8*(i&3), tmp)); } var baBuff = [sjcl.bitArray.partial(8, 0)]; baBuff = w.concat(baBuff, [sjcl.bitArray.partial(8, bt)]); baBuff = w.concat(baBuff, ps); baBuff = w.concat(baBuff, [sjcl.bitArray.partial(8, 0)]); return w.concat(baBuff, a); }, unpad: function(a){ var w = sjcl.bitArray; var bl = w.bitLength(a); var blb = bl / 8; if (bl & 7 || blb < 3 || !a.length) { throw new sjcl.exception.corrupt("data size block is invalid"); } // Check the first byte. var bOffset = 0; var prefixByte = w.extract(a, bOffset, 8); if (prefixByte !== 0x0){ throw new sjcl.exception.corrupt("data size block is invalid"); } bOffset += 8; var bt = w.extract(a, bOffset, 8); // BT can be only from set {0,1,2}. if (bt !== 0 && bt !== 1 && bt !== 2){ throw new sjcl.exception.corrupt("Padding data error, BT is outside of the definition set"); } // Find D in the padded data. Strategy depends on the BT. var dataPosStart = -1, i= 0, cur=0; if (bt === 0){ // Scan for first non-null character. for(i = 2; i < blb; i++){ cur = w.extract(a, 8*i, 8); if (cur !== 0){ dataPosStart = i; break; } } } else if (bt == 1){ // Find 0x0, report failure in 0xff var ffCorrect = true; for(i = 2; i < blb; i++){ cur = w.extract(a, 8*i, 8); if (cur !== 0 && cur !== 0xff) { ffCorrect = false; } if (cur === 0){ dataPosStart = i+1; break; } } if (!ffCorrect){ throw new sjcl.exception.corrupt("Trail of 0xFF in padding contains also unexpected characters"); } } else { // bt == 2, find 0x0. for(i = 2; i < blb; i++){ cur = w.extract(a, 8*i, 8); if (cur === 0){ dataPosStart = i+1; break; } } } // If data position is out of scope, return nothing. if (dataPosStart < 0 || dataPosStart > blb){ throw new sjcl.exception.corrupt("Padding could not be parsed, dataStart=" + dataPosStart + ", len="+blb); } // Check size of the output buffer. note: dataLen = blb - dataPosStart; return w.bitSlice(a, dataPosStart*8); } }; /** * Extracts 32bit number from the bitArray. * Original extract does not work with blength = 32 as 1<<32 == 1, it returns 0 always. * * @param a * @param bstart * @returns {*} */ sjcl.bitArray.extract32 = function(a, bstart){ var x, sh = Math.floor((-bstart-32) & 31); if ((bstart + 32 - 1 ^ bstart) & -32) { x = (a[bstart/32|0] << (32 - sh)) ^ (a[bstart/32+1|0] >>> sh); } else { x = a[bstart/32|0] >>> sh; } return x; }; /** * CBC-MAC with given cipher & padding. * @param Cipher * @param bs * @param padding */ sjcl.misc.hmac_cbc = function (Cipher, bs, padding) { this._cipher = Cipher; this._bs = bs = bs || 16; this._padding = padding = padding || eb.padding.empty; }; /** * HMAC with the specified hash function. Also called encrypt since it's a prf. * @param {bitArray|String} data The data to mac. */ sjcl.misc.hmac_cbc.prototype.encrypt = sjcl.misc.hmac_cbc.prototype.mac = function (data) { var i, w = sjcl.bitArray, bl = w.bitLength(data), bp = 0, xor = eb.misc.xor; var bsb = this._bs << 3; data = this._padding.pad(data, this._bs); var c = eb.misc.getZeroBits(this._bs*8); for (i = 0; bp + bsb <= bl; i += 4, bp += bsb) { c = this._cipher.encrypt(xor(c, data.slice(i, i + 4))); } return c; }; /** * CBC encryption mode implementation. * @type {{name: string, encrypt: sjcl.mode.cbc.encrypt, decrypt: sjcl.mode.cbc.decrypt}} */ sjcl.mode.cbc = { name: "cbc", encrypt: function (a, b, c, d, noPad) { if (d && d.length) { throw new sjcl.exception.invalid("cbc can't authenticate data"); } if (sjcl.bitArray.bitLength(c) !== 128) { throw new sjcl.exception.invalid("cbc iv must be 128 bits"); } var i, w = sjcl.bitArray, bl = w.bitLength(b), bp = 0, output = [], xor = eb.misc.xor; if (noPad && (bl & 127) !== 0){ throw new sjcl.exception.invalid("when padding is disabled, plaintext has to be a positive multiple of a block size"); } if ((bl & 7) !== 0) { throw new sjcl.exception.invalid("pkcs#5 padding only works for multiples of a byte"); } for (i = 0; bp + 128 <= bl; i += 4, bp += 128) { c = a.encrypt(xor(c, b.slice(i, i + 4))); output.splice(i, 0, c[0], c[1], c[2], c[3]); } if (!noPad){ bl = (16 - ((bl >> 3) & 15)) * 0x1010101; c = a.encrypt(xor(c, w.concat(b, [bl, bl, bl, bl]).slice(i, i + 4))); output.splice(i, 0, c[0], c[1], c[2], c[3]); } return output; }, decrypt: function (a, b, c, d, noPad) { if (d && d.length) { throw new sjcl.exception.invalid("cbc can't authenticate data"); } if (sjcl.bitArray.bitLength(c) !== 128) { throw new sjcl.exception.invalid("cbc iv must be 128 bits"); } if ((sjcl.bitArray.bitLength(b) & 127) || !b.length) { throw new sjcl.exception.corrupt("cbc ciphertext must be a positive multiple of the block size"); } var i, w = sjcl.bitArray, bi, bo, output = [], xor = eb.misc.xor; d = d || []; for (i = 0; i < b.length; i += 4) { bi = b.slice(i, i + 4); bo = xor(c, a.decrypt(bi)); output.splice(i, 0, bo[0], bo[1], bo[2], bo[3]); c = bi; } if (!noPad) { bi = output[i - 1] & 255; if (bi === 0 || bi > 16) { throw new sjcl.exception.corrupt("pkcs#5 padding corrupt"); //TODO: padding oracle? } bo = bi * 0x1010101; if (!w.equal(w.bitSlice([bo, bo, bo, bo], 0, bi << 3), w.bitSlice(output, (output.length << 5) - (bi << 3), output.length << 5))) { throw new sjcl.exception.corrupt("pkcs#5 padding corrupt"); //TODO: padding oracle? } return w.bitSlice(output, 0, (output.length << 5) - (bi << 3)); } else { return output; } } }; /** * Request builder. * @type {{}} */ eb.comm = { name: "comm", REQ_METHOD_GET: "GET", REQ_METHOD_POST: "POST", /** * General status constants. */ status: { ERROR_CLASS_SECURITY: 0x2000, ERROR_CLASS_WRONGDATA: 0x8000, SW_INVALID_TLV_FORMAT: 0x8000 | 0x04c, SW_WRONG_PADDING: 0x8000 | 0x03d, SW_STAT_INVALID_APIKEY: 0x8000 | 0x068, SW_AUTHMETHOD_NOT_ALLOWED: 0x8000 | 0x0b9, ERROR_CLASS_SECURITY_USER: 0xa000, SW_HOTP_KEY_WRONG_LENGTH: 0xa000 | 0x056, SW_HOTP_TOO_MANY_FAILED_TRIES: 0xa000 | 0x066, SW_HOTP_WRONG_CODE: 0xa000 | 0x0b0, SW_HOTP_COUNTER_OVERFLOW: 0xa000 | 0x0b3, SW_AUTHMETHOD_UNKNOWN: 0xa000 | 0x0ba, SW_AUTH_TOO_MANY_FAILED_TRIES: 0xa000 | 0x0b1, SW_AUTH_MISMATCH_USER_ID: 0xa000 | 0x0b6, SW_PASSWD_TOO_MANY_FAILED_TRIES:0xa000 | 0x063, SW_PASSWD_INVALID_LENGTH: 0xa000 | 0x064, SW_WRONG_PASSWD: 0xa000 | 0x065, SW_STAT_OK: 0x9000, ERROR_CLASS_ERR_CHECK_ERRORS_6f:0x6f00, PDATA_FAIL_CONNECTION: 0x1, PDATA_FAIL_RESPONSE_PARSING: 0x3, PDATA_FAIL_RESPONSE_FAILED: 0x2, }, /** * Converts mangled nonce value to the original one in ProcessData response. * ProcessData response has nonce return value response_nonce[i] = request_nonce[i] + 0x1 * @param nonce * @returns {*} */ demangleNonce: function(nonce){ var w = sjcl.bitArray; var bl = w.bitLength(nonce); if ((bl&7) !== 0){ throw new sjcl.exception.invalid("nonce has to be aligned to bytes"); } var i, bp = 0, output = [], c; for (i = 0; bp + 32 <= bl; i += 1, bp += 32) { c = nonce.slice(i, i + 1)[0] - 0x01010101; output.splice(i, 0, c); } if (bp+32 == bl){ return output; } var rbl = bl - (bp-32); var sub = 0x01010101 & (((1<<rbl)-1)<<(32-rbl)); c = (nonce.slice(i, i + 1)[0] - sub) >>> rbl; output.splice(i, 0, c); return w.clamp(output, bl); }, /** * Constructs UO handle. * * @param {String} apiKey * @param {Number|String} [uoId] * @param {Number|String} [uoType] * @returns {String} handle */ getUoHandle: function(apiKey, uoId, uoType){ // TEST_API 00 00000013 00 00a00004 if (uoId === undefined){ return apiKey; } if (uoType === undefined){ uoType = 0; } return sprintf("%s00%08x00%08x", apiKey, eb.misc.inputToHexNum(uoId), eb.misc.inputToHexNum(uoType)); }, /** * Parses handle string to its components. * @param {String} handle * @returns {{apiKey:String, uoId:String, uoType:String}} parsed handle */ parseHandle: function(handle) { var handleRe = /^([a-zA-Z0-9_-]+?)00([0-9a-fA-F]{8})(?:00([0-9a-fA-F]{8}))?$/; var res = handle.match(handleRe); if (res === null){ throw new eb.exception.invalid("Invalid handle: " + handle); } return { apiKey: res[1], uoId: res[2], uoType: res[3] }; }, /** * Base class constructor. */ base: function(){ }, /** * User object constructor */ uo: function(uoid, encKey, macKey){ var av = eb.misc.absorbValue; av(this, uoid, 'uoid'); av(this, encKey, 'encKey'); av(this, macKey, 'macKey'); } }; eb.comm.base.prototype = { /** * If set to true, request body building steps are logged. * @input */ debuggingLog: false, /** * Aux logging function * @input */ logger: null, _log: function(x) { if (!this.debuggingLog){ return; } if (console && console.log){ console.log(x); } if (this.logger){ this.logger(x); } } }; eb.comm.uo.prototype = { /** * User object ID. */ uoid: undefined, /** * Encryption communication key. */ encKey: undefined, /** * MAC communication key. */ macKey: undefined, }; /** * Raw EB request builder. * * Data format before encryption: * buff = 0x1f | <UOID-4B> | <freshness-nonce-8B> | userdata * * Encryption * AES-256/CBC/PKCS7, IV = 0x00000000000000000000000000000000 * * MAC * AES-256-CBC-MAC. * * encBlock = enc(buff) * result = encBlock || mac(encBlock) * * output = Packet0| _PLAINAES_ | <plain-data-length-4B> | <plaindata> | hexcode(result) * * @param nonce * @param aesKey * @param macKey * @param userObjectId * @param reqType */ eb.comm.processDataRequestBodyBuilder = function(nonce, aesKey, macKey, userObjectId, reqType){ this.userObjectId = eb.misc.def(userObjectId, -1); this.nonce = nonce || ""; this.aesKey = aesKey || ""; this.macKey = macKey || ""; this.reqType = reqType || "PLAINAES"; }; eb.comm.processDataRequestBodyBuilder.prototype = { /** * User object ID, integer type. * @input */ userObjectId : -1, /** * AES communication encryption key, hexcoded string. * @input */ aesKey: "", /** * AES MAC communication key, hexcoded string. * @input */ macKey: "", /** * Freshness nonce / IV, hexcoded string. * @input */ nonce: "", /** * Request type. PLAINAES by default. * @input */ reqType: "", /** * If set to true, request body building steps are logged. * @input */ debuggingLog: false, /** * Aux logging function * @input */ logger: null, genNonce: function(){ this.nonce = eb.misc.genHexNonce(16); return this.nonce; }, /** * Builds EB request. * * @param plainData - bitArray of the plaintext data. * @param requestData - bitArray with userdata to perform operation on (will be encrypted, MAC protected) * @returns request body string. */ build: function(plainData, requestData){ this.nonce = this.nonce || eb.misc.genHexNonce(16); var h = sjcl.codec.hex; var ba = sjcl.bitArray; var pad = eb.padding.pkcs7; // Plain data is empty for now. var baPlain = plainData; var plainDataLength = ba.bitLength(baPlain)/8; // Input data flag var baBuff = [ba.partial(8, 0x1f)]; // User Object ID baBuff = ba.concat(baBuff, [eb.misc.inputToHexNum(this.userObjectId)]); // Freshness nonce baBuff = ba.concat(baBuff, eb.misc.inputToBits(this.nonce)); // User data baBuff = ba.concat(baBuff, requestData); // Add padding. baBuff = pad.pad(baBuff); this._log('ProcessData function input PDIN (0x1f | <UOID-4B> | <nonce-8B> | data | pkcs#7padding) : ' + h.fromBits(baBuff) + "; len: " + ba.bitLength(baBuff)); var aesKeyBits = eb.misc.inputToBits(this.aesKey); var macKeyBits = eb.misc.inputToBits(this.macKey); var aes = new sjcl.cipher.aes(aesKeyBits); var aesMac = new sjcl.cipher.aes(macKeyBits); var hmac = new sjcl.misc.hmac_cbc(aesMac, 16, eb.padding.empty); // IV is null, nonce in the first block is kind of IV. var IV = [0, 0, 0, 0]; var encryptedData = sjcl.mode.cbc.encrypt(aes, baBuff, IV, [], true); this._log('Encrypted ProcessData input ENC(PDIN): ' + h.fromBits(encryptedData) + ", len=" + ba.bitLength(encryptedData)); // include plain data in the MAC if non-empty. var hmacData = hmac.mac(encryptedData); this._log('MAC(ENC(PDIN)): ' + h.fromBits(hmacData)); // Build the request block. var requestBase = sprintf('Packet0_%s_%04X%s%s%s', this.reqType, plainDataLength, h.fromBits(plainData), h.fromBits(encryptedData), h.fromBits(hmacData) ); this._log('ProcessData request body: ' + requestBase); return requestBase; }, _log: function(x) { if (!this.debuggingLog){ return; } if (console && console.log){ console.log(x); } if (this.logger){ this.logger(x); } } }; /** * Base class for parsed raw EB response. */ eb.comm.response = function(){ }; eb.comm.response.prototype = { /** * Parsed status code. 0x9000 = OK. * @output */ statusCode: 0, /** * Parsed status detail. * @output */ statusDetail: "", /** * Function name extracted from the request. */ function: "", /** * Raw result of the call. * Usually processed by child classes. */ result: "", /** * Returns true if after parsing, code is OK. * @returns {boolean} */ isCodeOk: function(){ return this.statusCode == eb.comm.status.SW_STAT_OK; }, toString: function(){ return sprintf("Response{statusCode=0x%4X, statusDetail=[%s], userObjectId: 0x%08X, function: [%s], result: [%s]}", this.statusCode, this.statusDetail, eb.misc.inputToHexNum(this.userObjectID, true), this.function, JSON.stringify(this.result) ); } }; /** * Process data response. * Parsed from processData EB response. * @extends eb.comm.response */ eb.comm.processDataResponse = function(){ }; eb.comm.processDataResponse.inheritsFrom(eb.comm.response, { /** * Plain data parsed from the response. * Nor MACed neither encrypted. * @output */ plainData: "", /** * Protected data parsed from the response. * Protected by MAC, encrypted in transit. * @output */ protectedData: "", /** * USerObjectID parsed from the response. * Ingeter, 4B. */ userObjectID: 0, /** * Nonce parsed from the RAW response. */ nonce: "", /** * MAC value parsed from the message. * If macOk is true, it is same as computed MAC. */ mac: "", /** * Computed MAC value for the message. */ computedMac: "", /** * Returns true if MAC verification is OK. */ isMacOk: function(){ var ba = sjcl.bitArray; return this.mac && this.computedMac && ba.bitLength(this.mac) == 16 * 8 && ba.bitLength(this.computedMac) == 16 * 8 && ba.equal(this.mac, this.computedMac); }, toString: function(){ return sprintf("ProcessDataResponse{statusCode=0x%4X, statusDetail=[%s], userObjectId: 0x%08X, function: [%s], " + "nonce: [%s], protectedData: [%s], plainData: [%s], mac: [%s], computedMac: [%s], macOK: %d", this.statusCode, this.statusDetail, eb.misc.inputToHexNum(this.userObjectID, true), this.function, sjcl.codec.hex.fromBits(this.nonce), sjcl.codec.hex.fromBits(this.protectedData), sjcl.codec.hex.fromBits(this.plainData), sjcl.codec.hex.fromBits(this.mac), sjcl.codec.hex.fromBits(this.computedMac), this.isMacOk() ); } }); /** * EB Import public key. */ eb.comm.pubKey = function(){}; eb.comm.pubKey.prototype = { id: undefined, type: undefined, certificate: undefined, key: undefined, toString: function(){ return sprintf("pubKey{id=0x%04X, type=[%s], certificate:[%s], key:[%s]", this.id, this.type, this.certificate ? sjcl.codec.hex.fromBits(this.certificate) : "null", this.key ? sjcl.codec.hex.fromBits(this.key) : "null" ); } }; /** * pubKey response. * @extends eb.comm.response */ eb.comm.pubKeyResponse = function(x){ eb.misc.absorb(this, x); }; eb.comm.pubKeyResponse.inheritsFrom(eb.comm.response, { /** * Plain data parsed from the response. * Nor MACed neither encrypted. * @output */ keys: [], toString: function(){ var stringKeys = [], index, len, c; for (index = 0, len =this.keys.length; index < len; ++index) { c = this.keys[index]; if (c){ stringKeys.push(c.toString()); } } return sprintf("pubKeyResponse{statusCode=0x%4X, statusDetail=[%s], function: [%s], keys:[%s]", this.statusCode, this.statusDetail, this.function, stringKeys.join(", ") ); } }); /** * Raw EB Response parser. */ eb.comm.responseParser = function(){ }; eb.comm.responseParser.prototype = { /** * Parsed response * @output */ response: null, /** * If set to true, response body parsing steps are logged to the console. * @input */ debuggingLog: false, /** * Aux logging function * @input */ logger: null, /** * User can define response parsing function here, called in the main parse body. * It is optional function callback, must return response. * @input */ _responseParsingFunction: undefined, parsingFunction: function(x){ this._responseParsingFunction = x; return this; }, /** * Returns true if after parsing, code is OK. * @returns {boolean} */ success: function(){ return this.response.isCodeOk(); }, /** * Parses common JSON headers from the response, e.g., status, to the provided message. * @param resp * @param data * @returns {eb.comm.response} */ parseCommonHeaders: function(resp, data){ if (!data || !data.status || !data.function){ throw new sjcl.exception.invalid("response data invalid"); } // Build new response message. resp.statusCode = parseInt(data.status, 16); resp.statusDetail = data.statusdetail || ""; resp.function = data.function; resp.result = data.result; return resp; }, /** * Parse EB response * * @param data - json response * @param resp - response object to put data to. * @param options * @returns request unwrapped response. */ parse: function(data, resp, options){ resp = resp || this.response; resp = resp || new eb.comm.response(); this.response = resp; this.parseCommonHeaders(resp, data); // Build new response message. if (!this.