secrets.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. // secrets.js - by Alexander Stetsyuk - released under MIT License
  2. (function(exports, global){
  3. var defaults = {
  4. bits: 8, // default number of bits
  5. radix: 16, // work with HEX by default
  6. minBits: 3,
  7. maxBits: 20, // this permits 1,048,575 shares, though going this high is NOT recommended in JS!
  8. bytesPerChar: 2,
  9. maxBytesPerChar: 6, // Math.pow(256,7) > Math.pow(2,53)
  10. // Primitive polynomials (in decimal form) for Galois Fields GF(2^n), for 2 <= n <= 30
  11. // The index of each term in the array corresponds to the n for that polynomial
  12. // i.e. to get the polynomial for n=16, use primitivePolynomials[16]
  13. primitivePolynomials: [null,null,1,3,3,5,3,3,29,17,9,5,83,27,43,3,45,9,39,39,9,5,3,33,27,9,71,39,9,5,83],
  14. // warning for insecure PRNG
  15. warning: 'WARNING:\nA secure random number generator was not found.\nUsing Math.random(), which is NOT cryptographically strong!'
  16. };
  17. // Protected settings object
  18. var config = {};
  19. /** @expose **/
  20. exports.getConfig = function(){
  21. return {
  22. 'bits': config.bits,
  23. 'unsafePRNG': config.unsafePRNG
  24. };
  25. };
  26. function init(bits){
  27. if(bits && (typeof bits !== 'number' || bits%1 !== 0 || bits<defaults.minBits || bits>defaults.maxBits)){
  28. throw new Error('Number of bits must be an integer between ' + defaults.minBits + ' and ' + defaults.maxBits + ', inclusive.')
  29. }
  30. config.radix = defaults.radix;
  31. config.bits = bits || defaults.bits;
  32. config.size = Math.pow(2, config.bits);
  33. config.max = config.size - 1;
  34. // Construct the exp and log tables for multiplication.
  35. var logs = [], exps = [], x = 1, primitive = defaults.primitivePolynomials[config.bits];
  36. for(var i=0; i<config.size; i++){
  37. exps[i] = x;
  38. logs[x] = i;
  39. x <<= 1;
  40. if(x >= config.size){
  41. x ^= primitive;
  42. x &= config.max;
  43. }
  44. }
  45. config.logs = logs;
  46. config.exps = exps;
  47. };
  48. /** @expose **/
  49. exports.init = init;
  50. function isInited(){
  51. if(!config.bits || !config.size || !config.max || !config.logs || !config.exps || config.logs.length !== config.size || config.exps.length !== config.size){
  52. return false;
  53. }
  54. return true;
  55. };
  56. // Returns a pseudo-random number generator of the form function(bits){}
  57. // which should output a random string of 1's and 0's of length `bits`
  58. function getRNG(){
  59. var randomBits, crypto;
  60. function construct(bits, arr, radix, size){
  61. var str = '',
  62. i = 0,
  63. len = arr.length-1;
  64. while( i<len || (str.length < bits) ){
  65. str += padLeft(parseInt(arr[i], radix).toString(2), size);
  66. i++;
  67. }
  68. str = str.substr(-bits);
  69. if( (str.match(/0/g)||[]).length === str.length){ // all zeros?
  70. return null;
  71. }else{
  72. return str;
  73. }
  74. }
  75. // node.js crypto.randomBytes()
  76. if(typeof require === 'function' && (crypto=require('crypto')) && (randomBits=crypto['randomBytes'])){
  77. return function(bits){
  78. var bytes = Math.ceil(bits/8),
  79. str = null;
  80. while( str === null ){
  81. str = construct(bits, randomBits(bytes).toString('hex'), 16, 4);
  82. }
  83. return str;
  84. }
  85. }
  86. // browsers with window.crypto.getRandomValues()
  87. if(global['crypto'] && typeof global['crypto']['getRandomValues'] === 'function' && typeof global['Uint32Array'] === 'function'){
  88. crypto = global['crypto'];
  89. return function(bits){
  90. var elems = Math.ceil(bits/32),
  91. str = null,
  92. arr = new global['Uint32Array'](elems);
  93. while( str === null ){
  94. crypto['getRandomValues'](arr);
  95. str = construct(bits, arr, 10, 32);
  96. }
  97. return str;
  98. }
  99. }
  100. // A totally insecure RNG!!! (except in Safari)
  101. // Will produce a warning every time it is called.
  102. config.unsafePRNG = true;
  103. warn();
  104. var bitsPerNum = 32;
  105. var max = Math.pow(2,bitsPerNum)-1;
  106. return function(bits){
  107. var elems = Math.ceil(bits/bitsPerNum);
  108. var arr = [], str=null;
  109. while(str===null){
  110. for(var i=0; i<elems; i++){
  111. arr[i] = Math.floor(Math.random() * max + 1);
  112. }
  113. str = construct(bits, arr, 10, bitsPerNum);
  114. }
  115. return str;
  116. };
  117. };
  118. // Warn about using insecure rng.
  119. // Called when Math.random() is being used.
  120. function warn(){
  121. global['console']['warn'](defaults.warning);
  122. if(typeof global['alert'] === 'function' && config.alert){
  123. global['alert'](defaults.warning);
  124. }
  125. }
  126. // Set the PRNG to use. If no RNG function is supplied, pick a default using getRNG()
  127. /** @expose **/
  128. exports.setRNG = function(rng, alert){
  129. if(!isInited()){
  130. this.init();
  131. }
  132. config.unsafePRNG=false;
  133. rng = rng || getRNG();
  134. // test the RNG (5 times)
  135. if(typeof rng !== 'function' || typeof rng(config.bits) !== 'string' || !parseInt(rng(config.bits),2) || rng(config.bits).length > config.bits || rng(config.bits).length < config.bits){
  136. throw new Error("Random number generator is invalid. Supply an RNG of the form function(bits){} that returns a string containing 'bits' number of random 1's and 0's.")
  137. }else{
  138. config.rng = rng;
  139. }
  140. config.alert = !!alert;
  141. return !!config.unsafePRNG;
  142. };
  143. function isSetRNG(){
  144. return typeof config.rng === 'function';
  145. };
  146. // Generates a random bits-length number string using the PRNG
  147. /** @expose **/
  148. exports.random = function(bits){
  149. if(!isSetRNG()){
  150. this.setRNG();
  151. }
  152. if(typeof bits !== 'number' || bits%1 !== 0 || bits < 2){
  153. throw new Error('Number of bits must be an integer greater than 1.')
  154. }
  155. if(config.unsafePRNG){
  156. warn();
  157. }
  158. return bin2hex(config.rng(bits));
  159. }
  160. // Divides a `secret` number String str expressed in radix `inputRadix` (optional, default 16)
  161. // into `numShares` shares, each expressed in radix `outputRadix` (optional, default to `inputRadix`),
  162. // requiring `threshold` number of shares to reconstruct the secret.
  163. // Optionally, zero-pads the secret to a length that is a multiple of padLength before sharing.
  164. /** @expose **/
  165. exports.share = function(secret, numShares, threshold, padLength, withoutPrefix){
  166. if(!isInited()){
  167. this.init();
  168. }
  169. if(!isSetRNG()){
  170. this.setRNG();
  171. }
  172. padLength = padLength || 0;
  173. if(typeof secret !== 'string'){
  174. throw new Error('Secret must be a string.');
  175. }
  176. if(typeof numShares !== 'number' || numShares%1 !== 0 || numShares < 2){
  177. throw new Error('Number of shares must be an integer between 2 and 2^bits-1 (' + config.max + '), inclusive.')
  178. }
  179. if(numShares > config.max){
  180. var neededBits = Math.ceil(Math.log(numShares +1)/Math.LN2);
  181. throw new Error('Number of shares must be an integer between 2 and 2^bits-1 (' + config.max + '), inclusive. To create ' + numShares + ' shares, use at least ' + neededBits + ' bits.')
  182. }
  183. if(typeof threshold !== 'number' || threshold%1 !== 0 || threshold < 2){
  184. throw new Error('Threshold number of shares must be an integer between 2 and 2^bits-1 (' + config.max + '), inclusive.');
  185. }
  186. if(threshold > config.max){
  187. var neededBits = Math.ceil(Math.log(threshold +1)/Math.LN2);
  188. throw new Error('Threshold number of shares must be an integer between 2 and 2^bits-1 (' + config.max + '), inclusive. To use a threshold of ' + threshold + ', use at least ' + neededBits + ' bits.');
  189. }
  190. if(typeof padLength !== 'number' || padLength%1 !== 0 ){
  191. throw new Error('Zero-pad length must be an integer greater than 1.');
  192. }
  193. if(config.unsafePRNG){
  194. warn();
  195. }
  196. secret = '1' + hex2bin(secret); // append a 1 so that we can preserve the correct number of leading zeros in our secret
  197. secret = split(secret, padLength);
  198. var x = new Array(numShares), y = new Array(numShares);
  199. for(var i=0, len = secret.length; i<len; i++){
  200. var subShares = this._getShares(secret[i], numShares, threshold);
  201. for(var j=0; j<numShares; j++){
  202. x[j] = x[j] || subShares[j].x.toString(config.radix);
  203. y[j] = padLeft(subShares[j].y.toString(2)) + (y[j] ? y[j] : '');
  204. }
  205. }
  206. var padding = config.max.toString(config.radix).length;
  207. if(withoutPrefix){
  208. for(var i=0; i<numShares; i++){
  209. x[i] = bin2hex(y[i]);
  210. }
  211. }else{
  212. for(var i=0; i<numShares; i++){
  213. x[i] = config.bits.toString(36).toUpperCase() + padLeft(x[i],padding) + bin2hex(y[i]);
  214. }
  215. }
  216. return x;
  217. };
  218. // This is the basic polynomial generation and evaluation function
  219. // for a `config.bits`-length secret (NOT an arbitrary length)
  220. // Note: no error-checking at this stage! If `secrets` is NOT
  221. // a NUMBER less than 2^bits-1, the output will be incorrect!
  222. /** @expose **/
  223. exports._getShares = function(secret, numShares, threshold){
  224. var shares = [];
  225. var coeffs = [secret];
  226. for(var i=1; i<threshold; i++){
  227. coeffs[i] = parseInt(config.rng(config.bits),2);
  228. }
  229. for(var i=1, len = numShares+1; i<len; i++){
  230. shares[i-1] = {
  231. x: i,
  232. y: horner(i, coeffs)
  233. }
  234. }
  235. return shares;
  236. };
  237. // Polynomial evaluation at `x` using Horner's Method
  238. // TODO: this can possibly be sped up using other methods
  239. // NOTE: fx=fx * x + coeff[i] -> exp(log(fx) + log(x)) + coeff[i],
  240. // so if fx===0, just set fx to coeff[i] because
  241. // using the exp/log form will result in incorrect value
  242. function horner(x, coeffs){
  243. var logx = config.logs[x];
  244. var fx = 0;
  245. for(var i=coeffs.length-1; i>=0; i--){
  246. if(fx === 0){
  247. fx = coeffs[i];
  248. continue;
  249. }
  250. fx = config.exps[ (logx + config.logs[fx]) % config.max ] ^ coeffs[i];
  251. }
  252. return fx;
  253. };
  254. function inArray(arr,val){
  255. for(var i = 0,len=arr.length; i < len; i++) {
  256. if(arr[i] === val){
  257. return true;
  258. }
  259. }
  260. return false;
  261. };
  262. function processShare(share){
  263. var bits = parseInt(share[0], 36);
  264. if(bits && (typeof bits !== 'number' || bits%1 !== 0 || bits<defaults.minBits || bits>defaults.maxBits)){
  265. throw new Error('Number of bits must be an integer between ' + defaults.minBits + ' and ' + defaults.maxBits + ', inclusive.')
  266. }
  267. var max = Math.pow(2, bits) - 1;
  268. var idLength = max.toString(config.radix).length;
  269. var id = parseInt(share.substr(1, idLength), config.radix);
  270. if(typeof id !== 'number' || id%1 !== 0 || id<1 || id>max){
  271. throw new Error('Share id must be an integer between 1 and ' + config.max + ', inclusive.');
  272. }
  273. share = share.substr(idLength + 1);
  274. if(!share.length){
  275. throw new Error('Invalid share: zero-length share.')
  276. }
  277. return {
  278. 'bits': bits,
  279. 'id': id,
  280. 'value': share
  281. };
  282. };
  283. /** @expose **/
  284. exports._processShare = processShare;
  285. // Protected method that evaluates the Lagrange interpolation
  286. // polynomial at x=`at` for individual config.bits-length
  287. // segments of each share in the `shares` Array.
  288. // Each share is expressed in base `inputRadix`. The output
  289. // is expressed in base `outputRadix'
  290. function combine(at, shares){
  291. var setBits, share, x = [], y = [], result = '', idx;
  292. for(var i=0, len = shares.length; i<len; i++){
  293. share = processShare(shares[i]);
  294. if(typeof setBits === 'undefined'){
  295. setBits = share['bits'];
  296. }else if(share['bits'] !== setBits){
  297. throw new Error('Mismatched shares: Different bit settings.')
  298. }
  299. if(config.bits !== setBits){
  300. init(setBits);
  301. }
  302. if(inArray(x, share['id'])){ // repeated x value?
  303. continue;
  304. }
  305. idx = x.push(share['id']) - 1;
  306. share = split(hex2bin(share['value']));
  307. for(var j=0, len2 = share.length; j<len2; j++){
  308. y[j] = y[j] || [];
  309. y[j][idx] = share[j];
  310. }
  311. }
  312. for(var i=0, len=y.length; i<len; i++){
  313. result = padLeft(lagrange(at, x, y[i]).toString(2)) + result;
  314. }
  315. if(at===0){// reconstructing the secret
  316. var idx = result.indexOf('1'); //find the first 1
  317. return bin2hex(result.slice(idx+1));
  318. }else{// generating a new share
  319. return bin2hex(result);
  320. }
  321. };
  322. // Combine `shares` Array into the original secret
  323. /** @expose **/
  324. exports.combine = function(shares){
  325. return combine(0, shares);
  326. };
  327. // Generate a new share with id `id` (a number between 1 and 2^bits-1)
  328. // `id` can be a Number or a String in the default radix (16)
  329. /** @expose **/
  330. exports.newShare = function(id, shares){
  331. if(typeof id === 'string'){
  332. id = parseInt(id, config.radix);
  333. }
  334. var share = processShare(shares[0]);
  335. var max = Math.pow(2, share['bits']) - 1;
  336. if(typeof id !== 'number' || id%1 !== 0 || id<1 || id>max){
  337. throw new Error('Share id must be an integer between 1 and ' + config.max + ', inclusive.');
  338. }
  339. var padding = max.toString(config.radix).length;
  340. return config.bits.toString(36).toUpperCase() + padLeft(id.toString(config.radix), padding) + combine(id, shares);
  341. };
  342. // Evaluate the Lagrange interpolation polynomial at x = `at`
  343. // using x and y Arrays that are of the same length, with
  344. // corresponding elements constituting points on the polynomial.
  345. function lagrange(at, x, y){
  346. var sum = 0,
  347. product,
  348. i, j;
  349. for(var i=0, len = x.length; i<len; i++){
  350. if(!y[i]){
  351. continue;
  352. }
  353. product = config.logs[y[i]];
  354. for(var j=0; j<len; j++){
  355. if(i === j){ continue; }
  356. if(at === x[j]){ // happens when computing a share that is in the list of shares used to compute it
  357. product = -1; // fix for a zero product term, after which the sum should be sum^0 = sum, not sum^1
  358. break;
  359. }
  360. product = ( product + config.logs[at ^ x[j]] - config.logs[x[i] ^ x[j]] + config.max/* to make sure it's not negative */ ) % config.max;
  361. }
  362. sum = product === -1 ? sum : sum ^ config.exps[product]; // though exps[-1]= undefined and undefined ^ anything = anything in chrome, this behavior may not hold everywhere, so do the check
  363. }
  364. return sum;
  365. };
  366. /** @expose **/
  367. exports._lagrange = lagrange;
  368. // Splits a number string `bits`-length segments, after first
  369. // optionally zero-padding it to a length that is a multiple of `padLength.
  370. // Returns array of integers (each less than 2^bits-1), with each element
  371. // representing a `bits`-length segment of the input string from right to left,
  372. // i.e. parts[0] represents the right-most `bits`-length segment of the input string.
  373. function split(str, padLength){
  374. if(padLength){
  375. str = padLeft(str, padLength)
  376. }
  377. var parts = [];
  378. for(var i=str.length; i>config.bits; i-=config.bits){
  379. parts.push(parseInt(str.slice(i-config.bits, i), 2));
  380. }
  381. parts.push(parseInt(str.slice(0, i), 2));
  382. return parts;
  383. };
  384. // Pads a string `str` with zeros on the left so that its length is a multiple of `bits`
  385. function padLeft(str, bits){
  386. bits = bits || config.bits
  387. var missing = str.length % bits;
  388. return (missing ? new Array(bits - missing + 1).join('0') : '') + str;
  389. };
  390. function hex2bin(str){
  391. var bin = '', num;
  392. for(var i=str.length - 1; i>=0; i--){
  393. num = parseInt(str[i], 16)
  394. if(isNaN(num)){
  395. throw new Error('Invalid hex character.')
  396. }
  397. bin = padLeft(num.toString(2), 4) + bin;
  398. }
  399. return bin;
  400. }
  401. function bin2hex(str){
  402. var hex = '', num;
  403. str = padLeft(str, 4);
  404. for(var i=str.length; i>=4; i-=4){
  405. num = parseInt(str.slice(i-4, i), 2);
  406. if(isNaN(num)){
  407. throw new Error('Invalid binary character.')
  408. }
  409. hex = num.toString(16) + hex;
  410. }
  411. return hex;
  412. }
  413. // Converts a given UTF16 character string to the HEX representation.
  414. // Each character of the input string is represented by
  415. // `bytesPerChar` bytes in the output string.
  416. /** @expose **/
  417. exports.str2hex = function(str, bytesPerChar){
  418. if(typeof str !== 'string'){
  419. throw new Error('Input must be a character string.');
  420. }
  421. bytesPerChar = bytesPerChar || defaults.bytesPerChar;
  422. if(typeof bytesPerChar !== 'number' || bytesPerChar%1 !== 0 || bytesPerChar<1 || bytesPerChar > defaults.maxBytesPerChar){
  423. throw new Error('Bytes per character must be an integer between 1 and ' + defaults.maxBytesPerChar + ', inclusive.')
  424. }
  425. var hexChars = 2*bytesPerChar;
  426. var max = Math.pow(16, hexChars) - 1;
  427. var out = '', num;
  428. for(var i=0, len=str.length; i<len; i++){
  429. num = str[i].charCodeAt();
  430. if(isNaN(num)){
  431. throw new Error('Invalid character: ' + str[i]);
  432. }else if(num > max){
  433. var neededBytes = Math.ceil(Math.log(num+1)/Math.log(256));
  434. throw new Error('Invalid character code (' + num +'). Maximum allowable is 256^bytes-1 (' + max + '). To convert this character, use at least ' + neededBytes + ' bytes.')
  435. }else{
  436. out = padLeft(num.toString(16), hexChars) + out;
  437. }
  438. }
  439. return out;
  440. };
  441. // Converts a given HEX number string to a UTF16 character string.
  442. /** @expose **/
  443. exports.hex2str = function(str, bytesPerChar){
  444. if(typeof str !== 'string'){
  445. throw new Error('Input must be a hexadecimal string.');
  446. }
  447. bytesPerChar = bytesPerChar || defaults.bytesPerChar;
  448. if(typeof bytesPerChar !== 'number' || bytesPerChar%1 !== 0 || bytesPerChar<1 || bytesPerChar > defaults.maxBytesPerChar){
  449. throw new Error('Bytes per character must be an integer between 1 and ' + defaults.maxBytesPerChar + ', inclusive.')
  450. }
  451. var hexChars = 2*bytesPerChar;
  452. var out = '';
  453. str = padLeft(str, hexChars);
  454. for(var i=0, len = str.length; i<len; i+=hexChars){
  455. out = String.fromCharCode(parseInt(str.slice(i, i+hexChars),16)) + out;
  456. }
  457. return out;
  458. };
  459. // by default, initialize without an RNG
  460. exports.init();
  461. })(typeof module !== 'undefined' && module['exports'] ? module['exports'] : (window['secrets'] = {}), typeof global !== 'undefined' ? global : window );