All test cases completed & some bugfixes & 'remember-me' recovery with PIN now supported

This commit is contained in:
mhelander 2017-09-07 00:49:34 +03:00
parent 4808a9d945
commit 0adfcb3b03
2 changed files with 359 additions and 176 deletions

278
sea.js
View File

@ -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){

View File

@ -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;
});
});