diff --git a/.npmignore b/.npmignore index bb580d92..b1e119da 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,5 @@ *.ts /temp/ !*.d.ts +*data* +isolate-* diff --git a/sea/aeskey.js b/sea/aeskey.js index dcd4d560..20390b3f 100644 --- a/sea/aeskey.js +++ b/sea/aeskey.js @@ -5,7 +5,7 @@ const importGen = async (key, salt, opt) => { //const combo = shim.Buffer.concat([shim.Buffer.from(key, 'utf8'), salt || shim.random(8)]).toString('utf8') // old - var opt = opt || {}; + opt = opt || {}; const combo = key + (salt || shim.random(8)).toString('utf8'); // new const hash = shim.Buffer.from(await sha256hash(combo), 'binary') diff --git a/sea/auth.js b/sea/auth.js new file mode 100644 index 00000000..18bcb59a --- /dev/null +++ b/sea/auth.js @@ -0,0 +1,143 @@ + + var User = require('./user'), SEA = User.SEA, Gun = User.GUN, noop = function(){}; + // now that we have created a user, we want to authenticate them! + User.prototype.auth = function(...args){ // TODO: this PR with arguments need to be cleaned up / refactored. + var pair = typeof args[0] === 'object' && (args[0].pub || args[0].epub) ? args[0] : typeof args[1] === 'object' && (args[1].pub || args[1].epub) ? args[1] : null; + var alias = !pair && typeof args[0] === 'string' ? args[0] : null; + var pass = alias && typeof args[1] === 'string' ? args[1] : null; + var cb = args.filter(arg => typeof arg === 'function')[0] || null; // cb now can stand anywhere, after alias/pass or pair + var opt = args && args.length > 1 && typeof args[args.length-1] === 'object' ? args[args.length-1] : {}; // opt is always the last parameter which typeof === 'object' and stands after cb + + var gun = this, cat = (gun._), root = gun.back(-1); + + if(cat.ing){ + (cb || noop)({err: Gun.log("User is already being created or authenticated!"), wait: true}); + return gun; + } + cat.ing = true; + + var act = {}, u; + act.a = function(data){ + if(!data){ return act.b() } + if(!data.pub){ + var tmp = []; Object.keys(data).forEach(function(k){ if('_'==k){ return } tmp.push(data[k]) }) + return act.b(tmp); + } + if(act.name){ return act.f(data) } + act.c((act.data = data).auth); + } + act.b = function(list){ + var get = (act.list = (act.list||[]).concat(list||[])).shift(); + if(u === get){ + if(act.name){ return act.err('Your user account is not published for dApps to access, please consider syncing it online, or allowing local access by adding your device as a peer.') } + return act.err('Wrong user or password.') + } + root.get(get).once(act.a); + } + act.c = function(auth){ + if(u === auth){ return act.b() } + if('string' == typeof auth){ return act.c(obj_ify(auth)) } // in case of legacy + SEA.work(pass, (act.auth = auth).s, act.d, act.enc); // the proof of work is evidence that we've spent some time/effort trying to log in, this slows brute force. + } + act.d = function(proof){ + SEA.decrypt(act.auth.ek, proof, act.e, act.enc); + } + act.e = function(half){ + if(u === half){ + if(!act.enc){ // try old format + act.enc = {encode: 'utf8'}; + return act.c(act.auth); + } act.enc = null; // end backwards + return act.b(); + } + act.half = half; + act.f(act.data); + } + act.f = function(pair){ + var half = act.half || {}, data = act.data || {}; + act.g(act.lol = {pub: pair.pub || data.pub, epub: pair.epub || data.epub, priv: pair.priv || half.priv, epriv: pair.epriv || half.epriv}); + } + act.g = function(pair){ + if(!pair || !pair.pub || !pair.epub){ return act.b() } + act.pair = pair; + var user = (root._).user, at = (user._); + var tmp = at.tag; + var upt = at.opt; + at = user._ = root.get('~'+pair.pub)._; + at.opt = upt; + // add our credentials in-memory only to our root user instance + user.is = {pub: pair.pub, epub: pair.epub, alias: alias || pair}; + at.sea = act.pair; + cat.ing = false; + try{if(pass && u == (obj_ify(cat.root.graph['~'+pair.pub].auth)||'')[':']){ opt.shuffle = opt.change = pass; } }catch(e){} // migrate UTF8 & Shuffle! + opt.change? act.z() : (cb || noop)(at); + if(SEA.window && ((gun.back('user')._).opt||opt).remember){ + // TODO: this needs to be modular. + try{var sS = {}; + sS = window.sessionStorage; + sS.recall = true; + sS.pair = JSON.stringify(pair); // auth using pair is more reliable than alias/pass + }catch(e){} + } + try{ + if(root._.tag.auth){ // auth handle might not be registered yet + (root._).on('auth', at) // TODO: Deprecate this, emit on user instead! Update docs when you do. + } else { setTimeout(function(){ (root._).on('auth', at) },1) } // if not, hackily add a timeout. + //at.on('auth', at) // Arrgh, this doesn't work without event "merge" code, but "merge" code causes stack overflow and crashes after logging in & trying to write data. + }catch(e){ + Gun.log("Your 'auth' callback crashed with:", e); + } + } + act.z = function(){ + // password update so encrypt private key using new pwd + salt + act.salt = String.random(64); // pseudo-random + SEA.work(opt.change, act.salt, act.y); + } + act.y = function(proof){ + SEA.encrypt({priv: act.pair.priv, epriv: act.pair.epriv}, proof, act.x, {raw:1}); + } + act.x = function(auth){ + act.w(JSON.stringify({ek: auth, s: act.salt})); + } + act.w = function(auth){ + if(opt.shuffle){ // delete in future! + console.log('migrate core account from UTF8 & shuffle'); + var tmp = {}; Object.keys(act.data).forEach(function(k){ tmp[k] = act.data[k] }); + delete tmp._; + tmp.auth = auth; + root.get('~'+act.pair.pub).put(tmp); + } // end delete + root.get('~'+act.pair.pub).get('auth').put(auth, cb || noop); + } + act.err = function(e){ + var ack = {err: Gun.log(e || 'User cannot be found!')}; + cat.ing = false; + (cb || noop)(ack); + } + act.plugin = function(name){ + if(!(act.name = name)){ return act.err() } + var tmp = [name]; + if('~' !== name[0]){ + tmp[1] = '~'+name; + tmp[2] = '~@'+name; + } + act.b(tmp); + } + if(pair){ + act.g(pair); + } else + if(alias){ + root.get('~@'+alias).once(act.a); + } else + if(!alias && !pass){ + SEA.name(act.plugin); + } + return gun; + } + function obj_ify(o){ + if('string' != typeof o){ return o } + try{o = JSON.parse(o); + }catch(e){o={}}; + return o; + } + \ No newline at end of file diff --git a/sea/certify.js b/sea/certify.js new file mode 100644 index 00000000..2d09ec01 --- /dev/null +++ b/sea/certify.js @@ -0,0 +1,71 @@ + + var SEA = require('./root'); + // This is to certify that a group of "certificants" can "put" anything at a group of matched "paths" to the certificate authority's graph + SEA.certify = SEA.certify || (async (certificants, policy = {}, authority, cb, opt = {}) => { try { + /* + IMPORTANT: A Certificate is like a Signature. No one knows who (authority) created/signed a cert until you put it into their graph. + "certificants": '*' or a String (Bob.pub) || an Object that contains "pub" as a key || an array of [object || string]. These people will have the rights. + "policy": A string ('inbox'), or a RAD/LEX object {'*': 'inbox'}, or an Array of RAD/LEX objects or strings. RAD/LEX object can contain key "?" with indexOf("*") > -1 to force key equals certificant pub. This rule is used to check against soul+'/'+key using Gun.text.match or String.match. + "authority": Key pair or priv of the certificate authority. + "cb": A callback function after all things are done. + "opt": If opt.expiry (a timestamp) is set, SEA won't sync data after opt.expiry. If opt.blacklist is set, SEA will look for blacklist before syncing. + */ + console.log('SEA.certify() is an early experimental community supported method that may change API behavior without warning in any future version.') + + certificants = (() => { + var data = [] + if (certificants) { + if ((typeof certificants === 'string' || Array.isArray(certificants)) && certificants.indexOf('*') !== -1) return '*' + if (typeof certificants === 'string') { + return certificants + } + + if (Array.isArray(certificants)) { + if (certificants.length === 1 && certificants[0]) return typeof certificants[0] === 'object' && certificants[0].pub ? certificants[0].pub : typeof certificants[0] === 'string' ? certificants[0] : null + certificants.map(certificant => { + if (typeof certificant ==='string') data.push(certificant) + else if (typeof certificant === 'object' && certificant.pub) data.push(certificant.pub) + }) + } + + if (typeof certificants === 'object' && certificants.pub) return certificants.pub + return data.length > 0 ? data : null + } + return null + })() + + if (!certificants) return console.log("No certificant found.") + + const expiry = opt.expiry && (typeof opt.expiry === 'number' || typeof opt.expiry === 'string') ? parseFloat(opt.expiry) : null + const readPolicy = (policy || {}).read ? policy.read : null + const writePolicy = (policy || {}).write ? policy.write : typeof policy === 'string' || Array.isArray(policy) || policy["+"] || policy["#"] || policy["."] || policy["="] || policy["*"] || policy[">"] || policy["<"] ? policy : null + const readBlacklist = ((opt || {}).blacklist || {}).read && (typeof opt.blacklist.read === 'string' || opt.blacklist.read['#']) ? opt.blacklist.read : null + const writeBlacklist = typeof (opt || {}).blacklist === 'string' || (((opt || {}).blacklist || {}).write || {})['#'] ? opt.blacklist : ((opt || {}).blacklist || {}).write && (typeof opt.blacklist.write === 'string' || opt.blacklist.write['#']) ? opt.blacklist.write : null + + if (!readPolicy && !writePolicy) return console.log("No policy found.") + + // reserved keys: c, e, r, w, rb, wb + const data = JSON.stringify({ + c: certificants, + ...(expiry ? {e: expiry} : {}), // inject expiry if possible + ...(readPolicy ? {r: readPolicy } : {}), // "r" stands for read, which means read permission. + ...(writePolicy ? {w: writePolicy} : {}), // "w" stands for write, which means write permission. + ...(readBlacklist ? {rb: readBlacklist} : {}), // inject READ blacklist if possible + ...(writeBlacklist ? {wb: writeBlacklist} : {}), // inject WRITE blacklist if possible + }) + + const certificate = await SEA.sign(data, authority, null, {raw:1}) + + var r = certificate + if(!opt.raw){ r = 'SEA'+JSON.stringify(r) } + if(cb){ try{ cb(r) }catch(e){console.log(e)} } + return r; + } catch(e) { + SEA.err = e; + if(SEA.throw){ throw e } + if(cb){ cb() } + return; + }}); + + module.exports = SEA.certify; + \ No newline at end of file diff --git a/sea/create.js b/sea/create.js index 0b77e5a4..15d0c190 100644 --- a/sea/create.js +++ b/sea/create.js @@ -1,24 +1,31 @@ - // TODO: This needs to be split into all separate functions. - // Not just everything thrown into 'create'. - - var SEA = require('./sea'); - var User = require('./user'); - var authsettings = require('./settings'); - var Gun = SEA.Gun; - - var noop = function(){}; + var User = require('./user'), SEA = User.SEA, Gun = User.GUN, noop = function(){}; // Well first we have to actually create a user. That is what this function does. - User.prototype.create = function(alias, pass, cb, opt){ + User.prototype.create = function(...args){ + var pair = typeof args[0] === 'object' && (args[0].pub || args[0].epub) ? args[0] : typeof args[1] === 'object' && (args[1].pub || args[1].epub) ? args[1] : null; + var alias = pair && (pair.pub || pair.epub) ? pair.pub : typeof args[0] === 'string' ? args[0] : null; + var pass = pair && (pair.pub || pair.epub) ? pair : alias && typeof args[1] === 'string' ? args[1] : null; + var cb = args.filter(arg => typeof arg === 'function')[0] || null; // cb now can stand anywhere, after alias/pass or pair + var opt = args && args.length > 1 && typeof args[args.length-1] === 'object' ? args[args.length-1] : {}; // opt is always the last parameter which typeof === 'object' and stands after cb + var gun = this, cat = (gun._), root = gun.back(-1); cb = cb || noop; + opt = opt || {}; + if(false !== opt.check){ + var err; + if(!alias){ err = "No user." } + if((pass||'').length < 8){ err = "Password too short!" } + if(err){ + cb({err: Gun.log(err)}); + return gun; + } + } if(cat.ing){ - cb({err: Gun.log("User is already being created or authenticated!"), wait: true}); + (cb || noop)({err: Gun.log("User is already being created or authenticated!"), wait: true}); return gun; } cat.ing = true; - opt = opt || {}; var act = {}, u; act.a = function(pubs){ act.pubs = pubs; @@ -26,18 +33,19 @@ // If we can enforce that a user name is already taken, it might be nice to try, but this is not guaranteed. var ack = {err: Gun.log('User already created!')}; cat.ing = false; - cb(ack); + (cb || noop)(ack); gun.leave(); return; } - act.salt = Gun.text.random(64); // pseudo-randomly create a salt, then use PBKDF2 function to extend the password with it. + act.salt = String.random(64); // pseudo-randomly create a salt, then use PBKDF2 function to extend the password with it. SEA.work(pass, act.salt, act.b); // this will take some short amount of time to produce a proof, which slows brute force attacks. } act.b = function(proof){ act.proof = proof; - SEA.pair(act.c); // now we have generated a brand new ECDSA key pair for the user account. + pair ? act.c(pair) : SEA.pair(act.c) // generate a brand new key pair or use the existing. } - act.c = function(pair){ var tmp; + act.c = function(pair){ + var tmp act.pair = pair || {}; if(tmp = cat.root.user){ tmp._.sea = pair; @@ -61,152 +69,22 @@ } act.g = function(auth){ var tmp; act.data.auth = act.data.auth || auth; - root.get(tmp = '~'+act.pair.pub).put(act.data); // awesome, now we can actually save the user with their public key as their ID. - root.get('~@'+alias).put(Gun.obj.put({}, tmp, Gun.val.link.ify(tmp))); // next up, we want to associate the alias with the public key. So we add it to the alias list. - setTimeout(function(){ // we should be able to delete this now, right? + root.get(tmp = '~'+act.pair.pub).put(act.data).on(act.h); // awesome, now we can actually save the user with their public key as their ID. + var link = {}; link[tmp] = {'#': tmp}; root.get('~@'+alias).put(link).get(tmp).on(act.i); // next up, we want to associate the alias with the public key. So we add it to the alias list. + } + act.h = function(data, key, msg, eve){ + eve.off(); act.h.ok = 1; act.i(); + } + act.i = function(data, key, msg, eve){ + if(eve){ act.i.ok = 1; eve.off() } + if(!act.h.ok || !act.i.ok){ return } cat.ing = false; cb({ok: 0, pub: act.pair.pub}); // callback that the user has been created. (Note: ok = 0 because we didn't wait for disk to ack) - if(noop === cb){ gun.auth(alias, pass) } // if no callback is passed, auto-login after signing up. - },10); + if(noop === cb){ pair? gun.auth(pair) : gun.auth(alias, pass) } // if no callback is passed, auto-login after signing up. } root.get('~@'+alias).once(act.a); return gun; } - // now that we have created a user, we want to authenticate them! - User.prototype.auth = function(alias, pass, cb, opt){ - var gun = this, cat = (gun._), root = gun.back(-1); - cb = cb || function(){}; - if(cat.ing){ - cb({err: Gun.log("User is already being created or authenticated!"), wait: true}); - return gun; - } - cat.ing = true; - opt = opt || {}; - var pair = (alias && (alias.pub || alias.epub))? alias : (pass && (pass.pub || pass.epub))? pass : null; - var act = {}, u; - act.a = function(data){ - if(!data){ return act.b() } - if(!data.pub){ - var tmp = []; - Gun.node.is(data, function(v){ tmp.push(v) }) - return act.b(tmp); - } - if(act.name){ return act.f(data) } - act.c((act.data = data).auth); - } - act.b = function(list){ - var get = (act.list = (act.list||[]).concat(list||[])).shift(); - if(u === get){ - if(act.name){ return act.err('Your user account is not published for dApps to access, please consider syncing it online, or allowing local access by adding your device as a peer.') } - return act.err('Wrong user or password.') - } - root.get(get).once(act.a); - } - act.c = function(auth){ - if(u === auth){ return act.b() } - if(Gun.text.is(auth)){ return act.c(Gun.obj.ify(auth)) } // in case of legacy - SEA.work(pass, (act.auth = auth).s, act.d, act.enc); // the proof of work is evidence that we've spent some time/effort trying to log in, this slows brute force. - } - act.d = function(proof){ - SEA.decrypt(act.auth.ek, proof, act.e, act.enc); - } - act.e = function(half){ - if(u === half){ - if(!act.enc){ // try old format - act.enc = {encode: 'utf8'}; - return act.c(act.auth); - } act.enc = null; // end backwards - return act.b(); - } - act.half = half; - act.f(act.data); - } - act.f = function(data){ - if(!data || !data.pub){ return act.b() } - var tmp = act.half || {}; - act.g({pub: data.pub, epub: data.epub, priv: tmp.priv, epriv: tmp.epriv}); - } - act.g = function(pair){ - act.pair = pair; - var user = (root._).user, at = (user._); - var tmp = at.tag; - var upt = at.opt; - at = user._ = root.get('~'+pair.pub)._; - at.opt = upt; - // add our credentials in-memory only to our root user instance - user.is = {pub: pair.pub, epub: pair.epub, alias: alias}; - at.sea = act.pair; - cat.ing = false; - try{if(pass && !Gun.obj.has(Gun.obj.ify(cat.root.graph['~'+pair.pub].auth), ':')){ opt.shuffle = opt.change = pass; } }catch(e){} // migrate UTF8 & Shuffle! - opt.change? act.z() : cb(at); - if(SEA.window && ((gun.back('user')._).opt||opt).remember){ - // TODO: this needs to be modular. - try{var sS = {}; - sS = window.sessionStorage; - sS.recall = true; - sS.alias = alias; - sS.tmp = pass; - }catch(e){} - } - try{ - (root._).on('auth', at) // TODO: Deprecate this, emit on user instead! Update docs when you do. - //at.on('auth', at) // Arrgh, this doesn't work without event "merge" code, but "merge" code causes stack overflow and crashes after logging in & trying to write data. - }catch(e){ - Gun.log("Your 'auth' callback crashed with:", e); - } - } - act.z = function(){ - // password update so encrypt private key using new pwd + salt - act.salt = Gun.text.random(64); // pseudo-random - SEA.work(opt.change, act.salt, act.y); - } - act.y = function(proof){ - SEA.encrypt({priv: act.pair.priv, epriv: act.pair.epriv}, proof, act.x, {raw:1}); - } - act.x = function(auth){ - act.w(JSON.stringify({ek: auth, s: act.salt})); - } - act.w = function(auth){ - if(opt.shuffle){ // delete in future! - console.log('migrate core account from UTF8 & shuffle'); - var tmp = Gun.obj.to(act.data); - Gun.obj.del(tmp, '_'); - tmp.auth = auth; - root.get('~'+act.pair.pub).put(tmp); - } // end delete - root.get('~'+act.pair.pub).get('auth').put(auth, cb); - } - act.err = function(e){ - var ack = {err: Gun.log(e || 'User cannot be found!')}; - cat.ing = false; - cb(ack); - } - act.plugin = function(name){ - if(!(act.name = name)){ return act.err() } - var tmp = [name]; - if('~' !== name[0]){ - tmp[1] = '~'+name; - tmp[2] = '~@'+name; - } - act.b(tmp); - } - if(pair){ - act.g(pair); - } else - if(alias){ - root.get('~@'+alias).once(act.a); - } else - if(!alias && !pass){ - SEA.name(act.plugin); - } - return gun; - } - User.prototype.pair = function(){ - console.log("user.pair() IS DEPRECATED AND WILL BE DELETED!!!"); - var user = this; - if(!user.is){ return false } - return user._.sea; - } User.prototype.leave = function(opt, cb){ var gun = this, user = (gun.back(-1)._).user; if(user){ @@ -217,159 +95,10 @@ if(SEA.window){ try{var sS = {}; sS = window.sessionStorage; - delete sS.alias; - delete sS.tmp; delete sS.recall; + delete sS.pair; }catch(e){}; } return gun; } - // If authenticated user wants to delete his/her account, let's support it! - User.prototype.delete = async function(alias, pass, cb){ - console.log("user.delete() IS DEPRECATED AND WILL BE MOVED TO A MODULE!!!"); - var gun = this, root = gun.back(-1), user = gun.back('user'); - try { - user.auth(alias, pass, function(ack){ - var pub = (user.is||{}).pub; - // Delete user data - user.map().once(function(){ this.put(null) }); - // Wipe user data from memory - user.leave(); - (cb || noop)({ok: 0}); - }); - } catch (e) { - Gun.log('User.delete failed! Error:', e); - } - return gun; - } - User.prototype.recall = function(opt, cb){ - var gun = this, root = gun.back(-1), tmp; - opt = opt || {}; - if(opt && opt.sessionStorage){ - if(SEA.window){ - try{var sS = {}; - sS = window.sessionStorage; - if(sS){ - (root._).opt.remember = true; - ((gun.back('user')._).opt||opt).remember = true; - if(sS.recall || (sS.alias && sS.tmp)){ - root.user().auth(sS.alias, sS.tmp, cb); - } - } - }catch(e){} - } - return gun; - } - /* - TODO: copy mhelander's expiry code back in. - Although, we should check with community, - should expiry be core or a plugin? - */ - return gun; - } - User.prototype.alive = async function(){ - console.log("user.alive() IS DEPRECATED!!!"); - const gunRoot = this.back(-1) - try { - // All is good. Should we do something more with actual recalled data? - await authRecall(gunRoot) - return gunRoot._.user._ - } catch (e) { - const err = 'No session!' - Gun.log(err) - throw { err } - } - } - User.prototype.trust = async function(user){ - // TODO: BUG!!! SEA `node` read listener needs to be async, which means core needs to be async too. - //gun.get('alice').get('age').trust(bob); - if (Gun.is(user)) { - user.get('pub').get((ctx, ev) => { - console.log(ctx, ev) - }) - } - user.get('trust').get(path).put(theirPubkey); - - // do a lookup on this gun chain directly (that gets bob's copy of the data) - // do a lookup on the metadata trust table for this path (that gets all the pubkeys allowed to write on this path) - // do a lookup on each of those pubKeys ON the path (to get the collab data "layers") - // THEN you perform Jachen's mix operation - // and return the result of that to... - } - User.prototype.grant = function(to, cb){ - console.log("`.grant` API MAY BE DELETED OR CHANGED OR RENAMED, DO NOT USE!"); - var gun = this, user = gun.back(-1).user(), pair = user._.sea, path = ''; - gun.back(function(at){ if(at.is){ return } path += (at.get||'') }); - (async function(){ - var enc, sec = await user.get('grant').get(pair.pub).get(path).then(); - sec = await SEA.decrypt(sec, pair); - if(!sec){ - sec = SEA.random(16).toString(); - enc = await SEA.encrypt(sec, pair); - user.get('grant').get(pair.pub).get(path).put(enc); - } - var pub = to.get('pub').then(); - var epub = to.get('epub').then(); - pub = await pub; epub = await epub; - var dh = await SEA.secret(epub, pair); - enc = await SEA.encrypt(sec, dh); - user.get('grant').get(pub).get(path).put(enc, cb); - }()); - return gun; - } - User.prototype.secret = function(data, cb){ - console.log("`.secret` API MAY BE DELETED OR CHANGED OR RENAMED, DO NOT USE!"); - var gun = this, user = gun.back(-1).user(), pair = user.pair(), path = ''; - gun.back(function(at){ if(at.is){ return } path += (at.get||'') }); - (async function(){ - var enc, sec = await user.get('trust').get(pair.pub).get(path).then(); - sec = await SEA.decrypt(sec, pair); - if(!sec){ - sec = SEA.random(16).toString(); - enc = await SEA.encrypt(sec, pair); - user.get('trust').get(pair.pub).get(path).put(enc); - } - enc = await SEA.encrypt(data, sec); - gun.put(enc, cb); - }()); - return gun; - } - - /** - * returns the decrypted value, encrypted by secret - * @returns {Promise} - */ - User.prototype.decrypt = function(cb) { - let gun = this, - path = '' - gun.back(function(at) { - if (at.is) { - return - } - path += at.get || '' - }) - return gun - .then(async data => { - if (data == null) { - return - } - const user = gun.back(-1).user() - const pair = user.pair() - let sec = await user - .get('trust') - .get(pair.pub) - .get(path) - sec = await SEA.decrypt(sec, pair) - if (!sec) { - return data - } - let decrypted = await SEA.decrypt(data, sec) - return decrypted - }) - .then(res => { - cb && cb(res) - return res - }) - } - module.exports = User \ No newline at end of file diff --git a/sea/decrypt.js b/sea/decrypt.js index 1a68c938..79fe6a90 100644 --- a/sea/decrypt.js +++ b/sea/decrypt.js @@ -8,10 +8,11 @@ opt = opt || {}; var key = (pair||opt).epriv || pair; if(!key){ + if(!SEA.I){ throw 'No decryption key.' } pair = await SEA.I(null, {what: data, how: 'decrypt', why: opt.why}); key = pair.epriv || pair; } - var json = S.parse(data); + var json = await S.parse(data); var buf, bufiv, bufct; try{ buf = shim.Buffer.from(json.s, opt.encode || 'base64'); bufiv = shim.Buffer.from(json.iv, opt.encode || 'base64'); @@ -26,7 +27,7 @@ return await SEA.decrypt(data, pair, cb, opt); } } - var r = S.parse(new shim.TextDecoder('utf8').decode(ct)); + var r = await S.parse(new shim.TextDecoder('utf8').decode(ct)); if(cb){ try{ cb(r) }catch(e){console.log(e)} } return r; } catch(e) { diff --git a/sea/encrypt.js b/sea/encrypt.js index d5d5641c..4f0b6c9a 100644 --- a/sea/encrypt.js +++ b/sea/encrypt.js @@ -10,10 +10,11 @@ var key = (pair||opt).epriv || pair; if(u === data){ throw '`undefined` not allowed.' } if(!key){ + if(!SEA.I){ throw 'No encryption key.' } pair = await SEA.I(null, {what: data, how: 'encrypt', why: opt.why}); key = pair.epriv || pair; } - var msg = (typeof data == 'string')? data : JSON.stringify(data); + var msg = (typeof data == 'string')? data : await shim.stringify(data); var rand = {s: shim.random(9), iv: shim.random(15)}; // consider making this 9 and 15 or 18 or 12 to reduce == padding. var ct = await aeskey(key, rand.s, opt).then((aes) => (/*shim.ossl ||*/ shim.subtle).encrypt({ // Keeping the AES key scope as private as possible... name: opt.name || 'AES-GCM', iv: new Uint8Array(rand.iv) @@ -23,7 +24,7 @@ iv: rand.iv.toString(opt.encode || 'base64'), s: rand.s.toString(opt.encode || 'base64') } - if(!opt.raw){ r = 'SEA'+JSON.stringify(r) } + if(!opt.raw){ r = 'SEA' + await shim.stringify(r) } if(cb){ try{ cb(r) }catch(e){console.log(e)} } return r; diff --git a/sea/https.js b/sea/https.js index cb7916dc..4f12465f 100644 --- a/sea/https.js +++ b/sea/https.js @@ -3,7 +3,9 @@ try{ if(SEA.window){ if(location.protocol.indexOf('s') < 0 && location.host.indexOf('localhost') < 0 + && ! /^127\.\d+\.\d+\.\d+$/.test(location.hostname) && location.protocol.indexOf('file:') < 0){ + console.warn('HTTPS needed for WebCrypto in SEA, redirecting...'); location.protocol = 'https:'; // WebCrypto does NOT work without HTTPS! } } }catch(e){} diff --git a/sea/index.js b/sea/index.js index 3ce6ac4e..66fcbc13 100644 --- a/sea/index.js +++ b/sea/index.js @@ -1,20 +1,19 @@ - var SEA = require('./sea') - var Gun = SEA.Gun; + var SEA = require('./sea'), S = require('./settings'), noop = function() {}, u; + var Gun = (''+u != typeof window)? (window.Gun||{on:noop}) : require((''+u === typeof MODULE?'.':'')+'./gun', 1); // 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) Gun.on('opt', function(at){ if(!at.sea){ // only add SEA once per instance, on the "at" context. at.sea = {own: {}}; - //at.on('in', security, at); // now listen to all input data, acting as a firewall. - //at.on('out', signature, at); // and output listeners, to encrypt outgoing data. - at.on('put', check, at); + at.on('put', check, at); // SEA now runs its firewall on HAM diffs, not all i/o. } this.to.next(at); // make sure to call the "next" middleware adapter. }); // Alright, this next adapter gets run at the per node level in the graph database. + // correction: 2020 it gets run on each key/value pair in a node upon a HAM diff. // This will let us verify that every property on a node has a value signed by a public key we trust. // If the signature does not match, the data is just `undefined` so it doesn't get passed on. // If it does match, then we transform the in-memory "view" of the data into its plain value (without the signature). @@ -27,59 +26,30 @@ // Here is a problem: Multiple public keys can "claim" any node's ID, so this is dangerous! // 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! - // 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.$._).put, c = 0, d; - Gun.node.is(msg.put, function(val, key, node){ - // only process if SEA formatted? - var tmp = Gun.obj.ify(val) || noop; - if(u !== tmp[':']){ - node[key] = SEA.opt.unpack(tmp); - return; - } - if(!SEA.opt.check(val)){ return } - c++; // for each property on the node - SEA.verify(val, false, function(data){ c--; // false just extracts the plain data. - node[key] = SEA.opt.unpack(data, key, node);; // transform to plain value. - if(d && !c && (c = -1)){ to.next(msg) } - }); - }); - if((d = true) && !c){ to.next(msg) } - } - // signature handles data output, it is a proxy to the security function. - function signature(msg){ - if((msg._||noop).user){ - return this.to.next(msg); - } - var ctx = this.as; - (msg._||(msg._=function(){})).user = ctx.user; - security.call(this, msg); - } - - var u; function check(msg){ // REVISE / IMPROVE, NO NEED TO PASS MSG/EVE EACH SUB? var eve = this, at = eve.as, put = msg.put, soul = put['#'], key = put['.'], val = put[':'], state = put['>'], id = msg['#'], tmp; if(!soul || !key){ return } if((msg._||'').faith && (at.opt||'').faith && 'function' == typeof msg._){ - SEA.verify(SEA.opt.pack(put), false, function(data){ // this is synchronous if false + SEA.opt.pack(put, function(raw){ + SEA.verify(raw, false, function(data){ // this is synchronous if false put['='] = SEA.opt.unpack(data); eve.to.next(msg); - }); + })}) return } - var no = function(why){ at.on('in', {'@': id, err: why}) }; + var no = function(why){ at.on('in', {'@': id, err: msg.err = why}) }; // exploit internal relay stun for now, maybe violates spec, but testing for now. // Note: this may be only the sharded message, not original batch. //var no = function(why){ msg.ack(why) }; (msg._||'').DBG && ((msg._||'').DBG.c = +new Date); - if(0 <= soul.indexOf(' { + if (certificate.m && certificate.s && certificant && pub) + // now verify certificate + return SEA.verify(certificate, pub, data => { // check if "pub" (of the graph owner) really issued this cert + if (u !== data && u !== data.e && msg.put['>'] && msg.put['>'] > parseFloat(data.e)) return no("Certificate expired.") // certificate expired + // "data.c" = a list of certificants/certified users + // "data.w" = lex WRITE permission, in the future, there will be "data.r" which means lex READ permission + if (u !== data && data.c && data.w && (data.c === certificant || data.c.indexOf('*' || certificant) > -1)) { + // ok, now "certificant" is in the "certificants" list, but is "path" allowed? Check path + let path = soul.indexOf('/') > -1 ? soul.replace(soul.substring(0, soul.indexOf('/') + 1), '') : '' + String.match = String.match || Gun.text.match + const w = Array.isArray(data.w) ? data.w : typeof data.w === 'object' || typeof data.w === 'string' ? [data.w] : [] + for (const lex of w) { + if ((String.match(path, lex['#']) && String.match(key, lex['.'])) || (!lex['.'] && String.match(path, lex['#'])) || (!lex['#'] && String.match(key, lex['.'])) || String.match((path ? path + '/' + key : key), lex['#'] || lex)) { + // is Certificant forced to present in Path + if (lex['+'] && lex['+'].indexOf('*') > -1 && path && path.indexOf(certificant) == -1 && key.indexOf(certificant) == -1) return no(`Path "${path}" or key "${key}" must contain string "${certificant}".`) + // path is allowed, but is there any WRITE blacklist? Check it out + if (data.wb && (typeof data.wb === 'string' || ((data.wb || {})['#']))) { // "data.wb" = path to the WRITE blacklist + var root = at.$.back(-1) + if (typeof data.wb === 'string' && '~' !== data.wb.slice(0, 1)) root = root.get('~' + pub) + return root.get(data.wb).get(certificant).once(value => { + if (value && (value === 1 || value === true)) return no("Certificant blacklisted.") + return cb(data) + }) + } + return cb(data) + } + } + return no("Certificate verification fail.") + } + }) + return } - if((tmp = user.is) && pub === tmp.pub){ - SEA.sign(SEA.opt.pack(msg.put), (user._).sea, function(data){ - if(u === data){ return no(SEA.err || 'Signature fail.') } - if(tmp = link_is(val)){ (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1 } - msg.put[':'] = JSON.stringify({':': tmp = SEA.opt.unpack(data.m), '~': data.s}); - msg.put['='] = tmp; - eve.to.next(msg); - }, {raw: 1}); + + if ('pub' === key && '~' + pub === soul) { + if (val === pub) return eve.to.next(msg) // the account MUST match `pub` property that equals the ID of the public key. + return no("Account not same!") + } + + if ((tmp = user.is) && tmp.pub && !raw['*'] && !raw['+'] && (pub === tmp.pub || (pub !== tmp.pub && ((msg._.msg || {}).opt || {}).cert))){ + SEA.opt.pack(msg.put, packed => { + SEA.sign(packed, (user._).sea, async function(data) { + if (u === data) return no(SEA.err || 'Signature fail.') + msg.put[':'] = {':': tmp = SEA.opt.unpack(data.m), '~': data.s} + msg.put['='] = tmp + + // if writing to own graph, just allow it + if (pub === user.is.pub) { + if (tmp = link_is(val)) (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1 + JSON.stringifyAsync(msg.put[':'], function(err,s){ + if(err){ return no(err || "Stringify error.") } + msg.put[':'] = s; + return eve.to.next(msg); + }) + return + } + + // if writing to other's graph, check if cert exists then try to inject cert into put, also inject self pub so that everyone can verify the put + if (pub !== user.is.pub && ((msg._.msg || {}).opt || {}).cert) { + const cert = await S.parse(msg._.msg.opt.cert) + // even if cert exists, we must verify it + if (cert && cert.m && cert.s) + verify(cert, user.is.pub, _ => { + msg.put[':']['+'] = cert // '+' is a certificate + msg.put[':']['*'] = user.is.pub // '*' is pub of the user who puts + JSON.stringifyAsync(msg.put[':'], function(err,s){ + if(err){ return no(err || "Stringify error.") } + msg.put[':'] = s; + return eve.to.next(msg); + }) + return + }) + } + }, {raw: 1}) + }) return; } - SEA.verify(SEA.opt.pack(msg.put), pub, function(data){ var tmp; - data = SEA.opt.unpack(data); - if(u === data){ return no("Unverified data.") } // make sure the signature matches the account it claims to be on. // reject any updates that are signed with a mismatched account. - if((tmp = link_is(data)) && pub === SEA.opt.pub(tmp)){ (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1 } - msg.put['='] = data; - eve.to.next(msg); - }); + + SEA.opt.pack(msg.put, packed => { + SEA.verify(packed, raw['*'] || pub, function(data){ var tmp; + data = SEA.opt.unpack(data); + if (u === data) return no("Unverified data.") // make sure the signature matches the account it claims to be on. // reject any updates that are signed with a mismatched account. + if ((tmp = link_is(data)) && pub === SEA.opt.pub(tmp)) (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1 + + // check if cert ('+') and putter's pub ('*') exist + if (raw['+'] && raw['+']['m'] && raw['+']['s'] && raw['*']) + // now verify certificate + verify(raw['+'], raw['*'], _ => { + msg.put['='] = data; + return eve.to.next(msg); + }) + else { + msg.put['='] = data; + return eve.to.next(msg); + } + }); + }) + return }; check.any = function(eve, msg, val, key, soul, at, no, user){ var tmp, pub; if(at.opt.secure){ return no("Soul missing public key at '" + key + "'.") } @@ -144,162 +192,9 @@ }).on.on('secure', msg); return; } - var link_is = Gun.val.link.is, state_ify = Gun.state.ify; - // okay! The security function handles all the heavy lifting. - // It needs to deal read and write of input and output of system data, account/public key data, and regular data. - // This is broken down into some pretty clear edge cases, let's go over them: - function security(msg){ - var at = this.as, sea = at.sea, to = this.to; - if(at.opt.faith && (msg._||noop).faith){ // you probably shouldn't have faith in this! - this.to.next(msg); // why do we allow skipping security? I'm very scared about it actually. - return; // but so that way storage adapters that already verified something can get performance boost. This was a community requested feature. If anybody finds an exploit with it, please report immediately. It should only be exploitable if you have XSS control anyways, which if you do, you can bypass security regardless of this. - } - if(msg.get){ - // if there is a request to read data from us, then... - var soul = msg.get['#']; - if(soul){ // for now, only allow direct IDs to be read. - if(typeof soul !== 'string'){ return to.next(msg) } // do not handle lexical cursors. - if('alias' === soul){ // Allow reading the list of usernames/aliases in the system? - return to.next(msg); // yes. - } else - if('~@' === soul.slice(0,2)){ // Allow reading the list of public keys associated with an alias? - return to.next(msg); // yes. - } else { // Allow reading everything? - return to.next(msg); // yes // TODO: No! Make this a callback/event that people can filter on. - } - } - } - if(msg.put){ - /* - NOTICE: THIS IS OLD AND GETTING DEPRECATED. - ANY SECURITY CHANGES SHOULD HAPPEN ABOVE FIRST - THEN PORTED TO HERE. - */ - // potentially parallel async operations!!! - var check = {}, 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}); - }; - each.way = function(val, key){ - var soul = this.soul, node = this.node, tmp; - if('_' === key){ return } // ignore meta data - if('~@' === soul){ // special case for shared system data, the list of aliases. - each.alias(val, key, node, soul); return; - } - if('~@' === soul.slice(0,2)){ // special case for shared system data, the list of public keys for an alias. - each.pubs(val, key, node, soul); return; - } - if('~' === soul.slice(0,1) && 2 === (tmp = soul.slice(1)).split('.').length){ // special case, account data for a public key. - each.pub(val, key, node, soul, tmp, (msg._||noop).user); return; - } - each.any(val, key, node, soul, (msg._||noop).user); return; - return each.end({err: "No other data allowed!"}); - }; - each.alias = function(val, key, node, soul){ // Example: {_:#~@, ~@alice: {#~@alice}} - if(!val){ return each.end({err: "Data must exist!"}) } // data MUST exist - if('~@'+key === Gun.val.link.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){ // Example: {_:#~@alice, ~asdf: {#~asdf}} - if(!val){ return each.end({err: "Alias must exist!"}) } // data MUST exist - if(key === Gun.val.link.is(val)){ return check['pubs'+soul+key] = 0 } // and the ID must be EXACTLY equal to its property - 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, user){ var tmp; // Example: {_:#~asdf, hello:'world'~fdsa}} - if('pub' === key){ - 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!"}); - } - check['user'+soul+key] = 1; - if(Gun.is(msg.$) && user && user.is && pub === user.is.pub){ - SEA.sign(SEA.opt.prep(tmp = SEA.opt.parse(val), key, node, soul), (user._).sea, function(data){ var rel; - if(u === data){ return each.end({err: SEA.err || 'Pub signature fail.'}) } - if(rel = Gun.val.link.is(val)){ - (at.sea.own[rel] = at.sea.own[rel] || {})[pub] = true; - } - node[key] = JSON.stringify({':': SEA.opt.unpack(data.m), '~': data.s}); - check['user'+soul+key] = 0; - each.end({ok: 1}); - }, {check: SEA.opt.pack(tmp, key, node, soul), raw: 1}); - return; - } - SEA.verify(SEA.opt.pack(val,key,node,soul), pub, function(data){ var rel, tmp; - data = SEA.opt.unpack(data, key, node); - 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. - } - if((rel = Gun.val.link.is(data)) && pub === SEA.opt.pub(rel)){ - (at.sea.own[rel] = at.sea.own[rel] || {})[pub] = true; - } - check['user'+soul+key] = 0; - each.end({ok: 1}); - }); - }; - each.any = function(val, key, node, soul, user){ var tmp, pub; - if(!(pub = SEA.opt.pub(soul))){ - if(at.opt.secure){ - each.end({err: "Soul is missing public key at '" + key + "'."}); - return; - } - // TODO: Ask community if should auto-sign non user-graph data. - check['any'+soul+key] = 1; - at.on('secure', function(msg){ this.off(); - check['any'+soul+key] = 0; - if(at.opt.secure){ msg = null } - each.end(msg || {err: "Data cannot be modified."}); - }).on.on('secure', msg); - //each.end({err: "Data cannot be modified."}); - return; - } - if(Gun.is(msg.$) && user && user.is && pub === user.is.pub){ - /*var other = Gun.obj.map(at.sea.own[soul], function(v, p){ - if((user.is||{}).pub !== p){ return p } - }); - if(other){ - each.any(val, key, node, soul); - return; - }*/ - check['any'+soul+key] = 1; - SEA.sign(SEA.opt.prep(tmp = SEA.opt.parse(val), key, node, soul), (user._).sea, function(data){ - if(u === data){ return each.end({err: 'My signature fail.'}) } - node[key] = JSON.stringify({':': SEA.opt.unpack(data.m), '~': data.s}); - check['any'+soul+key] = 0; - each.end({ok: 1}); - }, {check: SEA.opt.pack(tmp, key, node, soul), raw: 1}); - return; - } - check['any'+soul+key] = 1; - SEA.verify(SEA.opt.pack(val,key,node,soul), pub, function(data){ var rel; - data = SEA.opt.unpack(data, key, node); - if(u === data){ return each.end({err: "Mismatched owner on '" + key + "'."}) } // thanks @rogowski ! - if((rel = Gun.val.link.is(data)) && pub === SEA.opt.pub(rel)){ - (at.sea.own[rel] = at.sea.own[rel] || {})[pub] = true; - } - check['any'+soul+key] = 0; - each.end({ok: 1}); - }); - } - each.end = function(ctx){ // TODO: Can't you just switch this to each.end = cb? - if(each.err){ return } - if((each.err = ctx.err) || ctx.no){ - console.log('NO!', each.err, msg.put); // 451 mistmached data FOR MARTTI - return; - } - if(!each.end.ed){ return } - if(Gun.obj.map(check, function(no){ - if(no){ return true } - })){ return } - (msg._||{}).user = at.user || security; // already been through firewall, does not need to again on out. - to.next(msg); - }; - Gun.obj.map(msg.put, each.node); - 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). - } + var valid = Gun.valid, link_is = function(d,l){ return 'string' == typeof (l = valid(d)) && l }, state_ify = (Gun.state||'').ify; + var pubcut = /[^\w_-]/; // anything not alphanumeric or _ - SEA.opt.pub = function(s){ if(!s){ return } @@ -311,13 +206,17 @@ s = s.slice(0,2).join('.'); return s; } - SEA.opt.prep = function(d,k, n,s){ // prep for signing - return {'#':s,'.':k,':':SEA.opt.parse(d),'>':Gun.state.is(n, k)}; + SEA.opt.stringy = function(t){ + // TODO: encrypt etc. need to check string primitive. Make as breaking change. } - SEA.opt.pack = function(d,k, n,s){ // pack for verifying - if(SEA.opt.check(d)){ return d } - var meta = (Gun.obj.ify((d && d[':'])||d)||''), sig = meta['~']; - return sig? {m: {'#':s||d['#'],'.':k||d['.'],':':meta[':'],'>':d['>']||Gun.state.is(n, k)}, s: sig} : d; + SEA.opt.pack = function(d,cb,k, n,s){ var tmp, f; // pack for verifying + if(SEA.opt.check(d)){ return cb(d) } + if(d && d['#'] && d['.'] && d['>']){ tmp = d[':']; f = 1 } + JSON.parseAsync(f? tmp : d, function(err, meta){ + var sig = ((u !== (meta||'')[':']) && (meta||'')['~']); // or just ~ check? + if(!sig){ cb(d); return } + cb({m: {'#':s||d['#'],'.':k||d['.'],':':(meta||'')[':'],'>':d['>']||Gun.state.is(n, k)}, s: sig}); + }); } var O = SEA.opt; SEA.opt.unpack = function(d, k, n){ var tmp; @@ -327,7 +226,7 @@ if(!k || !n){ return } if(d === n[k]){ return d } if(!SEA.opt.check(n[k])){ return d } - var soul = Gun.node.soul(n) || O.fall_soul, s = Gun.state.is(n, k) || O.fall_state; + var soul = (n && n._ && n._['#']) || O.fall_soul, s = Gun.state.is(n, k) || O.fall_state; if(d && 4 === d.length && soul === d[0] && k === d[1] && fl(s) === fl(d[3])){ return d[2]; } @@ -336,10 +235,7 @@ } } SEA.opt.shuffle_attack = 1546329600000; // Jan 1, 2019 - var noop = function(){}, u; var fl = Math.floor; // TODO: Still need to fix inconsistent state issue. - var rel_is = Gun.val.rel.is; - var obj_ify = Gun.obj.ify; // TODO: Potential bug? If pub/priv key starts with `-`? IDK how possible. \ No newline at end of file diff --git a/sea/recall.js b/sea/recall.js new file mode 100644 index 00000000..a8a82e93 --- /dev/null +++ b/sea/recall.js @@ -0,0 +1,27 @@ + + var User = require('./user'), SEA = User.SEA, Gun = User.GUN; + User.prototype.recall = function(opt, cb){ + var gun = this, root = gun.back(-1), tmp; + opt = opt || {}; + if(opt && opt.sessionStorage){ + if(SEA.window){ + try{ + var sS = {}; + sS = window.sessionStorage; + if(sS){ + (root._).opt.remember = true; + ((gun.back('user')._).opt||opt).remember = true; + if(sS.recall || sS.pair) root.user().auth(JSON.parse(sS.pair), cb); // pair is more reliable than alias/pass + } + }catch(e){} + } + return gun; + } + /* + TODO: copy mhelander's expiry code back in. + Although, we should check with community, + should expiry be core or a plugin? + */ + return gun; + } + \ No newline at end of file diff --git a/sea/sea.js b/sea/sea.js index dadc2bac..dd886ca8 100644 --- a/sea/sea.js +++ b/sea/sea.js @@ -7,7 +7,8 @@ SEA.verify = require('./verify'); SEA.encrypt = require('./encrypt'); SEA.decrypt = require('./decrypt'); - SEA.opt.aeskey = require('./aeskey'); // not official! + SEA.certify = require('./certify'); + //SEA.opt.aeskey = require('./aeskey'); // not official! // this causes problems in latest WebCrypto. SEA.random = SEA.random || shim.random; @@ -49,9 +50,9 @@ // But all other behavior needs to be equally easy, like opinionated ways of // Adding friends (trusted public keys), sending private messages, etc. // Cheers! Tell me what you think. - var Gun = (SEA.window||{}).Gun || require((typeof MODULE == "undefined"?'.':'')+'./gun', 1); - Gun.SEA = SEA; - SEA.GUN = SEA.Gun = Gun; + ((SEA.window||{}).GUN||{}).SEA = SEA; module.exports = SEA + // -------------- END SEA MODULES -------------------- + // -- BEGIN SEA+GUN MODULES: BUNDLED BY DEFAULT UNTIL OTHERS USE SEA ON OWN ------- \ No newline at end of file diff --git a/sea/secret.js b/sea/secret.js index 1e3e3272..1ed4cfd6 100644 --- a/sea/secret.js +++ b/sea/secret.js @@ -6,6 +6,7 @@ SEA.secret = SEA.secret || (async (key, pair, cb, opt) => { try { opt = opt || {}; if(!pair || !pair.epriv || !pair.epub){ + if(!SEA.I){ throw 'No secret mix.' } pair = await SEA.I(null, {what: key, how: 'secret', why: opt.why}); } var pub = key.epub || key; diff --git a/sea/settings.js b/sea/settings.js index 2843bb0d..c9008822 100644 --- a/sea/settings.js +++ b/sea/settings.js @@ -1,6 +1,6 @@ var SEA = require('./root'); - var Buffer = require('./buffer'); + var shim = require('./shim'); var s = {}; s.pbkdf2 = {hash: {name : 'SHA-256'}, iter: 100000, ks: 64}; s.ecdsa = { @@ -31,10 +31,10 @@ }; s.check = function(t){ return (typeof t == 'string') && ('SEA{' === t.slice(0,4)) } - s.parse = function p(t){ try { + s.parse = async function p(t){ try { var yes = (typeof t == 'string'); if(yes && 'SEA{' === t.slice(0,4)){ t = t.slice(3) } - return yes ? JSON.parse(t) : t; + return yes ? await shim.parse(t) : t; } catch (e) {} return t; } diff --git a/sea/sha256.js b/sea/sha256.js index c3182317..f64660de 100644 --- a/sea/sha256.js +++ b/sea/sha256.js @@ -1,7 +1,7 @@ var shim = require('./shim'); module.exports = async function(d, o){ - var t = (typeof d == 'string')? d : JSON.stringify(d); + var t = (typeof d == 'string')? d : await shim.stringify(d); var hash = await shim.subtle.digest({name: o||'SHA-256'}, new shim.TextEncoder().encode(t)); return shim.Buffer.from(hash); } diff --git a/sea/share.js b/sea/share.js new file mode 100644 index 00000000..1fd24f82 --- /dev/null +++ b/sea/share.js @@ -0,0 +1,136 @@ + + var User = require('./user'), SEA = User.SEA, Gun = User.GUN, noop = function(){}; + User.prototype.pair = function(){ + var user = this, proxy; // undeprecated, hiding with proxies. + try{ proxy = new Proxy({DANGER:'\u2620'}, {get: function(t,p,r){ + if(!user.is || !(user._||'').sea){ return } + return user._.sea[p]; + }})}catch(e){} + return proxy; + } + // If authenticated user wants to delete his/her account, let's support it! + User.prototype.delete = async function(alias, pass, cb){ + console.log("user.delete() IS DEPRECATED AND WILL BE MOVED TO A MODULE!!!"); + var gun = this, root = gun.back(-1), user = gun.back('user'); + try { + user.auth(alias, pass, function(ack){ + var pub = (user.is||{}).pub; + // Delete user data + user.map().once(function(){ this.put(null) }); + // Wipe user data from memory + user.leave(); + (cb || noop)({ok: 0}); + }); + } catch (e) { + Gun.log('User.delete failed! Error:', e); + } + return gun; + } + User.prototype.alive = async function(){ + console.log("user.alive() IS DEPRECATED!!!"); + const gunRoot = this.back(-1) + try { + // All is good. Should we do something more with actual recalled data? + await authRecall(gunRoot) + return gunRoot._.user._ + } catch (e) { + const err = 'No session!' + Gun.log(err) + throw { err } + } + } + User.prototype.trust = async function(user){ + console.log("`.trust` API MAY BE DELETED OR CHANGED OR RENAMED, DO NOT USE!"); + // TODO: BUG!!! SEA `node` read listener needs to be async, which means core needs to be async too. + //gun.get('alice').get('age').trust(bob); + if (Gun.is(user)) { + user.get('pub').get((ctx, ev) => { + console.log(ctx, ev) + }) + } + user.get('trust').get(path).put(theirPubkey); + + // do a lookup on this gun chain directly (that gets bob's copy of the data) + // do a lookup on the metadata trust table for this path (that gets all the pubkeys allowed to write on this path) + // do a lookup on each of those pubKeys ON the path (to get the collab data "layers") + // THEN you perform Jachen's mix operation + // and return the result of that to... + } + User.prototype.grant = function(to, cb){ + console.log("`.grant` API MAY BE DELETED OR CHANGED OR RENAMED, DO NOT USE!"); + var gun = this, user = gun.back(-1).user(), pair = user._.sea, path = ''; + gun.back(function(at){ if(at.is){ return } path += (at.get||'') }); + (async function(){ + var enc, sec = await user.get('grant').get(pair.pub).get(path).then(); + sec = await SEA.decrypt(sec, pair); + if(!sec){ + sec = SEA.random(16).toString(); + enc = await SEA.encrypt(sec, pair); + user.get('grant').get(pair.pub).get(path).put(enc); + } + var pub = to.get('pub').then(); + var epub = to.get('epub').then(); + pub = await pub; epub = await epub; + var dh = await SEA.secret(epub, pair); + enc = await SEA.encrypt(sec, dh); + user.get('grant').get(pub).get(path).put(enc, cb); + }()); + return gun; + } + User.prototype.secret = function(data, cb){ + console.log("`.secret` API MAY BE DELETED OR CHANGED OR RENAMED, DO NOT USE!"); + var gun = this, user = gun.back(-1).user(), pair = user.pair(), path = ''; + gun.back(function(at){ if(at.is){ return } path += (at.get||'') }); + (async function(){ + var enc, sec = await user.get('trust').get(pair.pub).get(path).then(); + sec = await SEA.decrypt(sec, pair); + if(!sec){ + sec = SEA.random(16).toString(); + enc = await SEA.encrypt(sec, pair); + user.get('trust').get(pair.pub).get(path).put(enc); + } + enc = await SEA.encrypt(data, sec); + gun.put(enc, cb); + }()); + return gun; + } + + /** + * returns the decrypted value, encrypted by secret + * @returns {Promise} + // Mark needs to review 1st before officially supported + User.prototype.decrypt = function(cb) { + let gun = this, + path = '' + gun.back(function(at) { + if (at.is) { + return + } + path += at.get || '' + }) + return gun + .then(async data => { + if (data == null) { + return + } + const user = gun.back(-1).user() + const pair = user.pair() + let sec = await user + .get('trust') + .get(pair.pub) + .get(path) + sec = await SEA.decrypt(sec, pair) + if (!sec) { + return data + } + let decrypted = await SEA.decrypt(data, sec) + return decrypted + }) + .then(res => { + cb && cb(res) + return res + }) + } + */ + module.exports = User + \ No newline at end of file diff --git a/sea/shim.js b/sea/shim.js index 6427d1b2..f7d1b963 100644 --- a/sea/shim.js +++ b/sea/shim.js @@ -4,6 +4,17 @@ const api = {Buffer: Buffer} var o = {}; + // ideally we can move away from JSON entirely? unlikely due to compatibility issues... oh well. + JSON.parseAsync = JSON.parseAsync || function(t,cb,r){ var u; try{ cb(u, JSON.parse(t,r)) }catch(e){ cb(e) } } + JSON.stringifyAsync = JSON.stringifyAsync || function(v,cb,r,s){ var u; try{ cb(u, JSON.stringify(v,r,s)) }catch(e){ cb(e) } } + + api.parse = function(t,r){ return new Promise(function(res, rej){ + JSON.parseAsync(t,function(err, raw){ err? rej(err) : res(raw) },r); + })} + api.stringify = function(v,r,s){ return new Promise(function(res, rej){ + JSON.stringifyAsync(v,function(err, raw){ err? rej(err) : res(raw) },r,s); + })} + if(SEA.window){ api.crypto = window.crypto || window.msCrypto api.subtle = (api.crypto||o).subtle || (api.crypto||o).webkitSubtle; @@ -30,7 +41,7 @@ api.ossl = api.subtle = new WebCrypto({directory: 'ossl'}).subtle // ECDH } catch(e){ - console.log("text-encoding and peculiar/nwebcrypto may not be included by default, please add it to your package.json!"); + console.log("text-encoding and @peculiar/webcrypto may not be included by default, please add it to your package.json!"); }} module.exports = api diff --git a/sea/sign.js b/sea/sign.js index 65d57ba0..f286dd0c 100644 --- a/sea/sign.js +++ b/sea/sign.js @@ -8,15 +8,16 @@ SEA.sign = SEA.sign || (async (data, pair, cb, opt) => { try { opt = opt || {}; if(!(pair||opt).priv){ + if(!SEA.I){ throw 'No signing key.' } pair = await SEA.I(null, {what: data, how: 'sign', why: opt.why}); } if(u === data){ throw '`undefined` not allowed.' } - var json = S.parse(data); + var json = await S.parse(data); var check = opt.check = opt.check || json; if(SEA.verify && (SEA.opt.check(check) || (check && check.s && check.m)) && u !== await SEA.verify(check, pair)){ // don't sign if we already signed it. - var r = S.parse(check); - if(!opt.raw){ r = 'SEA'+JSON.stringify(r) } + var r = await S.parse(check); + if(!opt.raw){ r = 'SEA' + await shim.stringify(r) } if(cb){ try{ cb(r) }catch(e){console.log(e)} } return r; } @@ -27,7 +28,7 @@ var sig = await (shim.ossl || shim.subtle).importKey('jwk', jwk, {name: 'ECDSA', namedCurve: 'P-256'}, false, ['sign']) .then((key) => (shim.ossl || shim.subtle).sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, new Uint8Array(hash))) // privateKey scope doesn't leak out from here! var r = {m: json, s: shim.Buffer.from(sig, 'binary').toString(opt.encode || 'base64')} - if(!opt.raw){ r = 'SEA'+JSON.stringify(r) } + if(!opt.raw){ r = 'SEA' + await shim.stringify(r) } if(cb){ try{ cb(r) }catch(e){console.log(e)} } return r; diff --git a/sea/then.js b/sea/then.js index 6e6ded1e..6c127ee3 100644 --- a/sea/then.js +++ b/sea/then.js @@ -1,7 +1,6 @@ - var Gun = require('./sea').Gun; - Gun.chain.then = function(cb, opt = {}){ - opt = {wait: 200, ...opt} + var u, Gun = (''+u != typeof window)? (window.Gun||{chain:{}}) : require((''+u === typeof MODULE?'.':'')+'./gun', 1); + Gun.chain.then = function(cb, opt){ var gun = this, p = (new Promise(function(res, rej){ gun.once(res, opt); })); diff --git a/sea/user.js b/sea/user.js index 33b8793e..7e3cab49 100644 --- a/sea/user.js +++ b/sea/user.js @@ -1,7 +1,11 @@ - var SEA = require('./sea'); - var Gun = SEA.Gun; - var then = require('./then'); + var SEA = require('./sea'), Gun, u; + if(SEA.window){ + Gun = SEA.window.GUN || {chain:{}}; + } else { + Gun = require((typeof MODULE == "undefined"?'.':'')+'./gun', 1); + } + SEA.GUN = Gun; function User(root){ this._ = {$: this}; @@ -13,19 +17,25 @@ // only one user can be logged in at a time, per gun instance. Gun.chain.user = function(pub){ var gun = this, root = gun.back(-1), user; - if(pub){ return root.get('~'+pub) } + if(pub){ + pub = SEA.opt.pub((pub._||'')['#']) || pub; + return root.get('~'+pub); + } if(user = root.back('user')){ return user } - var root = (root._), at = root, uuid = at.opt.uuid || Gun.state.lex; + var root = (root._), at = root, uuid = at.opt.uuid || lex; (at = (user = at.user = gun.chain(new User))._).opt = {}; at.opt.uuid = function(cb){ var id = uuid(), pub = root.user; if(!pub || !(pub = pub.is) || !(pub = pub.pub)){ return id } - id = id + '~' + pub + '/'; + id = '~' + pub + '/' + id; if(cb && cb.call){ cb(null, id) } return id; } return user; } + function lex(){ return Gun.state().toString(36).replace('.','') } Gun.User = User; + User.GUN = Gun; + User.SEA = Gun.SEA = SEA; module.exports = User; \ No newline at end of file diff --git a/sea/verify.js b/sea/verify.js index 8e4eaf0c..e2e4ec97 100644 --- a/sea/verify.js +++ b/sea/verify.js @@ -6,9 +6,9 @@ var u; SEA.verify = SEA.verify || (async (data, pair, cb, opt) => { try { - var json = S.parse(data); + var json = await S.parse(data); if(false === pair){ // don't verify! - var raw = S.parse(json.m); + var raw = await S.parse(json.m); if(cb){ try{ cb(raw) }catch(e){console.log(e)} } return raw; } @@ -27,7 +27,7 @@ return await SEA.opt.fall_verify(data, pair, cb, opt); } } - var r = check? S.parse(json.m) : u; + var r = check? await S.parse(json.m) : u; if(cb){ try{ cb(r) }catch(e){console.log(e)} } return r; @@ -55,20 +55,22 @@ if(f === SEA.opt.fallback){ throw "Signature did not match" } f = f || 1; var tmp = data||''; data = SEA.opt.unpack(data) || data; - var json = S.parse(data), pub = pair.pub || pair, key = await SEA.opt.slow_leak(pub); - var hash = (f <= SEA.opt.fallback)? shim.Buffer.from(await shim.subtle.digest({name: 'SHA-256'}, new shim.TextEncoder().encode(S.parse(json.m)))) : await sha(json.m); // this line is old bad buggy code but necessary for old compatibility. + var json = await S.parse(data), pub = pair.pub || pair, key = await SEA.opt.slow_leak(pub); + var hash = (f <= SEA.opt.fallback)? shim.Buffer.from(await shim.subtle.digest({name: 'SHA-256'}, new shim.TextEncoder().encode(await S.parse(json.m)))) : await sha(json.m); // this line is old bad buggy code but necessary for old compatibility. var buf; var sig; var check; try{ buf = shim.Buffer.from(json.s, opt.encode || 'base64') // NEW DEFAULT! sig = new Uint8Array(buf) check = await (shim.ossl || shim.subtle).verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, sig, new Uint8Array(hash)) if(!check){ throw "Signature did not match." } - }catch(e){ + }catch(e){ try{ buf = shim.Buffer.from(json.s, 'utf8') // AUTO BACKWARD OLD UTF8 DATA! sig = new Uint8Array(buf) check = await (shim.ossl || shim.subtle).verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, sig, new Uint8Array(hash)) + }catch(e){ if(!check){ throw "Signature did not match." } + } } - var r = check? S.parse(json.m) : u; + var r = check? await S.parse(json.m) : u; O.fall_soul = tmp['#']; O.fall_key = tmp['.']; O.fall_val = data; O.fall_state = tmp['>']; if(cb){ try{ cb(r) }catch(e){console.log(e)} } return r; diff --git a/sea/work.js b/sea/work.js index 8351223b..e6f68493 100644 --- a/sea/work.js +++ b/sea/work.js @@ -7,12 +7,12 @@ SEA.work = SEA.work || (async (data, pair, cb, opt) => { try { // used to be named `proof` var salt = (pair||{}).epub || pair; // epub not recommended, salt should be random! - var opt = opt || {}; + opt = opt || {}; if(salt instanceof Function){ cb = salt; salt = u; } - data = (typeof data == 'string')? data : JSON.stringify(data); + data = (typeof data == 'string')? data : await shim.stringify(data); if('sha' === (opt.name||'').toLowerCase().slice(0,3)){ var rsha = shim.Buffer.from(await sha(data, opt.name), 'binary').toString(opt.encode || 'base64') if(cb){ try{ cb(rsha) }catch(e){console.log(e)} }