diff --git a/sea.js b/sea.js index e00fdd96..89eddf94 100644 --- a/sea.js +++ b/sea.js @@ -282,7 +282,7 @@ } salt = salt || shim.random(9); if('SHA-256' === opt.name){ - var rsha = shim.Buffer.from(await sha(data), 'binary').toString('utf8') + var rsha = shim.Buffer.from(await sha(data), 'binary').toString(opt.encode || 'base64') if(cb){ try{ cb(rsha) }catch(e){console.log(e)} } return rsha; } @@ -296,7 +296,7 @@ hash: opt.hash || S.pbkdf2.hash, }, key, opt.length || (S.pbkdf2.ks * 8)) data = shim.random(data.length) // Erase data in case of passphrase - const r = shim.Buffer.from(result, 'binary').toString('utf8') + const r = shim.Buffer.from(result, 'binary').toString(opt.encode || 'base64') if(cb){ try{ cb(r) }catch(e){console.log(e)} } return r; } catch(e) { @@ -408,11 +408,10 @@ const pub = pair.pub const priv = pair.priv const jwk = S.jwk(pub, priv) - const msg = JSON.stringify(data) - const hash = await sha256hash(msg) + const hash = await sha256hash(JSON.stringify(data)) const sig = await (shim.ossl || shim.subtle).importKey('jwk', jwk, S.ecdsa.pair, false, ['sign']) .then((key) => (shim.ossl || shim.subtle).sign(S.ecdsa.sign, key, new Uint8Array(hash))) // privateKey scope doesn't leak out from here! - const r = 'SEA'+JSON.stringify({m: msg, s: shim.Buffer.from(sig, 'binary').toString('utf8')}); + const r = 'SEA'+JSON.stringify({m: data, s: shim.Buffer.from(sig, 'binary').toString(opt.encode || 'base64')}); if(cb){ try{ cb(r) }catch(e){console.log(e)} } return r; @@ -451,7 +450,9 @@ const jwk = S.jwk(pub) const key = await (shim.ossl || shim.subtle).importKey('jwk', jwk, S.ecdsa.pair, false, ['verify']) const hash = await sha256hash(json.m) - const sig = new Uint8Array(shim.Buffer.from(json.s, 'utf8')) + var buf; try{buf = shim.Buffer.from(json.s, opt.encode || 'base64') // NEW DEFAULT! + }catch(e){buf = shim.Buffer.from(json.s, 'utf8')} // AUTO BACKWARD OLD UTF8 DATA! + const sig = new Uint8Array(buf) const check = await (shim.ossl || shim.subtle).verify(S.ecdsa.sign, key, sig, new Uint8Array(hash)) if(!check){ throw "Signature did not match." } const r = check? parse(json.m) : u; @@ -503,9 +504,9 @@ name: opt.name || 'AES-GCM', iv: new Uint8Array(rand.iv) }, aes, new shim.TextEncoder().encode(msg))) const r = 'SEA'+JSON.stringify({ - ct: shim.Buffer.from(ct, 'binary').toString('utf8'), - iv: rand.iv.toString('utf8'), - s: rand.s.toString('utf8') + ct: shim.Buffer.from(ct, 'binary').toString(opt.encode || 'base64'), + iv: rand.iv.toString(opt.encode || 'base64'), + s: rand.s.toString(opt.encode || 'base64') }); if(cb){ try{ cb(r) }catch(e){console.log(e)} } @@ -535,10 +536,18 @@ key = pair.epriv || pair; } const json = parse(data) - const ct = await aeskey(key, shim.Buffer.from(json.s, 'utf8'), opt) + + var buf; try{buf = shim.Buffer.from(json.s, opt.encode || 'base64') // NEW DEFAULT! + }catch(e){buf = shim.Buffer.from(json.s, 'utf8')} // AUTO BACKWARD OLD UTF8 DATA! + var bufiv; try{bufiv = shim.Buffer.from(json.iv, opt.encode || 'base64') // NEW DEFAULT! + }catch(e){bufiv = shim.Buffer.from(json.iv, 'utf8')} // AUTO BACKWARD OLD UTF8 DATA! + var bufct; try{bufct = shim.Buffer.from(json.ct, opt.encode || 'base64') // NEW DEFAULT! + }catch(e){bufct = shim.Buffer.from(json.ct, 'utf8')} // AUTO BACKWARD OLD UTF8 DATA! + + const ct = await aeskey(key, buf, opt) .then((aes) => (/*shim.ossl ||*/ shim.subtle).decrypt({ // Keeping aesKey scope as private as possible... - name: opt.name || 'AES-GCM', iv: new Uint8Array(shim.Buffer.from(json.iv, 'utf8')) - }, aes, new Uint8Array(shim.Buffer.from(json.ct, 'utf8')))) + name: opt.name || 'AES-GCM', iv: new Uint8Array(bufiv) + }, aes, new Uint8Array(bufct))) const r = parse(new shim.TextDecoder('utf8').decode(ct)) if(cb){ try{ cb(r) }catch(e){console.log(e)} } return r; @@ -743,26 +752,30 @@ act.proof = proof; SEA.pair(act.c); // now we have generated a brand new ECDSA key pair for the user account. } - act.c = function(pair){ + act.c = function(pair){ var tmp; act.pair = pair || {}; - // the user's public key doesn't need to be signed. But everything else needs to be signed with it! + if(tmp = cat.root.user){ + tmp._.sea = pair; + tmp.is = {pub: pair.pub, epub: pair.epub, alias: alias}; + } + // the user's public key doesn't need to be signed. But everything else needs to be signed with it! // we have now automated it! clean up these extra steps now! act.data = {pub: pair.pub}; SEA.sign(alias, pair, act.d); } - act.d = function(alias){ - act.data.alias = alias; + act.d = function(sig){ + act.data.alias = alias || sig; SEA.sign(act.pair.epub, act.pair, act.e); } act.e = function(epub){ - act.data.epub = epub; + act.data.epub = act.pair.epub || epub; SEA.encrypt({priv: act.pair.priv, epriv: act.pair.epriv}, act.proof, act.f); // to keep the private key safe, we AES encrypt it with the proof of work! } act.f = function(auth){ - act.data.auth = auth; + act.data.auth = JSON.stringify({ek: auth, s: act.salt}); SEA.sign({ek: auth, s: act.salt}, act.pair, act.g); } act.g = function(auth){ var tmp; - act.data.auth = auth; + 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? @@ -806,14 +819,20 @@ } act.c = function(auth){ if(u === auth){ return act.b() } - SEA.work(pass, (act.auth = auth).s, act.d); // the proof of work is evidence that we've spent some time/effort trying to log in, this slows brute force. + if(Gun.text.is(auth)){ return act.c(Gun.obj.ify(auth)) } // new format + 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){ - if(u === proof){ return act.b() } - SEA.decrypt(act.auth.ek, proof, act.e); + SEA.decrypt(act.auth.ek, proof, act.e, act.enc); } act.e = function(half){ - if(u === half){ return act.b() } + 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); } @@ -833,6 +852,7 @@ user.is = {pub: pair.pub, epub: pair.epub, alias: alias}; at.sea = act.pair; cat.ing = false; + if(pass && !Gun.text.is(act.data.auth)){ opt.shuffle = opt.change = pass; } // migrate UTF8 + Shuffle! Test against NAB alias test_sea_shuffle + passw0rd opt.change? act.z() : cb(at); if(SEA.window && ((gun.back('user')._).opt||opt).remember){ // TODO: this needs to be modular. @@ -859,9 +879,17 @@ SEA.encrypt({priv: act.pair.priv, epriv: act.pair.epriv}, proof, act.x); } act.x = function(auth){ - SEA.sign({ek: auth, s: act.salt}, act.pair, act.w); + act.w(JSON.stringify({ek: auth, s: act.salt})); + //SEA.sign({ek: auth, s: act.salt}, act.pair, act.w); } act.w = function(auth){ + if(opt.shuffle){ // delete in future! + var tmp = Gun.obj.to(act.data); + Gun.obj.del(tmp, '_'); + tmp.auth = auth; + console.log('migrate core account from UTF8 & shuffle', tmp); + root.get('~'+act.pair.pub).put(tmp); + } // end delete root.get('~'+act.pair.pub).get('auth').put(auth, cb); } act.err = function(e){ @@ -1052,6 +1080,7 @@ Gun.node.is(msg.put, function(val, key, node){ c++; // for each property on the node // TODO: consider async/await use here... SEA.verify(val, false, function(data){ c--; // false just extracts the plain data. + data = SEA.opt.unpack(data, key, node); node[key] = val = data; // transform to plain value. if(d && !c && (c = -1)){ to.next(msg) } }); @@ -1131,7 +1160,7 @@ check['user'+soul+key] = 1; if(user && user.is && pub === user.is.pub){ //var id = Gun.text.random(3); - SEA.sign(val, (user._).sea, function(data){ var rel; + SEA.sign([soul, key, val, Gun.state.is(node, key)], (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; @@ -1144,6 +1173,7 @@ return; } SEA.verify(val, 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. } @@ -1168,6 +1198,7 @@ if(tmp = relpub(soul)){ check['any'+soul+key] = 1; SEA.verify(val, pub = tmp, 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 === relpub(rel)){ (at.sea.own[rel] = at.sea.own[rel] || {})[pub] = true; @@ -1217,7 +1248,7 @@ return; }*/ check['any'+soul+key] = 1; - SEA.sign(val, (user._).sea, function(data){ + SEA.sign([soul, key, val, Gun.state.is(node, key)], (user._).sea, function(data){ if(u === data){ return each.end({err: 'My signature fail.'}) } node[key] = data; check['any'+soul+key] = 0; @@ -1242,7 +1273,18 @@ } to.next(msg); // pass forward any data we do not know how to handle or process (this allows custom security protocols). } - var noop = {}; + SEA.opt.unpack = function(data, key, node){ + if(u === data){ return } + var tmp = data, soul = Gun.node.soul(node), s = Gun.state.is(node, key); + if(tmp && 4 === tmp.length && soul === tmp[0] && key === tmp[1] && s === tmp[3]){ + return tmp[2]; + } + if(s < SEA.opt.shuffle_attack){ + return data; + } + } + SEA.opt.shuffle_attack = 1546329600000; // Jan 1, 2019 + var noop = {}, u; })(USE, './index'); }());