ebclient.js
Version:
Client library for using EnigmaBridge crypto services
1,685 lines (1,482 loc) • 185 kB
JavaScript
/*!
* 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.