diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 54a350162cdea99ed069928217df0c21cf9814b7..8171cb45f80faedd7a842d2a3721caa9dab89e4b 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -85,7 +85,6 @@ Subsequent steps use different software which you might need to install. * **curl** - used to iniate http requests from the cmd line (to kick the browserid server) * **java** - used to minify css - * **libsqlite3-dev** - database libraries ### 4. Set up post-update hook diff --git a/README.md b/README.md index e84edefb0d776fd66d4553e07bd87f77352498a3..518808162dd1ff1aad9c10ecd7bd72330778e21b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ Here's the software you'll need installed: * node.js (>= 0.4.5): http://nodejs.org/ * npm: http://npmjs.org/ -* sqlite (3) development libraries: http://www.sqlite.org/ * Several node.js 3rd party libraries - see `package.json` for details ## Getting started: diff --git a/browserid/lib/db.js b/browserid/lib/db.js index c4efe440dae79a48af71b81b5972654be462cda3..56c59253c75fb5667ace08b18166a82aca5a7d30 100644 --- a/browserid/lib/db.js +++ b/browserid/lib/db.js @@ -11,7 +11,7 @@ function checkReady() { // a touch tricky cause client must set dbPath before releasing // control of the runloop exports.open = function(cfg, cb) { - var driverName = "sqlite"; + var driverName = "json"; if (cfg && cfg.driver) driverName = cfg.driver; try { driver = require('./db_' + driverName + '.js'); diff --git a/browserid/lib/db_sqlite.js b/browserid/lib/db_sqlite.js deleted file mode 100644 index e750ba79d5a21ed395d31771c24176c15114f985..0000000000000000000000000000000000000000 --- a/browserid/lib/db_sqlite.js +++ /dev/null @@ -1,383 +0,0 @@ -const -sqlite = require('sqlite'), -path = require('path'), -secrets = require('./secrets'); - -var VAR_DIR = path.join(path.dirname(__dirname), "var"); - -var db = new sqlite.Database(); - -var dbPath = path.join(VAR_DIR, "authdb.sqlite"); - -// async break allow database path to be configured by calling code -// a touch tricky cause client must set dbPath before releasing -// control of the runloop -exports.open = function(cfg, cb) { - if (cfg && cfg.path) dbPath = cfg.path; - db.open(dbPath, function (error) { - if (error) { - if (cb) cb("Couldn't open database: " + error); - throw error; - } - db.executeScript( - "CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, password TEXT );" + - "CREATE TABLE IF NOT EXISTS emails ( id INTEGER PRIMARY KEY, user INTEGER, address TEXT UNIQUE );" + - "CREATE TABLE IF NOT EXISTS keys ( id INTEGER PRIMARY KEY, email INTEGER, key TEXT, expires INTEGER )", - function (error) { - if (cb) cb(error); - }); - }); -}; - -exports.close = function(cb) { - db.close(function(err) { - ready = false; - cb(err); - }); -}; - -// accepts a function that will be invoked once the database is ready for transactions. -// this hook is important to pause the rest of application startup until async database -// connection establishment is complete. -exports.onReady = function(f) { - setTimeout(function() { - if (ready) f(); - else waiting.push(f); - }, 0); -}; - -// XXX: g_staged and g_stagedEmails should be moved into persistent/fast storage. - -// half created user accounts (pending email verification) -// OR -// half added emails (pending verification) -var g_staged = { -}; - -// an email to secret map for efficient fulfillment of isStaged queries -var g_stagedEmails = { -}; - -function executeTransaction(statements, cb) { - function executeTransaction2(statements, cb) { - if (statements.length == 0) cb(); - else { - var s = statements.shift(); - db.execute(s[0], s[1], function(err, rows) { - if (err) cb(err); - else executeTransaction2(statements, cb); - }); - } - } - - db.execute('BEGIN', function(err, rows) { - executeTransaction2(statements, function(err) { - if (err) cb(err); - else db.execute('COMMIT', function(err, rows) { - cb(err); - }); - }); - }); -} - -function emailToUserID(email, cb) { - db.execute( - 'SELECT users.id FROM emails, users WHERE emails.address = ? AND users.id == emails.user', - [ email ], - function (err, rows) { - if (rows && rows.length == 1) { - cb(rows[0].id); - } else { - if (err) console.log("database error: " + err); - cb(undefined); - } - }); -} - -exports.emailKnown = function(email, cb) { - db.execute( - "SELECT id FROM emails WHERE address = ?", - [ email ], - function(error, rows) { - cb(rows.length > 0); - }); -}; - -exports.isStaged = function(email, cb) { - if (cb) { - setTimeout(function() { - cb(g_stagedEmails.hasOwnProperty(email)); - }, 0); - } -}; - -function addEmailToAccount(existing_email, email, pubkey, cb) { - emailToUserID(existing_email, function(userID) { - if (userID == undefined) { - cb("no such email: " + existing_email, undefined); - } else { - executeTransaction([ - [ "INSERT INTO emails (user, address) VALUES(?,?)", [ userID, email ] ], - [ "INSERT INTO keys (email, key, expires) VALUES(last_insert_rowid(),?,?)", - [ pubkey, ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000)) ] - ] - ], function (error) { - if (error) cb(error); - else cb(); - }); - } - }); -} - -exports.emailsBelongToSameAccount = function(lhs, rhs, cb) { - emailToUserID(lhs, function(lhs_uid) { - emailToUserID(rhs, function(rhs_uid) { - cb(lhs_uid === rhs_uid); - }, function (error) { - cb(false); - }); - }, function (error) { - cb(false); - }); -}; - -exports.addKeyToEmail = function(existing_email, email, pubkey, cb) { - emailToUserID(existing_email, function(userID) { - if (userID == undefined) { - cb("no such email: " + existing_email, undefined); - return; - } - - db.execute("SELECT emails.id FROM emails,users WHERE users.id = ? AND emails.address = ? AND emails.user = users.id", - [ userID, email ], - function(err, rows) { - if (err || rows.length != 1) { - cb(err); - return; - } - executeTransaction([ - [ "INSERT INTO keys (email, key, expires) VALUES(?,?,?)", - [ rows[0].id, pubkey, ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000)) ] - ] - ], function (error) { - if (error) cb(error); - else cb(); - }); - }); - }); -} - -/* takes an argument object including email, password hash, and pubkey. */ -exports.stageUser = function(obj, cb) { - var secret = secrets.generate(48); - - // overwrite previously staged users - g_staged[secret] = { - type: "add_account", - email: obj.email, - pubkey: obj.pubkey, - pass: obj.hash - }; - - g_stagedEmails[obj.email] = secret; - setTimeout(function() { cb(secret); }, 0); -}; - -/* takes an argument object including email, pass, and pubkey. */ -exports.stageEmail = function(existing_email, new_email, pubkey, cb) { - var secret = secrets.generate(48); - // overwrite previously staged users - g_staged[secret] = { - type: "add_email", - existing_email: existing_email, - email: new_email, - pubkey: pubkey - }; - g_stagedEmails[new_email] = secret; - setTimeout(function() { cb(secret); }, 0); -}; - -/* invoked when a user clicks on a verification URL in their email */ -exports.gotVerificationSecret = function(secret, cb) { - if (!g_staged.hasOwnProperty(secret)) return cb("unknown secret"); - - // simply move from staged over to the emails "database" - var o = g_staged[secret]; - delete g_staged[secret]; - delete g_stagedEmails[o.email]; - if (o.type === 'add_account') { - exports.emailKnown(o.email, function(known) { - function createAccount() { - executeTransaction([ - [ "INSERT INTO users (password) VALUES(?)", [ o.pass ] ] , - [ "INSERT INTO emails (user, address) VALUES(last_insert_rowid(),?)", [ o.email ] ], - [ "INSERT INTO keys (email, key, expires) VALUES(last_insert_rowid(),?,?)", - [ o.pubkey, ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000)) ] - ] - ], function (error) { - if (error) cb(error); - else cb(); - }); - } - - // if this email address is known and a user has completed a re-verification of this email - // address, remove the email from the old account that it was associated with, and then - // create a brand new account with only this email. - // NOTE: this might be sub-optimal, but it's a dead simple approach that mitigates many attacks - // and gives us reasonable behavior (without explicitly supporting) in the face of shared email - // addresses. - if (known) { - exports.removeEmail(o.email, o.email, function (err) { - if (err) cb(err); - else createAccount(); - }); - } else { - createAccount(); - } - }); - } else if (o.type === 'add_email') { - exports.emailKnown(o.email, function(known) { - function addIt() { - addEmailToAccount(o.existing_email, o.email, o.pubkey, cb); - } - if (known) { - exports.removeEmail(o.email, o.email, function (err) { - if (err) cb(err); - else addIt(); - }); - } else { - addIt(); - } - }); - } else { - cb("internal error"); - } -}; - -// check authentication credentials for a given email address. This will invoke the -// users callback with the authentication (password/hash/whatever - the database layer -// doesn't care). callback will be passed undefined if email cannot be found -exports.checkAuth = function(email, cb) { - db.execute("SELECT users.password FROM emails, users WHERE users.id = emails.user AND emails.address = ?", - [ email ], - function (error, rows) { - cb(rows.length !== 1 ? undefined : rows[0].password); - }); -}; - -function emailHasPubkey(email, pubkey, cb) { - db.execute( - 'SELECT keys.key FROM keys, emails WHERE emails.address = ? AND keys.email = emails.id AND keys.key = ?', - [ email, pubkey ], - function(err, rows) { - cb(rows.length === 1); - }); -} - -/* a high level operation that attempts to sync a client's view with that of the - * server. email is the identity of the authenticated channel with the user, - * identities is a map of email -> pubkey. - * We'll return an object that expresses three different types of information: - * there are several things we need to express: - * 1. emails that the client knows about but we do not - * 2. emails that we know about and the client does not - * 3. emails that we both know about but who need to be re-keyed - * NOTE: it's not neccesary to differentiate between #2 and #3, as the client action - * is the same (regen keypair and tell us about it). - */ -exports.getSyncResponse = function(email, identities, cb) { - var respBody = { - unknown_emails: [ ], - key_refresh: [ ] - }; - - // get the user id associated with this account - emailToUserID(email, function(userID) { - if (userID === undefined) { - cb("no such email: " + email); - return; - } - db.execute( - 'SELECT address FROM emails WHERE ? = user', - [ userID ], - function (err, rows) { - if (err) cb(err); - else { - var emails = [ ]; - var keysToCheck = [ ]; - for (var i = 0; i < rows.length; i++) emails.push(rows[i].address); - - // #1 - for (var e in identities) { - if (emails.indexOf(e) == -1) respBody.unknown_emails.push(e); - else keysToCheck.push(e); - } - - // #2 - for (var e in emails) { - e = emails[e]; - if (!identities.hasOwnProperty(e)) respBody.key_refresh.push(e); - } - - // #3 -- yes, this is sub-optimal in terms of performance. when we - // move away from public keys this will be unnec. - if (keysToCheck.length) { - var checked = 0; - keysToCheck.forEach(function(e) { - emailHasPubkey(e, identities[e], function(v) { - checked++; - if (!v) respBody.key_refresh.push(e); - if (checked === keysToCheck.length) { - cb(undefined, respBody); - } - }); - }); - } else { - cb(undefined, respBody); - } - } - }); - }); -}; - -// get all public keys associated with an email address -exports.pubkeysForEmail = function(identity, cb) { - db.execute( - 'SELECT keys.key FROM keys, emails WHERE emails.address = ? AND keys.email = emails.id', - [ identity ], - function(err, rows) { - var keys = undefined; - if (!err && rows && rows.length) { - keys = [ ]; - for (var i = 0; i < rows.length; i++) keys.push(rows[i].key); - } - cb(keys); - }); -}; - -exports.removeEmail = function(authenticated_email, email, cb) { - // figure out the user, and remove Email only from addressed - // linked to the authenticated email address - emailToUserID(authenticated_email, function(user_id) { - executeTransaction([ - [ "delete from emails where emails.address = ? and user = ?", [ email,user_id ] ] , - [ "delete from keys where email in (select address from emails where emails.address = ? and user = ?)", [ email,user_id ] ], - ], function (error) { - if (error) cb(error); - else cb(); - }); - }); -}; - -exports.cancelAccount = function(authenticated_email, cb) { - emailToUserID(authenticated_email, function(user_id) { - executeTransaction([ - [ "delete from emails where user = ?", [ user_id ] ] , - [ "delete from keys where email in (select address from emails where user = ?)", [ user_id ] ], - [ "delete from users where id = ?", [ user_id ] ], - ], function (error) { - if (error) cb(error); - else cb(); - }); - }); -}; diff --git a/package.json b/package.json index 2a163613f9a549c7ea68d172b56de34dc5fa711e..581cc554d814a3242bb471dd5388cb66135aa4de 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ , "dependencies": { "express": "2.4.3" , "xml2js": "0.1.5" - , "sqlite": "1.0.3" , "nodemailer": "0.1.18" , "mustache": "0.3.1-dev" , "cookie-sessions": "0.0.2"