From 0adfcb3b032dfaa11b08ec174d74e7d619e39542 Mon Sep 17 00:00:00 2001 From: mhelander Date: Thu, 7 Sep 2017 00:49:34 +0300 Subject: [PATCH] All test cases completed & some bugfixes & 'remember-me' recovery with PIN now supported --- sea.js | 278 ++++++++++++++++++++++++++++------------------------ test/sea.js | 257 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 359 insertions(+), 176 deletions(-) diff --git a/sea.js b/sea.js index 116f09f7..26ce443c 100644 --- a/sea.js +++ b/sea.js @@ -14,7 +14,7 @@ var crypto, TextEncoder, TextDecoder, localStorage, sessionStorage; - if (typeof window !== 'undefined') { + if(typeof window !== 'undefined'){ crypto = window.crypto; TextEncoder = window.TextEncoder; TextDecoder = window.TextDecoder; @@ -46,12 +46,17 @@ enc: 'aes-256-cbc' }; + var _initial_authsettings = { + validity: 12 * 60 * 60, // internally in seconds : 12 hours + session: true, + hook: function(props){ return props } // { iat, exp, alias, remember } + // or return new Promise(function(resolve, reject){(resolve(props))}) + } // These are used to persist user's authentication "session" var authsettings = { - validity: 60 * 60 * 12, // 12 hours - session: true, - hook: function(props) { return props } // { iat, exp, alias, remember } - // or return new Promise(function(resolve, reject){(resolve(props))}) + validity: _initial_authsettings.validity, + session: _initial_authsettings.session, + hook: _initial_authsettings.hook }; // let's extend the gun chain with a `user` function. @@ -84,7 +89,7 @@ // if no user, don't do anything. var err = 'No user!'; Gun.log(err); - return reject({err: err}); + return reject(err); } // then figuring out all possible candidates having matching username var aliases = []; @@ -99,7 +104,7 @@ }); }); return aliases.length && resolve(aliases) - || reject({err: 'Public key does not exist!'}) + || reject('Public key does not exist!') }); }); } @@ -171,7 +176,7 @@ return function(props){ return new Promise(function(resolve, reject){ if(!Gun.obj.has(props, 'alias')){ return resolve() } - if (proof && Gun.obj.has(props, 'iat')) { + if(proof && Gun.obj.has(props, 'iat')){ props.proof = proof; delete props.remember; // Not stored if present @@ -181,18 +186,16 @@ return SEA.write(JSON.stringify(remember), priv).then(function(signed){ sessionStorage.setItem('user', props.alias); sessionStorage.setItem('remember', signed); - if (!protected) { + if(!protected){ localStorage.removeItem('remember'); } return !protected || SEA.en(protected, pin).then(function(encrypted){ - return encrypted && SEA.write(encrypted, priv) - .then(function(encsig){ + return encrypted && SEA.write(encrypted, priv).then(function(encsig){ localStorage.setItem('remember', encsig); }).catch(reject); }).catch(reject); - }).then(function(){ - resolve(props); - }).catch(function(e){ reject({err: 'Session persisting failed!'}) }); + }).then(function(){ resolve(props); }) + .catch(function(e){ reject({err: 'Session persisting failed!'}) }); } else { localStorage.removeItem('remember'); sessionStorage.removeItem('user'); @@ -214,39 +217,66 @@ // ELSE if no PIN then window.sessionStorage var pin = Gun.obj.has(opts, 'pin') && opts.pin && new Buffer(opts.pin, 'utf8').toString('base64'); - var args = { alias: user.alias }; - if(proof && authsettings.validity){ + if(proof && user && user.alias && authsettings.validity){ + var args = { alias: user.alias }; args.iat = Math.ceil(Date.now() / 1000); // seconds - args.exp = authsettings.validity * 60; // seconds - if (Gun.obj.has(opts, 'pin')){ + args.exp = authsettings.validity; // seconds + if(Gun.obj.has(opts, 'pin')){ args.remember = true; // for hook - not stored } var props = authsettings.hook(args); if(props instanceof Promise){ return props.then(updatestorage(proof, user.sea, pin)); - } else { - return updatestorage(proof, user.sea, pin)(props); } - } else { - return updatestorage()(args); + return updatestorage(proof, user.sea, pin)(props); } + return updatestorage()({alias: 'delete'}); } // This internal func recalls persisted User authentication if so configured - function authrecall(root){ + function authrecall(root,authprops){ return new Promise(function(resolve, reject){ - var remember = sessionStorage.getItem('remember'); - var alias = sessionStorage.getItem('user'); - var err = 'Not authenticated'; - var pin; + var remember = authprops || sessionStorage.getItem('remember'); + var alias = Gun.obj.has(authprops, 'alias') && authprops.alias + || sessionStorage.getItem('user'); + var pin = Gun.obj.has(authprops, 'pin') + && new Buffer(authprops.pin, 'utf8').toString('base64'); + + var checkRememberData = function(decr){ + if(Gun.obj.has(decr, 'proof') + && Gun.obj.has(decr, 'alias') && decr.alias === alias){ + var proof = decr.proof; + var iat = decr.iat; // No way hook to update this + delete decr.proof; // We're not gonna give proof to hook! + var checkNotExpired = function(args){ + if(Math.floor(Date.now() / 1000) < (iat + args.exp)){ + args.iat = iat; + args.proof = proof; + return args; + } else { + Gun.log('Authentication expired!') } + }; + var hooked = authsettings.hook(decr); + return ((hooked instanceof Promise) + && hooked.then(checkNotExpired)) || checkNotExpired(hooked); + } + }; + var readAndDecrypt = function(data, pub, key){ + return SEA.read(data, pub).then(function(encrypted){ + return SEA.de(encrypted, key); + }).then(function(decrypted){ + try{ return decrypted.slice ? JSON.parse(decrypted) : decrypted }catch(e){} + return decrypted; + }); + }; // Already authenticated? - if(Gun.obj.has(root._.user._, 'pub')){ - return resolve(root._.user._.pub); + if(Gun.obj.has(root._.user._, 'pub') && Gun.obj.has(root._.user._, 'sea')){ + return resolve(root._.user._); } // No, got alias? - if (alias && remember){ + if(alias && remember){ return querygunaliases(alias, root).then(function(aliases){ return new Promise(function(resolve, reject){ // then attempt to log into each one until we find ours! @@ -257,60 +287,31 @@ if(!at.put){ return !remaining && reject({err: 'Public key does not exist!'}) } - // got pub, time to unwrap Storage data... - return SEA.read(remember, pub, true).then(function(props){ - props = !props.slice ? props : JSON.parse(props); - var checkProps = function(decr){ - return new Promise(function(resolve){ - if(Gun.obj.has(decr, 'proof') - && Gun.obj.has(decr, 'alias') && decr.alias === alias){ - var proof = decr.proof; - var iat = decr.iat; // No way hook to update this - delete decr.proof; // We're not gonna give proof to hook! - var doIt = function(args){ - if(Math.floor(Date.now() / 1000) < (iat + args.exp)){ - args.iat = iat; - args.proof = proof; - return args; - } else { Gun.log('Authentication expired!') } - }; - var hooked = authsettings.hook(decr); - return resolve(((hooked instanceof Promise) - && hooked.then(doIt)) - || doIt(decr)); - } - resolve(); - }); - }; - // Got PIN ? - if(Gun.obj.has(props, 'pin')){ - pin = props.pin; - // Yes! We can get localStorage secret if signature is ok - return SEA.read(localStorage.getItem('remember'), pub) - .then(function(encrypted){ - // And decrypt it - return SEA.de(encrypted, pin); - }).then(function(decr){ - decr = !decr.slice ? decr : JSON.parse(decr); - // And return proof if for matching alias - return checkProps(decr); - }); + // got pub, time to try auth with alias & PIN... + return ((pin && Promise.resolve({pin: pin, alias: alias})) + // or just unwrap Storage data... + || SEA.read(remember, pub, true)).then(function(props){ + try{ props = props.slice ? JSON.parse(props) : props }catch(e){} + if(Gun.obj.has(props, 'pin') && Gun.obj.has(props, 'alias') + && props.alias === alias){ + pin = props.pin; // Got PIN so get localStorage secret if signature is ok + return readAndDecrypt(localStorage.getItem('remember'), pub, pin) + .then(checkRememberData); // And return proof if for matching alias } // No PIN, let's try short-term proof if for matching alias - return checkProps(props); + return checkRememberData(props); }).then(function(args){ var proof = args && args.proof; - if (!proof){ - return updatestorage()(args).then(function(){ - reject({err: 'No secret found!'}); + if(!proof){ + return (!args && reject({err: 'No valid authentication session found!'})) + || updatestorage()(args).then(function(){ + reject({err: 'Expired session!'}); }).catch(function(){ - reject({err: 'No secret found!'}); + reject({err: 'Expired session!'}); }); } - // 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, pub).then(function(auth){ - return SEA.de(auth, proof) - .catch(function(e){ reject({err: 'Failed to decrypt secret!'}) }); + return readAndDecrypt(at.put.auth, pub, proof).catch(function(e){ + return !remaining && reject({err: 'Failed to decrypt private key!'}); }).then(function(priv){ // now we have AES decrypted the private key, // if we were successful, then that means we're logged in! @@ -333,7 +334,9 @@ reject({err: 'No authentication session found!'}); }); } - reject({err: 'No authentication session found!'}); + reject({ + err: (localStorage.getItem('remember') && 'Missing PIN and alias!') + || 'No authentication session found!'}); }); } @@ -341,10 +344,18 @@ function authleave(root, alias){ return function(resolve, reject){ // remove persisted authentication - authpersist((alias && { alias: alias }) || root._.user._).then(function(){ - root._.user = root.chain(); + user = root._.user; + alias = alias || (user._ && user._.alias); + var doIt = function(){ + // TODO: is this correct way to 'logout' user from Gun.User ? + [ 'alias', 'sea', 'pub' ].forEach(function(key){ + delete user._[key]; + }); + user._.is = user.is = {}; + // Let's use default resolve({ok: 0}); - }); + }; + authpersist(alias && { alias: alias }).then(doIt).catch(doIt); }; } @@ -352,8 +363,7 @@ function nodehash(m){ try{ m = m.slice ? m : JSON.stringify(m); - var ret = nodeCrypto.createHash(nHash).update(m, 'utf8').digest(); - return ret; + return nodeCrypto.createHash(nHash).update(m, 'utf8').digest(); }catch(e){ return m } } @@ -413,6 +423,16 @@ cb = typeof cb === 'function' && cb; var doIt = function(resolve, reject){ + // TODO: !pass && opt.pin => try to recall + // return reject({err: 'Auth attempt failed! Reason: No session data for alias & PIN'}); + if(!pass && Gun.obj.has(opts, 'pin')){ + return authrecall(root, {alias: alias, pin: opts.pin}).then(function(props){ + resolve(props); + }).catch(function(e){ + reject({err: 'Auth attempt failed! Reason: No session data for alias & PIN'}); + }); + } + authenticate(alias, pass, root).then(function(key){ // we're logged in! var pin = Gun.obj.has(opts, 'pin') && { pin: opts.pin }; @@ -439,7 +459,6 @@ // awesome, now we can update the user using public key ID. // root.get(tmp).put(null); root.get(tmp).put(user); - // then we're done finalizelogin(alias, key, root, pin).then(resolve).catch(function(e){ Gun.log('Failed to finalize login with new password!'); @@ -447,17 +466,17 @@ }); }).catch(function(e){ Gun.log('Failed encrypt private key using new password!'); - reject({err: 'Password set attempt failed! Reason: '+(e && e.err) || e || ''}); + reject({err: 'Password set attempt failed! Reason: ' + (e && e.err) || e || ''}); }); } else { finalizelogin(alias, key, root, pin).then(resolve).catch(function(e){ Gun.log('Failed to finalize login!'); - reject({err: 'Finalizing login failed! Reason: '+(e && e.err) || e || ''}); + reject({err: 'Finalizing login failed! Reason: ' + (e && e.err) || e || ''}); }); } }).catch(function(e){ Gun.log('Failed to sign in!'); - reject({err: 'Auth attempt failed! Reason: '+(e && e.err) || e || ''}); + reject({err: 'Auth attempt failed! Reason: ' + (e && e.err) || e || ''}); }); }; if(cb){doIt(cb, cb)} else { return new Promise(doIt) } @@ -473,37 +492,46 @@ authenticate(alias, pass, root).then(function(key){ new Promise(authleave(root, alias)).catch(function(){}) .then(function(){ + // Delete user data root.get('pub/'+key.pub).put(null); - root._.user = root.chain(); + // Wipe user data from memory + user = root._.user; + // TODO: is this correct way to 'logout' user from Gun.User ? + [ 'alias', 'sea', 'pub' ].forEach(function(key){ + delete user._[key]; + }); + user._.is = user.is = {}; resolve({ok: 0}); }).catch(function(e){ Gun.log('User.delete failed! Error:', e); - reject({err: 'Delete attempt failed! Reason:'+(e && e.err) || e || ''}); + reject({err: 'Delete attempt failed! Reason: ' + (e && e.err) || e || ''}); }); }).catch(function(e){ Gun.log('User.delete authentication failed! Error:', e); - reject({err: 'Delete attempt failed! Reason:'+(e && e.err) || e || ''}); + reject({err: 'Delete attempt failed! Reason: ' + (e && e.err) || e || ''}); }); }; if(cb){doIt(cb, cb)} else { return new Promise(doIt) } }; // If authentication is to be remembered over reloads or browser closing, - // set validity time in seconds. - User.recall = function(validity,cb,opts){ + // set validity time in minutes. + User.recall = function(v,cb,o){ var root = this.back(-1); - if(!opts){ - if(typeof cb !== 'function' && !Gun.val.is(cb)){ - opts = cb; - cb = undefined; - } + var validity, callback, opts; + if(!o && typeof cb !== 'function' && !Gun.val.is(cb)){ + opts = cb; + } else { + callback = cb; } - if(!cb){ - if(typeof validity === 'function'){ - cb = validity; - validity = undefined; - } else if(!Gun.val.is(validity)){ - opts = validity; - validity = undefined; + if(!callback){ + if(typeof v === 'function'){ + callback = v; + validity = _initial_authsettings.validity; + } else if(!Gun.val.is(v)){ + opts = v; + validity = _initial_authsettings.validity; + } else { + validity = v * 60; // minutes to seconds } } var doIt = function(resolve, reject){ @@ -514,25 +542,21 @@ // called when app bootstraps, with wanted options // IF validity === 0 THEN no remember-me, ever // IF opt.session === true THEN no window.localStorage in use; nor PIN - if(Gun.val.is(validity)){ - authsettings.validity = validity; - } + authsettings.validity = typeof validity !== 'undefined' ? validity + : _initial_authsettings.validity; if(Gun.obj.has(opts, 'session')){ authsettings.session = opts.session; } - if(Gun.obj.has(opts, 'hook')){ - authsettings.hook = opt.hook; - } - authrecall(root).then(function(props){ - // All is good. Should we do something more with actual recalled data? - resolve(root._.user._) - }).catch(function(e){ + authsettings.hook = (Gun.obj.has(opts, 'hook') && typeof opts.hook === 'function') + ? opts.hook : _initial_authsettings.hook; + // All is good. Should we do something more with actual recalled data? + authrecall(root).then(resolve).catch(function(e){ var err = 'No session!'; Gun.log(err); - resolve({ err: err }); + resolve({ err: (e && e.err) || err }); }); }; - if(cb){doIt(cb, cb)} else { return new Promise(doIt) } + if(callback){doIt(callback, callback)} else { return new Promise(doIt) } }; User.alive = function(cb){ var root = this.back(-1); @@ -643,7 +667,7 @@ if(val === tmp){ return } // the account MUST have a `pub` property that equals the ID of the public key. return no = true; // if not, reject the update. } - if(at.user){ // if we are logged in + if(at.user && at.user._){ // if we are logged in if(tmp === at.user._.pub){ // as this user SEA.write(val, at.user._.sea).then(function(data){ val = node[key] = data; // then sign our updates as we output them. @@ -789,8 +813,7 @@ } else { // NodeJS doesn't support crypto.subtle.importKey properly try{ var cipher = nodeCrypto.createCipheriv(aes.enc, key, iv); - r.ct = cipher.update(m, 'utf8', 'base64'); - r.ct += cipher.final('base64'); + r.ct = cipher.update(m, 'utf8', 'base64') + cipher.final('base64'); }catch(e){ Gun.log(e); return reject(e) } resolve(JSON.stringify(r)); } @@ -799,23 +822,24 @@ }; SEA.de = function(m,p,cb){ var doIt = function(resolve, reject){ - var d = !m.slice ? m : JSON.parse(m); - var key = makeKey(p, new Buffer(d.s, 'hex')); - var iv = new Buffer(d.iv, 'hex'); + try{ m = m.slice ? JSON.parse(m) : m }catch(e){} + var key = makeKey(p, new Buffer(m.s, 'hex')); + var iv = new Buffer(m.iv, 'hex'); if(typeof window !== 'undefined'){ // Browser doesn't run createDecipheriv crypto.subtle.importKey('raw', key, 'AES-CBC', false, ['decrypt']) .then(function(aesKey){ crypto.subtle.decrypt({ name: 'AES-CBC', iv: iv - }, aesKey, new Buffer(d.ct, 'base64')).then(function(ct){ + }, aesKey, new Buffer(m.ct, 'base64')).then(function(ct){ var ctUtf8 = new TextDecoder('utf8').decode(ct); - return !ctUtf8.slice ? ctUtf8 : JSON.parse(ctUtf8); + try{ return ctUtf8.slice ? JSON.parse(ctUtf8) : ctUtf8; + }catch(e){ return ctUtf8 } }).then(resolve).catch(function(e){Gun.log(e); reject(e)}); }).catch(function(e){Gun.log(e); reject(e)}); } else { // NodeJS doesn't support crypto.subtle.importKey properly try{ var decipher = nodeCrypto.createDecipheriv(aes.enc, key, iv); - r = decipher.update(d.ct, 'base64', 'utf8') + decipher.final('utf8'); + r = decipher.update(m.ct, 'base64', 'utf8') + decipher.final('utf8'); }catch(e){ Gun.log(e); return reject(e) } resolve(r); } @@ -829,7 +853,7 @@ if(mm.slice){ // Needs to remove previous signature envelope while('SEA[' === m.slice(0,4)){ - try{m = JSON.parse(m.slice(3))[0]; + try{ m = JSON.parse(m.slice(3))[0]; }catch(e){ m = mm; break } } } @@ -845,7 +869,7 @@ if(!m){ return resolve() } if(!m.slice || 'SEA[' !== m.slice(0,4)){ return resolve(m) } m = m.slice(3); - try{m = !m.slice ? m : JSON.parse(m); + try{ m = m.slice ? JSON.parse(m) : m; }catch(e){ return reject(e) } m = m || ''; SEA.verify(m[0], p, m[1]).then(function(ok){ diff --git a/test/sea.js b/test/sea.js index 982f03ab..8c456379 100644 --- a/test/sea.js +++ b/test/sea.js @@ -212,18 +212,39 @@ Gun().user && describe('Gun', function(){ var user = gun.user(); Gun.log.off = true; // Supress all console logging - // Simulate browser reload - gun.back(-1)._.user = gun.back(-1).chain(); + var throwOutUser = function(wipeStorageData){ + // Get rid of authenticated Gun user + var user = gun.back(-1)._.user; + // TODO: is this correct way to 'logout' user from Gun.User ? + [ 'alias', 'sea', 'pub' ].forEach(function(key){ + delete user._[key]; + }); + user._.is = user.is = {}; + + if(wipeStorageData){ + // ... and persisted session + localStorage.removeItem('remember') + sessionStorage.removeItem('remember'); + sessionStorage.removeItem('alias'); + } + }; ['callback', 'Promise'].forEach(function(type){ describe(type+':', function(){ + beforeEach(function(done){ + // Simulate browser reload + throwOutUser(true); + done(); + }); + describe('create', function(){ + it('new', function(done){ var check = function(ack){ try{ expect(ack).to.not.be(undefined); expect(ack).to.not.be(''); - expect(ack).to.have.keys(['ok','pub']); + expect(ack).to.have.keys([ 'ok', 'pub' ]); }catch(e){ done(e); return }; done(); }; @@ -234,6 +255,7 @@ Gun().user && describe('Gun', function(){ user.create(alias+type, pass).then(check).catch(done); } }); + it('conflict', function(done){ Gun.log.off = true; // Supress all console logging var check = function(ack){ @@ -243,6 +265,7 @@ Gun().user && describe('Gun', function(){ expect(ack).to.have.key('err'); expect(ack.err).not.to.be(undefined); expect(ack.err).not.to.be(''); + expect(ack.err.toLowerCase().indexOf('already created')).not.to.be(-1); }catch(e){ done(e); return }; done(); }; @@ -297,6 +320,8 @@ Gun().user && describe('Gun', function(){ expect(ack).to.have.key('err'); expect(ack.err).to.not.be(undefined); expect(ack.err).to.not.be(''); + expect(ack.err.toLowerCase().indexOf('failed to decrypt secret')) + .not.to.be(-1); }catch(e){ done(e); return }; done(); }; @@ -317,6 +342,7 @@ Gun().user && describe('Gun', function(){ expect(ack).to.have.key('err'); expect(ack.err).to.not.be(undefined); expect(ack.err).to.not.be(''); + expect(ack.err.toLowerCase().indexOf('no user')).not.to.be(-1); }catch(e){ done(e); return }; done(); }; @@ -355,6 +381,8 @@ Gun().user && describe('Gun', function(){ expect(ack).to.have.key('err'); expect(ack.err).to.not.be(undefined); expect(ack.err).to.not.be(''); + expect(ack.err.toLowerCase().indexOf('failed to decrypt secret')) + .not.to.be(-1); }catch(e){ done(e); return }; done(); }; @@ -391,7 +419,7 @@ Gun().user && describe('Gun', function(){ 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']); + expect(gun.back(-1)._.user).to.not.have.keys([ 'sea', 'pub' ]); }catch(e){ done(e); return }; done(); }; @@ -399,7 +427,7 @@ Gun().user && describe('Gun', function(){ user.create(usr, pass).then(function(ack){ expect(ack).to.not.be(undefined); expect(ack).to.not.be(''); - expect(ack).to.have.keys(['ok','pub']); + expect(ack).to.have.keys([ 'ok', 'pub' ]); user.auth(usr, pass).then(function(usr){ try{ expect(usr).to.not.be(undefined); @@ -427,7 +455,7 @@ Gun().user && describe('Gun', function(){ }catch(e){ done(e); return }; done(); }; - expect(gun.back(-1)._.user).to.not.have.keys(['sea', 'pub']); + expect(gun.back(-1)._.user).to.not.have.keys([ 'sea', 'pub' ]); if(type === 'callback'){ user.leave(check); } else { @@ -443,7 +471,7 @@ Gun().user && describe('Gun', function(){ return user.create(a, p).then(function(ack){ expect(ack).to.not.be(undefined); expect(ack).to.not.be(''); - expect(ack).to.have.keys(['ok','pub']); + expect(ack).to.have.keys([ 'ok', 'pub' ]); return ack; }); }; @@ -454,7 +482,7 @@ Gun().user && describe('Gun', function(){ 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']); + expect(gun.back(-1)._.user).to.not.have.keys([ 'sea', 'pub' ]); }catch(e){ done(e); return }; done(); }; @@ -497,6 +525,7 @@ Gun().user && describe('Gun', function(){ expect(ack).to.not.be(''); expect(ack).to.not.have.key('put'); expect(ack).to.have.key('err'); + expect(ack.err.toLowerCase().indexOf('no user')).not.to.be(-1); }catch(e){ done(e); return }; done(); }; @@ -511,29 +540,51 @@ Gun().user && describe('Gun', function(){ }); describe('recall', function(){ - var doCheck = function(done, hasPin){ - return function(){ - expect(root.sessionStorage.getItem('user')).to.not.be(undefined); - expect(root.sessionStorage.getItem('user')).to.not.be(''); - expect(root.sessionStorage.getItem('remember')).to.not.be(undefined); - expect(root.sessionStorage.getItem('remember')).to.not.be(''); + var doCheck = function(done, hasPin, wantAck){ + expect(typeof done).to.be('function'); + return function(ack){ + var user = root.sessionStorage.getItem('user'); + var sRemember = root.sessionStorage.getItem('remember'); + expect(user).to.not.be(undefined); + expect(user).to.not.be(''); + expect(sRemember).to.not.be(undefined); + expect(sRemember).to.not.be(''); if(hasPin){ - expect(root.localStorage.getItem('remember')).to.not.be(undefined); - expect(root.localStorage.getItem('remember')).to.not.be(''); + var lRemember = root.localStorage.getItem('remember'); + expect(lRemember).to.not.be(undefined); + expect(lRemember).to.not.be(''); } - return done(); + // NOTE: done can be Promise returning function + var ret; + if (wantAck) { + [ 'err', 'pub', 'sea', 'alias', 'put' ].forEach(function(key){ + if(typeof ack[key] !== 'undefined'){ + (ret = ret || {})[key] = ack[key]; + } + }); + } + return done(ret); }; }; // This re-constructs 'remember-me' data modified by manipulate func var manipulateStorage = function(manipulate, hasPin){ + expect(typeof manipulate).to.be('function'); + // We'll use Gun internal User data var usr = gun.back(-1)._.user; + expect(usr).to.not.be(undefined); + expect(usr).to.have.key('_'); + expect(usr._).to.have.keys([ 'pub', 'sea' ]); + // ... to validate 'remember' data var remember = hasPin ? localStorage.getItem('remember') : sessionStorage.getItem('remember'); return Gun.SEA.read(remember, usr._.pub).then(function(props){ - props = manipulate(JSON.parse(props)); + try{ props && (props = JSON.parse(props)) }catch(e){} + return props; + }).then(manipulate).then(function(props){ + expect(props).to.not.be(undefined); + expect(props).to.not.be(''); return Gun.SEA.write(JSON.stringify(props), usr._.sea) .then(function(remember){ - // remember = JSON.stringify(remember); return hasPin ? sessionStorage.setItem('remember', remember) : sessionStorage.setItem('remember', remember); }); @@ -581,9 +632,9 @@ Gun().user && describe('Gun', function(){ user.auth(alias+type, pass+' new').then(doCheck(done)).catch(done); }; if(type === 'callback'){ - user.recall(12 * 60 * 60, doAction, {session: false}); + user.recall(12 * 60, doAction, {session: false}); } else { - user.recall(12 * 60 * 60, {session: false}).then(doAction) + user.recall(12 * 60, {session: false}).then(doAction) .catch(done); } }); @@ -607,7 +658,7 @@ Gun().user && describe('Gun', function(){ user.leave().then(function(ack){ try{ expect(ack).to.have.key('ok'); - expect(gun.back(-1)._.user).to.not.have.keys(['sea', 'pub']); + expect(gun.back(-1)._.user).to.not.have.keys([ 'sea', 'pub' ]); expect(root.sessionStorage.getItem('user')).to.not.be(sUser); expect(root.sessionStorage.getItem('remember')).to.not.be(sRemember); }catch(e){ done(e); return }; @@ -615,7 +666,7 @@ Gun().user && describe('Gun', function(){ root.sessionStorage.setItem('user', sUser); root.sessionStorage.setItem('remember', sRemember); - user.recall(12 * 60 * 60, {session: false}).then(doCheck(done)) + user.recall(12 * 60, {session: false}).then(doCheck(done)) .catch(done); }).catch(done); }).catch(done); @@ -645,7 +696,7 @@ Gun().user && describe('Gun', function(){ user.leave().then(function(ack){ try{ expect(ack).to.have.key('ok'); - expect(gun.back(-1)._.user).to.not.have.keys(['sea', 'pub']); + expect(gun.back(-1)._.user).to.not.have.keys([ 'sea', 'pub' ]); expect(root.sessionStorage.getItem('user')).to.not.be(sUser); expect(root.sessionStorage.getItem('remember')).to.not.be(sRemember); expect(root.localStorage.getItem('remember')).to.not.be(lRemember); @@ -655,34 +706,115 @@ Gun().user && describe('Gun', function(){ root.sessionStorage.setItem('remember', sRemember); root.localStorage.setItem('remember', lRemember); - user.recall(12 * 60 * 60, {session: false}).then(doCheck(done)) + user.recall(12 * 60, {session: false}).then(doCheck(done)) .catch(done); }).catch(done); }).catch(done); }); - it.skip('invalid sessionStorage session'); - it.skip('valid localStorage data but not in sessionStorage'); + it('valid localStorage session bootstraps using PIN', function(done){ + user.recall(12 * 60, {session: false}).then(function(){ + return user.auth(alias+type, pass+' new', { pin: 'PIN' }); + }).then(doCheck(function(){ + // Let's save remember props + var sUser = root.sessionStorage.getItem('user'); + var sRemember = root.sessionStorage.getItem('remember'); + var lRemember = root.localStorage.getItem('remember'); + // Then logout user + return user.leave().then(function(ack){ + try{ + expect(ack).to.have.key('ok'); + expect(gun.back(-1)._.user).to.not.have.keys([ 'sea', 'pub' ]); + expect(root.sessionStorage.getItem('user')).to.not.be(sUser); + expect(root.sessionStorage.getItem('remember')).to.not.be(sRemember); + expect(root.localStorage.getItem('remember')).to.not.be(lRemember); + }catch(e){ done(e); return }; + // Then restore localStorage remember data, skip sessionStorage + root.localStorage.setItem('remember', lRemember); + }); + }, true)).then(function(){ + // Then try to recall authentication + return user.recall(12 * 60, {session: false}).then(function(props){ + try{ + expect(props).to.not.be(undefined); + expect(props).to.not.be(''); + expect(props).to.have.key('err'); + // Which fails to missing PIN + expect(props.err.toLowerCase() + .indexOf('missing pin')).not.to.be(-1); + }catch(e){ done(e); return }; + // Ok, time to try auth with alias & PIN + return user.auth(alias+type, undefined, { pin: 'PIN' }); + }); + }).then(doCheck(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 }; + // We've recalled authenticated session using alias & PIN! + done(); + }, true, true)).catch(done); + }); - it('expired session', function(done){ + it('valid localStorage session fails to bootstrap using wrong PIN', + function(done){ + user.recall(12 * 60, {session: false}).then(function(){ + return user.auth(alias+type, pass+' new', { pin: 'PIN' }); + }).then(doCheck(function(){ + var sUser = root.sessionStorage.getItem('user'); + var sRemember = root.sessionStorage.getItem('remember'); + var lRemember = root.localStorage.getItem('remember'); + return user.leave().then(function(ack){ + try{ + expect(ack).to.have.key('ok'); + expect(gun.back(-1)._.user).to.not.have.keys([ 'sea', 'pub' ]); + expect(root.sessionStorage.getItem('user')).to.not.be(sUser); + expect(root.sessionStorage.getItem('remember')).to.not.be(sRemember); + expect(root.localStorage.getItem('remember')).to.not.be(lRemember); + }catch(e){ done(e); return }; + root.localStorage.setItem('remember', lRemember); + }); + }, true)).then(function(){ + // Ok, time to try auth with alias & PIN + return user.auth(alias+type, undefined, { pin: 'PiN' }); + }).then(function(){ + done('Unexpected login success!'); + }).catch(function(ack){ + try{ + expect(ack).to.not.be(undefined); + expect(ack).to.not.be(''); + expect(ack).to.have.key('err'); + expect(ack.err.toLowerCase() + .indexOf('no session data for alias & pin')).not.to.be(-1); + }catch(e){ done(e); return }; + // We've recalled authenticated session using alias & PIN! + done(); + }); + }); + + it('expired session fails to bootstrap', function(done){ user.recall(60, {session: true}).then(function(){ return user.auth(alias+type, pass+' new'); }).then(doCheck(function(){ - // Storage data OK, let's back up time of auth 65 minutes + // Storage data OK, let's back up time of auth to exp + 65 seconds return manipulateStorage(function(props){ - props.iat -= 65 * 60; - return props; + var ret = Object.assign({}, props, { iat: props.iat - 65 - props.exp }); + return ret; }, false); })).then(function(){ // Simulate browser reload - gun.back(-1)._.user = gun.back(-1).chain(); - // TODO: re-make sessionStorage.remember to 65 seconds past - user.recall(60, {session: true}).then(function(props){ - expect(props).to.not.be(undefined); - expect(props).to.not.be(''); - expect(props).to.have.key('err'); - expect(props.err).to.not.be(undefined); - expect(props.err).to.not.be(''); + throwOutUser(); + user.recall(60, {session: true}).then(function(ack){ + expect(ack).to.not.be(undefined); + expect(ack).to.not.be(''); + expect(ack).to.not.have.keys([ 'pub', 'sea' ]); + expect(ack).to.have.key('err'); + expect(ack.err).to.not.be(undefined); + expect(ack.err).to.not.be(''); + expect(ack.err.toLowerCase() + .indexOf('no authentication session')).not.to.be(-1); done(); }).catch(done); }).catch(done); @@ -724,7 +856,7 @@ Gun().user && describe('Gun', function(){ try{ expect(ack).to.have.key('ok'); }catch(e){ done(e); return }; - gun.back(-1)._.user = gun.back(-1).chain(); + throwOutUser(); }); }).then(function(){ // Simulate browser reload @@ -739,13 +871,35 @@ Gun().user && describe('Gun', function(){ expect(props).to.have.key('err'); expect(props.err).to.not.be(undefined); expect(props.err).to.not.be(''); + expect(props.err.toLowerCase() + .indexOf('no authentication session')).not.to.be(-1); done(); }).catch(done); }).catch(done); }).catch(done); }); - it.skip('no session'); + it('recall hook session manipulation', function(done){ + var exp; + var hookFunc = function(props){ + exp = props.exp * 2; + var ret = Object.assign({}, props, { exp: exp }); + return (type === 'callback' && ret) || new Promise(function(resolve){ + resolve(ret); + }); + }; + user.recall(60, { session: true, hook: hookFunc }).then(function(){ + return user.auth(alias+type, pass); + }).then(function(){ + // Storage data OK, let's back up time of auth 65 minutes + return manipulateStorage(function(props){ + expect(props).to.not.be(undefined); + expect(props).to.have.key('exp'); + expect(props.exp).to.be(exp); + return props; + }, false); + }).then(done).catch(done); + }); }); describe('alive', function(){ @@ -755,16 +909,16 @@ Gun().user && describe('Gun', function(){ expect(ack).to.not.be(undefined); expect(ack).to.not.be(''); expect(ack).to.not.have.key('err'); - expect(ack).to.have.keys(['sea', 'pub']); + expect(ack).to.have.keys([ 'sea', 'pub' ]); }catch(e){ done(e); return }; done(); }; - var usr = alias+type+'alive'; - user.create(usr, pass).then(function(ack){ + var aliveUser = alias+type+'alive'; + user.create(aliveUser, pass).then(function(ack){ expect(ack).to.not.be(undefined); expect(ack).to.not.be(''); - expect(ack).to.have.keys(['ok','pub']); - user.auth(usr, pass, { pin: 'PIN' }).then(function(usr){ + expect(ack).to.have.keys([ 'ok', 'pub' ]); + user.auth(aliveUser, pass, { pin: 'PIN' }).then(function(usr){ try{ expect(usr).to.not.be(undefined); expect(usr).to.not.be(''); @@ -786,8 +940,9 @@ Gun().user && describe('Gun', function(){ try{ expect(ack).to.not.be(undefined); expect(ack).to.not.be(''); - expect(ack).to.not.have.keys(['sea', 'pub']); + expect(ack).to.not.have.keys([ 'sea', 'pub' ]); expect(ack).to.have.key('err'); + expect(ack.err.toLowerCase().indexOf('no session')).not.to.be(-1); }catch(e){ done(e); return }; done(); }; @@ -797,11 +952,15 @@ Gun().user && describe('Gun', function(){ }).catch(check); }).catch(done); }); - - it.skip('recall hook session manipulation'); }); }); }); + + process.env.SEA_CHANNEL && describe('User channel', function(){ + it.skip('create'); + it.skip('add member'); + }); + Gun.log.off = false; }); });