From ba0892d804d23b3285273f7390657eab4765997d Mon Sep 17 00:00:00 2001
From: Lloyd Hilaiel <lloyd@hilaiel.com>
Date: Fri, 22 Jul 2011 16:26:24 -0600
Subject: [PATCH] partial implementation of a trivial json database, further
 generalization of persistence layer

---
 browserid/lib/db_json.js   | 275 +++++++++++++++++++++++++++++++++++++
 browserid/lib/db_sqlite.js |  16 +--
 browserid/lib/secrets.js   |   6 +-
 browserid/tests/db-test.js |   2 +-
 4 files changed, 283 insertions(+), 16 deletions(-)
 create mode 100644 browserid/lib/db_json.js

diff --git a/browserid/lib/db_json.js b/browserid/lib/db_json.js
new file mode 100644
index 000000000..954007c38
--- /dev/null
+++ b/browserid/lib/db_json.js
@@ -0,0 +1,275 @@
+const
+path = require('path'),
+fs = require('fs'),
+secrets = require('./secrets');
+
+var VAR_DIR = path.join(path.dirname(__dirname), "var");
+
+var dbPath = path.join(VAR_DIR, "authdb.json");
+
+var db = [];
+var stagedEmails = { };
+var staged = { };
+
+function flush() {
+  fs.writeFileSync(JSON.stringify(db));
+}
+
+exports.open = function(cfg, cb) {
+  if (cfg && cfg.path) dbPath = cfg.path;
+  try {
+    db = JSON.parse(fs.readFileSync(dbPath));
+  } catch(e) {
+  }
+
+  setTimeout(cb, 0);
+};
+
+exports.close = function(cb) {
+  flush();
+  setTimeout(cb, 0);
+};
+
+exports.emailKnown = function(email, cb) {
+  for (var i = 0; i < db.length; i++) {
+    if (db[i].emails.hasOwnProperty(email)) {
+      setTimeout(function() { cb(true) }, 0);
+      return;
+    }
+  }
+  setTimeout(function() { cb(false) }, 0);
+};
+
+exports.isStaged = function(email, cb) {
+  if (cb) {
+    setTimeout(function() {
+      cb(stagedEmails.hasOwnProperty(email));
+    }, 0);
+  }
+};
+
+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();
+                 });
+               });
+  });
+}
+
+exports.stageUser = function(obj, cb) {
+  var secret = secrets.generate(48);
+
+  // overwrite previously staged users
+  staged[secret] = {
+    type: "add_account",
+    email: obj.email,
+    pubkey: obj.pubkey,
+    pass: obj.hash
+  };
+
+  stagedEmails[obj.email] = secret;
+  setTimeout(function() { cb(secret); }, 0);
+};
+
+exports.stageEmail = function(existing_email, new_email, pubkey, cb) {
+  var secret = secrets.generate(48);
+  // overwrite previously staged users
+  staged[secret] = {
+    type: "add_email",
+    existing_email: existing_email,
+    email: new_email,
+    pubkey: pubkey
+  };
+  stagedEmails[new_email] = secret;
+  setTimeout(function() { cb(secret); }, 0);
+};
+
+exports.gotVerificationSecret = function(secret, cb) {
+  if (!staged.hasOwnProperty(secret)) return cb("unknown secret");
+
+  // simply move from staged over to the emails "database"
+  var o = staged[secret];
+  delete staged[secret];
+  delete 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");
+  }
+};
+
+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);
+             });
+};
+
+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);
+          }
+        }
+      });
+  });
+};
+
+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/browserid/lib/db_sqlite.js b/browserid/lib/db_sqlite.js
index 8fbeb0ff8..7d8295ac7 100644
--- a/browserid/lib/db_sqlite.js
+++ b/browserid/lib/db_sqlite.js
@@ -1,6 +1,7 @@
 const
 sqlite = require('sqlite'),
-path = require('path');
+path = require('path'),
+secrets = require('./secrets');
 
 var VAR_DIR = path.join(path.dirname(__dirname), "var");
 
@@ -110,15 +111,6 @@ exports.isStaged = function(email, cb) {
   }
 };
 
-function generateSecret() {
-  var str = "";
-  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
-  for (var i=0; i < 48; i++) {
-    str += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
-  }
-  return str;
-}
-
 function addEmailToAccount(existing_email, email, pubkey, cb) {
   emailToUserID(existing_email, function(userID) {
     if (userID == undefined) {
@@ -177,7 +169,7 @@ exports.addKeyToEmail = function(existing_email, email, pubkey, cb) {
 
 /* takes an argument object including email, password hash, and pubkey. */
 exports.stageUser = function(obj, cb) {
-  var secret = generateSecret();
+  var secret = secrets.generate(48);
 
   // overwrite previously staged users
   g_staged[secret] = {
@@ -193,7 +185,7 @@ exports.stageUser = function(obj, cb) {
 
 /* takes an argument object including email, pass, and pubkey. */
 exports.stageEmail = function(existing_email, new_email, pubkey, cb) {
-  var secret = generateSecret();
+  var secret = secrets.generate(48);
   // overwrite previously staged users
   g_staged[secret] = {
     type: "add_email",
diff --git a/browserid/lib/secrets.js b/browserid/lib/secrets.js
index 7aca40b98..1bd2eb46a 100644
--- a/browserid/lib/secrets.js
+++ b/browserid/lib/secrets.js
@@ -2,10 +2,10 @@ const
 path = require('path'),
 fs = require('fs');
 
-function generateSecret() {
+exports.generate = function(chars) {
   var str = "";
   const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
-  for (var i=0; i < 128; i++) {
+  for (var i=0; i < chars; i++) {
     str += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
   }
   return str;
@@ -19,7 +19,7 @@ exports.hydrateSecret = function(name, dir) {
   try{ secret = fs.readFileSync(p).toString(); } catch(e) {};
 
   if (secret === undefined) {
-    secret = generateSecret();
+    secret = exports.generateSecret(128);
     fs.writeFileSync(p, secret);
   }
   return secret;
diff --git a/browserid/tests/db-test.js b/browserid/tests/db-test.js
index 809e1cea7..a65f842a6 100755
--- a/browserid/tests/db-test.js
+++ b/browserid/tests/db-test.js
@@ -25,7 +25,7 @@ suite.addBatch({
   },
   "opening the database": {
     topic: function() {
-      db.open({ path: dbPath }, this.callback);
+      db.open({ /* driver: 'json', */ path: dbPath }, this.callback);
     },
     "and its ready": function(r) {
       assert.isUndefined(r);
-- 
GitLab