Almost completed Gun.user test cases - remember is still TODO:

This commit is contained in:
mhelander 2017-08-29 19:49:08 +03:00
parent 50b69db8d5
commit 09db7fd9d2
2 changed files with 201 additions and 116 deletions

236
sea.js
View File

@ -44,6 +44,7 @@
var user = root._.user || (root._.user = root.chain()); // create a user context. var user = root._.user || (root._.user = root.chain()); // create a user context.
user.create = User.create; // attach a factory method to it. user.create = User.create; // attach a factory method to it.
user.auth = User.auth; // and a login method. user.auth = User.auth; // and a login method.
user.remember = User.remember; // and a credentials persisting method.
return user; // return the user! return user; // return the user!
} }
@ -74,130 +75,147 @@
// Well first we have to actually create a user. That is what this function does. // Well first we have to actually create a user. That is what this function does.
User.create = function(alias, pass, cb){ User.create = function(alias, pass, cb){
var root = this.back(-1); var root = this.back(-1);
cb = cb || function(){}; var doCreate = function(resolve, reject){
// Because more than 1 user might have the same username, we treat the alias as a list of those users. // Because more than 1 user might have the same username, we treat the alias as a list of those users.
root.get('alias/'+alias).get(function(at, ev){ root.get('alias/'+alias).get(function(at, ev){
ev.off(); ev.off();
if(at.put){ 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. // If we can enforce that a user name is already taken, it might be nice to try, but this is not guaranteed.
return cb({err: Gun.log("User already created!")}); var err = 'User already created!';
} Gun.log(err);
var user = {alias: alias, salt: Gun.text.random(64)}; return reject({err: err});
// pseudo-randomly create a salt, then use CryptoJS's PBKDF2 function to extend the password with it. }
SEA.proof(pass, user.salt).then(function(proof){ var user = {alias: alias, salt: Gun.text.random(64)};
// this will take some short amount of time to produce a proof, which slows brute force attacks. // pseudo-randomly create a salt, then use CryptoJS's PBKDF2 function to extend the password with it.
SEA.pair().then(function(pair){ SEA.proof(pass, user.salt).then(function(proof){
// now we have generated a brand new ECDSA key pair for the user account. // this will take some short amount of time to produce a proof, which slows brute force attacks.
user.pub = pair.pub; SEA.pair().then(function(pair){
// the user's public key doesn't need to be signed. But everything else needs to be signed with it! // now we have generated a brand new ECDSA key pair for the user account.
SEA.write(alias, pair.priv).then(function(sAlias){ user.pub = pair.pub;
user.alias = sAlias; // the user's public key doesn't need to be signed. But everything else needs to be signed with it!
return SEA.write(user.salt, pair.priv); SEA.write(alias, pair.priv).then(function(sAlias){
}).then(function(sSalt){ user.alias = sAlias;
user.salt = sSalt; return SEA.write(user.salt, pair.priv);
// to keep the private key safe, we AES encrypt it with the proof of work! }).then(function(sSalt){
return SEA.en(pair.priv, proof); user.salt = sSalt;
}).then(function(encVal){ // to keep the private key safe, we AES encrypt it with the proof of work!
return SEA.write(encVal, pair.priv); return SEA.en(pair.priv, proof);
}).then(function(sAuth){ }).then(function(encVal){
user.auth = sAuth; return SEA.write(encVal, pair.priv);
var tmp = 'pub/'+pair.pub; }).then(function(sAuth){
//console.log("create", user, pair.pub); user.auth = sAuth;
// awesome, now we can actually save the user with their public key as their ID. var tmp = 'pub/'+pair.pub;
root.get(tmp).put(user); //console.log("create", user, pair.pub);
// next up, we want to associate the alias with the public key. So we add it to the alias list. // awesome, now we can actually save the user with their public key as their ID.
var ref = root.get('alias/'+alias).put(Gun.obj.put({}, tmp, Gun.val.rel.ify(tmp))); root.get(tmp).put(user);
// callback that the user has been created. (Note: ok = 0 because we didn't wait for disk to ack) // next up, we want to associate the alias with the public key. So we add it to the alias list.
cb({ok: 0, pub: pair.pub}); var ref = 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)
resolve({ok: 0, pub: pair.pub});
});
}); });
}); });
}); });
}); };
if (cb){doCreate(cb, cb)} else {return new Promise(doCreate)}
}; };
// now that we have created a user, we want to authenticate them! // now that we have created a user, we want to authenticate them!
User.auth = function(props, cb){ User.auth = function(props, cb){
var alias = props.alias, pass = props.pass, newpass = props.newpass; var alias = props.alias, pass = props.pass, newpass = props.newpass;
var root = this.back(-1); var root = this.back(-1);
cb = cb || function(){}; var doAuth = function(resolve, reject){
// load all public keys associated with the username alias we want to log in with. // load all public keys associated with the username alias we want to log in with.
root.get('alias/'+alias).get(function(at, ev){ root.get('alias/'+alias).get(function(at, ev){
ev.off(); ev.off();
if(!at.put){ if(!at.put){
// if no user, don't do anything. // if no user, don't do anything.
return cb({err: Gun.log("No user!")}); var err = 'No user!';
} Gun.log(err);
// then attempt to log into each one until we find ours! return reject({err: err});
// (if two users have the same username AND the same password... that would be bad) }
Gun.obj.map(at.put, function(val, key){ // then attempt to log into each one until we find ours!
// grab the account associated with this public key. // (if two users have the same username AND the same password... that would be bad)
root.get(key).get(function(at, ev){ Gun.obj.map(at.put, function(val, key){
key = key.slice(4); // grab the account associated with this public key.
ev.off(); root.get(key).get(function(at, ev){
if(!at.put){ return cb({err: "Public key does not exist!"}) } key = key.slice(4);
// attempt to PBKDF2 extend the password with the salt. (Verifying the signature gives us the plain text salt.) ev.off();
SEA.read(at.put.salt, key).then(function(salt){ if(!at.put){ return reject({err: 'Public key does not exist!'}) }
return SEA.proof(pass, salt); // attempt to PBKDF2 extend the password with the salt. (Verifying the signature gives us the plain text salt.)
}).then(function(proof){ SEA.read(at.put.salt, key).then(function(salt){
// the proof of work is evidence that we've spent some time/effort trying to log in, this slows brute force. return SEA.proof(pass, salt);
return SEA.read(at.put.auth, key).then(function(auth){ }).then(function(proof){
return SEA.de(auth, proof); // the proof of work is evidence that we've spent some time/effort trying to log in, this slows brute force.
}); return SEA.read(at.put.auth, key).then(function(auth){
}).then(function(priv){ return SEA.de(auth, proof);
// now we have AES decrypted the private key, from when we encrypted it with the proof at registration. });
if(priv){ // if we were successful, then that means... }).then(function(priv){
// we're logged in! // now we have AES decrypted the private key, from when we encrypted it with the proof at registration.
function doLogin(){ if(priv){ // if we were successful, then that means...
var user = root._.user; // we're logged in!
// add our credentials in-memory only to our root gun instance function doLogin(){
user._ = at.gun._; var user = root._.user;
// so that way we can use the credentials to encrypt/decrypt data // add our credentials in-memory only to our root gun instance
user._.is = user.is = {}; user._ = at.gun._;
// that is input/output through gun (see below) // so that way we can use the credentials to encrypt/decrypt data
user._.sea = priv; user._.is = user.is = {};
user._.pub = key; // that is input/output through gun (see below)
//console.log("authorized", user._); user._.sea = priv;
// callbacks success with the user data credentials. user._.pub = key;
cb(user._); //console.log("authorized", user._);
// emit an auth event, useful for page redirects and stuff. // callbacks success with the user data credentials.
Gun.on('auth', user._); resolve(user._);
} // emit an auth event, useful for page redirects and stuff.
if(newpass) { Gun.on('auth', user._);
// password update so encrypt private key using new pwd + salt }
var newsalt = Gun.text.random(64); if(newpass) {
SEA.proof(newpass, newsalt).then(function(proof){ // password update so encrypt private key using new pwd + salt
SEA.en(priv, proof).then(function(encVal){ var newsalt = Gun.text.random(64);
return SEA.write(encVal, priv).then(function(sAuth){ SEA.proof(newpass, newsalt).then(function(proof){
return { pub: key, auth: sAuth }; SEA.en(priv, proof).then(function(encVal){
return SEA.write(encVal, priv).then(function(sAuth){
return { pub: key, auth: sAuth };
});
}).then(function(user){
return SEA.write(alias, priv).then(function(sAlias){
user.alias = sAlias; return user;
});
}).then(function(user){
return SEA.write(newsalt, priv).then(function(sSalt){
user.salt = sSalt; return user;
});
}).then(function(user){
var tmp = 'pub/'+key;
// awesome, now we can update the user using public key ID.
root.get(tmp).put(user);
// then we're done
doLogin();
}); });
}).then(function(user){
return SEA.write(alias, priv).then(function(sAlias){
user.alias = sAlias; return user;
});
}).then(function(user){
return SEA.write(newsalt, priv).then(function(sSalt){
user.salt = sSalt; return user;
});
}).then(function(user){
var tmp = 'pub/'+key;
// awesome, now we can update the user using public key ID.
root.get(tmp).put(user);
// then we're done
doLogin();
}); });
}); } else {
} else { doLogin();
doLogin(); }
return;
} }
return; // Or else we failed to log in...
} Gun.log('Failed to sign in!');
// Or else we failed to log in... reject({err: 'Attempt failed'});
console.log("Failed to sign in!"); });
cb({err: "Attempt failed"});
}); });
}); });
}); });
}); };
if (cb){doAuth(cb, cb)} else {return new Promise(doAuth)}
}; };
// now that we have created a user, we want to authenticate them!
User.remember = function(props, cb){
var doRemember = function(resolve, reject){
Gun.log('User.remember is TODO: still');
reject({ err: 'Not implemented.' });
}
if (cb){doRemember(cb, cb)} else {return new Promise(doRemember)}
};
// After we have a GUN extension to make user registration/login easy, we then need to handle everything else. // After we have a GUN extension to make user registration/login easy, we then need to handle everything else.
// We do this with a GUN adapter, we first listen to when a gun instance is created (and when its options change) // We do this with a GUN adapter, we first listen to when a gun instance is created (and when its options change)

View File

@ -8035,20 +8035,87 @@ describe('Gun', function(){
Gun().user && describe('User', function(){ Gun().user && describe('User', function(){
console.log('TODO: User! THIS IS AN EARLY ALPHA!!!'); console.log('TODO: User! THIS IS AN EARLY ALPHA!!!');
var user = Gun().user; var alias = 'dude';
var pass = 'my secret password';
var user = Gun().user();
['callback', 'Promise'].forEach(function(type){ ['callback', 'Promise'].forEach(function(type){
describe(type, function(){ describe(type, function(){
it.skip('create', function(done){ describe('create', function(){
done(); it('new', function(done){
var check = function(ack){
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.have.keys(['ok','pub']);
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('conflict', function(done){
var gunLog = Gun.log; // Temporarily removing logging
Gun.log = function(){};
var check = function(ack){
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.have.key('err');
expect(ack.err).not.to.be(undefined);
expect(ack.err).not.to.be('');
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);
}
Gun.log = gunLog;
});
}); });
it.skip('auth', function(done){ describe('auth', function(){
done(); it('login', function(done){
var check = function(ack){
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.not.have.key('err');
done();
};
var props = {alias: alias+'-'+type, pass: pass};
user.create(props.alias, props.pass).catch(function(){})
.then(function(){
// Gun.user.create - creates new user
if(type === 'callback'){
user.auth(props, check);
} else {
user.auth(props).then(check).catch(done);
}
});
});
it.skip('failed login', function(done){
done();
});
it.skip('new password', function(done){
done();
});
it.skip('failed new password', function(done){
done();
});
}); });
it.skip('remember', function(done){ describe('remember', function(){
done(); it.skip('TBD', function(done){
done();
});
}); });
}); });
}); });