UNPKG

12.3 kBJavaScriptView Raw
1/**
2 * A javascript implementation of a cryptographically-secure
3 * Pseudo Random Number Generator (PRNG). The Fortuna algorithm is followed
4 * here though the use of SHA-256 is not enforced; when generating an
5 * a PRNG context, the hashing algorithm and block cipher used for
6 * the generator are specified via a plugin.
7 *
8 * @author Dave Longley
9 *
10 * Copyright (c) 2010-2014 Digital Bazaar, Inc.
11 */
12var forge = require('./forge');
13require('./util');
14
15var _crypto = null;
16if(forge.util.isNodejs && !forge.options.usePureJavaScript &&
17 !process.versions['node-webkit']) {
18 _crypto = require('crypto');
19}
20
21/* PRNG API */
22var prng = module.exports = forge.prng = forge.prng || {};
23
24/**
25 * Creates a new PRNG context.
26 *
27 * A PRNG plugin must be passed in that will provide:
28 *
29 * 1. A function that initializes the key and seed of a PRNG context. It
30 * will be given a 16 byte key and a 16 byte seed. Any key expansion
31 * or transformation of the seed from a byte string into an array of
32 * integers (or similar) should be performed.
33 * 2. The cryptographic function used by the generator. It takes a key and
34 * a seed.
35 * 3. A seed increment function. It takes the seed and returns seed + 1.
36 * 4. An api to create a message digest.
37 *
38 * For an example, see random.js.
39 *
40 * @param plugin the PRNG plugin to use.
41 */
42prng.create = function(plugin) {
43 var ctx = {
44 plugin: plugin,
45 key: null,
46 seed: null,
47 time: null,
48 // number of reseeds so far
49 reseeds: 0,
50 // amount of data generated so far
51 generated: 0,
52 // no initial key bytes
53 keyBytes: ''
54 };
55
56 // create 32 entropy pools (each is a message digest)
57 var md = plugin.md;
58 var pools = new Array(32);
59 for(var i = 0; i < 32; ++i) {
60 pools[i] = md.create();
61 }
62 ctx.pools = pools;
63
64 // entropy pools are written to cyclically, starting at index 0
65 ctx.pool = 0;
66
67 /**
68 * Generates random bytes. The bytes may be generated synchronously or
69 * asynchronously. Web workers must use the asynchronous interface or
70 * else the behavior is undefined.
71 *
72 * @param count the number of random bytes to generate.
73 * @param [callback(err, bytes)] called once the operation completes.
74 *
75 * @return count random bytes as a string.
76 */
77 ctx.generate = function(count, callback) {
78 // do synchronously
79 if(!callback) {
80 return ctx.generateSync(count);
81 }
82
83 // simple generator using counter-based CBC
84 var cipher = ctx.plugin.cipher;
85 var increment = ctx.plugin.increment;
86 var formatKey = ctx.plugin.formatKey;
87 var formatSeed = ctx.plugin.formatSeed;
88 var b = forge.util.createBuffer();
89
90 // paranoid deviation from Fortuna:
91 // reset key for every request to protect previously
92 // generated random bytes should the key be discovered;
93 // there is no 100ms based reseeding because of this
94 // forced reseed for every `generate` call
95 ctx.key = null;
96
97 generate();
98
99 function generate(err) {
100 if(err) {
101 return callback(err);
102 }
103
104 // sufficient bytes generated
105 if(b.length() >= count) {
106 return callback(null, b.getBytes(count));
107 }
108
109 // if amount of data generated is greater than 1 MiB, trigger reseed
110 if(ctx.generated > 0xfffff) {
111 ctx.key = null;
112 }
113
114 if(ctx.key === null) {
115 // prevent stack overflow
116 return forge.util.nextTick(function() {
117 _reseed(generate);
118 });
119 }
120
121 // generate the random bytes
122 var bytes = cipher(ctx.key, ctx.seed);
123 ctx.generated += bytes.length;
124 b.putBytes(bytes);
125
126 // generate bytes for a new key and seed
127 ctx.key = formatKey(cipher(ctx.key, increment(ctx.seed)));
128 ctx.seed = formatSeed(cipher(ctx.key, ctx.seed));
129
130 forge.util.setImmediate(generate);
131 }
132 };
133
134 /**
135 * Generates random bytes synchronously.
136 *
137 * @param count the number of random bytes to generate.
138 *
139 * @return count random bytes as a string.
140 */
141 ctx.generateSync = function(count) {
142 // simple generator using counter-based CBC
143 var cipher = ctx.plugin.cipher;
144 var increment = ctx.plugin.increment;
145 var formatKey = ctx.plugin.formatKey;
146 var formatSeed = ctx.plugin.formatSeed;
147
148 // paranoid deviation from Fortuna:
149 // reset key for every request to protect previously
150 // generated random bytes should the key be discovered;
151 // there is no 100ms based reseeding because of this
152 // forced reseed for every `generateSync` call
153 ctx.key = null;
154
155 var b = forge.util.createBuffer();
156 while(b.length() < count) {
157 // if amount of data generated is greater than 1 MiB, trigger reseed
158 if(ctx.generated > 0xfffff) {
159 ctx.key = null;
160 }
161
162 if(ctx.key === null) {
163 _reseedSync();
164 }
165
166 // generate the random bytes
167 var bytes = cipher(ctx.key, ctx.seed);
168 ctx.generated += bytes.length;
169 b.putBytes(bytes);
170
171 // generate bytes for a new key and seed
172 ctx.key = formatKey(cipher(ctx.key, increment(ctx.seed)));
173 ctx.seed = formatSeed(cipher(ctx.key, ctx.seed));
174 }
175
176 return b.getBytes(count);
177 };
178
179 /**
180 * Private function that asynchronously reseeds a generator.
181 *
182 * @param callback(err) called once the operation completes.
183 */
184 function _reseed(callback) {
185 if(ctx.pools[0].messageLength >= 32) {
186 _seed();
187 return callback();
188 }
189 // not enough seed data...
190 var needed = (32 - ctx.pools[0].messageLength) << 5;
191 ctx.seedFile(needed, function(err, bytes) {
192 if(err) {
193 return callback(err);
194 }
195 ctx.collect(bytes);
196 _seed();
197 callback();
198 });
199 }
200
201 /**
202 * Private function that synchronously reseeds a generator.
203 */
204 function _reseedSync() {
205 if(ctx.pools[0].messageLength >= 32) {
206 return _seed();
207 }
208 // not enough seed data...
209 var needed = (32 - ctx.pools[0].messageLength) << 5;
210 ctx.collect(ctx.seedFileSync(needed));
211 _seed();
212 }
213
214 /**
215 * Private function that seeds a generator once enough bytes are available.
216 */
217 function _seed() {
218 // update reseed count
219 ctx.reseeds = (ctx.reseeds === 0xffffffff) ? 0 : ctx.reseeds + 1;
220
221 // goal is to update `key` via:
222 // key = hash(key + s)
223 // where 's' is all collected entropy from selected pools, then...
224
225 // create a plugin-based message digest
226 var md = ctx.plugin.md.create();
227
228 // consume current key bytes
229 md.update(ctx.keyBytes);
230
231 // digest the entropy of pools whose index k meet the
232 // condition 'n mod 2^k == 0' where n is the number of reseeds
233 var _2powK = 1;
234 for(var k = 0; k < 32; ++k) {
235 if(ctx.reseeds % _2powK === 0) {
236 md.update(ctx.pools[k].digest().getBytes());
237 ctx.pools[k].start();
238 }
239 _2powK = _2powK << 1;
240 }
241
242 // get digest for key bytes
243 ctx.keyBytes = md.digest().getBytes();
244
245 // paranoid deviation from Fortuna:
246 // update `seed` via `seed = hash(key)`
247 // instead of initializing to zero once and only
248 // ever incrementing it
249 md.start();
250 md.update(ctx.keyBytes);
251 var seedBytes = md.digest().getBytes();
252
253 // update state
254 ctx.key = ctx.plugin.formatKey(ctx.keyBytes);
255 ctx.seed = ctx.plugin.formatSeed(seedBytes);
256 ctx.generated = 0;
257 }
258
259 /**
260 * The built-in default seedFile. This seedFile is used when entropy
261 * is needed immediately.
262 *
263 * @param needed the number of bytes that are needed.
264 *
265 * @return the random bytes.
266 */
267 function defaultSeedFile(needed) {
268 // use window.crypto.getRandomValues strong source of entropy if available
269 var getRandomValues = null;
270 var globalScope = forge.util.globalScope;
271 var _crypto = globalScope.crypto || globalScope.msCrypto;
272 if(_crypto && _crypto.getRandomValues) {
273 getRandomValues = function(arr) {
274 return _crypto.getRandomValues(arr);
275 };
276 }
277
278 var b = forge.util.createBuffer();
279 if(getRandomValues) {
280 while(b.length() < needed) {
281 // max byte length is 65536 before QuotaExceededError is thrown
282 // http://www.w3.org/TR/WebCryptoAPI/#RandomSource-method-getRandomValues
283 var count = Math.max(1, Math.min(needed - b.length(), 65536) / 4);
284 var entropy = new Uint32Array(Math.floor(count));
285 try {
286 getRandomValues(entropy);
287 for(var i = 0; i < entropy.length; ++i) {
288 b.putInt32(entropy[i]);
289 }
290 } catch(e) {
291 /* only ignore QuotaExceededError */
292 if(!(typeof QuotaExceededError !== 'undefined' &&
293 e instanceof QuotaExceededError)) {
294 throw e;
295 }
296 }
297 }
298 }
299
300 // be sad and add some weak random data
301 if(b.length() < needed) {
302 /* Draws from Park-Miller "minimal standard" 31 bit PRNG,
303 implemented with David G. Carta's optimization: with 32 bit math
304 and without division (Public Domain). */
305 var hi, lo, next;
306 var seed = Math.floor(Math.random() * 0x010000);
307 while(b.length() < needed) {
308 lo = 16807 * (seed & 0xFFFF);
309 hi = 16807 * (seed >> 16);
310 lo += (hi & 0x7FFF) << 16;
311 lo += hi >> 15;
312 lo = (lo & 0x7FFFFFFF) + (lo >> 31);
313 seed = lo & 0xFFFFFFFF;
314
315 // consume lower 3 bytes of seed
316 for(var i = 0; i < 3; ++i) {
317 // throw in more pseudo random
318 next = seed >>> (i << 3);
319 next ^= Math.floor(Math.random() * 0x0100);
320 b.putByte(String.fromCharCode(next & 0xFF));
321 }
322 }
323 }
324
325 return b.getBytes(needed);
326 }
327 // initialize seed file APIs
328 if(_crypto) {
329 // use nodejs async API
330 ctx.seedFile = function(needed, callback) {
331 _crypto.randomBytes(needed, function(err, bytes) {
332 if(err) {
333 return callback(err);
334 }
335 callback(null, bytes.toString());
336 });
337 };
338 // use nodejs sync API
339 ctx.seedFileSync = function(needed) {
340 return _crypto.randomBytes(needed).toString();
341 };
342 } else {
343 ctx.seedFile = function(needed, callback) {
344 try {
345 callback(null, defaultSeedFile(needed));
346 } catch(e) {
347 callback(e);
348 }
349 };
350 ctx.seedFileSync = defaultSeedFile;
351 }
352
353 /**
354 * Adds entropy to a prng ctx's accumulator.
355 *
356 * @param bytes the bytes of entropy as a string.
357 */
358 ctx.collect = function(bytes) {
359 // iterate over pools distributing entropy cyclically
360 var count = bytes.length;
361 for(var i = 0; i < count; ++i) {
362 ctx.pools[ctx.pool].update(bytes.substr(i, 1));
363 ctx.pool = (ctx.pool === 31) ? 0 : ctx.pool + 1;
364 }
365 };
366
367 /**
368 * Collects an integer of n bits.
369 *
370 * @param i the integer entropy.
371 * @param n the number of bits in the integer.
372 */
373 ctx.collectInt = function(i, n) {
374 var bytes = '';
375 for(var x = 0; x < n; x += 8) {
376 bytes += String.fromCharCode((i >> x) & 0xFF);
377 }
378 ctx.collect(bytes);
379 };
380
381 /**
382 * Registers a Web Worker to receive immediate entropy from the main thread.
383 * This method is required until Web Workers can access the native crypto
384 * API. This method should be called twice for each created worker, once in
385 * the main thread, and once in the worker itself.
386 *
387 * @param worker the worker to register.
388 */
389 ctx.registerWorker = function(worker) {
390 // worker receives random bytes
391 if(worker === self) {
392 ctx.seedFile = function(needed, callback) {
393 function listener(e) {
394 var data = e.data;
395 if(data.forge && data.forge.prng) {
396 self.removeEventListener('message', listener);
397 callback(data.forge.prng.err, data.forge.prng.bytes);
398 }
399 }
400 self.addEventListener('message', listener);
401 self.postMessage({forge: {prng: {needed: needed}}});
402 };
403 } else {
404 // main thread sends random bytes upon request
405 var listener = function(e) {
406 var data = e.data;
407 if(data.forge && data.forge.prng) {
408 ctx.seedFile(data.forge.prng.needed, function(err, bytes) {
409 worker.postMessage({forge: {prng: {err: err, bytes: bytes}}});
410 });
411 }
412 };
413 // TODO: do we need to remove the event listener when the worker dies?
414 worker.addEventListener('message', listener);
415 }
416 };
417
418 return ctx;
419};