enable lazy initialization of aes cipher key
This commit is contained in:
parent
1a42430b8f
commit
bece314c34
54
README.md
54
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
|
||||
===
|
||||
|
||||
|
|
64
client.js
64
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 {
|
||||
|
|
33
server.js
33
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
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -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???
|
||||
|
|
58
wrapper.js
58
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;
|
||||
|
|
Loading…
Reference in New Issue