diff --git a/gun.js b/gun.js index 50c4c3bf..a9b1c223 100644 --- a/gun.js +++ b/gun.js @@ -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); } diff --git a/package.json b/package.json index 96198a5f..f23e9bef 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/sea.js b/sea.js index 683ca98f..ad4ec616 100644 --- a/sea.js +++ b/sea.js @@ -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)}); diff --git a/test/panic/users.js b/test/panic/users.js new file mode 100644 index 00000000..9dac3a9a --- /dev/null +++ b/test/panic/users.js @@ -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(); + }); + }); +}); \ No newline at end of file diff --git a/test/sea.js b/test/sea.js index 387dd936..aa9d66c6 100644 --- a/test/sea.js +++ b/test/sea.js @@ -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(); };