diff --git a/sea.js b/sea.js index 067da9dc..47562199 100644 --- a/sea.js +++ b/sea.js @@ -45,7 +45,7 @@ user.create = User.create; // attach a factory method to it. user.auth = User.auth; // and a login method. user.leave = User.leave; // and a logout method. - // TODO: definitely needed this: user.delete = User.delete; + user.delete = User.delete; // and, account delete method. return user; // return the user! } @@ -71,12 +71,53 @@ }()); + // This internal auth func - more used in future... + function authenticate(alias,pass,root){ + return new Promise(function(resolve, reject){ + // load all public keys associated with the username alias we want to log in with. + root.get('alias/'+alias).get(function(at, ev){ + ev.off(); + if(!at.put){ + // if no user, don't do anything. + var err = 'No user!'; + Gun.log(err); + return reject({err: err}); + } + // then attempt to log into each one until we find ours! + // (if two users have the same username AND the same password... that would be bad) + Gun.obj.map(at.put, function(val, key){ + // grab the account associated with this public key. + root.get(key).get(function(at, ev){ + key = key.slice(4); + ev.off(); + if(!at.put){return} // reject({err: 'Public key does not exist!'}) + // attempt to PBKDF2 extend the password with the salt. (Verifying the signature gives us the plain text salt.) + SEA.read(at.put.salt, key).then(function(salt){ + return SEA.proof(pass, salt); + }).then(function(proof){ + // the proof of work is evidence that we've spent some time/effort trying to log in, this slows brute force. + return SEA.read(at.put.auth, key).then(function(auth){ + return SEA.de(auth, proof); + }).catch(function(){reject({err: 'Failed to decrypt private key!'})}); + }).then(function(priv){ + // now we have AES decrypted the private key, from when we encrypted it with the proof at registration. + // if we were successful, then that meanswe're logged in! + return (priv && resolve({pub: key, priv: priv, at: at})) + // Or else we failed to log in... + || reject({err: 'Failed to decrypt private key!'}); + }).catch(function(){reject({err: 'Failed to create proof!'})}); + }); + }); + }); + }); + }; + // How does it work? function User(){}; // Well first we have to actually create a user. That is what this function does. User.create = function(alias, pass, cb){ var root = this.back(-1); - var doCreate = function(resolve, reject){ + var doIt = function(resolve, reject){ // Because more than 1 user might have the same username, we treat the alias as a list of those users. root.get('alias/'+alias).get(function(at, ev){ ev.off(); @@ -118,109 +159,89 @@ }); }); }; - if (cb){doCreate(cb, cb)} else {return new Promise(doCreate)} + if (cb){doIt(cb, cb)} else {return new Promise(doIt)} }; // now that we have created a user, we want to authenticate them! - User.auth = function(alias, pass, cb, opt){ + User.auth = function(alias,pass,cb,opt){ var opts = opt || (typeof cb !== 'function' && cb) || {}; var root = this.back(-1); cb = typeof cb === 'function' && cb; - var doAuth = function(resolve, reject){ - // load all public keys associated with the username alias we want to log in with. - root.get('alias/'+alias).get(function(at, ev){ - ev.off(); - if(!at.put){ - // if no user, don't do anything. - var err = 'No user!'; - Gun.log(err); - return reject({err: err}); + var doIt = function(resolve, reject){ + authenticate(alias, pass, root).then(function(key){ + // we're logged in! + function doLogin(){ + var user = root._.user; + // add our credentials in-memory only to our root gun instance + user._ = key.at.gun._; + // so that way we can use the credentials to encrypt/decrypt data + user._.is = user.is = {}; + // that is input/output through gun (see below) + user._.sea = key.priv; + user._.pub = key.pub; + //console.log("authorized", user._); + // callbacks success with the user data credentials. + resolve(user._); + // emit an auth event, useful for page redirects and stuff. + Gun.on('auth', user._); } - // then attempt to log into each one until we find ours! - // (if two users have the same username AND the same password... that would be bad) - Gun.obj.map(at.put, function(val, key){ - // grab the account associated with this public key. - root.get(key).get(function(at, ev){ - key = key.slice(4); - ev.off(); - if(!at.put){return} - // attempt to PBKDF2 extend the password with the salt. (Verifying the signature gives us the plain text salt.) - SEA.read(at.put.salt, key).then(function(salt){ - return SEA.proof(pass, salt); - }).then(function(proof){ - // the proof of work is evidence that we've spent some time/effort trying to log in, this slows brute force. - return SEA.read(at.put.auth, key).then(function(auth){ - return SEA.de(auth, proof); + if(opts.newpass) { + // password update so encrypt private key using new pwd + salt + var newsalt = Gun.text.random(64); + SEA.proof(opts.newpass, newsalt).then(function(proof){ + SEA.en(key.priv, proof).then(function(encVal){ + return SEA.write(encVal, key.priv).then(function(sAuth){ + return { pub: key.pub, auth: sAuth }; }); - }).then(function(priv){ - // now we have AES decrypted the private key, from when we encrypted it with the proof at registration. - if(priv){ // if we were successful, then that means... - // we're logged in! - function doLogin(){ - var user = root._.user; - // add our credentials in-memory only to our root gun instance - user._ = at.gun._; - // so that way we can use the credentials to encrypt/decrypt data - user._.is = user.is = {}; - // that is input/output through gun (see below) - user._.sea = priv; - user._.pub = key; - //console.log("authorized", user._); - // callbacks success with the user data credentials. - resolve(user._); - // emit an auth event, useful for page redirects and stuff. - Gun.on('auth', user._); - } - if(opts.newpass) { - // password update so encrypt private key using new pwd + salt - var newsalt = Gun.text.random(64); - SEA.proof(opts.newpass, newsalt).then(function(proof){ - SEA.en(priv, proof).then(function(encVal){ - return SEA.write(encVal, priv).then(function(sAuth){ - return { pub: key, auth: sAuth }; - }); - }).then(function(user){ - return SEA.write(alias, priv).then(function(sAlias){ - user.alias = sAlias; return user; - }); - }).then(function(user){ - return SEA.write(newsalt, priv).then(function(sSalt){ - user.salt = sSalt; return user; - }); - }).then(function(user){ - var tmp = 'pub/'+key; - // awesome, now we can update the user using public key ID. - root.get(tmp).put(user); - // then we're done - doLogin(); - }); - }); - } else { - doLogin(); - } - return; - } - // Or else we failed to log in... - }).catch(function(e){ - Gun.log('Failed to sign in!'); - reject({err: 'Attempt failed'}); + }).then(function(user){ + return SEA.write(alias, key.priv).then(function(sAlias){ + user.alias = sAlias; return user; + }); + }).then(function(user){ + return SEA.write(newsalt, key.priv).then(function(sSalt){ + user.salt = sSalt; return user; + }); + }).then(function(user){ + var tmp = 'pub/'+key.pub; + // awesome, now we can update the user using public key ID. + root.get(tmp).put(user); + // then we're done + doLogin(); }); }); - // if (!found) { - // reject({err: 'Public key does not exist!'}) - // } - }); + } else { + doLogin(); + } + return; + }).catch(function(e){ + Gun.log('Failed to sign in!'); + reject({err: 'Auth attempt failed'}); }); }; - if (cb){doAuth(cb, cb)} else {return new Promise(doAuth)} + if (cb){doIt(cb, cb)} else {return new Promise(doIt)} }; // now that we authenticated a user, we want to support logout too! User.leave = function(cb){ var root = this.back(-1); - var doLogout = function(resolve, reject){ + var doIt = function(resolve, reject){ root._.user = root.chain(); resolve({ok: 0}); } - if (cb){doLogout(cb, cb)} else {return new Promise(doLogout)} + if (cb){doIt(cb, cb)} else {return new Promise(doIt)} + }; + // If authenticated user wants to delete his/her account, let's support it! + User.delete = function(alias,pass,cb){ + var root = this.back(-1); + var doIt = function(resolve, reject){ + authenticate(alias, pass, root).then(function(key){ + root.get(key.pub).put(null); + root._.user = root.chain(); + resolve({ok: 0}); + }).catch(function(e){ + Gun.log('User.delete failed! Error:', e); + reject({err: 'Delete attempt failed! Reason:'+(e && e.err) || e || ''}); + }); + } + if (cb){doIt(cb, cb)} else {return new Promise(doIt)} }; // After we have a GUN extension to make user registration/login easy, we then need to handle everything else. @@ -372,14 +393,14 @@ }; // Does enc/dec key like OpenSSL - works with CryptoJS encryption/decryption - function makeKey(p, s) { - var ps = Buffer.concat([ new Buffer(p, 'utf8'), s ]); + function makeKey(p,s){ + var ps = Buffer.concat([new Buffer(p, 'utf8'), s]); var h128 = new Buffer(nodeCrypto.createHash('md5').update(ps).digest('hex'), 'hex'); // TODO: 'md5' is insecure, do we need OpenSSL compatibility anymore ? return Buffer.concat([ h128, new Buffer(nodeCrypto.createHash('md5').update( - Buffer.concat([ h128, ps ]).toString('base64'), 'base64' + Buffer.concat([h128, ps]).toString('base64'), 'base64' ).digest('hex'), 'hex') ]); } @@ -391,7 +412,7 @@ // create a wrapper library around NodeJS crypto & ecCrypto and Web Crypto API. // now wrap the various AES, ECDSA, PBKDF2 functions we called above. SEA.proof = function(pass,salt,cb){ - var doProof = (typeof window !== 'undefined' && function(resolve, reject){ + var doIt = (typeof window !== 'undefined' && function(resolve, reject){ crypto.subtle.importKey( // For browser crypto.subtle works fine 'raw', new TextEncoder().encode(pass), {name: 'PBKDF2'}, false, ['deriveBits'] ).then(function(key){ @@ -409,29 +430,29 @@ resolve(!err && hash && hash.toString('base64')); }); }; - if(cb){doProof(cb, function(){cb()})} else {return new Promise(doProof)} + if(cb){doIt(cb, function(){cb()})} else {return new Promise(doIt)} }; SEA.pair = function(cb){ - var doPair = function(resolve, reject){ + var doIt = function(resolve, reject){ var priv = nodeCrypto.randomBytes(32); resolve({ pub: new Buffer(ecCrypto.getPublic(priv), 'binary').toString('hex'), priv: new Buffer(priv, 'binary').toString('hex') }); }; - if(cb){doPair(cb, function(){cb()})} else {return new Promise(doPair)} + if(cb){doIt(cb, function(){cb()})} else {return new Promise(doIt)} }; SEA.derive = function(m,p,cb){ - var doDerive = function(resolve, reject){ + var doIt = function(resolve, reject){ ecCrypto.derive(new Buffer(p, 'hex'), new Buffer(m, 'hex')) .then(function(secret){ resolve(new Buffer(secret, 'binary').toString('hex')); }).catch(function(e){Gun.log(e); reject(e)}); }; - if(cb){doDerive(cb, function(){cb()})} else {return new Promise(doDerive)} + if(cb){doIt(cb, function(){cb()})} else {return new Promise(doIt)} }; - SEA.sign = function(m, p, cb){ - var doSign = function(resolve, reject){ + SEA.sign = function(m,p,cb){ + var doIt = function(resolve, reject){ ecCrypto.sign( new Buffer(p, 'hex'), nodeCrypto.createHash(nHash).update(JSON.stringify(m), 'utf8').digest() @@ -439,20 +460,20 @@ resolve(new Buffer(sig, 'binary').toString('hex')); }).catch(function(e){Gun.log(e); reject(e)}); }; - if(cb){doSign(cb, function(){cb()})} else {return new Promise(doSign)} + if(cb){doIt(cb, function(){cb()})} else {return new Promise(doIt)} }; SEA.verify = function(m, p, s, cb){ - var doVerify = function(resolve, reject){ + var doIt = function(resolve, reject){ ecCrypto.verify( new Buffer(p, 'hex'), nodeCrypto.createHash(nHash).update(JSON.stringify(m), 'utf8').digest(), new Buffer(s, 'hex') ).then(function(){resolve(true)}).catch(function(e){Gun.log(e);reject(e)}) }; - if(cb){doVerify(cb, function(){cb()})} else {return new Promise(doVerify)} + if(cb){doIt(cb, function(){cb()})} else {return new Promise(doIt)} }; SEA.en = function(m,p,cb){ - var doEncrypt = function(resolve, reject){ + var doIt = function(resolve, reject){ var s = nodeCrypto.randomBytes(8); var iv = nodeCrypto.randomBytes(16); var r = {iv: iv.toString('hex'), s: s.toString('hex')}; @@ -476,10 +497,10 @@ resolve(JSON.stringify(r)); } }; - if(cb){doEncrypt(cb, function(){cb()})} else {return new Promise(doEncrypt)} + if(cb){doIt(cb, function(){cb()})} else {return new Promise(doIt)} }; SEA.de = function(m,p,cb){ - var doDecrypt = function(resolve, reject){ + var doIt = function(resolve, reject){ var d = JSON.parse(m); var key = makeKey(p, new Buffer(d.s, 'hex')); var iv = new Buffer(d.iv, 'hex'); @@ -502,20 +523,18 @@ resolve(r); } }; - if(cb){doDecrypt(cb, function(){cb()})} else {return new Promise(doDecrypt)} + if(cb){doIt(cb, function(){cb()})} else {return new Promise(doIt)} }; SEA.write = function(m,p,cb){ - var doSign = function(resolve, reject) { + var doIt = function(resolve, reject) { SEA.sign(m, p).then(function(signature){ resolve('SEA'+JSON.stringify([m,signature])); }).catch(function(e){Gun.log(e); reject(e)}); }; - if(cb){doSign(cb, function(){cb()})} else {return new Promise(doSign)} - // TODO: what's this ? - // return JSON.stringify([m,SEA.sign(m,p)]); + if(cb){doIt(cb, function(){cb()})} else {return new Promise(doIt)} }; SEA.read = function(m,p,cb){ - var doRead = function(resolve, reject) { + var doIt = function(resolve, reject) { if(!m){ return resolve(); } if(!m.slice || 'SEA[' !== m.slice(0,4)){ return resolve(m); } m = m.slice(3); @@ -526,7 +545,7 @@ resolve(ok && m[0]); }); }; - if(cb){doRead(cb, function(){cb()})} else {return new Promise(doRead)} + if(cb){doIt(cb, function(){cb()})} else {return new Promise(doIt)} }; Gun.SEA = SEA; diff --git a/test/common.js b/test/common.js index 4a246d4a..5362db5e 100644 --- a/test/common.js +++ b/test/common.js @@ -8046,9 +8046,11 @@ describe('Gun', function(){ describe('create', function(){ it('new', function(done){ var check = function(ack){ - expect(ack).to.not.be(undefined); - expect(ack).to.not.be(''); - expect(ack).to.have.keys(['ok','pub']); + try{ + expect(ack).to.not.be(undefined); + expect(ack).to.not.be(''); + expect(ack).to.have.keys(['ok','pub']); + }catch(e){done(e); return}; done(); }; // Gun.user.create - creates new user @@ -8061,11 +8063,13 @@ describe('Gun', function(){ it('conflict', function(done){ Gun.log.off = true; // Supress all console logging var check = function(ack){ - expect(ack).to.not.be(undefined); - expect(ack).to.not.be(''); - expect(ack).to.have.key('err'); - expect(ack.err).not.to.be(undefined); - expect(ack.err).not.to.be(''); + try{ + expect(ack).to.not.be(undefined); + expect(ack).to.not.be(''); + expect(ack).to.have.key('err'); + expect(ack.err).not.to.be(undefined); + expect(ack.err).not.to.be(''); + }catch(e){done(e); return}; done(); }; // Gun.user.create - fails to create existing user @@ -8082,9 +8086,11 @@ describe('Gun', function(){ describe('auth', function(){ it('login', function(done){ var check = function(ack){ - expect(ack).to.not.be(undefined); - expect(ack).to.not.be(''); - expect(ack).to.not.have.key('err'); + try{ + expect(ack).to.not.be(undefined); + expect(ack).to.not.be(''); + expect(ack).to.not.have.key('err'); + }catch(e){done(e); return}; done(); }; // Gun.user.auth - authenticates existing user @@ -8097,11 +8103,13 @@ describe('Gun', function(){ it('wrong password', function(done){ var check = function(ack){ - expect(ack).to.not.be(undefined); - expect(ack).to.not.be(''); - expect(ack).to.have.key('err'); - expect(ack.err).to.not.be(undefined); - expect(ack.err).to.not.be(''); + try{ + expect(ack).to.not.be(undefined); + expect(ack).to.not.be(''); + expect(ack).to.have.key('err'); + expect(ack.err).to.not.be(undefined); + expect(ack.err).to.not.be(''); + }catch(e){done(e); return}; done(); }; if(type === 'callback'){ @@ -8115,11 +8123,13 @@ describe('Gun', function(){ it('unknown alias', function(done){ var check = function(ack){ - expect(ack).to.not.be(undefined); - expect(ack).to.not.be(''); - expect(ack).to.have.key('err'); - expect(ack.err).to.not.be(undefined); - expect(ack.err).to.not.be(''); + try{ + expect(ack).to.not.be(undefined); + expect(ack).to.not.be(''); + expect(ack).to.have.key('err'); + expect(ack.err).to.not.be(undefined); + expect(ack.err).to.not.be(''); + }catch(e){done(e); return}; done(); }; if(type === 'callback'){ @@ -8133,9 +8143,11 @@ describe('Gun', function(){ it('new password', function(done){ var check = function(ack){ - expect(ack).to.not.be(undefined); - expect(ack).to.not.be(''); - expect(ack).to.not.have.key('err'); + try{ + expect(ack).to.not.be(undefined); + expect(ack).to.not.be(''); + expect(ack).to.not.have.key('err'); + }catch(e){done(e); return}; done(); }; // Gun.user.auth - with newpass props sets new password @@ -8149,11 +8161,13 @@ describe('Gun', function(){ it('failed new password', function(done){ var check = function(ack){ - expect(ack).to.not.be(undefined); - expect(ack).to.not.be(''); - expect(ack).to.have.key('err'); - expect(ack.err).to.not.be(undefined); - expect(ack.err).to.not.be(''); + try{ + expect(ack).to.not.be(undefined); + expect(ack).to.not.be(''); + expect(ack).to.have.key('err'); + expect(ack.err).to.not.be(undefined); + expect(ack.err).to.not.be(''); + }catch(e){done(e); return}; done(); }; var props = {alias: alias+type, pass: pass+'not', newpass: pass+' new'}; @@ -8173,18 +8187,22 @@ describe('Gun', function(){ describe('leave', function(){ it('valid session', function(done){ var check = function(ack){ - expect(ack).to.not.be(undefined); - expect(ack).to.not.be(''); - expect(ack).to.not.have.key('err'); - expect(ack).to.have.key('ok'); - expect(gun.back(-1)._.user).to.not.have.keys(['sea', 'pub']); + try{ + expect(ack).to.not.be(undefined); + expect(ack).to.not.be(''); + expect(ack).to.not.have.key('err'); + expect(ack).to.have.key('ok'); + expect(gun.back(-1)._.user).to.not.have.keys(['sea', 'pub']); + }catch(e){done(e); return}; done(); }; user.auth(alias+type, pass+' new').then(function(usr){ - expect(usr).to.not.be(undefined); - expect(usr).to.not.be(''); - expect(usr).to.not.have.key('err'); - expect(usr).to.have.key('put'); + try{ + expect(usr).to.not.be(undefined); + expect(usr).to.not.be(''); + expect(usr).to.not.have.key('err'); + expect(usr).to.have.key('put'); + }catch(e){done(e); return}; // Gun.user.leave - performs logout for authenticated user if(type === 'callback'){ user.leave(check); @@ -8196,10 +8214,12 @@ describe('Gun', function(){ it('no session', function(done){ var check = function(ack){ - expect(ack).to.not.be(undefined); - expect(ack).to.not.be(''); - expect(ack).to.not.have.key('err'); - expect(ack).to.have.key('ok'); + try{ + expect(ack).to.not.be(undefined); + expect(ack).to.not.be(''); + expect(ack).to.not.have.key('err'); + expect(ack).to.have.key('ok'); + }catch(e){done(e); return}; done(); }; user.leave().then(function(ack){ @@ -8213,9 +8233,44 @@ describe('Gun', function(){ }); }); - describe('delete - TODO: how?', function(){ - it.skip('existing authenticated user'); - it.skip('unauthenticated user'); + describe('delete', function(){ + it('existing authenticated user', function(done){ + var check = function(ack){ + try{ + expect(ack).to.not.be(undefined); + expect(ack).to.not.be(''); + expect(ack).to.not.have.key('err'); + expect(ack).to.have.key('ok'); + expect(gun.back(-1)._.user).to.not.have.keys(['sea', 'pub']); + }catch(e){done(e); return}; + done(); + }; + var aalias = alias+type+'del'; + user.create(aalias, pass).catch(check).then(function(ack){ + try{ + expect(ack).to.not.be(undefined); + expect(ack).to.not.be(''); + expect(ack).to.have.keys(['ok','pub']); + }catch(e){done(e); return}; + user.auth(aalias, pass).then(function(usr){ + try{ + expect(usr).to.not.be(undefined); + expect(usr).to.not.be(''); + expect(usr).to.not.have.key('err'); + expect(usr).to.have.key('put'); + }catch(e){done(e); return}; + // Gun.user.delete - deƶetes existing user account + if(type === 'callback'){ + user.delete(alias+type, pass+' new', check); + } else { + user.delete(alias+type, pass+' new').then(check).catch(done); + } + }).catch(done); + }); + }); + + it.skip('unauthenticated existing user'); + it.skip('unauthenticated other user'); }); describe('recall', function(){