From a173c04a3c721b476e0cb08af6d6d30d9e9eaaf1 Mon Sep 17 00:00:00 2001 From: Mark Nadal Date: Sat, 21 Apr 2018 00:48:35 -0700 Subject: [PATCH] fix user API --- gun.js | 8 +- sea.js | 499 ++++++++++++++++++++++++++----------------------- test/user.html | 1 - 3 files changed, 265 insertions(+), 243 deletions(-) diff --git a/gun.js b/gun.js index 42858394..553c0662 100644 --- a/gun.js +++ b/gun.js @@ -657,7 +657,7 @@ return Gun.create(this._ = {gun: this, opt: o}); } - Gun.is = function(gun){ return (gun instanceof Gun) } + Gun.is = function(gun){ return (gun instanceof Gun) || (gun && gun._ && gun._.gun && true) || false } Gun.version = 0.9; @@ -923,11 +923,11 @@ // is complicated and was extremely hard to build. If you port GUN to another // language, consider implementing an easier API to build. var Gun = USE('./root'); - Gun.chain.chain = function(){ - var at = this._, chain = new this.constructor(this), cat = chain._, root; + Gun.chain.chain = function(sub){ + var gun = this, at = gun._, chain = new (sub || gun).constructor(gun), cat = chain._, root; cat.root = root = at.root; cat.id = ++root.once; - cat.back = this._; + cat.back = gun._; cat.on = Gun.on; cat.on('in', input, cat); // For 'in' if I add my own listeners to each then I MUST do it before in gets called. If I listen globally for all incoming data instead though, regardless of individual listeners, I can transform the data there and then as well. cat.on('out', output, cat); // However for output, there isn't really the global option. I must listen by adding my own listener individually BEFORE this one is ever called. diff --git a/sea.js b/sea.js index 8c1218ba..2a2de402 100644 --- a/sea.js +++ b/sea.js @@ -831,7 +831,9 @@ const finalizeLogin = async (alias, key, gunRoot, opts) => { const { user } = gunRoot._ // add our credentials in-memory only to our root gun instance + //var tmp = user._.tag; user._ = key.at.gun._ + //user._.tag = tmp || user._.tag; // so that way we can use the credentials to encrypt/decrypt data // that is input/output through gun (see below) const { pub, priv, epub, epriv } = key @@ -1015,11 +1017,43 @@ })(USE, './leave'); ;USE(function(module){ - // How does it work? - // TODO: Bug! Need to include SEA! - //const Gun = (typeof window !== 'undefined' ? window : global).Gun || USE('gun/gun') + var Gun = USE('./sea').Gun; + Gun.chain.then = function(cb){ + var gun = this, p = (new Promise(function(res, rej){ + gun.once(res); + })); + return cb? p.then(cb) : p; + } + })(USE, './then'); + + ;USE(function(module){ + var SEA = USE('./sea'); + var Gun = SEA.Gun; + var then = USE('./then'); + + function User(){ + this._ = {gun: this} + Gun.call() + } + User.prototype = (function(){ function F(){}; F.prototype = Gun.chain; return new F() }()) // Object.create polyfill + User.prototype.constructor = User; + + // let's extend the gun chain with a `user` function. + // 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); + if(pub){ return root.get('pub/'+pub) } + return root.back('user') || ((root._).user = gun.chain(new User)); + } + module.exports = User; + })(USE, './user'); + + ;USE(function(module){ + // TODO: This needs to be split into all separate functions. + // Not just everything thrown into 'create'. const SEA = USE('./sea') + const User = USE('./user') const authRecall = USE('./recall') const authsettings = USE('./settings') const authenticate = USE('./authenticate') @@ -1028,250 +1062,239 @@ const { recall: _initial_authsettings } = USE('./settings') const Gun = SEA.Gun; - // let's extend the gun chain with a `user` function. - // only one user can be logged in at a time, per gun instance. - Gun.chain.user = function() { - const gunRoot = this.back(-1) // always reference the root gun instance. - const user = gunRoot._.user || (gunRoot._.user = gunRoot.chain()); // create a user context. - // then methods... - [ 'create', // factory - 'auth', // login - 'leave', // logout - 'delete', // account delete - 'recall', // existing auth boostrap - 'alive' // keep/check auth validity - ].map((method)=> user[method] = User[method]) - return user // return the user! - } var u; - function User(){} // Well first we have to actually create a user. That is what this function does. - Object.assign(User, { - async create(username, pass, cb) { - const gunRoot = this.back(-1) - var gun = this, cat = (gun._); - cb = cb || function(){}; - if(cat.ing){ - cb({err: Gun.log("User is already being created or authenticated!"), wait: true}); - return gun; - } - cat.ing = true; - var p = new Promise((resolve, reject) => { // Because no Promises or async - // Because more than 1 user might have the same username, we treat the alias as a list of those users. - if(cb){ resolve = reject = cb } - gunRoot.get(`alias/${username}`).get(async (at, ev) => { - ev.off() - if (at.put) { - // If we can enforce that a user name is already taken, it might be nice to try, but this is not guaranteed. - const err = 'User already created!' - Gun.log(err) - cat.ing = false; - gun.leave(); - return reject({ err }) - } - const salt = Gun.text.random(64) - // pseudo-randomly create a salt, then use CryptoJS's PBKDF2 function to extend the password with it. - try { - const proof = await SEA.work(pass, salt) - // this will take some short amount of time to produce a proof, which slows brute force attacks. - const pairs = await SEA.pair() - // now we have generated a brand new ECDSA key pair for the user account. - const { pub, priv, epriv } = pairs - // the user's public key doesn't need to be signed. But everything else needs to be signed with it! - const alias = await SEA.sign(username, pairs) - if(u === alias){ throw SEA.err } - const epub = await SEA.sign(pairs.epub, pairs) - if(u === epub){ throw SEA.err } - // to keep the private key safe, we AES encrypt it with the proof of work! - const auth = await SEA.encrypt({ priv, epriv }, proof) - .then((auth) => // TODO: So signedsalt isn't needed? - // SEA.sign(salt, pairs).then((signedsalt) => - SEA.sign({ek: auth, s: salt}, pairs) - // ) - ).catch((e) => { Gun.log('SEA.en or SEA.write calls failed!'); cat.ing = false; gun.leave(); reject(e) }) - const user = { alias, pub, epub, auth } - const tmp = `pub/${pairs.pub}` - // awesome, now we can actually save the user with their public key as their ID. - try{ - - gunRoot.get(tmp).put(user) - }catch(e){console.log(e)} - // next up, we want to associate the alias with the public key. So we add it to the alias list. - gunRoot.get(`alias/${username}`).put(Gun.obj.put({}, tmp, Gun.val.rel.ify(tmp))) - // callback that the user has been created. (Note: ok = 0 because we didn't wait for disk to ack) - setTimeout(() => { cat.ing = false; resolve({ ok: 0, pub: pairs.pub}) }, 10) // TODO: BUG! If `.auth` happens synchronously after `create` finishes, auth won't work. This setTimeout is a temporary hack until we can properly fix it. - } catch (e) { - Gun.log('SEA.create failed!') - cat.ing = false; - gun.leave(); - reject(e) - } - }) - }) - return gun // gun chain commands must return gun chains! - }, - // now that we have created a user, we want to authenticate them! - async auth(alias, pass, cb, opt) { - const opts = opt || (typeof cb !== 'function' && cb) - let { pin, newpass } = opts || {} - const gunRoot = this.back(-1) - cb = typeof cb === 'function' ? cb : () => {} - newpass = newpass || (opts||{}).change; - var gun = this, cat = (gun._); - if(cat.ing){ - cb({err: "User is already being created or authenticated!", wait: true}); - return gun; - } - cat.ing = true; - - if (!pass && pin) { - try { - var r = await authRecall(gunRoot, { alias, pin }) - return cat.ing = false, cb(r), gun; - } catch (e) { - var err = { err: 'Auth attempt failed! Reason: No session data for alias & PIN' } - return cat.ing = false, gun.leave(), cb(err), gun; - } - } - - const putErr = (msg) => (e) => { - const { message, err = message || '' } = e - Gun.log(msg) - var error = { err: `${msg} Reason: ${err}` } - return cat.ing = false, gun.leave(), cb(error), gun; - } - - try { - const keys = await authenticate(alias, pass, gunRoot) - if (!keys) { - return putErr('Auth attempt failed!')({ message: 'No keys' }) - } - const { pub, priv, epub, epriv } = keys - // we're logged in! - if (newpass) { - // password update so encrypt private key using new pwd + salt - try { - const salt = Gun.text.random(64); - const encSigAuth = await SEA.work(newpass, salt) - .then((key) => - SEA.encrypt({ priv, epriv }, key) - .then((auth) => SEA.sign({ek: auth, s: salt}, keys)) - ) - const signedEpub = await SEA.sign(epub, keys) - const signedAlias = await SEA.sign(alias, keys) - const user = { - pub, - alias: signedAlias, - auth: encSigAuth, - epub: signedEpub - } - // awesome, now we can update the user using public key ID. - gunRoot.get(`pub/${user.pub}`).put(user) - // then we're done - const login = finalizeLogin(alias, keys, gunRoot, { pin }) - login.catch(putErr('Failed to finalize login with new password!')) - return cat.ing = false, cb(await login), gun - } catch (e) { - return putErr('Password set attempt failed!')(e) - } - } else { - const login = finalizeLogin(alias, keys, gunRoot, { pin }) - login.catch(putErr('Finalizing login failed!')) - return cat.ing = false, cb(await login), gun; - } - } catch (e) { - return putErr('Auth attempt failed!')(e) - } + User.prototype.create = function(username, pass, cb){ + // TODO: Needs to be cleaned up!!! + const gunRoot = this.back(-1) + var gun = this, cat = (gun._); + cb = cb || function(){}; + if(cat.ing){ + cb({err: Gun.log("User is already being created or authenticated!"), wait: true}); return gun; - }, - async leave() { - return await authLeave(this.back(-1)) - }, - // If authenticated user wants to delete his/her account, let's support it! - async delete(alias, pass) { - const gunRoot = this.back(-1) - try { - const { pub } = await authenticate(alias, pass, gunRoot) - await authLeave(gunRoot, alias) - // Delete user data - gunRoot.get(`pub/${pub}`).put(null) - // Wipe user data from memory - const { user = { _: {} } } = gunRoot._; - // TODO: is this correct way to 'logout' user from Gun.User ? - [ 'alias', 'sea', 'pub' ].map((key) => delete user._[key]) - user._.is = user.is = {} - gunRoot.user() - return { ok: 0 } // TODO: proper return codes??? - } catch (e) { - Gun.log('User.delete failed! Error:', e) - throw e // TODO: proper error codes??? + } + cat.ing = true; + var resolve = function(){}, reject = resolve; + // Because more than 1 user might have the same username, we treat the alias as a list of those users. + if(cb){ resolve = reject = cb } + gunRoot.get(`alias/${username}`).get(async (at, ev) => { + ev.off() + if (at.put) { + // If we can enforce that a user name is already taken, it might be nice to try, but this is not guaranteed. + const err = 'User already created!' + Gun.log(err) + cat.ing = false; + gun.leave(); + return reject({ err }) } - }, - // If authentication is to be remembered over reloads or browser closing, - // set validity time in minutes. - async recall(setvalidity, options) { - const gunRoot = this.back(-1) + const salt = Gun.text.random(64) + // pseudo-randomly create a salt, then use CryptoJS's PBKDF2 function to extend the password with it. + try { + const proof = await SEA.work(pass, salt) + // this will take some short amount of time to produce a proof, which slows brute force attacks. + const pairs = await SEA.pair() + // now we have generated a brand new ECDSA key pair for the user account. + const { pub, priv, epriv } = pairs + // the user's public key doesn't need to be signed. But everything else needs to be signed with it! + const alias = await SEA.sign(username, pairs) + if(u === alias){ throw SEA.err } + const epub = await SEA.sign(pairs.epub, pairs) + if(u === epub){ throw SEA.err } + // to keep the private key safe, we AES encrypt it with the proof of work! + const auth = await SEA.encrypt({ priv, epriv }, proof) + .then((auth) => // TODO: So signedsalt isn't needed? + // SEA.sign(salt, pairs).then((signedsalt) => + SEA.sign({ek: auth, s: salt}, pairs) + // ) + ).catch((e) => { Gun.log('SEA.en or SEA.write calls failed!'); cat.ing = false; gun.leave(); reject(e) }) + const user = { alias, pub, epub, auth } + const tmp = `pub/${pairs.pub}` + // awesome, now we can actually save the user with their public key as their ID. + try{ - let validity - let opts - - var o = setvalidity; - if(o && o.sessionStorage){ - if(typeof window !== 'undefined'){ - var tmp = window.sessionStorage; - if(tmp){ - gunRoot._.opt.remember = true; - if(tmp.alias && tmp.tmp){ - gunRoot.user().auth(tmp.alias, tmp.tmp); + gunRoot.get(tmp).put(user) + }catch(e){console.log(e)} + // next up, we want to associate the alias with the public key. So we add it to the alias list. + gunRoot.get(`alias/${username}`).put(Gun.obj.put({}, tmp, Gun.val.rel.ify(tmp))) + // callback that the user has been created. (Note: ok = 0 because we didn't wait for disk to ack) + setTimeout(() => { cat.ing = false; resolve({ ok: 0, pub: pairs.pub}) }, 10) // TODO: BUG! If `.auth` happens synchronously after `create` finishes, auth won't work. This setTimeout is a temporary hack until we can properly fix it. + } catch (e) { + Gun.log('SEA.create failed!') + cat.ing = false; + gun.leave(); + reject(e) + } + }) + return gun; // gun chain commands must return gun chains! + } + // now that we have created a user, we want to authenticate them! + User.prototype.auth = function(alias, pass, cb, opt){ + // TODO: Needs to be cleaned up!!!! + const opts = opt || (typeof cb !== 'function' && cb) + let { pin, newpass } = opts || {} + const gunRoot = this.back(-1) + cb = typeof cb === 'function' ? cb : () => {} + newpass = newpass || (opts||{}).change; + var gun = this, cat = (gun._); + if(cat.ing){ + cb({err: "User is already being created or authenticated!", wait: true}); + return gun; + } + cat.ing = true; + + if (!pass && pin) { (async function(){ + try { + var r = await authRecall(gunRoot, { alias, pin }) + return cat.ing = false, cb(r), gun; + } catch (e) { + var err = { err: 'Auth attempt failed! Reason: No session data for alias & PIN' } + return cat.ing = false, gun.leave(), cb(err), gun; + }}()) + return gun; + } + + const putErr = (msg) => (e) => { + const { message, err = message || '' } = e + Gun.log(msg) + var error = { err: `${msg} Reason: ${err}` } + return cat.ing = false, gun.leave(), cb(error), gun; + } + + (async function(){ try { + const keys = await authenticate(alias, pass, gunRoot) + if (!keys) { + return putErr('Auth attempt failed!')({ message: 'No keys' }) + } + const { pub, priv, epub, epriv } = keys + // we're logged in! + if (newpass) { + // password update so encrypt private key using new pwd + salt + try { + const salt = Gun.text.random(64); + const encSigAuth = await SEA.work(newpass, salt) + .then((key) => + SEA.encrypt({ priv, epriv }, key) + .then((auth) => SEA.sign({ek: auth, s: salt}, keys)) + ) + const signedEpub = await SEA.sign(epub, keys) + const signedAlias = await SEA.sign(alias, keys) + const user = { + pub, + alias: signedAlias, + auth: encSigAuth, + epub: signedEpub + } + // awesome, now we can update the user using public key ID. + gunRoot.get(`pub/${user.pub}`).put(user) + // then we're done + const login = finalizeLogin(alias, keys, gunRoot, { pin }) + login.catch(putErr('Failed to finalize login with new password!')) + return cat.ing = false, cb(await login), gun + } catch (e) { + return putErr('Password set attempt failed!')(e) + } + } else { + const login = finalizeLogin(alias, keys, gunRoot, { pin }) + login.catch(putErr('Finalizing login failed!')) + return cat.ing = false, cb(await login), gun; + } + } catch (e) { + return putErr('Auth attempt failed!')(e) + } }()); + return gun; + } + User.prototype.pair = function(){ + var user = this; + if(!user.is){ return false } + return user._.sea; + } + User.prototype.leave = async function(){ + return await authLeave(this.back(-1)) + } + // If authenticated user wants to delete his/her account, let's support it! + User.prototype.delete = async function(alias, pass){ + const gunRoot = this.back(-1) + try { + const { pub } = await authenticate(alias, pass, gunRoot) + await authLeave(gunRoot, alias) + // Delete user data + gunRoot.get(`pub/${pub}`).put(null) + // Wipe user data from memory + const { user = { _: {} } } = gunRoot._; + // TODO: is this correct way to 'logout' user from Gun.User ? + [ 'alias', 'sea', 'pub' ].map((key) => delete user._[key]) + user._.is = user.is = {} + gunRoot.user() + return { ok: 0 } // TODO: proper return codes??? + } catch (e) { + Gun.log('User.delete failed! Error:', e) + throw e // TODO: proper error codes??? + } + } + // If authentication is to be remembered over reloads or browser closing, + // set validity time in minutes. + User.prototype.recall = async function(setvalidity, options){ + const gunRoot = this.back(-1) + + let validity + let opts + + var o = setvalidity; + if(o && o.sessionStorage){ + if(typeof window !== 'undefined'){ + var tmp = window.sessionStorage; + if(tmp){ + gunRoot._.opt.remember = true; + if(tmp.alias && tmp.tmp){ + gunRoot.user().auth(tmp.alias, tmp.tmp); + } } } + return this; + } + + if (!Gun.val.is(setvalidity)) { + opts = setvalidity + validity = _initial_authsettings.validity + } else { + opts = options + validity = setvalidity * 60 // minutes to seconds + } + + try { + // opts = { hook: function({ iat, exp, alias, proof }) } + // iat == Date.now() when issued, exp == seconds to expire from iat + // How this works: + // called when app bootstraps, with wanted options + // IF authsettings.validity === 0 THEN no remember-me, ever + // IF PIN then signed 'remember' to window.sessionStorage and 'auth' to IndexedDB + authsettings.validity = typeof validity !== 'undefined' + ? validity : _initial_authsettings.validity + authsettings.hook = (Gun.obj.has(opts, 'hook') && typeof opts.hook === 'function') + ? opts.hook : _initial_authsettings.hook + // All is good. Should we do something more with actual recalled data? + return await authRecall(gunRoot) + } catch (e) { + const err = 'No session!' + Gun.log(err) + // NOTE! It's fine to resolve recall with reason why not successful + // instead of rejecting... + return { err: (e && e.err) || err } } - return this; } - - if (!Gun.val.is(setvalidity)) { - opts = setvalidity - validity = _initial_authsettings.validity - } else { - opts = options - validity = setvalidity * 60 // minutes to seconds - } - - try { - // opts = { hook: function({ iat, exp, alias, proof }) } - // iat == Date.now() when issued, exp == seconds to expire from iat - // How this works: - // called when app bootstraps, with wanted options - // IF authsettings.validity === 0 THEN no remember-me, ever - // IF PIN then signed 'remember' to window.sessionStorage and 'auth' to IndexedDB - authsettings.validity = typeof validity !== 'undefined' - ? validity : _initial_authsettings.validity - authsettings.hook = (Gun.obj.has(opts, 'hook') && typeof opts.hook === 'function') - ? opts.hook : _initial_authsettings.hook - // All is good. Should we do something more with actual recalled data? - return await authRecall(gunRoot) - } catch (e) { - const err = 'No session!' - Gun.log(err) - // NOTE! It's fine to resolve recall with reason why not successful - // instead of rejecting... - return { err: (e && e.err) || err } - } - }, - async alive() { - 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.alive = async function(){ + 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 } } - }) - Gun.chain.trust = function(user) { + } + 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)) { @@ -1281,7 +1304,7 @@ } } module.exports = User - })(USE, './user'); + })(USE, './create'); ;USE(function(module){ const Gun = (typeof window !== 'undefined' ? window : global).Gun || USE('gun/gun') diff --git a/test/user.html b/test/user.html index 4837bf66..30de03f8 100644 --- a/test/user.html +++ b/test/user.html @@ -17,7 +17,6 @@ -