From bece314c3433e06e778417a17ec6eb533716154c Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 28 Jul 2015 18:04:54 -0600 Subject: [PATCH] enable lazy initialization of aes cipher key --- README.md | 54 ++++++++++++++++++++++++++++++++++++++- client.js | 64 +++++++++++++++++++++++++++++++++++++++++++--- server.js | 33 ++++++++++++++++++++++++ test-cluster.js | 67 +++++++++++++++++++++++++++++++++++++++---------- wrapper.js | 58 +++++++++++++++++++++++++++++++----------- 5 files changed, 244 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 85a09ac..91ceb9a 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ var opts = { sqlite.create(opts).then(function (db) { // same api as new sqlite3.Database(options.filename) - client.run("SELECT ?", ['Hello World!'], function (err) { + db.run("SELECT ?", ['Hello World!'], function (err) { if (err) { console.error('[ERROR]', cluster.isMaster && '0' || cluster.worker.id); 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`. +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 === diff --git a/client.js b/client.js index 03be23a..d29fbb0 100644 --- a/client.js +++ b/client.js @@ -45,7 +45,9 @@ function getConnection(opts) { return startServer(opts).then(function (client) { // ws.masterClient = client; resolve({ masterClient: client }); - }, function () { + }, function (err) { + console.error('[ERROR] failed to connect to sqlite3-cluster service. retrying...'); + console.error(err); retry(); }); } @@ -102,7 +104,55 @@ function create(opts) { var proto = sqlite3real.Database.prototype; 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 cb; @@ -142,6 +192,9 @@ function create(opts) { return; } + if (cmd.self) { + cmd.args = [db]; + } cb.apply(cmd.this, cmd.args); if ('on' !== fname) { @@ -156,16 +209,19 @@ function create(opts) { db.sanitize = require('./wrapper').sanitize; 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) { if ('function' === typeof proto[key]) { 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) { messages.forEach(function (fn) { try { diff --git a/server.js b/server.js index 89f3a08..eec7d4d 100644 --- a/server.js +++ b/server.js @@ -44,15 +44,48 @@ function createApp(server, options) { switch(cmd.type) { 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; 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 () { var args = Array.prototype.slice.call(arguments); + var myself; + + if (args[0] === db) { + args = []; + myself = true; + } ws.send(JSON.stringify({ this: this , args: args + , self: myself , id: cmd.id })); }); diff --git a/test-cluster.js b/test-cluster.js index 8caa0be..6148201 100644 --- a/test-cluster.js +++ b/test-cluster.js @@ -5,19 +5,16 @@ var cluster = require('cluster'); var numCores = require('os').cpus().length; var i; -function run() { - var sqlite3 = require('./cluster'); +function testSelect(client) { + 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) { console.error('[ERROR]', cluster.isMaster && '0' || cluster.worker.id); 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) { // not a bad idea to setup the master before forking the workers run().then(function () { @@ -41,7 +75,14 @@ if (cluster.isMaster) { } }); } 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??? diff --git a/wrapper.js b/wrapper.js index a959bb2..7c10144 100644 --- a/wrapper.js +++ b/wrapper.js @@ -19,43 +19,73 @@ function create(opts) { sqlite3.verbose(); } - if (!dbs[opts.filename] || dbs[opts.filename].__key !== opts.key) { + if (!dbs[opts.filename]) { dbs[opts.filename] = new sqlite3.Database(opts.filename); } db = dbs[opts.filename]; db.sanitize = sanitize; db.escape = sanitize; - db.__key = opts.key; - return new Promise(function (resolve, reject) { - db.serialize(function() { - var setup = []; + db.init = function (newOpts) { + if (!newOpts) { + newOpts = {}; + } - if (opts.key) { - // TODO test key length - if (!opts.bits) { - opts.bits = 128; + var key = newOpts.key || opts.key; + var bits = newOpts.bits || opts.bits; + + return new Promise(function (resolve, reject) { + 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 = []; + + if (!bits) { + bits = 128; } // TODO db.run(sql, function () { resolve() }); 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; } resolve(this); }); })); 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; } 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;