diff --git a/sea.js b/sea.js index 62d0e928..a7a0c1c8 100644 --- a/sea.js +++ b/sea.js @@ -252,7 +252,6 @@ const seaWrite = async (...props) => SEA.write(...props) const seaEnc = async (...props) => SEA.enc(...props) const seaDec = async (...props) => SEA.dec(...props) - const seaPair = async (...props) => SEA.pair(...props) const seaVerify = async (...props) => SEA.verify(...props) const seaSign = async (...props) => SEA.sign(...props) const seaDerive = async (...props) => SEA.derive(...props) @@ -634,7 +633,7 @@ const root = this.back(-1) const doIt = (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((at, ev) => { + root.get(`alias/${alias}`).get(async (at, ev) => { ev.off() if (at.put) { // If we can enforce that a user name is already taken, it might be nice to try, but this is not guaranteed. @@ -644,35 +643,38 @@ } const salt = Gun.text.random(64) // pseudo-randomly create a salt, then use CryptoJS's PBKDF2 function to extend the password with it. - SEA.proof(pass, salt).then((proof) => { + try { + const proof = await SEA.proof(pass, salt) // this will take some short amount of time to produce a proof, which slows brute force attacks. - seaPair().then((pairs) => { - // now we have generated a brand new ECDSA key pair for the user account. - const { pub, priv, epriv } = pairs - const user = { pub } - // the user's public key doesn't need to be signed. But everything else needs to be signed with it! - seaWrite(alias, pairs).then((alias) => - Object.assign(user, {alias }) && seaWrite(pairs.epub, pairs) - ).then((epub) => Object.assign(user, { epub }) - // to keep the private key safe, we AES encrypt it with the proof of work! - && seaEnc({ priv, epriv }, { pub: pairs.epub, key: proof }) - ).then((auth) => // TODO: So signedsalt isn't needed? - // seaWrite(salt, pairs).then((signedsalt) => - seaWrite({ salt, auth }, pairs) - // ) - ).then((auth) => { - Object.assign(user, { auth }) - const tmp = `pub/${pairs.pub}` - //console.log("create", user, pair.pub); - // awesome, now we can actually save the user with their public key as their ID. - root.get(tmp).put(user) - // next up, we want to associate the alias with the public key. So we add it to the alias list. - root.get(`alias/${alias}`).put(Gun.obj.put({}, tmp, Gun.val.rel.ify(tmp))) - // callback that the user has been created. (Note: ok = 0 because we didn't wait for disk to ack) - setTimeout(() => { resolve({ ok: 0, pub: pairs.pub}) }, 10) // TODO: BUG! If `.auth` happens synchronously after `create` finishes, auth won't work. This setTimeout is a temporary hack until we can properly fix it. - }).catch((e) => { Gun.log('SEA.en or SEA.write calls failed!'); reject(e) }) - }).catch((e) => { Gun.log('SEA.pair call failed!'); reject(e) }) - }) + const pairs = await SEA.pair() + // now we have generated a brand new ECDSA key pair for the user account. + const { pub, priv, epriv } = pairs + const user = { pub } + // the user's public key doesn't need to be signed. But everything else needs to be signed with it! + seaWrite(alias, pairs).then((alias) => + Object.assign(user, {alias }) && seaWrite(pairs.epub, pairs) + ).then((epub) => Object.assign(user, { epub }) + // to keep the private key safe, we AES encrypt it with the proof of work! + && seaEnc({ priv, epriv }, { pub: pairs.epub, key: proof }) + ).then((auth) => // TODO: So signedsalt isn't needed? + // seaWrite(salt, pairs).then((signedsalt) => + seaWrite({ salt, auth }, pairs) + // ) + ).then((auth) => { + Object.assign(user, { auth }) + const tmp = `pub/${pairs.pub}` + //console.log("create", user, pair.pub); + // awesome, now we can actually save the user with their public key as their ID. + root.get(tmp).put(user) + // next up, we want to associate the alias with the public key. So we add it to the alias list. + root.get(`alias/${alias}`).put(Gun.obj.put({}, tmp, Gun.val.rel.ify(tmp))) + // callback that the user has been created. (Note: ok = 0 because we didn't wait for disk to ack) + setTimeout(() => { resolve({ ok: 0, pub: pairs.pub}) }, 10) // TODO: BUG! If `.auth` happens synchronously after `create` finishes, auth won't work. This setTimeout is a temporary hack until we can properly fix it. + }).catch((e) => { Gun.log('SEA.en or SEA.write calls failed!'); reject(e) }) + } catch (e) { + Gun.log('SEA.create failed!') + reject(e) + } }) } if (cb){ doIt(cb, cb) } else { return new Promise(doIt) } @@ -1146,39 +1148,34 @@ throw e } }, - pair(cb) { - const doIt = (resolve, reject) => { - const catcher = (e) => { Gun.log(e); reject(e) } + async pair() { + try { const ecdhSubtle = subtleossl || subtle // First: ECDSA keys for signing/verifying... - subtle.generateKey(ecdsakeyprops, true, [ 'sign', 'verify' ]) - .then(({ publicKey, privateKey }) => subtle.exportKey('jwk', privateKey) - // privateKey scope doesn't leak out from here! - .then(({ d: priv }) => ({ priv })) - .then((keys) => subtle.exportKey('jwk', publicKey) - .then(({ x, y }) => Object.assign(keys, { - pub: Buffer.from([ x, y ].join(':')).toString('base64') - })) - ).catch(catcher) - // return seaKeyid(keys.pub).then(function(id){ - // keys.pubId = id; - // return keys; - // }); - ).then((signingKeys) => ecdhSubtle.generateKey(ecdhkeyprops, true, ['deriveKey']) - // Next: ECDH keys for encryption/decryption... - .then(({ publicKey, privateKey }) => ecdhSubtle.exportKey('jwk', privateKey) + const { pub, priv } = await subtle.generateKey(ecdsakeyprops, true, [ 'sign', 'verify' ]) + .then(async ({ publicKey, privateKey }) => { + const { d: priv } = await subtle.exportKey('jwk', privateKey) // privateKey scope doesn't leak out from here! - .then(({ d: epriv }) => Object.assign(signingKeys, { epriv })) - .then((bothKeys) => ecdhSubtle.exportKey('jwk', publicKey) - .then(({ x, y }) => Object.assign(bothKeys, { - epub: Buffer.from([ x, y ].join(':')).toString('base64') - })) - ).catch(catcher) - ).catch(catcher) - ).then(resolve) - .catch(catcher) + const { x, y } = await subtle.exportKey('jwk', publicKey) + const pub = Buffer.from([ x, y ].join(':')).toString('base64') + return { pub, priv } + }) + // To include PGPv4 kind of keyId: + // const pubId = await seaKeyid(keys.pub) + // Next: ECDH keys for encryption/decryption... + const { epub, epriv } = await ecdhSubtle.generateKey(ecdhkeyprops, true, ['deriveKey']) + .then(async ({ publicKey, privateKey }) => { + // privateKey scope doesn't leak out from here! + const { d: epriv } = await ecdhSubtle.exportKey('jwk', privateKey) + const { x, y } = await ecdhSubtle.exportKey('jwk', publicKey) + const epub = Buffer.from([ x, y ].join(':')).toString('base64') + return { epub, epriv } + }) + return { pub, priv, /* pubId, */ epub, epriv } + } catch (e) { + Gun.log(e) + throw e } - if (cb){ doIt(cb, () => cb()) } else { return new Promise(doIt) } }, // Derive shared secret from other's pub and my epub/epriv derive(pub, { epub, epriv }, cb) { diff --git a/test/sea.js b/test/sea.js index e6d6eb95..8b8889ce 100644 --- a/test/sea.js +++ b/test/sea.js @@ -59,212 +59,169 @@ SEA && describe('SEA', function(){ var clearText = 'My precious secret!'; var encKeys = ['ct', 'iv', 's']; - ['callback', 'Promise'].forEach(function(type){ - describe(type+':', function(){ - it('proof', function(done){ - var check = function(proof){ - expect(proof).to.not.be(undefined); - expect(proof).to.not.be(''); - done(); - }; - // proof - generates PBKDF2 hash from user's alias and password - // which is then used to decrypt user's auth record - if(type === 'callback'){ - SEA.proof(pass, Gun.text.random(64), check); - } else { - SEA.proof(pass, Gun.text.random(64)).then(check).catch(done); - } - }); + const type = 'Promise' // TODO: this is leftover... + it('proof', function(done){ + var check = function(proof){ + expect(proof).to.not.be(undefined); + expect(proof).to.not.be(''); + done(); + }; + // proof - generates PBKDF2 hash from user's alias and password + // which is then used to decrypt user's auth record + SEA.proof(pass, Gun.text.random(64)).then(check).catch(done); + }); - it('pair', function(done){ - var check = function(key){ - expect(key).to.not.be(undefined); - expect(key).to.not.be(''); - expect(key).to.have.keys(userKeys); - userKeys.map(function(fld){ - expect(key[fld]).to.not.be(undefined); - expect(key[fld]).to.not.be(''); - }); - done(); - }; - // pair - generates ECDH key pair (for new user when created) - if(type === 'callback'){ - SEA.pair(check); - } else { - SEA.pair().then(check).catch(done); - } + it('pair', function(done){ + var check = function(key){ + expect(key).to.not.be(undefined); + expect(key).to.not.be(''); + expect(key).to.have.keys(userKeys); + userKeys.map(function(fld){ + expect(key[fld]).to.not.be(undefined); + expect(key[fld]).to.not.be(''); }); + done(); + }; + // pair - generates ECDH key pair (for new user when created) + SEA.pair().then(check).catch(done); + }); - it('keyid', function(done){ - SEA.pair().then(function(key){ - var check = function(keyid){ - expect(keyid).to.not.be(undefined); - expect(keyid).to.not.be(''); - expect(keyid.length).to.eql(16); - done(); - }; - // keyid - creates 8 byte KeyID from public key - if(type === 'callback'){ - SEA.keyid(key.pub, check); - } else { - SEA.keyid(key.pub).then(check); - } - }).catch(function(e){done(e)}); + it('keyid', function(done){ + SEA.pair().then(function(key){ + var check = function(keyid){ + expect(keyid).to.not.be(undefined); + expect(keyid).to.not.be(''); + expect(keyid.length).to.eql(16); + done(); + }; + // keyid - creates 8 byte KeyID from public key + SEA.keyid(key.pub).then(check); + }).catch(function(e){done(e)}); + }); + + it('enc', function(done){ + SEA.pair().then(function(key){ + var check = function(jsonSecret){ + expect(jsonSecret).to.not.be(undefined); + expect(jsonSecret).to.not.be(''); + expect(jsonSecret).to.not.eql(clearText); + var objSecret = JSON.parse(jsonSecret); + expect(objSecret).to.have.keys(encKeys); + encKeys.map(function(key){ + expect(objSecret[key]).to.not.be(undefined); + expect(objSecret[key]).to.not.be(''); + }); + done(); + }; + // en - encrypts JSON data using user's private or derived ECDH key + SEA.enc(clearText, key.priv).then(check); + }).catch(function(e){done(e)}); + }); + + it('sign', function(done){ + SEA.pair().then(function(key){ + var check = function(signature){ + expect(signature).to.not.be(undefined); + expect(signature).to.not.be(''); + expect(signature).to.not.eql(key.pub); + done(); + }; + // sign - calculates signature for data using user's private ECDH key + SEA.sign(key.pub, key).then(check); + }).catch(function(e){done(e)}); + }); + + it('verify', function(done){ + SEA.pair().then(function(key){ + var check = function(ok){ + expect(ok).to.not.be(undefined); + expect(ok).to.not.be(''); + expect(ok).to.be(true); + done(); + }; + // sign - calculates signature for data using user's private ECDH key + SEA.sign(key.pub, key).then(function(signature){ + SEA.verify(key.pub, key.pub, signature).then(check); }); + }).catch(function(e){done(e)}); + }); - it('enc', function(done){ - SEA.pair().then(function(key){ - var check = function(jsonSecret){ - expect(jsonSecret).to.not.be(undefined); - expect(jsonSecret).to.not.be(''); - expect(jsonSecret).to.not.eql(clearText); - var objSecret = JSON.parse(jsonSecret); - expect(objSecret).to.have.keys(encKeys); - encKeys.map(function(key){ - expect(objSecret[key]).to.not.be(undefined); - expect(objSecret[key]).to.not.be(''); - }); - done(); - }; - // en - encrypts JSON data using user's private or derived ECDH key - if(type === 'callback'){ - SEA.enc(clearText, key.priv, check); - } else { - SEA.enc(clearText, key.priv).then(check); - } - }).catch(function(e){done(e)}); + it('dec', function(done){ + SEA.pair().then(function(key){ + var check = function(decText){ + expect(decText).to.not.be(undefined); + expect(decText).to.not.be(''); + expect(decText).to.be.eql(clearText); + done(); + }; + SEA.enc(clearText, key.priv).then(function(jsonSecret){ + // de - decrypts JSON data using user's private or derived ECDH key + SEA.dec(jsonSecret, key.priv).then(check); }); + }).catch(function(e){done(e)}); + }); - it('sign', function(done){ - SEA.pair().then(function(key){ - var check = function(signature){ - expect(signature).to.not.be(undefined); - expect(signature).to.not.be(''); - expect(signature).to.not.eql(key.pub); - done(); - }; - // sign - calculates signature for data using user's private ECDH key - if(type === 'callback'){ - SEA.sign(key.pub, key, check); - } else { - SEA.sign(key.pub, key).then(check); - } - }).catch(function(e){done(e)}); + it('derive', function(done){ + SEA.pair().then(function(txKey){ + return SEA.pair().then(function(rxKey){ + return {tx: txKey, rx: rxKey}; }); + }).then(function(keys){ + var check = function(shared){ + expect(shared).to.not.be(undefined); + expect(shared).to.not.be(''); + [keys.rx.pub, keys.rx.priv, keys.tx.pub, keys.tx.priv] + .map(function(val){ + expect(shared).to.not.eql(val); + }); + done(); + }; + // derive - provides shared secret for both receiver and sender + // which can be used to encrypt or sign data + SEA.derive(keys.rx.pub, keys.tx).then(check); + }).catch(function(e){done(e)}); + }); - it('verify', function(done){ - SEA.pair().then(function(key){ - var check = function(ok){ - expect(ok).to.not.be(undefined); - expect(ok).to.not.be(''); - expect(ok).to.be(true); - done(); - }; - // sign - calculates signature for data using user's private ECDH key - SEA.sign(key.pub, key).then(function(signature){ - if(type === 'callback'){ - SEA.verify(key.pub, key.pub, signature, check); - } else { - SEA.verify(key.pub, key.pub, signature).then(check); - } - }); - }).catch(function(e){done(e)}); - }); - - it('dec', function(done){ - SEA.pair().then(function(key){ - var check = function(decText){ - expect(decText).to.not.be(undefined); - expect(decText).to.not.be(''); - expect(decText).to.be.eql(clearText); - done(); - }; - SEA.enc(clearText, key.priv).then(function(jsonSecret){ - // de - decrypts JSON data using user's private or derived ECDH key - if(type === 'callback'){ - SEA.dec(jsonSecret, key.priv, check); - } else { - SEA.dec(jsonSecret, key.priv).then(check); - } - }); - }).catch(function(e){done(e)}); - }); - - it('derive', function(done){ - SEA.pair().then(function(txKey){ - return SEA.pair().then(function(rxKey){ - return {tx: txKey, rx: rxKey}; - }); - }).then(function(keys){ - var check = function(shared){ - expect(shared).to.not.be(undefined); - expect(shared).to.not.be(''); - [keys.rx.pub, keys.rx.priv, keys.tx.pub, keys.tx.priv] - .map(function(val){ - expect(shared).to.not.eql(val); - }); - done(); - }; - // derive - provides shared secret for both receiver and sender - // which can be used to encrypt or sign data - if(type === 'callback'){ - SEA.derive(keys.rx.pub, keys.tx, check); - } else { - SEA.derive(keys.rx.pub, keys.tx).then(check); - } - }).catch(function(e){done(e)}); - }); - - it('write', function(done){ - SEA.pair().then(function(key){ - SEA.sign(key.pub, key).then(function(signature){ - var check = function(result){ - var parts; - try{ - expect(result).to.not.be(undefined); - expect(result).to.not.be(''); - expect(result.slice(0, 4)).to.eql('SEA['); - parts = JSON.parse(result.slice(3)); - expect(parts).to.not.be(undefined); - expect(parts[0]).to.be.eql(key.pub); - // expect(parts[1]).to.be.eql(signature); - }catch(e){ return done(e) } - SEA.verify(key.pub, key.pub, parts[1]).then(function(flag){ - expect(flag).to.be.true; - done(); - }); - }; - // write - wraps data to 'SEA["data","signature"]' - if(type === 'callback'){ - SEA.write(key.pub, key, check); - } else { - SEA.write(key.pub, key).then(check); - } - }); - }).catch(function(e){done(e)}); - }); - - it('read', function(done){ - SEA.pair().then(function(key){ - var check = function(result){ + it('write', function(done){ + SEA.pair().then(function(key){ + SEA.sign(key.pub, key).then(function(signature){ + var check = function(result){ + var parts; + try{ expect(result).to.not.be(undefined); expect(result).to.not.be(''); - expect(result).to.be.equal(key.pub); + expect(result.slice(0, 4)).to.eql('SEA['); + parts = JSON.parse(result.slice(3)); + expect(parts).to.not.be(undefined); + expect(parts[0]).to.be.eql(key.pub); + // expect(parts[1]).to.be.eql(signature); + }catch(e){ return done(e) } + SEA.verify(key.pub, key.pub, parts[1]).then(function(flag){ + expect(flag).to.be.true; done(); - }; - SEA.sign(key.pub, key).then(function(signature){ - SEA.write(key.pub, key).then(function(signed){ - // read - unwraps data from 'SEA["data","signature"]' - if(type === 'callback'){ - SEA.read(signed, key.pub, check); - } else { - SEA.read(signed, key.pub).then(check); - } - }); }); - }).catch(function(e){done(e)}); + }; + // write - wraps data to 'SEA["data","signature"]' + SEA.write(key.pub, key).then(check); }); - }); + }).catch(function(e){done(e)}); + }); + + it('read', function(done){ + SEA.pair().then(function(key){ + var check = function(result){ + expect(result).to.not.be(undefined); + expect(result).to.not.be(''); + expect(result).to.be.equal(key.pub); + done(); + }; + SEA.sign(key.pub, key).then(function(signature){ + SEA.write(key.pub, key).then(function(signed){ + // read - unwraps data from 'SEA["data","signature"]' + SEA.read(signed, key.pub).then(check); + }); + }); + }).catch(function(e){done(e)}); }); }); @@ -300,813 +257,738 @@ Gun().user && describe('Gun', function(){ return Promise.resolve() } - ['callback', 'Promise'].forEach(function(type){ - describe(type+':', function(){ - // Simulate browser reload - beforeEach((done) => { throwOutUser(true, done) }) + const type = 'Promise' // TODO: this is leftover... + // Simulate browser reload + beforeEach((done) => { throwOutUser(true, done) }) - describe('create', function(){ + 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' ]); - }catch(e){ done(e); return } - done(); - }; - // Gun.user.create - creates new user - if(type === 'callback'){ - user.create(alias+type, pass, check); - } else { - user.create(alias+type, pass).then(check).catch(done); - } - }); + 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' ]); + }catch(e){ done(e); return } + done(); + }; + // Gun.user.create - creates new user + 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){ - 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(''); - expect(ack.err.toLowerCase().indexOf('already created')).not.to.be(-1); - }catch(e){ done(e); return } - done(); - }; - // Gun.user.create - fails to create existing user - if(type === 'callback'){ - user.create(alias+type, pass, check); - } else { - user.create(alias+type, pass).then(function(ack){ - done('Failed to decline creating existing user!'); - }).catch(check); - } - }); - }); + it('conflict', function(done){ + Gun.log.off = true; // Supress all console logging + var check = function(ack){ + 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(''); + expect(ack.err.toLowerCase().indexOf('already created')).not.to.be(-1); + }catch(e){ done(e); return } + done(); + }; + // Gun.user.create - fails to create existing user + user.create(alias+type, pass).then(function(ack){ + done('Failed to decline creating existing user!'); + }).catch(check); + }); + }); - describe('auth', function(){ - const checkStorage = (done, notStored) => () => { - const checkValue = (data, val) => { - if (notStored) { - expect(typeof data !== 'undefined' && data !== null && data !== '') - .to.not.eql(true) - } else { - expect(data).to.not.be(undefined) - expect(data).to.not.be('') - if (val) { - expect(data).to.eql(val) - } - } - } - const alias = root.sessionStorage.getItem('user') - checkValue(alias) - checkValue(root.sessionStorage.getItem('remember')) - if (alias) { - checkIndexedDB(alias, 'auth').then((auth) => { - checkValue(auth) - done() - }).catch(done) - } else { - done() + describe('auth', function(){ + const checkStorage = (done, notStored) => () => { + const checkValue = (data, val) => { + if (notStored) { + expect(typeof data !== 'undefined' && data !== null && data !== '') + .to.not.eql(true) + } else { + expect(data).to.not.be(undefined) + expect(data).to.not.be('') + if (val) { + expect(data).to.eql(val) } } + } + const alias = root.sessionStorage.getItem('user') + checkValue(alias) + checkValue(root.sessionStorage.getItem('remember')) + if (alias) { + checkIndexedDB(alias, 'auth').then((auth) => { + checkValue(auth) + done() + }).catch(done) + } else { + done() + } + } - it('login', 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'); - }catch(e){ done(e); return } - done(); - }; - // Gun.user.auth - authenticates existing user - if(type === 'callback'){ - user.auth(alias+type, pass, check); - } else { - user.auth(alias+type, pass).then(check).catch(done); - } - }); + it('login', 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'); + }catch(e){ done(e); return } + done(); + }; + // Gun.user.auth - authenticates existing user + user.auth(alias+type, pass).then(check).catch(done); + }); - it('wrong password', function(done){ - var check = function(ack){ - 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(''); - expect(ack.err.toLowerCase().indexOf('failed to decrypt secret')) - .not.to.be(-1); - }catch(e){ done(e); return } - done(); - }; - if(type === 'callback'){ - user.auth(alias+type, pass+'not', check); - } else { - user.auth(alias+type, pass+'not').then(function(ack){ - done('Unexpected login success!'); - }).catch(check); - } - }); + it('wrong password', function(done){ + var check = function(ack){ + 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(''); + expect(ack.err.toLowerCase().indexOf('failed to decrypt secret')) + .not.to.be(-1); + }catch(e){ done(e); return } + done(); + }; + user.auth(alias+type, pass+'not').then(function(ack){ + done('Unexpected login success!'); + }).catch(check); + }); - it('unknown alias', function(done){ - var check = function(ack){ - 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(''); - expect(ack.err.toLowerCase().indexOf('no user')).not.to.be(-1); - }catch(e){ done(e); return } - done(); - }; - if(type === 'callback'){ - user.auth(alias+type+'not', pass, check); - } else { - user.auth(alias+type+'not', pass).then(function(ack){ - done('Unexpected login success!'); - }).catch(check); - } - }); + it('unknown alias', function(done){ + var check = function(ack){ + 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(''); + expect(ack.err.toLowerCase().indexOf('no user')).not.to.be(-1); + }catch(e){ done(e); return } + done(); + }; + user.auth(alias+type+'not', pass).then(function(ack){ + done('Unexpected login success!'); + }).catch(check); + }); - it('new password', 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'); - }catch(e){ done(e); return } - done(); - }; - // Gun.user.auth - with newpass props sets new password - if(type === 'callback'){ - user.auth(alias+type, pass, check, {newpass: pass+' new'}); - } else { - user.auth(alias+type, pass, {newpass: pass+' new'}).then(check) - .catch(done); - } - }); + it('new password', 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'); + }catch(e){ done(e); return } + done(); + }; + // Gun.user.auth - with newpass props sets new password + user.auth(alias+type, pass, {newpass: pass+' new'}).then(check) + .catch(done); + }); - it('failed new password', function(done){ - var check = function(ack){ - 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(''); - expect(ack.err.toLowerCase().indexOf('failed to decrypt secret')) - .not.to.be(-1); - }catch(e){ done(e); return } - done(); - }; - if(type === 'callback'){ - user.auth(alias+type, pass+'not', check, {newpass: pass+' new'}); - } else { - user.auth(alias+type, pass+'not', {newpass: pass+' new'}) - .then(function(ack){ - done('Unexpected password change success!'); - }).catch(check); - } - }); + it('failed new password', function(done){ + var check = function(ack){ + 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(''); + expect(ack.err.toLowerCase().indexOf('failed to decrypt secret')) + .not.to.be(-1); + }catch(e){ done(e); return } + done(); + }; + user.auth(alias+type, pass+'not', {newpass: pass+' new'}) + .then(function(ack){ + done('Unexpected password change success!'); + }).catch(check); + }); - it('without PIN auth session stored', function(done){ - user.auth(alias+type, pass+' new').then(checkStorage(done)).catch(done); - }); + it('without PIN auth session stored', function(done){ + user.auth(alias+type, pass+' new').then(checkStorage(done)).catch(done); + }); - it('with PIN auth session stored', function(done){ - if (type === 'callback'){ - user.auth(alias+type, pass+' new', checkStorage(done), { pin: 'PIN' }) - } else { - user.auth(alias+type, pass+' new', { pin: 'PIN' }) - .then(checkStorage(done)).catch(done) - } - }) + it('with PIN auth session stored', function(done){ + user.auth(alias+type, pass+' new', { pin: 'PIN' }) + .then(checkStorage(done)).catch(done) + }) - it('without PIN and zero validity no auth session storing', function(done){ - user.recall(0).then(function(){ - user.auth(alias+type, pass+' new') - .then(checkStorage(done, true)).catch(done); - }); - }); - - it('with PIN and zero validity no auth session storing', function(done){ - user.recall(0).then(function(){ - user.auth(alias+type, pass+' new', {pin: 'PIN'}) - .then(checkStorage(done, true)).catch(done); - }); - }); + it('without PIN and zero validity no auth session storing', function(done){ + user.recall(0).then(function(){ + user.auth(alias+type, pass+' new') + .then(checkStorage(done, true)).catch(done); }); + }); - describe('leave', function(){ - it('valid session', 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' ]); - // expect(gun.back(-1)._.user).to.not.be.ok(); - }catch(e){ done(e); return } - done(); - }; - var usr = alias+type+'leave'; - 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' ]); - user.auth(usr, 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.leave - performs logout for authenticated user - if(type === 'callback'){ - user.leave(check); - } else { - user.leave().then(check).catch(done); - } - }).catch(done); - }).catch(done); - }); + it('with PIN and zero validity no auth session storing', function(done){ + user.recall(0).then(function(){ + user.auth(alias+type, pass+' new', {pin: 'PIN'}) + .then(checkStorage(done, true)).catch(done); + }); + }); + }); - it('no session', 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'); - }catch(e){ done(e); return } - done(); - }; + describe('leave', function(){ + it('valid session', 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' ]); + // expect(gun.back(-1)._.user).to.not.be.ok(); + }catch(e){ done(e); return } + done(); + }; + var usr = alias+type+'leave'; + 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' ]); + user.auth(usr, 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.leave - performs logout for authenticated user + user.leave().then(check).catch(done); + }).catch(done); + }).catch(done); + }); + + it('no session', 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'); + }catch(e){ done(e); return } + done(); + }; + expect(gun.back(-1)._.user).to.not.have.keys([ 'sea', 'pub' ]); + user.leave().then(check).catch(done); + }); + }); + + describe('delete', function(){ + var usr = alias+type+'del'; + + var createUser = function(a, p){ + 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' ]); + return ack; + }); + }; + var check = function(done){ + return 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' ]); - if(type === 'callback'){ - user.leave(check); - } else { - user.leave().then(check).catch(done); - } - }); - }); + }catch(e){ done(e); return } + done(); + }; + }; - describe('delete', function(){ - var usr = alias+type+'del'; - - var createUser = function(a, p){ - return user.create(a, p).then(function(ack){ + it('existing authenticated user', function(done){ + createUser(usr, pass).then(function(){ + user.auth(usr, pass).then(function(ack){ + try{ expect(ack).to.not.be(undefined); expect(ack).to.not.be(''); - expect(ack).to.have.keys([ 'ok', 'pub' ]); - return ack; - }); - }; - var check = function(done){ - return 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(); - }; - }; + expect(ack).to.not.have.key('err'); + expect(ack).to.have.key('put'); + }catch(e){ done(e); return } + // Gun.user.delete - deletes existing user account + user.delete(usr, pass).then(check(done)).catch(done); + }).catch(done); + }).catch(done); + }); - it('existing authenticated user', function(done){ - createUser(usr, pass).then(function(){ - user.auth(usr, pass).then(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('put'); - }catch(e){ done(e); return } - // Gun.user.delete - deletes existing user account - if(type === 'callback'){ - user.delete(usr, pass, check(done)); - } else { - user.delete(usr, pass).then(check(done)).catch(done); - } - }).catch(done); - }).catch(done); - }); + it('unauthenticated existing user', function(done){ + createUser(usr, pass).catch(function(){}) + .then(function(){ + user.delete(usr, pass).then(check(done)).catch(done); + }); + }); - it('unauthenticated existing user', function(done){ - createUser(usr, pass).catch(function(){}) - .then(function(){ - if(type === 'callback'){ - user.delete(usr, pass, check(done)); - } else { - user.delete(usr, pass).then(check(done)).catch(done); + it('non-existing user', function(done){ + var notFound = function(ack){ + try{ + expect(ack).to.not.be(undefined); + 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(); + }; + user.delete('someone', 'password guess').then(function(){ + done('Unexpectedly deleted guessed user!'); + }).catch(notFound); + }); + }); + + describe('recall (from IndexedDB)', function(){ + 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(''); + + var ret; + if(wantAck && ack){ + ['err', 'pub', 'sea', 'alias', 'put'].forEach(function(key){ + if(typeof ack[key] !== 'undefined'){ + (ret = ret || {})[key] = ack[key]; } }); + } + // NOTE: done can be Promise returning function + return !hasPin || !wantAck || !ack ? done(ret) + : new Promise(function(resolve){ + checkIndexedDB(ack.alias, 'auth', function(auth){ + expect(auth).to.not.be(undefined); + expect(auth).to.not.be(''); + resolve(done(wantAck && Object.assign(ret || {}, {auth: auth}))); + }); }); - - it('non-existing user', function(done){ - var notFound = function(ack){ - try{ - expect(ack).to.not.be(undefined); - 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(); - }; - if(type === 'callback'){ - user.delete('someone', 'password guess', notFound); - } else { - user.delete('someone', 'password guess').then(function(){ - done('Unexpectedly deleted guessed user!'); - }).catch(notFound); - } + }; + }; + // This re-constructs 'remember-me' data modified by manipulate func + var manipulateStorage = function(manipulate, pin){ + 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 + pin = pin && Buffer.from(pin, 'utf8').toString('base64'); + return !pin ? Promise.resolve(sessionStorage.getItem('remember')) + : new Promise(function(resolve){ + checkIndexedDB(usr._.alias, 'auth', resolve); + }).then(function(remember){ + return SEA.read(remember, usr._.pub).then(function(props){ + return !pin ? props + : SEA.dec(props, pin); + }); + }).then(function(props){ + try{ props && (props = JSON.parse(props)) }catch(e){} //eslint-disable-line no-empty + return props; + }).then(manipulate).then(function(props){ + expect(props).to.not.be(undefined); + expect(props).to.not.be(''); + var keys = {pub: usr._.pub, priv: usr._.sea.priv}; + return SEA.write(JSON.stringify(props), keys) + .then(function(remember){ + return !pin ? sessionStorage.setItem('remember', remember) + : SEA.enc(remember, pin).then(function(encauth){ + return new Promise(function(resolve){ + setIndexedDB(usr._.alias, encauth, resolve); + }); + }); }); }); + }; - describe('recall (from IndexedDB)', function(){ - 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(''); + it('with PIN auth session stores', function(done){ + var doAction = function(){ + user.auth(alias+type, pass+' new', {pin: 'PIN'}) + .then(doCheck(done, true)).catch(done); + }; + user.recall().then(doAction).catch(done); + }); - var ret; - if(wantAck && ack){ - ['err', 'pub', 'sea', 'alias', 'put'].forEach(function(key){ - if(typeof ack[key] !== 'undefined'){ - (ret = ret || {})[key] = ack[key]; - } - }); - } - // NOTE: done can be Promise returning function - return !hasPin || !wantAck || !ack ? done(ret) - : new Promise(function(resolve){ - checkIndexedDB(ack.alias, 'auth', function(auth){ - expect(auth).to.not.be(undefined); - expect(auth).to.not.be(''); - resolve(done(wantAck && Object.assign(ret || {}, {auth: auth}))); - }); - }); - }; - }; - // This re-constructs 'remember-me' data modified by manipulate func - var manipulateStorage = function(manipulate, pin){ - expect(typeof manipulate).to.be('function'); - // We'll use Gun internal User data - var usr = gun.back(-1)._.user; + it('without PIN auth session stores', function(done){ + var doAction = function(){ + user.auth(alias+type, pass+' new').then(doCheck(done)); + }; + user.leave().then(function(){ + user.recall().then(doAction).catch(done); + }).catch(done); + }); + + it('no validity no session storing', function(done){ + var doAction = function(){ + user.auth(alias+type, pass+' new').then(doCheck(done)).catch(done); + }; + user.recall(0).then(doAction).catch(done); + }); + + it('with validity but no PIN stores using random PIN', function(done){ + var doAction = function(){ + user.auth(alias+type, pass+' new').then(doCheck(done)).catch(done); + }; + user.recall(12 * 60).then(doAction) + .catch(done); + }); + + it('validity and auth with PIN but storage empty', function(done){ + user.auth(alias+type, pass+' new').then(function(usr){ + var sUser; + var sRemember; + try{ expect(usr).to.not.be(undefined); - expect(usr).to.have.key('_'); - expect(usr._).to.have.keys(['pub', 'sea']); - // ... to validate 'remember' data - pin = pin && Buffer.from(pin, 'utf8').toString('base64'); - return !pin ? Promise.resolve(sessionStorage.getItem('remember')) - : new Promise(function(resolve){ - checkIndexedDB(usr._.alias, 'auth', resolve); - }).then(function(remember){ - return SEA.read(remember, usr._.pub).then(function(props){ - return !pin ? props - : SEA.dec(props, pin); + expect(usr).to.not.be(''); + expect(usr).to.not.have.key('err'); + expect(usr).to.have.key('put'); + + sUser = root.sessionStorage.getItem('user'); + expect(sUser).to.be(alias+type); + + sRemember = root.sessionStorage.getItem('remember'); + expect(sRemember).to.not.be(undefined); + expect(sRemember).to.not.be(''); + }catch(e){ done(e); 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); + }catch(e){ done(e); return } + // Restore but leave IndexedDB empty + root.sessionStorage.setItem('user', sUser); + root.sessionStorage.setItem('remember', sRemember); + + user.recall(12 * 60).then( + doCheck(function(ack){ + expect(ack).to.have.key('err'); + expect(ack.err.toLowerCase().indexOf('no session')).to.not.be(-1); + checkIndexedDB(alias+type, 'auth', function(auth){ + expect((typeof auth !== 'undefined' && auth !== null && auth !== '')) + .to.not.eql(true); + done(); + }); + }, false, true)) + .catch(done); + }).catch(done); + }).catch(done); + }); + + it('valid session bootstrap', function(done){ + var sUser; + var sRemember; + var iAuth; + user.auth(alias+type, pass+' new', {pin: 'PIN'}).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'); + expect(root.sessionStorage.getItem('user')).to.be(alias+type); + expect(root.sessionStorage.getItem('remember')).to.not.be(undefined); + expect(root.sessionStorage.getItem('remember')).to.not.be(''); + + sUser = root.sessionStorage.getItem('user'); + sRemember = root.sessionStorage.getItem('remember'); + }catch(e){ done(e); return } + + return new Promise(function(resolve){ + checkIndexedDB(sUser, 'auth', function(auth){ resolve(iAuth = auth) }); + }); + }).then(function(){ + 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); + }catch(e){ done(e); return } + + return new Promise(function(resolve){ + checkIndexedDB(sUser, 'auth', function(auth){ + expect(auth).to.not.be(iAuth); + resolve(); }); - }).then(function(props){ - try{ props && (props = JSON.parse(props)) }catch(e){} //eslint-disable-line no-empty - return props; - }).then(manipulate).then(function(props){ + }); + }).then(function(){ + root.sessionStorage.setItem('user', sUser); + root.sessionStorage.setItem('remember', sRemember); + + return new Promise(function(resolve){ + setIndexedDB(sUser, iAuth, resolve); + }); + }).then(function(){ + user.recall(12 * 60).then(doCheck(done)) + .catch(done); + }).catch(done); + }).catch(done); + }); + + it('valid session bootstrap using alias & PIN', function(done){ + let sRemember + user.recall(12 * 60).then(function(){ + return user.auth(alias+type, pass+' new', {pin: 'PIN'}); + }).then(doCheck(function(ack){ + // Let's save remember props + var sUser = root.sessionStorage.getItem('user'); + sRemember = root.sessionStorage.getItem('remember') + var iAuth = ack.auth; + return new Promise(function(resolve){ + checkIndexedDB(sUser, 'auth', function(auth){ + iAuth = auth; + resolve(user.leave()); // Then logout user + }); + }).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); + }catch(e){ done(e); return } + return new Promise(function(resolve){ + checkIndexedDB(sUser, 'auth', function(auth){ + try{ expect(auth).to.not.be(iAuth) }catch(e){ done(e) } + // Then restore IndexedDB but skip sessionStorage remember + setIndexedDB(sUser, iAuth, function(){ + root.sessionStorage.setItem('user', sUser); + resolve(ack); + }); + }); + }); + }); + }, true, true)).then(function(){ + // Then try to recall authentication + return user.recall(12 * 60).then(function(props){ + try{ expect(props).to.not.be(undefined); expect(props).to.not.be(''); - var keys = {pub: usr._.pub, priv: usr._.sea.priv}; - return SEA.write(JSON.stringify(props), keys) - .then(function(remember){ - return !pin ? sessionStorage.setItem('remember', remember) - : SEA.enc(remember, pin).then(function(encauth){ - return new Promise(function(resolve){ - setIndexedDB(usr._.alias, encauth, resolve); - }); + 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 } + root.sessionStorage.setItem('remember', sRemember) + // 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('valid session fails to bootstrap with alias & wrong PIN', + function(done){ + user.recall(12 * 60).then(function(){ + return user.auth(alias+type, pass+' new', {pin: 'PIN'}); + }).then(doCheck(function(ack){ + var sUser = root.sessionStorage.getItem('user'); + var sRemember = root.sessionStorage.getItem('remember'); + var iAuth = ack.auth; + return new Promise(function(resolve){ + checkIndexedDB(sUser, 'auth', function(auth){ + iAuth = auth; + resolve(user.leave()); // Then logout user + }); + }).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); + }catch(e){ done(e); return } + return new Promise(function(resolve){ + checkIndexedDB(sUser, 'auth', function(auth){ + try{ expect(auth).to.not.be(iAuth) }catch(e){ done(e) } + // Then restore IndexedDB auth data, skip sessionStorage + setIndexedDB(sUser, iAuth, function(){ + root.sessionStorage.setItem('user', sUser); + resolve(ack); }); }); }); - }; - - it('with PIN auth session stores', function(done){ - var doAction = function(){ - user.auth(alias+type, pass+' new', {pin: 'PIN'}) - .then(doCheck(done, true)).catch(done); - }; - if(type === 'callback'){ - user.recall(doAction); - } else { - user.recall().then(doAction).catch(done); - } }); + }, true, 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('without PIN auth session stores', function(done){ - var doAction = function(){ - user.auth(alias+type, pass+' new').then(doCheck(done)); - }; - user.leave().then(function(){ - if(type === 'callback'){ - user.recall(doAction); - } else { - user.recall().then(doAction).catch(done); - } - }).catch(done); + it('expired session fails to bootstrap', function(done){ + var pin = 'PIN'; + user.recall(60).then(function(){ + return user.auth(alias+type, pass+' new', {pin: pin}); + }).then(doCheck(function(){ + // Storage data OK, let's back up time of auth to exp + 65 seconds + return manipulateStorage(function(props){ + var ret = Object.assign({}, props, {iat: props.iat - 65 - props.exp}); + return ret; + }, pin); + })).then(() => throwOutUser(false)) // Simulate browser reload + .then(() => user.recall(60).then((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 session')).not.to.be(-1) + done() + })).catch(done) + }); + + it('changed password', function(done){ + var pin = 'PIN'; + var sUser; + var sRemember; + var iAuth; + user.recall(60).then(function(){ + return user.auth(alias+type, pass+' new', {pin: pin}); + }).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'); + + sUser = root.sessionStorage.getItem('user'); + expect(sUser).to.be(alias+type); + + sRemember = root.sessionStorage.getItem('remember'); + expect(sRemember).to.not.be(undefined); + expect(sRemember).to.not.be(''); + }catch(e){ done(e); return } + + return new Promise(function(resolve){ + checkIndexedDB(sUser, 'auth', function(auth){ resolve(iAuth = auth) }); }); + }).then(function(){ + return user.leave().then(function(ack){ + try{ expect(ack).to.have.key('ok') }catch(e){ done(e); return } - it('no validity no session storing', function(done){ - var doAction = function(){ - user.auth(alias+type, pass+' new').then(doCheck(done)).catch(done); - }; - if(type === 'callback'){ - user.recall(0, doAction); - } else { - user.recall(0).then(doAction).catch(done); - } - }); - - it('with validity but no PIN stores using random PIN', function(done){ - var doAction = function(){ - user.auth(alias+type, pass+' new').then(doCheck(done)).catch(done); - }; - if(type === 'callback'){ - user.recall(12 * 60, doAction); - } else { - user.recall(12 * 60).then(doAction) - .catch(done); - } - }); - - it('validity and auth with PIN but storage empty', function(done){ - user.auth(alias+type, pass+' new').then(function(usr){ - var sUser; - var sRemember; - 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'); - - sUser = root.sessionStorage.getItem('user'); - expect(sUser).to.be(alias+type); - - sRemember = root.sessionStorage.getItem('remember'); - expect(sRemember).to.not.be(undefined); - expect(sRemember).to.not.be(''); - }catch(e){ done(e); 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); - }catch(e){ done(e); return } - // Restore but leave IndexedDB empty - root.sessionStorage.setItem('user', sUser); - root.sessionStorage.setItem('remember', sRemember); - - user.recall(12 * 60).then( - doCheck(function(ack){ - expect(ack).to.have.key('err'); - expect(ack.err.toLowerCase().indexOf('no session')).to.not.be(-1); - checkIndexedDB(alias+type, 'auth', function(auth){ - expect((typeof auth !== 'undefined' && auth !== null && auth !== '')) - .to.not.eql(true); - done(); - }); - }, false, true)) - .catch(done); - }).catch(done); - }).catch(done); - }); - - it('valid session bootstrap', function(done){ - var sUser; - var sRemember; - var iAuth; - user.auth(alias+type, pass+' new', {pin: 'PIN'}).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'); - expect(root.sessionStorage.getItem('user')).to.be(alias+type); - expect(root.sessionStorage.getItem('remember')).to.not.be(undefined); - expect(root.sessionStorage.getItem('remember')).to.not.be(''); - - sUser = root.sessionStorage.getItem('user'); - sRemember = root.sessionStorage.getItem('remember'); - }catch(e){ done(e); return } - - return new Promise(function(resolve){ - checkIndexedDB(sUser, 'auth', function(auth){ resolve(iAuth = auth) }); - }); - }).then(function(){ - 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); - }catch(e){ done(e); return } - - return new Promise(function(resolve){ - checkIndexedDB(sUser, 'auth', function(auth){ - expect(auth).to.not.be(iAuth); - resolve(); - }); - }); - }).then(function(){ - root.sessionStorage.setItem('user', sUser); - root.sessionStorage.setItem('remember', sRemember); - - return new Promise(function(resolve){ - setIndexedDB(sUser, iAuth, resolve); - }); - }).then(function(){ - user.recall(12 * 60).then(doCheck(done)) - .catch(done); - }).catch(done); - }).catch(done); - }); - - it('valid session bootstrap using alias & PIN', function(done){ - let sRemember - user.recall(12 * 60).then(function(){ - return user.auth(alias+type, pass+' new', {pin: 'PIN'}); - }).then(doCheck(function(ack){ - // Let's save remember props - var sUser = root.sessionStorage.getItem('user'); - sRemember = root.sessionStorage.getItem('remember') - var iAuth = ack.auth; - return new Promise(function(resolve){ - checkIndexedDB(sUser, 'auth', function(auth){ - iAuth = auth; - resolve(user.leave()); // Then logout user - }); - }).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); - }catch(e){ done(e); return } - return new Promise(function(resolve){ - checkIndexedDB(sUser, 'auth', function(auth){ - try{ expect(auth).to.not.be(iAuth) }catch(e){ done(e) } - // Then restore IndexedDB but skip sessionStorage remember - setIndexedDB(sUser, iAuth, function(){ - root.sessionStorage.setItem('user', sUser); - resolve(ack); - }); - }); - }); - }); - }, true, true)).then(function(){ - // Then try to recall authentication - return user.recall(12 * 60).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 } - root.sessionStorage.setItem('remember', sRemember) - // 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('valid session fails to bootstrap with alias & wrong PIN', - function(done){ - user.recall(12 * 60).then(function(){ - return user.auth(alias+type, pass+' new', {pin: 'PIN'}); - }).then(doCheck(function(ack){ - var sUser = root.sessionStorage.getItem('user'); - var sRemember = root.sessionStorage.getItem('remember'); - var iAuth = ack.auth; - return new Promise(function(resolve){ - checkIndexedDB(sUser, 'auth', function(auth){ - iAuth = auth; - resolve(user.leave()); // Then logout user - }); - }).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); - }catch(e){ done(e); return } - return new Promise(function(resolve){ - checkIndexedDB(sUser, 'auth', function(auth){ - try{ expect(auth).to.not.be(iAuth) }catch(e){ done(e) } - // Then restore IndexedDB auth data, skip sessionStorage - setIndexedDB(sUser, iAuth, function(){ - root.sessionStorage.setItem('user', sUser); - resolve(ack); - }); - }); - }); - }); - }, true, 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(); + return user.auth(alias+type, pass+' new', {newpass: pass, pin: pin}) + .then(function(usr){ expect(usr).to.not.have.key('err') }); + }).then(() => user.leave().then((ack) => { + try { + expect(ack).to.have.key('ok') + } catch (e) { done(e); return } + return throwOutUser(false) + })).then(function(){ + // Simulate browser reload + // Call back pre-update remember... + root.sessionStorage.setItem('user', sUser); + root.sessionStorage.setItem('remember', sRemember); + // ... and IndexedDB auth + return new Promise(function(resolve){ + setIndexedDB(sUser, iAuth, resolve); }); - }); - - it('expired session fails to bootstrap', function(done){ - var pin = 'PIN'; - user.recall(60).then(function(){ - return user.auth(alias+type, pass+' new', {pin: pin}); - }).then(doCheck(function(){ - // Storage data OK, let's back up time of auth to exp + 65 seconds - return manipulateStorage(function(props){ - var ret = Object.assign({}, props, {iat: props.iat - 65 - props.exp}); - return ret; - }, pin); - })).then(() => throwOutUser(false)) // Simulate browser reload - .then(() => user.recall(60).then((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 session')).not.to.be(-1) - done() - })).catch(done) - }); - - it('changed password', function(done){ - var pin = 'PIN'; - var sUser; - var sRemember; - var iAuth; - user.recall(60).then(function(){ - return user.auth(alias+type, pass+' new', {pin: pin}); - }).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'); - - sUser = root.sessionStorage.getItem('user'); - expect(sUser).to.be(alias+type); - - sRemember = root.sessionStorage.getItem('remember'); - expect(sRemember).to.not.be(undefined); - expect(sRemember).to.not.be(''); - }catch(e){ done(e); return } - - return new Promise(function(resolve){ - checkIndexedDB(sUser, 'auth', function(auth){ resolve(iAuth = auth) }); - }); - }).then(function(){ - return user.leave().then(function(ack){ - try{ expect(ack).to.have.key('ok') }catch(e){ done(e); return } - - return user.auth(alias+type, pass+' new', {newpass: pass, pin: pin}) - .then(function(usr){ expect(usr).to.not.have.key('err') }); - }).then(() => user.leave().then((ack) => { - try { - expect(ack).to.have.key('ok') - } catch (e) { done(e); return } - return throwOutUser(false) - })).then(function(){ - // Simulate browser reload - // Call back pre-update remember... - root.sessionStorage.setItem('user', sUser); - root.sessionStorage.setItem('remember', sRemember); - // ... and IndexedDB auth - return new Promise(function(resolve){ - setIndexedDB(sUser, iAuth, resolve); - }); - }).then(function(){ - user.recall(60).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(''); - expect(props.err.toLowerCase() - .indexOf('no session')).not.to.be(-1); - done(); - }).catch(done); - }).catch(done); - }).catch(done); - }); - - it('recall hook session manipulation', function(done){ - var pin = 'PIN'; - var exp; - var hookFunc = function(props){ - exp = props.exp * 2; // Doubles session expiration time - var ret = Object.assign({}, props, {exp: exp}); - return (type === 'callback' && ret) || new Promise(function(resolve){ - resolve(ret); // Both callback & Promise methods here - }); - }; - user.recall(60, {hook: hookFunc}).then(function(){ - return user.auth(alias+type, pass, {pin: pin}); - }).then(function(){ - return manipulateStorage(function(props){ - expect(props).to.not.be(undefined); - expect(props).to.have.key('exp'); - expect(props.exp).to.be(exp); - return props; - }, pin); - }).then(done).catch(done); - }); - }); - - describe('alive', function(){ - it('valid session', 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.keys([ 'sea', 'pub' ]); - }catch(e){ done(e); return } + }).then(function(){ + user.recall(60).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(''); + expect(props.err.toLowerCase() + .indexOf('no session')).not.to.be(-1); done(); - }; - 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(aliveUser, pass, {pin: 'PIN'}).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.alive - keeps/checks User authentiation state - if(type === 'callback'){ - user.alive(check); - } else { - user.alive().then(check).catch(done); - } - }).catch(done); }).catch(done); - }); + }).catch(done); + }).catch(done); + }); - it('expired session', function(done){ - var check = function(ack){ - try{ - expect(ack).to.not.be(undefined); - expect(ack).to.not.be(''); - 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(); - }; - user.leave().catch(function(){}).then(function(){ - user.alive().then(function(){ - done('Unexpected alive session!'); - }).catch(check); - }).catch(done); + it('recall hook session manipulation', function(done){ + var pin = 'PIN'; + var exp; + var hookFunc = function(props){ + exp = props.exp * 2; // Doubles session expiration time + var ret = Object.assign({}, props, {exp: exp}); + return new Promise(function(resolve){ + resolve(ret); // Both callback & Promise methods here }); - }); + }; + user.recall(60, {hook: hookFunc}).then(function(){ + return user.auth(alias+type, pass, {pin: pin}); + }).then(function(){ + return manipulateStorage(function(props){ + expect(props).to.not.be(undefined); + expect(props).to.have.key('exp'); + expect(props.exp).to.be(exp); + return props; + }, pin); + }).then(done).catch(done); + }); + }); + + describe('alive', function(){ + it('valid session', 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.keys([ 'sea', 'pub' ]); + }catch(e){ done(e); return } + done(); + }; + 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(aliveUser, pass, {pin: 'PIN'}).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.alive - keeps/checks User authentiation state + user.alive().then(check).catch(done); + }).catch(done); + }).catch(done); + }); + + it('expired session', function(done){ + var check = function(ack){ + try{ + expect(ack).to.not.be(undefined); + expect(ack).to.not.be(''); + 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(); + }; + user.leave().catch(function(){}).then(function(){ + user.alive().then(function(){ + done('Unexpected alive session!'); + }).catch(check); + }).catch(done); }); });