manager.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. /*
  2. * Copyright 2018 Scytl Secure Electronic Voting SA
  3. *
  4. * All rights reserved
  5. *
  6. * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
  7. */
  8. /* jshint node:true */
  9. 'use strict';
  10. var Tools = require('../tools');
  11. var AjaxCollector = require('./collectors/ajax');
  12. var PrivateKeyCollector = require('./collectors/privatekey');
  13. var JSExecutionCollector = require('./collectors/jsexecution');
  14. var NavigatorInfoCollector = require('./collectors/navigatorinfo');
  15. var MathRandomCollector = require('./collectors/mathrandom');
  16. var WebKitRandomCollector = require('./collectors/webkitrandom');
  17. var DeviceOrientationCollector = require('./events/deviceorientation');
  18. var DeviceMotionCollector = require('./events/devicemotion');
  19. var MouseMoveCollector = require('./events/mousemove');
  20. var MouseWheelCollector = require('./events/mousewheel');
  21. var MouseButtonCollector = require('./events/mousebutton');
  22. var TouchStartCollector = require('./events/touchstart');
  23. var TouchMoveCollector = require('./events/touchmove');
  24. var TouchEndCollector = require('./events/touchend');
  25. var KeyCollector = require('./events/key');
  26. var codec = require('scytl-codec');
  27. module.exports = EntropyManager;
  28. /**
  29. * Manager in charge of collecting the amount of entropy required to perform
  30. * secure random operations.
  31. *
  32. * @class EntropyManager
  33. * @private
  34. * @param {Object}
  35. * [options] An object containing optional arguments.
  36. *
  37. * <p>
  38. * This object can be used to override the default values of the entropy
  39. * manager, by making use of the following fields. For further explanation, see
  40. * the README.md file for this module.
  41. * <ul>
  42. * <li>maxEntropyCounter</li>
  43. * <li>hashUpdaterInterval</li>
  44. * <li>windowObject</li>
  45. * <li>cryptoApi</li>
  46. * <li>privateKeyData</li>
  47. * <li>noDefaultCollectors</li>
  48. * </ul>
  49. * <p>
  50. */
  51. function EntropyManager(options) {
  52. ////////////////////////////////// PRIVATE //////////////////////////////////
  53. var that = this;
  54. var tools_;
  55. var maxEntropyCounter_;
  56. var hashUpdaterInterval_;
  57. var windowObject_;
  58. var cryptoApi_;
  59. var privateKeyData_;
  60. var collectors_ = [];
  61. var events_ = [];
  62. var collectorHashUpdater_ = null;
  63. var entropyHashHex_ = '';
  64. var entropyCounter_ = 0;
  65. var isCollecting_ = false;
  66. var fullEntropyReached_ = false;
  67. var fullEntropyCallback_ = null;
  68. /**
  69. * Initializes the entropy manager.
  70. *
  71. * @function init
  72. * @memberof EntropyManager
  73. * @param {Object}
  74. * [options] The optional arguments passed by the constructor to
  75. * override the default values of the entropy manager
  76. */
  77. function init(options) {
  78. tools_ = new Tools();
  79. // Optional arguments.
  80. options = options || {};
  81. // Amount of entropy to accumulate before stopping collectors. Default is
  82. // 256.
  83. maxEntropyCounter_ = options.maxEntropyCounter || 256;
  84. // Interval (in milliseconds) at which entropy data is to be hashed. Default
  85. // is 1 second.
  86. hashUpdaterInterval_ = options.hashUpdaterInterval || 1000;
  87. // "window" object provided to collectors. Default is built-in "window"
  88. // object.
  89. windowObject_ =
  90. options.windowObject || (typeof window !== 'undefined') ? window : null;
  91. // Cryptographic library supplied to WebKit collector (if being used).
  92. // Default is user's built-in cryptographic library.
  93. cryptoApi_ = options.cryptoApi ||
  94. (windowObject_ ? (windowObject_.crypto || windowObject_.msCrypto) :
  95. null);
  96. // Private key collector data. By default, this collector is not used.
  97. privateKeyData_ = options.privateKeyData || null;
  98. // Set up standard set of collectors, unless specified not to.
  99. if (!options.noDefaultCollectors) {
  100. // Set up the default collectors.
  101. setupCollectors();
  102. }
  103. }
  104. /**
  105. * Sets up all of the entropy collectors to be used. If WebKit is available,
  106. * its entropy collector will be used exclusively. Otherwise, a combination
  107. * of other types of collectors will used, including the ajax calls
  108. * collector, JS calls execution collector, navigator information collector,
  109. * math random collector, private key collector, mouse events collectors,
  110. * key events collectors, load event collector and scroll collector.
  111. *
  112. * @function setupCollectors
  113. * @memberof EntropyManager
  114. */
  115. function setupCollectors() {
  116. if (!windowObject_) {
  117. addNonWindowCollectors();
  118. return;
  119. }
  120. if (cryptoApi_ && (cryptoApi_.getRandomValues)) {
  121. that.addCollector(new WebKitRandomCollector(
  122. collectEntropy, cryptoApi_, maxEntropyCounter_));
  123. } else {
  124. addNonWindowCollectors();
  125. // The API detects and delivers accelerometer data 50 times per
  126. // second
  127. if (window.DeviceOrientationEvent) {
  128. // Listen for the device orientation event and handle
  129. // DeviceOrientationEvent object
  130. that.addEvent(
  131. 'deviceorientation',
  132. new DeviceOrientationCollector(collectEntropy).handleEvent);
  133. } else if (window.OrientationEvent) {
  134. // Listen for the MozOrientation event and handle
  135. // OrientationData object
  136. that.addEvent(
  137. 'MozOrientation',
  138. new DeviceOrientationCollector(collectEntropy).handleEvent);
  139. }
  140. if (window.DeviceMotionEvent) {
  141. that.addEvent(
  142. 'devicemotion',
  143. new DeviceMotionCollector(collectEntropy).handleEvent);
  144. }
  145. }
  146. }
  147. /**
  148. * Registers the entropy collectors that do not need a window object to
  149. * work.
  150. *
  151. * @function addNonWindowCollectors
  152. * @memberof EntropyManager
  153. */
  154. function addNonWindowCollectors() {
  155. addCollectors([
  156. AjaxCollector, JSExecutionCollector, NavigatorInfoCollector,
  157. MathRandomCollector
  158. ]);
  159. if (privateKeyData_) {
  160. collectors_.push(new PrivateKeyCollector(
  161. collectEntropy, privateKeyData_.privateKeyPem,
  162. privateKeyData_.pinEntropy));
  163. }
  164. addEvents({
  165. 'mousemove': MouseMoveCollector,
  166. 'mousewheel': MouseWheelCollector,
  167. 'mouseup': MouseButtonCollector,
  168. 'mousedown': MouseButtonCollector,
  169. 'touchstart': TouchStartCollector,
  170. 'touchmove': TouchMoveCollector,
  171. 'touchend': TouchEndCollector,
  172. 'gesturestart': TouchStartCollector,
  173. 'gestureend': TouchEndCollector,
  174. 'keyup': KeyCollector,
  175. 'keydown': KeyCollector,
  176. 'load': KeyCollector,
  177. 'scroll': KeyCollector,
  178. 'beforeload': KeyCollector,
  179. });
  180. }
  181. /**
  182. * Registers a set of entropy collectors from an array of their
  183. * constructors.
  184. *
  185. * @function addCollectors
  186. * @memberof EntropyManager
  187. * @param {Array}
  188. * collectors The array of entropy collector constructors.
  189. */
  190. function addCollectors(collectors) {
  191. for (var i = 0; i < collectors.length; ++i) {
  192. collectors_.push(new collectors[i](collectEntropy));
  193. }
  194. }
  195. /**
  196. * Registers a set of entropy events from a map of their constructors, using
  197. * the event names as keys.
  198. *
  199. * @function addEvents
  200. * @memberof EntropyManager
  201. * @param {Object}
  202. * events The map of entropy event constructors, using the event
  203. * names as keys.
  204. */
  205. function addEvents(events) {
  206. for (var i = 0, eventNames = Object.keys(events), size = eventNames.length;
  207. i < size; i++) {
  208. that.addEvent(
  209. eventNames[i],
  210. (new events[eventNames[i]](collectEntropy)).handleEvent);
  211. }
  212. }
  213. /**
  214. * Registers an entropy collector. The "collector" argument can be an
  215. * object, in which case it will be added directly to the collector array,
  216. * or it can be a constructor, in which case it will be instantiated and the
  217. * resulting object will be added to the collector array.
  218. *
  219. * @function addCollector
  220. * @memberof EntropyManager
  221. * @param {Object}
  222. * collector An object or constructor that implements the
  223. * "startCollector" and "stopCollector" methods.
  224. * @returns {Object} The instantiation of the entropy collector that was
  225. * registered.
  226. */
  227. this.addCollector = function(Collector) {
  228. if (typeof Collector === 'function') {
  229. // If a constructor is passed, create a new object and link it to the
  230. // entropy collector.
  231. Collector = new Collector(collectEntropy);
  232. }
  233. collectors_.push(Collector);
  234. return Collector;
  235. };
  236. /**
  237. * Registers a new event.
  238. *
  239. * @function addEvent
  240. * @memberof EntropyManager
  241. * @param {string}
  242. * type The event type.
  243. * @param {function}
  244. * listener The event listener.
  245. */
  246. this.addEvent = function(type, listener) {
  247. events_.push({'type': type, 'listener': listener});
  248. };
  249. /**
  250. * Adds entropy to the entropy collected so far, and increments the entropy
  251. * counter. This method is usually called internally by the entropy collectors
  252. * and the events but is made public here for use as a callback function when
  253. * adding individual collectors via the secure random service.
  254. *
  255. * @function collectEntropy
  256. * @memberof EntropyManager
  257. * @param {string}
  258. * data The entropy hash data to be added.
  259. * @param {number}
  260. * increment The entropy counter increment to be added.
  261. */
  262. this.collectEntropy = function(data, increment) {
  263. return collectEntropy(data, increment);
  264. };
  265. /**
  266. * Adds entropy to the entropy collected so far, and increments the entropy
  267. * counter. This method is usually called by the entropy collectors and the
  268. * events.
  269. *
  270. * @function collectEntropy
  271. * @memberof EntropyManager
  272. * @param {string}
  273. * data The entropy hash data to be added.
  274. * @param {number}
  275. * increment The entropy counter increment to be added.
  276. */
  277. function collectEntropy(data, increment) {
  278. entropyHashHex_ += data;
  279. entropyCounter_ += (increment ? increment : 0);
  280. // if the entropy counter has reached the limit, update the hash and stop
  281. // the collectors
  282. if (isCollecting_ && (entropyCounter_ >= maxEntropyCounter_)) {
  283. // Full entropy reached.
  284. fullEntropyReached_ = true;
  285. // Stop collecting entropy.
  286. that.stopCollectors();
  287. // Make the full entropy callback, if present.
  288. if (typeof fullEntropyCallback_ === 'function') {
  289. fullEntropyCallback_();
  290. }
  291. }
  292. }
  293. /**
  294. * Updates the entropy hash, using the entropy collected so far.
  295. *
  296. * @function updateEntropyHash
  297. * @memberof EntropyManager
  298. */
  299. function updateEntropyHash() {
  300. var digester = tools_.getUpdateDigester();
  301. digester.update(entropyHashHex_);
  302. entropyHashHex_ =
  303. codec.hexEncode(codec.binaryDecode(digester.digest().getBytes()));
  304. }
  305. /////////////////////////////////// PUBLIC ///////////////////////////////////
  306. /**
  307. * Starts the entropy collectors.
  308. *
  309. * @function startCollectors
  310. * @memberof EntropyManager
  311. * @param {function}
  312. * fullEntropyCallback The function that is called when the
  313. * required amount of entropy has been collected.
  314. */
  315. this.startCollectors = function(fullEntropyCallback) {
  316. if (isCollecting_) {
  317. // Entropy collection is in progress -- nothing to do.
  318. return;
  319. } else {
  320. isCollecting_ = true;
  321. }
  322. if (fullEntropyCallback) {
  323. // Assign the full entropy callback.
  324. fullEntropyCallback_ = fullEntropyCallback;
  325. }
  326. var i = 0;
  327. // start all the collectors
  328. while (i < collectors_.length) {
  329. try {
  330. collectors_[i].startCollector();
  331. } catch (e) {
  332. // do not do anything about any exception that is thrown
  333. // by the execution of the collectors
  334. }
  335. i++;
  336. }
  337. i = 0;
  338. // start all the events
  339. while (i < events_.length) {
  340. try {
  341. if (window.addEventListener) {
  342. window.addEventListener(events_[i].type, events_[i].listener, false);
  343. } else if (document.attachEvent) {
  344. document.attachEvent('on' + events_[i].type, events_[i].listener);
  345. }
  346. } catch (e) {
  347. // do not do anything about any exception that is thrown
  348. // by the execution of the events
  349. }
  350. i++;
  351. }
  352. tools_.getUpdateDigester().start();
  353. collectorHashUpdater_ = setInterval(function() {
  354. updateEntropyHash();
  355. }, hashUpdaterInterval_);
  356. };
  357. /**
  358. * Stops the entropy collectors. This method will have no effect if the
  359. * required amount of entropy has already been collected.
  360. *
  361. * @function stopCollectors
  362. * @memberof EntropyManager
  363. * @returns {boolean} True if the required amount of entropy has been
  364. * collected, false otherwise.
  365. */
  366. this.stopCollectors = function() {
  367. if (!isCollecting_) {
  368. // Entropy collection is not in progress -- nothing to do.
  369. if (entropyCounter_ >= maxEntropyCounter_) {
  370. return true;
  371. } else {
  372. return false;
  373. }
  374. } else {
  375. isCollecting_ = false;
  376. }
  377. var i = 0;
  378. while (i < collectors_.length) {
  379. try {
  380. collectors_[i].stopCollector();
  381. } catch (e) {
  382. // do not do anything about any exception that is thrown
  383. // by the execution of the collectors
  384. }
  385. i++;
  386. }
  387. i = 0;
  388. while (i < events_.length) {
  389. try {
  390. if (window.removeEventListener) {
  391. window.removeEventListener(
  392. events_[i].type, events_[i].listener, false);
  393. } else if (document.detachEvent) {
  394. document.detachEvent('on' + events_[i].type, events_[i].listener);
  395. }
  396. } catch (e) {
  397. // do not do anything about any exception that is thrown
  398. // by the removal of the events
  399. }
  400. i++;
  401. }
  402. if (collectorHashUpdater_) {
  403. clearInterval(collectorHashUpdater_);
  404. }
  405. updateEntropyHash();
  406. return fullEntropyReached_;
  407. };
  408. /**
  409. * @function getCollectors.
  410. * @memberof EntropyManager
  411. * @returns {Array} The array of collectors currently in use.
  412. */
  413. this.getCollectors = function() {
  414. return collectors_;
  415. };
  416. /**
  417. * @function getEntropyHash
  418. * @memberof EntropyManager
  419. * @returns {Uint8Array} The hash of entropy that has been collected so far.
  420. */
  421. this.getEntropyHash = function() {
  422. return codec.hexDecode(entropyHashHex_);
  423. };
  424. /**
  425. * @function getEntropyPercentage returns {number} The percentage of the
  426. * required entropy that has been collected so far.
  427. * @memberof EntropyManager
  428. */
  429. this.getEntropyPercentage = function() {
  430. if (entropyCounter_ >= maxEntropyCounter_) {
  431. return 100;
  432. }
  433. return entropyCounter_ * 100 / maxEntropyCounter_;
  434. };
  435. /**
  436. * @function isCollecting
  437. * @memberof EntropyManager
  438. * @returns {boolean} True if entropy is still being collected, false
  439. * otherwise.
  440. */
  441. this.isCollecting = function() {
  442. return isCollecting_;
  443. };
  444. //////////////////////////////// CONSTRUCTOR ////////////////////////////////
  445. // Initialize the object.
  446. init(options);
  447. }