Added User.delete & one test case - refactored test cases for smoother operation in case of error(s)

This commit is contained in:
mhelander 2017-08-31 14:54:34 +03:00
parent 8bb6e67599
commit 41c0f9ed79
2 changed files with 230 additions and 156 deletions

241
sea.js
View File

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

View File

@ -8046,9 +8046,11 @@ describe('Gun', function(){
describe('create', function(){
it('new', function(done){
var check = function(ack){
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.have.keys(['ok','pub']);
try{
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.have.keys(['ok','pub']);
}catch(e){done(e); return};
done();
};
// Gun.user.create - creates new user
@ -8061,11 +8063,13 @@ describe('Gun', function(){
it('conflict', function(done){
Gun.log.off = true; // Supress all console logging
var check = function(ack){
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.have.key('err');
expect(ack.err).not.to.be(undefined);
expect(ack.err).not.to.be('');
try{
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.have.key('err');
expect(ack.err).not.to.be(undefined);
expect(ack.err).not.to.be('');
}catch(e){done(e); return};
done();
};
// Gun.user.create - fails to create existing user
@ -8082,9 +8086,11 @@ describe('Gun', function(){
describe('auth', function(){
it('login', function(done){
var check = function(ack){
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.not.have.key('err');
try{
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.not.have.key('err');
}catch(e){done(e); return};
done();
};
// Gun.user.auth - authenticates existing user
@ -8097,11 +8103,13 @@ describe('Gun', function(){
it('wrong password', function(done){
var check = function(ack){
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.have.key('err');
expect(ack.err).to.not.be(undefined);
expect(ack.err).to.not.be('');
try{
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.have.key('err');
expect(ack.err).to.not.be(undefined);
expect(ack.err).to.not.be('');
}catch(e){done(e); return};
done();
};
if(type === 'callback'){
@ -8115,11 +8123,13 @@ describe('Gun', function(){
it('unknown alias', function(done){
var check = function(ack){
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.have.key('err');
expect(ack.err).to.not.be(undefined);
expect(ack.err).to.not.be('');
try{
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.have.key('err');
expect(ack.err).to.not.be(undefined);
expect(ack.err).to.not.be('');
}catch(e){done(e); return};
done();
};
if(type === 'callback'){
@ -8133,9 +8143,11 @@ describe('Gun', function(){
it('new password', function(done){
var check = function(ack){
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.not.have.key('err');
try{
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.not.have.key('err');
}catch(e){done(e); return};
done();
};
// Gun.user.auth - with newpass props sets new password
@ -8149,11 +8161,13 @@ describe('Gun', function(){
it('failed new password', function(done){
var check = function(ack){
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.have.key('err');
expect(ack.err).to.not.be(undefined);
expect(ack.err).to.not.be('');
try{
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.have.key('err');
expect(ack.err).to.not.be(undefined);
expect(ack.err).to.not.be('');
}catch(e){done(e); return};
done();
};
var props = {alias: alias+type, pass: pass+'not', newpass: pass+' new'};
@ -8173,18 +8187,22 @@ describe('Gun', function(){
describe('leave', function(){
it('valid session', function(done){
var check = function(ack){
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.not.have.key('err');
expect(ack).to.have.key('ok');
expect(gun.back(-1)._.user).to.not.have.keys(['sea', 'pub']);
try{
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.not.have.key('err');
expect(ack).to.have.key('ok');
expect(gun.back(-1)._.user).to.not.have.keys(['sea', 'pub']);
}catch(e){done(e); return};
done();
};
user.auth(alias+type, pass+' new').then(function(usr){
expect(usr).to.not.be(undefined);
expect(usr).to.not.be('');
expect(usr).to.not.have.key('err');
expect(usr).to.have.key('put');
try{
expect(usr).to.not.be(undefined);
expect(usr).to.not.be('');
expect(usr).to.not.have.key('err');
expect(usr).to.have.key('put');
}catch(e){done(e); return};
// Gun.user.leave - performs logout for authenticated user
if(type === 'callback'){
user.leave(check);
@ -8196,10 +8214,12 @@ describe('Gun', function(){
it('no session', function(done){
var check = function(ack){
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.not.have.key('err');
expect(ack).to.have.key('ok');
try{
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.not.have.key('err');
expect(ack).to.have.key('ok');
}catch(e){done(e); return};
done();
};
user.leave().then(function(ack){
@ -8213,9 +8233,44 @@ describe('Gun', function(){
});
});
describe('delete - TODO: how?', function(){
it.skip('existing authenticated user');
it.skip('unauthenticated user');
describe('delete', function(){
it('existing authenticated user', function(done){
var check = function(ack){
try{
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.not.have.key('err');
expect(ack).to.have.key('ok');
expect(gun.back(-1)._.user).to.not.have.keys(['sea', 'pub']);
}catch(e){done(e); return};
done();
};
var aalias = alias+type+'del';
user.create(aalias, pass).catch(check).then(function(ack){
try{
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.have.keys(['ok','pub']);
}catch(e){done(e); return};
user.auth(aalias, pass).then(function(usr){
try{
expect(usr).to.not.be(undefined);
expect(usr).to.not.be('');
expect(usr).to.not.have.key('err');
expect(usr).to.have.key('put');
}catch(e){done(e); return};
// Gun.user.delete - deöetes existing user account
if(type === 'callback'){
user.delete(alias+type, pass+' new', check);
} else {
user.delete(alias+type, pass+' new').then(check).catch(done);
}
}).catch(done);
});
});
it.skip('unauthenticated existing user');
it.skip('unauthenticated other user');
});
describe('recall', function(){