random number web worker buffer automatic refill

This commit is contained in:
Bart Butler 2018-03-05 17:57:35 -08:00
parent 433ae5cce7
commit 572abadc91
6 changed files with 33 additions and 53 deletions

View File

@ -37,7 +37,7 @@ export default {
* @param {Integer} length Length in bytes to generate * @param {Integer} length Length in bytes to generate
* @return {Uint8Array} Random byte array * @return {Uint8Array} Random byte array
*/ */
getRandomBytes: function(length) { getRandomBytes: async function(length) {
const buf = new Uint8Array(length); const buf = new Uint8Array(length);
if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) { if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) {
window.crypto.getRandomValues(buf); window.crypto.getRandomValues(buf);
@ -47,7 +47,7 @@ export default {
const bytes = nodeCrypto.randomBytes(buf.length); const bytes = nodeCrypto.randomBytes(buf.length);
buf.set(bytes); buf.set(bytes);
} else if (this.randomBuffer.buffer) { } else if (this.randomBuffer.buffer) {
this.randomBuffer.get(buf); await this.randomBuffer.get(buf);
} else { } else {
throw new Error('No secure random number generator available.'); throw new Error('No secure random number generator available.');
} }
@ -60,7 +60,7 @@ export default {
* @param {module:type/mpi} max Upper bound, excluded * @param {module:type/mpi} max Upper bound, excluded
* @return {module:BN} Random MPI * @return {module:BN} Random MPI
*/ */
getRandomBN: function(min, max) { getRandomBN: async function(min, max) {
if (max.cmp(min) <= 0) { if (max.cmp(min) <= 0) {
throw new Error('Illegal parameter value: max <= min'); throw new Error('Illegal parameter value: max <= min');
} }
@ -71,7 +71,7 @@ export default {
// Using a while loop is necessary to avoid bias introduced by the mod operation. // Using a while loop is necessary to avoid bias introduced by the mod operation.
// However, we request 64 extra random bits so that the bias is negligible. // However, we request 64 extra random bits so that the bias is negligible.
// Section B.1.1 here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf // Section B.1.1 here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
const r = new BN(this.getRandomBytes(bytes + 8)); const r = new BN(await this.getRandomBytes(bytes + 8));
return r.mod(modulus).add(min); return r.mod(modulus).add(min);
}, },
@ -121,7 +121,7 @@ RandomBuffer.prototype.set = function(buf) {
* Take numbers out of buffer and copy to array * Take numbers out of buffer and copy to array
* @param {Uint8Array} buf the destination array * @param {Uint8Array} buf the destination array
*/ */
RandomBuffer.prototype.get = function(buf) { RandomBuffer.prototype.get = async function(buf) {
if (!this.buffer) { if (!this.buffer) {
throw new Error('RandomBuffer is not initialized'); throw new Error('RandomBuffer is not initialized');
} }
@ -129,7 +129,12 @@ RandomBuffer.prototype.get = function(buf) {
throw new Error('Invalid type: buf not an Uint8Array'); throw new Error('Invalid type: buf not an Uint8Array');
} }
if (this.size < buf.length) { if (this.size < buf.length) {
throw new Error('Random number buffer depleted'); if (!this.callback) {
throw new Error('Random number buffer depleted');
}
// Wait for random bytes from main context, then try again
await this.callback();
return this.get(buf);
} }
for (let i = 0; i < buf.length; i++) { for (let i = 0; i < buf.length; i++) {
buf[i] = this.buffer[--this.size]; buf[i] = this.buffer[--this.size];

View File

@ -19,8 +19,6 @@ import util from '../util.js';
import crypto from '../crypto'; import crypto from '../crypto';
import packet from '../packet'; import packet from '../packet';
const INITIAL_RANDOM_SEED = 50000; // random bytes seeded to worker
/** /**
* Initializes a new proxy and loads the web worker * Initializes a new proxy and loads the web worker
* @constructor * @constructor
@ -35,7 +33,6 @@ export default function AsyncProxy({ path='openpgp.worker.js', worker, config }
this.worker.onerror = e => { this.worker.onerror = e => {
throw new Error('Unhandled error in openpgp worker: ' + e.message + ' (' + e.filename + ':' + e.lineno + ')'); throw new Error('Unhandled error in openpgp worker: ' + e.message + ' (' + e.filename + ':' + e.lineno + ')');
}; };
this.seedRandom(INITIAL_RANDOM_SEED);
if (config) { if (config) {
this.worker.postMessage({ event:'configure', config }); this.worker.postMessage({ event:'configure', config });

View File

@ -278,19 +278,19 @@ describe('API functional testing', function() {
}); });
function testCFB(plaintext, resync) { function testCFB(plaintext, resync) {
symmAlgos.forEach(function(algo) { symmAlgos.forEach(async function(algo) {
const symmKey = crypto.generateSessionKey(algo); const symmKey = await crypto.generateSessionKey(algo);
const symmencData = crypto.cfb.encrypt(crypto.getPrefixRandom(algo), algo, util.str_to_Uint8Array(plaintext), symmKey, resync); const symmencData = crypto.cfb.encrypt(await crypto.getPrefixRandom(algo), algo, util.str_to_Uint8Array(plaintext), symmKey, resync);
const text = util.Uint8Array_to_str(crypto.cfb.decrypt(algo, symmKey, symmencData, resync)); const text = util.Uint8Array_to_str(crypto.cfb.decrypt(algo, symmKey, symmencData, resync));
expect(text).to.equal(plaintext); expect(text).to.equal(plaintext);
}); });
} }
function testAESCFB(plaintext) { function testAESCFB(plaintext) {
symmAlgos.forEach(function(algo) { symmAlgos.forEach(async function(algo) {
if(algo.substr(0,3) === 'aes') { if(algo.substr(0,3) === 'aes') {
const symmKey = crypto.generateSessionKey(algo); const symmKey = await crypto.generateSessionKey(algo);
const rndm = crypto.getPrefixRandom(algo); const rndm = await crypto.getPrefixRandom(algo);
const repeat = new Uint8Array([rndm[rndm.length - 2], rndm[rndm.length - 1]]); const repeat = new Uint8Array([rndm[rndm.length - 2], rndm[rndm.length - 1]]);
const prefix = util.concatUint8Array([rndm, repeat]); const prefix = util.concatUint8Array([rndm, repeat]);
@ -307,9 +307,9 @@ describe('API functional testing', function() {
function testAESGCM(plaintext) { function testAESGCM(plaintext) {
symmAlgos.forEach(function(algo) { symmAlgos.forEach(function(algo) {
if(algo.substr(0,3) === 'aes') { if(algo.substr(0,3) === 'aes') {
it(algo, function() { it(algo, async function() {
const key = crypto.generateSessionKey(algo); const key = await crypto.generateSessionKey(algo);
const iv = crypto.random.getRandomBytes(crypto.gcm.ivLength); const iv = await crypto.random.getRandomBytes(crypto.gcm.ivLength);
return crypto.gcm.encrypt( return crypto.gcm.encrypt(
algo, util.str_to_Uint8Array(plaintext), key, iv algo, util.str_to_Uint8Array(plaintext), key, iv

View File

@ -12,9 +12,9 @@ describe('Random Buffer', function() {
expect(randomBuffer).to.exist; expect(randomBuffer).to.exist;
}); });
it('Throw error if not initialized', function () { it('Throw error if not initialized', async function () {
expect(randomBuffer.set.bind(randomBuffer)).to.throw('RandomBuffer is not initialized'); expect(randomBuffer.set.bind(randomBuffer)).to.throw('RandomBuffer is not initialized');
expect(randomBuffer.get.bind(randomBuffer)).to.throw('RandomBuffer is not initialized'); await expect(randomBuffer.get(new Uint8Array(1))).to.eventually.be.rejectedWith('RandomBuffer is not initialized');
}); });
it('Initialization', function () { it('Initialization', function () {
@ -56,13 +56,13 @@ describe('Random Buffer', function() {
expect(randomBuffer.size).to.equal(1); expect(randomBuffer.size).to.equal(1);
}); });
it('Get Method', function () { it('Get Method', async function () {
randomBuffer.init(5); randomBuffer.init(5);
let buf = new Uint8Array(5); let buf = new Uint8Array(5);
buf[0] = 1; buf[1] = 2; buf[2] = 5; buf[3] = 7; buf[4] = 8; buf[0] = 1; buf[1] = 2; buf[2] = 5; buf[3] = 7; buf[4] = 8;
randomBuffer.set(buf); randomBuffer.set(buf);
buf = new Uint32Array(2); buf = new Uint32Array(2);
expect(randomBuffer.get.bind(randomBuffer, buf)).to.throw('Invalid type: buf not an Uint8Array'); await expect(randomBuffer.get(buf)).to.eventually.be.rejectedWith('Invalid type: buf not an Uint8Array');
buf = new Uint8Array(2); buf = new Uint8Array(2);
randomBuffer.get(buf); randomBuffer.get(buf);
expect(equal(randomBuffer.buffer, [1,2,5,0,0])).to.be.true; expect(equal(randomBuffer.buffer, [1,2,5,0,0])).to.be.true;
@ -74,6 +74,6 @@ describe('Random Buffer', function() {
expect(buf).to.to.have.property('1', 2); expect(buf).to.to.have.property('1', 2);
expect(equal(randomBuffer.buffer, [1,0,0,0,0])).to.be.true; expect(equal(randomBuffer.buffer, [1,0,0,0,0])).to.be.true;
expect(randomBuffer.size).to.equal(1); expect(randomBuffer.size).to.equal(1);
expect(function() { randomBuffer.get(buf); }).to.throw('Random number buffer depleted'); await expect(randomBuffer.get(buf)).to.eventually.be.rejectedWith('Random number buffer depleted');
}); });
}); });

View File

@ -896,25 +896,6 @@ describe('OpenPGP.js public api tests', function() {
}); });
}); });
it('should encrypt then decrypt', function () {
const encOpt = {
data: plaintext,
publicKeys: publicKey.keys
};
const decOpt = {
privateKeys: privateKey.keys
};
return openpgp.encrypt(encOpt).then(function (encrypted) {
expect(encrypted.data).to.match(/^-----BEGIN PGP MESSAGE/);
decOpt.message = openpgp.message.readArmored(encrypted.data);
return openpgp.decrypt(decOpt);
}).then(function (decrypted) {
expect(decrypted.data).to.equal(plaintext);
expect(decrypted.signatures).to.exist;
expect(decrypted.signatures.length).to.equal(0);
});
});
it('should encrypt then decrypt with multiple private keys', function () { it('should encrypt then decrypt with multiple private keys', function () {
const privKeyDE = openpgp.key.readArmored(priv_key_de).keys[0]; const privKeyDE = openpgp.key.readArmored(priv_key_de).keys[0];
privKeyDE.decrypt(passphrase); privKeyDE.decrypt(passphrase);
@ -1001,9 +982,9 @@ describe('OpenPGP.js public api tests', function() {
}); });
}); });
it('should encrypt using custom session key and decrypt using session key', function () { it('should encrypt using custom session key and decrypt using session key', async function () {
const sessionKey = { const sessionKey = {
data: openpgp.crypto.generateSessionKey('aes256'), data: await openpgp.crypto.generateSessionKey('aes256'),
algorithm: 'aes256' algorithm: 'aes256'
}; };
const encOpt = { const encOpt = {

View File

@ -47,15 +47,12 @@ tryTests('Async Proxy', tests, {
function tests() { function tests() {
describe('Error handling', function() { describe('Random number pipeline', function() {
it('Depleted random buffer in worker gives error', function() { it('Random number buffer automatically reseeded', function() {
const wProxy = new openpgp.AsyncProxy({ path:'../dist/openpgp.worker.js' }); const worker = new Worker('../dist/openpgp.worker.js');
wProxy.worker = new Worker('../dist/openpgp.worker.js'); const wProxy = new openpgp.AsyncProxy({ path:'../dist/openpgp.worker.js', worker });
wProxy.worker.onmessage = wProxy.onMessage.bind(wProxy);
wProxy.seedRandom(10); return wProxy.delegate('encrypt', { publicKeys:[pubKey], data:plaintext });
return wProxy.delegate('encrypt', { publicKeys:[pubKey], data:plaintext }).catch(function(err) {
expect(err.message).to.match(/Random number buffer depleted/);
});
}); });
}); });