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) {
|
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
|
||||||
===
|
===
|
||||||
|
|
||||||
|
|
64
client.js
64
client.js
|
@ -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 {
|
||||||
|
|
33
server.js
33
server.js
|
@ -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
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
@ -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???
|
||||||
|
|
50
wrapper.js
50
wrapper.js
|
@ -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) {
|
||||||
|
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 () {
|
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;
|
||||||
|
|
Loading…
Reference in New Issue