|
|
/** * A javascript implementation of a cryptographically-secure * Pseudo Random Number Generator (PRNG). The Fortuna algorithm is followed * here though the use of SHA-256 is not enforced; when generating an * a PRNG context, the hashing algorithm and block cipher used for * the generator are specified via a plugin. * * @author Dave Longley * * Copyright (c) 2010-2014 Digital Bazaar, Inc. */ var forge = require('./forge'); require('./util');
var _crypto = null; if(forge.util.isNodejs && !forge.options.usePureJavaScript && !process.versions['node-webkit']) { _crypto = require('crypto'); }
/* PRNG API */ var prng = module.exports = forge.prng = forge.prng || {};
/** * Creates a new PRNG context. * * A PRNG plugin must be passed in that will provide: * * 1. A function that initializes the key and seed of a PRNG context. It * will be given a 16 byte key and a 16 byte seed. Any key expansion * or transformation of the seed from a byte string into an array of * integers (or similar) should be performed. * 2. The cryptographic function used by the generator. It takes a key and * a seed. * 3. A seed increment function. It takes the seed and returns seed + 1. * 4. An api to create a message digest. * * For an example, see random.js. * * @param plugin the PRNG plugin to use. */ prng.create = function(plugin) { var ctx = { plugin: plugin, key: null, seed: null, time: null, // number of reseeds so far
reseeds: 0, // amount of data generated so far
generated: 0, // no initial key bytes
keyBytes: '' };
// create 32 entropy pools (each is a message digest)
var md = plugin.md; var pools = new Array(32); for(var i = 0; i < 32; ++i) { pools[i] = md.create(); } ctx.pools = pools;
// entropy pools are written to cyclically, starting at index 0
ctx.pool = 0;
/** * Generates random bytes. The bytes may be generated synchronously or * asynchronously. Web workers must use the asynchronous interface or * else the behavior is undefined. * * @param count the number of random bytes to generate. * @param [callback(err, bytes)] called once the operation completes. * * @return count random bytes as a string. */ ctx.generate = function(count, callback) { // do synchronously
if(!callback) { return ctx.generateSync(count); }
// simple generator using counter-based CBC
var cipher = ctx.plugin.cipher; var increment = ctx.plugin.increment; var formatKey = ctx.plugin.formatKey; var formatSeed = ctx.plugin.formatSeed; var b = forge.util.createBuffer();
// paranoid deviation from Fortuna:
// reset key for every request to protect previously
// generated random bytes should the key be discovered;
// there is no 100ms based reseeding because of this
// forced reseed for every `generate` call
ctx.key = null;
generate();
function generate(err) { if(err) { return callback(err); }
// sufficient bytes generated
if(b.length() >= count) { return callback(null, b.getBytes(count)); }
// if amount of data generated is greater than 1 MiB, trigger reseed
if(ctx.generated > 0xfffff) { ctx.key = null; }
if(ctx.key === null) { // prevent stack overflow
return forge.util.nextTick(function() { _reseed(generate); }); }
// generate the random bytes
var bytes = cipher(ctx.key, ctx.seed); ctx.generated += bytes.length; b.putBytes(bytes);
// generate bytes for a new key and seed
ctx.key = formatKey(cipher(ctx.key, increment(ctx.seed))); ctx.seed = formatSeed(cipher(ctx.key, ctx.seed));
forge.util.setImmediate(generate); } };
/** * Generates random bytes synchronously. * * @param count the number of random bytes to generate. * * @return count random bytes as a string. */ ctx.generateSync = function(count) { // simple generator using counter-based CBC
var cipher = ctx.plugin.cipher; var increment = ctx.plugin.increment; var formatKey = ctx.plugin.formatKey; var formatSeed = ctx.plugin.formatSeed;
// paranoid deviation from Fortuna:
// reset key for every request to protect previously
// generated random bytes should the key be discovered;
// there is no 100ms based reseeding because of this
// forced reseed for every `generateSync` call
ctx.key = null;
var b = forge.util.createBuffer(); while(b.length() < count) { // if amount of data generated is greater than 1 MiB, trigger reseed
if(ctx.generated > 0xfffff) { ctx.key = null; }
if(ctx.key === null) { _reseedSync(); }
// generate the random bytes
var bytes = cipher(ctx.key, ctx.seed); ctx.generated += bytes.length; b.putBytes(bytes);
// generate bytes for a new key and seed
ctx.key = formatKey(cipher(ctx.key, increment(ctx.seed))); ctx.seed = formatSeed(cipher(ctx.key, ctx.seed)); }
return b.getBytes(count); };
/** * Private function that asynchronously reseeds a generator. * * @param callback(err) called once the operation completes. */ function _reseed(callback) { if(ctx.pools[0].messageLength >= 32) { _seed(); return callback(); } // not enough seed data...
var needed = (32 - ctx.pools[0].messageLength) << 5; ctx.seedFile(needed, function(err, bytes) { if(err) { return callback(err); } ctx.collect(bytes); _seed(); callback(); }); }
/** * Private function that synchronously reseeds a generator. */ function _reseedSync() { if(ctx.pools[0].messageLength >= 32) { return _seed(); } // not enough seed data...
var needed = (32 - ctx.pools[0].messageLength) << 5; ctx.collect(ctx.seedFileSync(needed)); _seed(); }
/** * Private function that seeds a generator once enough bytes are available. */ function _seed() { // update reseed count
ctx.reseeds = (ctx.reseeds === 0xffffffff) ? 0 : ctx.reseeds + 1;
// goal is to update `key` via:
// key = hash(key + s)
// where 's' is all collected entropy from selected pools, then...
// create a plugin-based message digest
var md = ctx.plugin.md.create();
// consume current key bytes
md.update(ctx.keyBytes);
// digest the entropy of pools whose index k meet the
// condition 'n mod 2^k == 0' where n is the number of reseeds
var _2powK = 1; for(var k = 0; k < 32; ++k) { if(ctx.reseeds % _2powK === 0) { md.update(ctx.pools[k].digest().getBytes()); ctx.pools[k].start(); } _2powK = _2powK << 1; }
// get digest for key bytes
ctx.keyBytes = md.digest().getBytes();
// paranoid deviation from Fortuna:
// update `seed` via `seed = hash(key)`
// instead of initializing to zero once and only
// ever incrementing it
md.start(); md.update(ctx.keyBytes); var seedBytes = md.digest().getBytes();
// update state
ctx.key = ctx.plugin.formatKey(ctx.keyBytes); ctx.seed = ctx.plugin.formatSeed(seedBytes); ctx.generated = 0; }
/** * The built-in default seedFile. This seedFile is used when entropy * is needed immediately. * * @param needed the number of bytes that are needed. * * @return the random bytes. */ function defaultSeedFile(needed) { // use window.crypto.getRandomValues strong source of entropy if available
var getRandomValues = null; var globalScope = forge.util.globalScope; var _crypto = globalScope.crypto || globalScope.msCrypto; if(_crypto && _crypto.getRandomValues) { getRandomValues = function(arr) { return _crypto.getRandomValues(arr); }; }
var b = forge.util.createBuffer(); if(getRandomValues) { while(b.length() < needed) { // max byte length is 65536 before QuotaExceededError is thrown
// http://www.w3.org/TR/WebCryptoAPI/#RandomSource-method-getRandomValues
var count = Math.max(1, Math.min(needed - b.length(), 65536) / 4); var entropy = new Uint32Array(Math.floor(count)); try { getRandomValues(entropy); for(var i = 0; i < entropy.length; ++i) { b.putInt32(entropy[i]); } } catch(e) { /* only ignore QuotaExceededError */ if(!(typeof QuotaExceededError !== 'undefined' && e instanceof QuotaExceededError)) { throw e; } } } }
// be sad and add some weak random data
if(b.length() < needed) { /* Draws from Park-Miller "minimal standard" 31 bit PRNG, implemented with David G. Carta's optimization: with 32 bit math and without division (Public Domain). */ var hi, lo, next; var seed = Math.floor(Math.random() * 0x010000); while(b.length() < needed) { lo = 16807 * (seed & 0xFFFF); hi = 16807 * (seed >> 16); lo += (hi & 0x7FFF) << 16; lo += hi >> 15; lo = (lo & 0x7FFFFFFF) + (lo >> 31); seed = lo & 0xFFFFFFFF;
// consume lower 3 bytes of seed
for(var i = 0; i < 3; ++i) { // throw in more pseudo random
next = seed >>> (i << 3); next ^= Math.floor(Math.random() * 0x0100); b.putByte(next & 0xFF); } } }
return b.getBytes(needed); } // initialize seed file APIs
if(_crypto) { // use nodejs async API
ctx.seedFile = function(needed, callback) { _crypto.randomBytes(needed, function(err, bytes) { if(err) { return callback(err); } callback(null, bytes.toString()); }); }; // use nodejs sync API
ctx.seedFileSync = function(needed) { return _crypto.randomBytes(needed).toString(); }; } else { ctx.seedFile = function(needed, callback) { try { callback(null, defaultSeedFile(needed)); } catch(e) { callback(e); } }; ctx.seedFileSync = defaultSeedFile; }
/** * Adds entropy to a prng ctx's accumulator. * * @param bytes the bytes of entropy as a string. */ ctx.collect = function(bytes) { // iterate over pools distributing entropy cyclically
var count = bytes.length; for(var i = 0; i < count; ++i) { ctx.pools[ctx.pool].update(bytes.substr(i, 1)); ctx.pool = (ctx.pool === 31) ? 0 : ctx.pool + 1; } };
/** * Collects an integer of n bits. * * @param i the integer entropy. * @param n the number of bits in the integer. */ ctx.collectInt = function(i, n) { var bytes = ''; for(var x = 0; x < n; x += 8) { bytes += String.fromCharCode((i >> x) & 0xFF); } ctx.collect(bytes); };
/** * Registers a Web Worker to receive immediate entropy from the main thread. * This method is required until Web Workers can access the native crypto * API. This method should be called twice for each created worker, once in * the main thread, and once in the worker itself. * * @param worker the worker to register. */ ctx.registerWorker = function(worker) { // worker receives random bytes
if(worker === self) { ctx.seedFile = function(needed, callback) { function listener(e) { var data = e.data; if(data.forge && data.forge.prng) { self.removeEventListener('message', listener); callback(data.forge.prng.err, data.forge.prng.bytes); } } self.addEventListener('message', listener); self.postMessage({forge: {prng: {needed: needed}}}); }; } else { // main thread sends random bytes upon request
var listener = function(e) { var data = e.data; if(data.forge && data.forge.prng) { ctx.seedFile(data.forge.prng.needed, function(err, bytes) { worker.postMessage({forge: {prng: {err: err, bytes: bytes}}}); }); } }; // TODO: do we need to remove the event listener when the worker dies?
worker.addEventListener('message', listener); } };
return ctx; };
|