enable lazy initialization of aes cipher key

This commit is contained in:
AJ ONeal 2015-07-28 18:04:54 -06:00
parent 1a42430b8f
commit bece314c34
5 changed files with 244 additions and 32 deletions

View File

@ -50,7 +50,7 @@ var opts = {
sqlite.create(opts).then(function (db) { sqlite.create(opts).then(function (db) {
// same api as new sqlite3.Database(options.filename) // same api as new sqlite3.Database(options.filename)
client.run("SELECT ?", ['Hello World!'], function (err) { db.run("SELECT ?", ['Hello World!'], function (err) {
if (err) { if (err) {
console.error('[ERROR]', cluster.isMaster && '0' || cluster.worker.id); console.error('[ERROR]', cluster.isMaster && '0' || cluster.worker.id);
console.error(err); console.error(err);
@ -75,6 +75,58 @@ If you wish to always use clustering, even on a single core system, see `test-cl
Likewise, if you wish to use standalone mode in a particular worker process see `test-standalone.js`. Likewise, if you wish to use standalone mode in a particular worker process see `test-standalone.js`.
SQLCipher Considerations
========================
In (hopefully) most cases your AES key won't be available at the time that you want your service
to start listening. (And if it is you might be using a form of
"[encraption](https://twitter.com/nmacdona/status/532677876685217795)"
where you were intending to use a form of "encryption" and should
look into that before going any further.)
To account for this you can pass the `bits` option on `create` and then call `init({ key: key })`
when you receive your key from user input, the key server, etc.
Calling any normal methods will result in an error until `init` is called.
**NOTE:** Because the server process (the master) will use `node-sqlite3` directly,
without any wrapper to protect it, *you* must make sure that it doesn't
make any calls before the key is supplied with `init`.
For this reason it is recommended to not use your master process as an http server, etc.
```js
var cluster = require('cluster');
var sqlite = require('sqlite3-cluster');
var numCores = require('os').cpus().length;
var opts = {
filename: '/tmp/mydb.sqlcipher'
, key: null
, bits: 128
};
sqlite.create(opts).then(function (db) {
// same api as new sqlite3.Database(options.filename)
db.init({
bits: 128
, key: '00000000000000000000000000000000'
}).then(function (db) {
db.run("SELECT ?", ['Hello World!'], function (err) {
if (err) {
console.error('[ERROR]', cluster.isMaster && '0' || cluster.worker.id);
console.error(err);
return;
}
console.log('[this]', cluster.isMaster && '0' || cluster.worker.id);
console.log(this);
});
});
});
```
API API
=== ===

View File

@ -45,7 +45,9 @@ function getConnection(opts) {
return startServer(opts).then(function (client) { return startServer(opts).then(function (client) {
// ws.masterClient = client; // ws.masterClient = client;
resolve({ masterClient: client }); resolve({ masterClient: client });
}, function () { }, function (err) {
console.error('[ERROR] failed to connect to sqlite3-cluster service. retrying...');
console.error(err);
retry(); retry();
}); });
} }
@ -102,7 +104,55 @@ function create(opts) {
var proto = sqlite3real.Database.prototype; var proto = sqlite3real.Database.prototype;
var messages = []; var messages = [];
function rpc(fname, args) { function init(opts) {
return new Promise(function (resolve) {
var id = Math.random();
ws.send(JSON.stringify({
type: 'init'
, args: [opts]
, func: 'init'
, filename: opts.filename
, id: id
}));
function onMessage(data) {
var cmd;
try {
cmd = JSON.parse(data.toString('utf8'));
} catch(e) {
console.error('[ERROR] in client, from sql server parse json');
console.error(e);
console.error(data);
console.error();
//ws.send(JSON.stringify({ type: 'error', value: { message: e.message, code: "E_PARSE_JSON" } }));
return;
}
if (cmd.id !== id) {
return;
}
if (cmd.self) {
cmd.args = [db];
}
messages.splice(messages.indexOf(onMessage), 1);
if ('error' === cmd.type) {
reject(cmd.args[0]);
return;
}
resolve(cmd.args[0]);
}
messages.push(onMessage);
});
}
function rpcThunk(fname, args) {
var id; var id;
var cb; var cb;
@ -142,6 +192,9 @@ function create(opts) {
return; return;
} }
if (cmd.self) {
cmd.args = [db];
}
cb.apply(cmd.this, cmd.args); cb.apply(cmd.this, cmd.args);
if ('on' !== fname) { if ('on' !== fname) {
@ -156,16 +209,19 @@ function create(opts) {
db.sanitize = require('./wrapper').sanitize; db.sanitize = require('./wrapper').sanitize;
db.escape = require('./wrapper').escape; db.escape = require('./wrapper').escape;
// TODO get methods from server (cluster-store does this)
// instead of using the prototype
Object.keys(sqlite3real.Database.prototype).forEach(function (key) { Object.keys(sqlite3real.Database.prototype).forEach(function (key) {
if ('function' === typeof proto[key]) { if ('function' === typeof proto[key]) {
db[key] = function () { db[key] = function () {
rpc(key, Array.prototype.slice.call(arguments)); rpcThunk(key, Array.prototype.slice.call(arguments));
}; };
} }
}); });
db.init = init;
ws.on('message', function (data) { ws.on('message', function (data) {
messages.forEach(function (fn) { messages.forEach(function (fn) {
try { try {

View File

@ -44,15 +44,48 @@ function createApp(server, options) {
switch(cmd.type) { switch(cmd.type) {
case 'init': case 'init':
db[cmd.func].apply(db, cmd.args).then(function () {
var args = Array.prototype.slice.call(arguments);
var myself;
if (args[0] === db) {
args = [];
myself = true;
}
ws.send(JSON.stringify({
id: cmd.id
, self: myself
, args: args
//, this: this
}));
});
break; break;
case 'rpc': case 'rpc':
if (!db._initialized) {
ws.send(JSON.stringify({
type: 'error'
, id: cmd.id
, args: [{ message: 'database has not been initialized' }]
, error: { message: 'database has not been initialized' }
}));
return;
}
cmd.args.push(function () { cmd.args.push(function () {
var args = Array.prototype.slice.call(arguments); var args = Array.prototype.slice.call(arguments);
var myself;
if (args[0] === db) {
args = [];
myself = true;
}
ws.send(JSON.stringify({ ws.send(JSON.stringify({
this: this this: this
, args: args , args: args
, self: myself
, id: cmd.id , id: cmd.id
})); }));
}); });

View File

@ -5,19 +5,16 @@ var cluster = require('cluster');
var numCores = require('os').cpus().length; var numCores = require('os').cpus().length;
var i; var i;
function run() { function testSelect(client) {
var sqlite3 = require('./cluster'); return client.run('CREATE TABLE IF NOT EXISTS meta (version TEXT)', function (err) {
if (err) {
console.error('[ERROR] create table', cluster.isMaster && '0' || cluster.worker.id);
console.error(err);
return;
}
return client.get("SELECT version FROM meta", [], function (err, result) {
return sqlite3.create({
key: '00000000000000000000000000000000'
, bits: 128
, filename: '/tmp/test.cluster.sqlcipher'
, verbose: null
, standalone: null
, serve: null
, connect: null
}).then(function (client) {
client.get("SELECT ?", ['Hello World!'], function (err, result) {
if (err) { if (err) {
console.error('[ERROR]', cluster.isMaster && '0' || cluster.worker.id); console.error('[ERROR]', cluster.isMaster && '0' || cluster.worker.id);
console.error(err); console.error(err);
@ -33,6 +30,43 @@ function run() {
}); });
} }
function init() {
var sqlite3 = require('./cluster');
return sqlite3.create({
bits: 128
, filename: '/tmp/test.cluster.sqlcipher'
, verbose: null
, standalone: null
, serve: null
, connect: null
}).then(function (client) {
console.log('[INIT] begin');
return client.init({ bits: 128, key: '00000000000000000000000000000000' });
}).then(testSelect, function (err) {
console.error('[ERROR]');
console.error(err);
}).then(function () {
console.log('success');
}, function (err) {
console.error('[ERROR 2]');
console.error(err);
});
}
function run() {
var sqlite3 = require('./cluster');
return sqlite3.create({
bits: 128
, filename: '/tmp/test.cluster.sqlcipher'
, verbose: null
, standalone: null
, serve: null
, connect: null
});//.then(testSelect);
}
if (cluster.isMaster) { if (cluster.isMaster) {
// not a bad idea to setup the master before forking the workers // not a bad idea to setup the master before forking the workers
run().then(function () { run().then(function () {
@ -41,7 +75,14 @@ if (cluster.isMaster) {
} }
}); });
} else { } else {
run(); if (1 === cluster.worker.id) {
init().then(testSelect);
return;
} else {
setTimeout(function () {
run().then(testSelect);
}, 100);
}
} }
// The native Promise implementation ignores errors because... dumbness??? // The native Promise implementation ignores errors because... dumbness???

View File

@ -19,43 +19,73 @@ function create(opts) {
sqlite3.verbose(); sqlite3.verbose();
} }
if (!dbs[opts.filename] || dbs[opts.filename].__key !== opts.key) { if (!dbs[opts.filename]) {
dbs[opts.filename] = new sqlite3.Database(opts.filename); dbs[opts.filename] = new sqlite3.Database(opts.filename);
} }
db = dbs[opts.filename]; db = dbs[opts.filename];
db.sanitize = sanitize; db.sanitize = sanitize;
db.escape = sanitize; db.escape = sanitize;
db.__key = opts.key;
db.init = function (newOpts) {
if (!newOpts) {
newOpts = {};
}
var key = newOpts.key || opts.key;
var bits = newOpts.bits || opts.bits;
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
db.serialize(function() { console.log('OPTS', opts);
console.log('BITS', bits);
if (db._initialized) {
resolve(db);
return;
}
if (!key) {
if (!bits) {
db._initialized = true;
}
resolve(db);
return;
}
// TODO test key length
db._initialized = true;
db.serialize(function () {
var setup = []; var setup = [];
if (opts.key) { if (!bits) {
// TODO test key length bits = 128;
if (!opts.bits) {
opts.bits = 128;
} }
// TODO db.run(sql, function () { resolve() }); // TODO db.run(sql, function () { resolve() });
setup.push(new Promise(function (resolve, reject) { setup.push(new Promise(function (resolve, reject) {
db.run("PRAGMA KEY = \"x'" + sanitize(opts.key) + "'\"", [], function (err) { db.run("PRAGMA KEY = \"x'" + sanitize(key) + "'\"", [], function (err) {
if (err) { reject(err); return; } if (err) { reject(err); return; }
resolve(this); resolve(this);
}); });
})); }));
setup.push(new Promise(function (resolve, reject) { setup.push(new Promise(function (resolve, reject) {
db.run("PRAGMA CIPHER = 'aes-" + sanitize(opts.bits) + "-cbc'", [], function (err) { //process.nextTick(function () {
db.run("PRAGMA CIPHER = 'aes-" + sanitize(bits) + "-cbc'", [], function (err) {
if (err) { reject(err); return; } if (err) { reject(err); return; }
resolve(this); resolve(this);
}); });
//});
})); }));
}
Promise.all(setup).then(function () { resolve(db); }, reject); Promise.all(setup).then(function () {
// restore original functions
resolve(db);
}, reject);
}); });
}); });
};
return db.init(opts);
} }
module.exports.sanitize = sanitize; module.exports.sanitize = sanitize;