SEA + GUN thanks to @mhelander !!! Tested by PANIC!

Sea
This commit is contained in:
Mark Nadal 2017-10-21 10:46:29 -07:00 committed by GitHub
commit b026a9c9f2
5 changed files with 367 additions and 70 deletions

8
gun.js
View File

@ -749,15 +749,19 @@
};
if(this.ack){ msg['@'] = this.ack }
obj_map(node, each, this);
cat.on('node', msg);
}
function each(val, key){
var graph = this.graph, soul = this.soul, at = (this.at._), tmp;
graph[soul] = Gun.state.to(this.node, key, graph[soul]);
at.put = Gun.state.to(this.node, key, at.put);
}
function map(msg, soul){
function map(msg, soul){ var tmp;
if(!msg.gun){ return }
if((tmp = this.gun._).tag.node){
return tmp.on('node', function(msg){ this.off();
(msg.gun._).on('in', msg);
}).on.on('node', msg);
}
(msg.gun._).on('in', msg);
}

View File

@ -8,6 +8,7 @@
"start": "node examples/http.js 8080",
"prepublish": "npm run unbuild",
"test": "mocha",
"testsea": "mocha test/sea.js",
"e2e": "mocha e2e/distributed.js",
"docker": "hooks/build",
"unbuild": "node lib/unbuild.js && uglifyjs gun.js -o gun.min.js -c -m"

163
sea.js
View File

@ -128,19 +128,21 @@
return reject(err);
}
// then figuring out all possible candidates having matching username
var aliases = [];
var aliases = [], c = 0;
Gun.obj.map(rat.put, function(at, pub){
if(!pub.slice || 'pub/' !== pub.slice(0,4)){ return }
c++;
// grab the account associated with this public key.
root.get(pub).get(function(at, ev){
if(!pub.slice || 'pub/' !== pub.slice(0,4)){ return }
pub = pub.slice(4);
ev.off();
if(!at.put){ return }
aliases.push({pub: pub, at: at});
ev.off(); c--;
if(at.put){
aliases.push({pub: pub, at: at});
}
if(!c && (c = -1)){ resolve(aliases) }
});
});
return aliases.length && resolve(aliases)
|| reject('Public key does not exist!');
if(!c){ reject('Public key does not exist!') }
});
});
}
@ -158,7 +160,7 @@
return !remaining && 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.auth, pub).then(function(auth){
var auth = at.put.auth; // SEA.read(at.put.auth, pub).then(function(auth){ // NOTE: aliasquery uses `gun.get` which internally SEA.read verifies the data for us, so we do not need to re-verify it here.
auth = auth.slice ? JSON.parse(auth) : auth;
return SEA.proof(pass, auth.salt)
.catch(function(e){ reject({err: 'Failed to create proof!'}) })
@ -173,12 +175,10 @@
if(sea){
user.priv = sea.priv;
user.salt = auth.salt; // TODO: needed?
SEA.read(at.put.epub, pub).then(function(epub){
var epub = at.put.epub; //SEA.read(at.put.epub, pub).then(function(epub){ // NOTE: see above "NOTE"!
Object.assign(user, {epub: epub, epriv: sea.epriv});
resolve(user);
}).catch(function(){
return !remaining && reject({err: 'Public key does not exist!'});
});
//}).catch(function(){ return !remaining && reject({err: 'Public key does not exist!'}) });
} else if(!remaining){
reject({err: 'Public key does not exist!'});
}
@ -188,7 +188,7 @@
// : reject({err: 'Failed to decrypt private key!'});
}).catch(function(e){ reject({err: 'Failed read secret!'})} );
});
}).catch(function(e){ reject({err: 'Failed to create proof!'}) });
//}).catch(function(e){ reject({err: 'Failed to create proof!'}) });
});
}).catch(function(e){ reject({err: e}) });
});
@ -329,7 +329,7 @@
};
// Already authenticated?
if(Gun.obj.has(root._.user._, 'pub') && Gun.obj.has(root._.user._, 'sea')){
if(root._.user && Gun.obj.has(root._.user._, 'pub') && Gun.obj.has(root._.user._, 'sea')){
return resolve(root._.user._);
}
// No, got alias?
@ -383,9 +383,9 @@
return !remaining && reject({err: 'Failed to decrypt private key!'});
}).then(function(sea){
if(!sea){ return }
return SEA.read(at.put.epub, pub).then(function(epub){
var epub = at.put.epub; //return SEA.read(at.put.epub, pub).then(function(epub){ // NOTE: queryalias uses `gun.get` which internally verifies data with `SEA.read` so we do not need to do it again.
return {pub: pub, priv: sea.priv, epriv: sea.epriv, epub: epub};
});
//});
}).then(function(key){
// now we have AES decrypted the private key,
// if we were successful, then that means we're logged in!
@ -431,8 +431,9 @@
// This internal func executes logout actions
function authleave(root, alias){
return function(resolve, reject){
var user = root._.user;
alias = alias || (user._ && user._.alias);
var user = root._.user || {_:{}};
root._.user = null;
alias = alias || user._.alias;
var doIt = function(){
// TODO: is this correct way to 'logout' user from Gun.User ?
[ 'alias', 'sea', 'pub' ].forEach(function(key){
@ -554,7 +555,6 @@
reject({err: 'Auth attempt failed! Reason: No session data for alias & PIN'});
});
}
authenticate(alias, pass, root).then(function(keys){
// we're logged in!
var pin = Gun.obj.has(opts, 'pin') && {pin: opts.pin};
@ -631,7 +631,7 @@
// Delete user data
root.get('pub/'+key.pub).put(null);
// Wipe user data from memory
var user = root._.user;
var user = root._.user || {_: {}};
// TODO: is this correct way to 'logout' user from Gun.User ?
[ 'alias', 'sea', 'pub' ].forEach(function(key){
delete user._[key];
@ -734,22 +734,37 @@
// This means we should ONLY trust our "friends" (our key ring) public keys, not any ones.
// I have not yet added that to SEA yet in this alpha release. That is coming soon, but beware in the meanwhile!
function each(msg){ // TODO: Warning: Need to switch to `gun.on('node')`! Do not use `Gun.on('node'` in your apps!
var ctx = this.as;
var own = ctx.sea.own, soul = msg.get;
var pub = own[soul] || soul.slice(4), vertex = (msg.gun._).put;
Gun.node.is(msg.put, function(val, key, node){ // for each property on the node.
SEA.read(val, pub).then(function(data){
vertex[key] = node[key] = val = data; // verify signature and get plain value.
if(val && val['#'] && (key = Gun.val.rel.is(val))){ // if it is a relation / edge
if('alias/' === soul.slice(0,6)){ return } // if it is itself
own[key] = pub; // associate the public key with a node
}
// NOTE: THE SECURITY FUNCTION HAS ALREADY VERIFIED THE DATA!!!
// WE DO NOT NEED TO RE-VERIFY AGAIN, JUST TRANSFORM IT TO PLAINTEXT.
var to = this.to, vertex = (msg.gun._).put, c = 0;
Gun.node.is(msg.put, function(val, key, node){ c++; // for each property on the node
SEA.read(val, false).then(function(data){ c--; // false just extracts the plain data.
vertex[key] = node[key] = val = data; // transform to plain value.
if(!c && (c = -1)){ to.next(msg) }
});
});
if(!c){ to.next(msg) }
return;
/*var to = this.to, ctx = this.as;
var own = ctx.sea.own, soul = msg.get, c = 0;
var pub = own[soul] || soul.slice(4), vertex = (msg.gun._).put;
Gun.node.is(msg.put, function(val, key, node){ c++; // for each property on the node.
SEA.read(val, pub).then(function(data){ c--;
vertex[key] = node[key] = val = data; // verify signature and get plain value.
if(val && val['#'] && (key = Gun.val.rel.is(val))){ // if it is a relation / edge
if('alias/' !== soul.slice(0,6)){ own[key] = pub; } // associate the public key with a node if it is itself
}
if(!c && (c = -1)){ to.next(msg) }
});
});
if(!c){ to.next(msg) }*/
}
// signature handles data output, it is a proxy to the security function.
function signature(msg){
if(msg.user){
return this.to.next(msg);
}
var ctx = this.as;
msg.user = ctx.user;
security.call(this, msg);
@ -776,7 +791,7 @@
}
if(msg.put){
// potentially parallel async operations!!!
var check = {}, on = Gun.on(), each = {};
var check = {}, on = Gun.on(), each = {}, u;
each.node = function(node, soul){
if(Gun.obj.empty(node, '_')){ return check['node'+soul] = 0 } // ignore empty updates, don't reject them.
Gun.obj.map(node, each.way, {soul: soul, node: node});
@ -785,56 +800,62 @@
var soul = this.soul, node = this.node, tmp;
if('_' === key){ return } // ignore meta data
if('alias' === soul){ // special case for shared system data, the list of aliases.
each.alias(val, key, node, soul);
each.alias(val, key, node, soul); return;
}
if('alias/' === soul.slice(0,6)){ // special case for shared system data, the list of public keys for an alias.
each.pubs(val, key, node, soul);
each.pubs(val, key, node, soul); return;
}
if('pub/' === soul.slice(0,4)){ // special case, account data for a public key.
each.pub(val, key, node, soul, soul.slice(4));
each.pub(val, key, node, soul, soul.slice(4), msg.user); return;
}
if(at.user && at.user._.sea){ // not special case, if we are logged in, then
var u = at.user._, p = u.sea;
return each.end({err: "No other data allowed!"});
/*if(!(tmp = at.user)){ return }
if(soul.slice(4) === (tmp = tmp._).pub){ // not a special case, if we are logged in and have outbound data on us.
each.user(val, key, node, soul, {
pub: u.pub, priv: p.priv, epub: u.epub, epriv: p.epriv
pub: tmp.pub, priv: tmp.sea.priv, epub: tmp.sea.epub, epriv: tmp.sea.epriv
});
}
if((tmp = sea.own[soul])){ // not special case, if we receive an update on an ID associated with a public key, then
each.own(val, key, node, soul, tmp);
}
}*/
};
each.alias = function(val, key, node, soul){
if(!val){ return on.to('end', {err: "Data must exist!"}) } // data MUST exist
if('alias/'+key !== Gun.val.rel.is(val)){ // in fact, it must be EXACTLY equal to itself
return on.to('end', {err: "Mismatching alias."}); // if it isn't, reject.
}
each.alias = function(val, key, node, soul){ // Example: {_:#alias, alias/alice: {#alias/alice}}
if(!val){ return each.end({err: "Data must exist!"}) } // data MUST exist
if('alias/'+key === Gun.val.rel.is(val)){ return check['alias'+key] = 0 } // in fact, it must be EXACTLY equal to itself
each.end({err: "Mismatching alias."}); // if it isn't, reject.
};
each.pubs = function(val, key, node, soul){
if(!val){ return on.to('end', {err: "Alias must exist!"}) } // data MUST exist
each.pubs = function(val, key, node, soul){ // Example: {_:#alias/alice, pub/asdf: {#pub/asdf}}
if(!val){ return each.end({err: "Alias must exist!"}) } // data MUST exist
if(key === Gun.val.rel.is(val)){ return check['pubs'+soul+key] = 0 } // and the ID must be EXACTLY equal to its property
return on.to('end', {err: "Alias must match!"}); // that way nobody can tamper with the list of public keys.
each.end({err: "Alias must match!"}); // that way nobody can tamper with the list of public keys.
};
each.pub = function(val, key, node, soul, pub){
//console.log("WE ARE HERE", key, val, soul, node, pub);
each.pub = function(val, key, node, soul, pub, user){ // Example: {_:#pub/asdf, hello:SEA['world',fdsa]}
if('pub' === key){
if(val === pub){ return check['pub'+soul+key] = 0 } // the account MUST have a `pub` property that equals the ID of the public key.
return on.to('end', {err: "Account must match!"});
if(val === pub){ return (check['pub'+soul+key] = 0) } // the account MUST match `pub` property that equals the ID of the public key.
return each.end({err: "Account must match!"});
}
/*
if(at.user && at.user._){ // if we are logged in
if(pub === 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.
check['user'+soul+key] = 1;
if(user && (user = user._) && user.sea){
if(pub === user.pub){
SEA.write(val, Gun.obj.to(user.sea, {pub: user.pub, epub: user.epub}), function(data){
node[key] = data;
check['user'+soul+key] = 0;
each.end({ok: 1});
});
} // (if we are lying about our signature, other peer's will reject our update)
} else {
each.end({err: "Please stop trying to hack/impersonate somebody else!"});
}
return;
}
SEA.read(val, pub).then(function(data){
if(u === (val = data)){ // make sure the signature matches the account it claims to be on.
return no = true; // reject any updates that are signed with a mismatched account.
if(u === data){ // make sure the signature matches the account it claims to be on.
return each.end({err: "Unverified data."}); // reject any updates that are signed with a mismatched account.
}
check['user'+soul+key] = 0;
each.end({ok: 1});
});
*/
};
/*
each.user = function(val, key, node, soul, tmp){
check['user'+soul+key] = 1;
SEA.write(val, tmp, function(data){ // TODO: BUG! Convert to use imported.
@ -853,8 +874,9 @@
on.to('end', {no: tmp = (u === (val = data)), err: tmp && "Signature mismatch!"});
});
};
on.to('end', function(ctx){ // TODO: Can't you just switch this to each.end = cb?
if(each.err || !each.end){ return }
*/
each.end = function(ctx){ // TODO: Can't you just switch this to each.end = cb?
if(each.err || !each.end.ed){ return }
if((each.err = ctx.err) || ctx.no){
console.log('NO!', each.err);
return;
@ -863,9 +885,9 @@
if(no){ return true }
})){ return }
to.next(msg);
});
};
Gun.obj.map(msg.put, each.node);
on.to('end', {end: each.end = true});
each.end({end: each.end.ed = true});
return; // need to manually call next after async.
}
to.next(msg); // pass forward any data we do not know how to handle or process (this allows custom security protocols).
@ -1070,14 +1092,15 @@
var doIt = function(resolve, reject) {
// TODO: something's bugging double 'SEA[]' treatment to mm...
var m = mm;
if(mm.slice){
if(m && m.slice && 'SEA[' === m.slice(0,4)){ return resolve(m) }
if(mm && mm.slice){
// Needs to remove previous signature envelope
while('SEA[' === m.slice(0,4)){
try{ m = JSON.parse(m.slice(3))[0];
}catch(e){ break }
}
}
m = m.slice ? m : JSON.stringify(m);
m = (m && m.slice) ? m : JSON.stringify(m);
SEA.sign(m, p).then(function(signature){
resolve('SEA'+JSON.stringify([m,signature]));
}).catch(function(e){Gun.log(e); reject(e)});
@ -1086,12 +1109,18 @@
};
SEA.read = function(m,p,cb){
var doIt = function(resolve, reject) {
if(!m){ return resolve() }
if(!m.slice || 'SEA[' !== m.slice(0,4)){ return resolve(m) }
if(!m){ if(false === p){ return resolve(m) }
return resolve();
}
if(!m.slice || 'SEA[' !== m.slice(0,4)){
if(false === p){ return resolve(m) }
return resolve()
}
m = m.slice(3);
try{ m = m.slice ? JSON.parse(m) : m;
}catch(e){ return reject(e) }
m = m || '';
if(false === p){ resolve(m[0]) }
SEA.verify(m[0], p, m[1]).then(function(ok){
resolve(ok && m[0]);
}).catch(function(e){reject(e)});

262
test/panic/users.js Normal file
View File

@ -0,0 +1,262 @@
var config = {
IP: require('ip').address(),
port: 8080,
servers: 2,
browsers: 2,
route: {
'/': __dirname + '/index.html',
'/gun.js': __dirname + '/../../gun.js',
'/jquery.js': __dirname + '/../../examples/jquery.js',
'/cryptomodules.js': __dirname + '/../../lib/cryptomodules.js',
'/sea.js': __dirname + '/../../sea.js'
}
}
var panic = require('panic-server');
panic.server().on('request', function(req, res){
config.route[req.url] && require('fs').createReadStream(config.route[req.url]).pipe(res);
}).listen(config.port);
var clients = panic.clients;
var manager = require('panic-manager')();
manager.start({
clients: Array(config.servers).fill().map(function(u, i){
return {
type: 'node',
port: config.port + (i + 1)
}
}),
panic: 'http://' + config.IP + ':' + config.port
});
var servers = clients.filter('Node.js');
var server = servers.pluck(1);
var spawn = servers.excluding(server).pluck(1);
var browsers = clients.excluding(servers);
var alice = browsers.pluck(1);
var bob = browsers.excluding(alice).pluck(1);
var again = {};
describe("End-to-End Encryption on User Accounts", function(){
//this.timeout(5 * 60 * 1000);
this.timeout(10 * 60 * 1000);
it("Servers have joined!", function(){
return servers.atLeast(config.servers);
});
it("GUN started!", function(){
return server.run(function(test){
var env = test.props;
test.async();
try{ require('fs').unlinkSync(env.i+'data') }catch(e){}
try{ require('fs').unlinkSync((env.i+1)+'data') }catch(e){}
var port = env.config.port + env.i;
var server = require('http').createServer(function(req, res){
res.end("I am "+ env.i +"!");
});
var Gun = require('gun');
var gun = Gun({file: env.i+'data', web: server});
server.listen(port, function(){
test.done();
});
}, {i: 1, config: config});
});
it(config.browsers +" browser(s) have joined!", function(){
console.log("PLEASE OPEN http://"+ config.IP +":"+ config.port +" IN "+ config.browsers +" BROWSER(S)!");
return browsers.atLeast(config.browsers);
});
it("Browsers load SEA!", function(){
var tests = [], i = 0;
browsers.each(function(client, id){
tests.push(client.run(function(test){
test.async();
console.log("load?");
function load(src, cb){
var script = document.createElement('script');
script.onload = cb; script.src = src;
document.head.appendChild(script);
}
load('cryptomodules.js', function(){
load('sea.js', function(){
test.done();
});
});
}, {i: i += 1, config: config}));
});
return Promise.all(tests);
});
it("Browsers initialized gun!", function(){
var tests = [], i = 0;
browsers.each(function(client, id){
tests.push(client.run(function(test){
localStorage.clear();
var env = test.props;
var gun = Gun('http://'+ env.config.IP + ':' + (env.config.port + 1) + '/gun');
window.gun = gun;
window.user = gun.user();
}, {i: i += 1, config: config}));
});
return Promise.all(tests);
});
it("Create Alice", function(){
return alice.run(function(test){
console.log("I AM ALICE");
test.async();
window.user.create('alice', 'xyzabcmnopq', function(ack){
if(ack.err || !ack.pub){ return }
test.done();
});
});
});
it("Create Bob", function(){
return bob.run(function(test){
test.async();
window.user.create('bob', 'zyxcbaqponm', function(ack){
alert("???");
if(ack.err || !ack.pub){ return }
test.done();
});
});
});
it("Auth Alice", function(){
return alice.run(function(test){
test.async();
window.user.auth('alice', 'xyzabcmnopq', function(ack){
if(ack.err || !ack.pub){ return }
test.done();
});
});
});
/*
it("Auth Bob typo", function(){
return bob.run(function(test){
test.async();
window.user.auth('bob', 'zyxcbaqponmb', function(ack){
if(ack.err && !ack.pub){ console.log("BAD SAUCE"); return test.done() }
});
});
});
*/
it("Auth Bob", function(){
return bob.run(function(test){
test.async();
window.user.auth('bob', 'zyxcbaqponm', function(ack){
if(ack.err || !ack.pub){ return }
console.log("AWESOME");
test.done();
});
});
});
it("Alice save & subscribe to Bob", function(){
return alice.run(function(test){
test.async();
window.user.on(function(alice){
console.log('alice!', alice);
if(alice.hello === 'world'){
test.done();
}
});
setTimeout(function(){
window.user.get('hello').put('world');
}, 100);
window.gun.get('alias/bob').map().on(function(data){
console.log("WOOOHOOOOOO!!!", data);
window.MARS = data.hello;
window.PUB = data.pub;
});
});
});
it("Bob save", function(){
return bob.run(function(test){
test.async();
window.user.on(function(bob){
console.log('bob!', bob);
if(bob.hello === 'mars'){
test.done();
}
});
setTimeout(function(){
window.user.get('hello').put('mars');
}, 100);
});
});
it("Alice should have Bob", function(){
return alice.run(function(test){
test.async();
setTimeout(function(){
if(window.PUB && 'mars' === window.MARS){
test.done();
}
}, 100);
});
});
it("Alice tries to crack Bob", function(){
return alice.run(function(test){
test.async();
gun.get('pub/' + window.PUB).get('crackers').put('gonna crack');
setTimeout(function(){
test.done();
}, 100);
});
});
it("Alice has no cracked Bob", function(){
return alice.run(function(test){
test.async();
gun.get('pub/' + window.PUB).val(function(data){
if(data.pub === window.PUB
&& data.hello === 'mars'
&& data.alias === 'bob'){
test.done();
}
});
});
});
it("Bob has no cracked Bob", function(){
return bob.run(function(test){
test.async();
user.val(function(data){
if(data.hello === 'mars'
&& data.alias === 'bob'){
test.done();
}
});
});
});
it("All finished!", function(done){
console.log("Done! Cleaning things up...");
setTimeout(function(){
done();
},1000);
});
after("Everything shut down.", function(){
browsers.run(function(){
//location.reload();
//setTimeout(function(){
//}, 15 * 1000);
});
return servers.run(function(){
process.exit();
});
});
});

View File

@ -509,7 +509,8 @@ 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' ]);
expect(gun.back(-1)._.user).to.not.be.ok();
}catch(e){ done(e); return }
done();
};