diff --git a/lib/stats.js b/lib/stats.js index 7b4956ef..2e7a943a 100644 --- a/lib/stats.js +++ b/lib/stats.js @@ -19,7 +19,6 @@ Gun.on('opt', function(root){ os.loadavg = os.loadavg || noop; os.cpus = os.cpus || noop; setTimeout(function(){ - root.opt.file += (process.argv[2]||''); root.stats = Gun.obj.ify((fs.existsSync(__dirname+'/../stats.'+root.opt.file) && fs.readFileSync(__dirname+'/../stats.'+root.opt.file).toString())) || {}; root.stats.up = root.stats.up || {}; root.stats.up.start = root.stats.up.start || +(new Date); @@ -56,4 +55,4 @@ Gun.on('opt', function(root){ stats.gap = {}; }, 1000 * 15); Object.keys = Object.keys || function(o){ return Gun.obj.map(o, function(v,k,t){t(k)}) } -}); \ No newline at end of file +}); diff --git a/sea.js b/sea.js index e58474f3..91bbd8c3 100644 --- a/sea.js +++ b/sea.js @@ -1,647 +1,1007 @@ -;(function(){ - +(function() { /* UNBUILD */ var root; - if(typeof window !== "undefined"){ root = window } - if(typeof global !== "undefined"){ root = global } + if (typeof window !== "undefined") { + root = window; + } + if (typeof global !== "undefined") { + root = global; + } root = root || {}; - var console = root.console || {log: function(){}}; - function USE(arg, req){ - return req? require(arg) : arg.slice? USE[R(arg)] : function(mod, path){ - arg(mod = {exports: {}}); - USE[R(path)] = mod.exports; - } - function R(p){ - return p.split('/').slice(-1).toString().replace('.js',''); + var console = root.console || { log: function() {} }; + function USE(arg, req) { + return req + ? require(arg) + : arg.slice + ? USE[R(arg)] + : function(mod, path) { + arg((mod = { exports: {} })); + USE[R(path)] = mod.exports; + }; + function R(p) { + return p + .split("/") + .slice(-1) + .toString() + .replace(".js", ""); } } - if(typeof module !== "undefined"){ var common = module } + if (typeof module !== "undefined") { + var common = module; + } /* UNBUILD */ - ;USE(function(module){ + USE(function(module) { // Security, Encryption, and Authorization: SEA.js // MANDATORY READING: https://gun.eco/explainers/data/security.html // IT IS IMPLEMENTED IN A POLYFILL/SHIM APPROACH. // THIS IS AN EARLY ALPHA! - if(typeof window !== "undefined"){ module.window = window } + if (typeof window !== "undefined") { + module.window = window; + } var tmp = module.window || module; var SEA = tmp.SEA || {}; - if(SEA.window = module.window){ SEA.window.SEA = SEA } + if ((SEA.window = module.window)) { + SEA.window.SEA = SEA; + } - try{ if(typeof common !== "undefined"){ common.exports = SEA } }catch(e){} - module.exports = SEA; - })(USE, './root'); - - ;USE(function(module){ - var SEA = USE('./root'); - try{ if(SEA.window){ - if(location.protocol.indexOf('s') < 0 - && location.host.indexOf('localhost') < 0 - && location.protocol.indexOf('file:') < 0){ - location.protocol = 'https:'; // WebCrypto does NOT work without HTTPS! + try { + if (typeof common !== "undefined") { + common.exports = SEA; } - } }catch(e){} - })(USE, './https'); + } catch (e) {} + module.exports = SEA; + })(USE, "./root"); - ;USE(function(module){ + USE(function(module) { + var SEA = USE("./root"); + try { + if (SEA.window) { + if ( + location.protocol.indexOf("s") < 0 && + location.host.indexOf("localhost") < 0 && + location.protocol.indexOf("file:") < 0 + ) { + location.protocol = "https:"; // WebCrypto does NOT work without HTTPS! + } + } + } catch (e) {} + })(USE, "./https"); + + USE(function(module) { // This is Array extended to have .toString(['utf8'|'hex'|'base64']) function SeaArray() {} - Object.assign(SeaArray, { from: Array.from }) - SeaArray.prototype = Object.create(Array.prototype) - SeaArray.prototype.toString = function(enc, start, end) { enc = enc || 'utf8'; start = start || 0; - const length = this.length - if (enc === 'hex') { - const buf = new Uint8Array(this) - return [ ...Array(((end && (end + 1)) || length) - start).keys()] - .map((i) => buf[ i + start ].toString(16).padStart(2, '0')).join('') + Object.assign(SeaArray, { from: Array.from }); + SeaArray.prototype = Object.create(Array.prototype); + SeaArray.prototype.toString = function(enc, start, end) { + enc = enc || "utf8"; + start = start || 0; + const length = this.length; + if (enc === "hex") { + const buf = new Uint8Array(this); + return [...Array(((end && end + 1) || length) - start).keys()] + .map(i => buf[i + start].toString(16).padStart(2, "0")) + .join(""); } - if (enc === 'utf8') { - return Array.from( - { length: (end || length) - start }, - (_, i) => String.fromCharCode(this[ i + start]) - ).join('') + if (enc === "utf8") { + return Array.from({ length: (end || length) - start }, (_, i) => + String.fromCharCode(this[i + start]) + ).join(""); } - if (enc === 'base64') { - return btoa(this) + if (enc === "base64") { + return btoa(this); } - } + }; module.exports = SeaArray; - })(USE, './array'); + })(USE, "./array"); - ;USE(function(module){ + USE(function(module) { // This is Buffer implementation used in SEA. Functionality is mostly // compatible with NodeJS 'safe-buffer' and is used for encoding conversions // between binary and 'hex' | 'utf8' | 'base64' // See documentation and validation for safe implementation in: // https://github.com/feross/safe-buffer#update - var SeaArray = USE('./array'); + var SeaArray = USE("./array"); function SafeBuffer(...props) { - console.warn('new SafeBuffer() is depreciated, please use SafeBuffer.from()') - return SafeBuffer.from(...props) + console.warn( + "new SafeBuffer() is depreciated, please use SafeBuffer.from()" + ); + return SafeBuffer.from(...props); } - SafeBuffer.prototype = Object.create(Array.prototype) + SafeBuffer.prototype = Object.create(Array.prototype); Object.assign(SafeBuffer, { // (data, enc) where typeof data === 'string' then enc === 'utf8'|'hex'|'base64' from() { if (!Object.keys(arguments).length) { - throw new TypeError('First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.') + throw new TypeError( + "First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object." + ); } - const input = arguments[0] - let buf - if (typeof input === 'string') { - const enc = arguments[1] || 'utf8' - if (enc === 'hex') { - const bytes = input.match(/([\da-fA-F]{2})/g) - .map((byte) => parseInt(byte, 16)) + const input = arguments[0]; + let buf; + if (typeof input === "string") { + const enc = arguments[1] || "utf8"; + if (enc === "hex") { + const bytes = input + .match(/([\da-fA-F]{2})/g) + .map(byte => parseInt(byte, 16)); if (!bytes || !bytes.length) { - throw new TypeError('Invalid first argument for type \'hex\'.') + throw new TypeError("Invalid first argument for type 'hex'."); } - buf = SeaArray.from(bytes) - } else if (enc === 'utf8') { - const length = input.length - const words = new Uint16Array(length) - Array.from({ length: length }, (_, i) => words[i] = input.charCodeAt(i)) - buf = SeaArray.from(words) - } else if (enc === 'base64') { - const dec = atob(input) - const length = dec.length - const bytes = new Uint8Array(length) - Array.from({ length: length }, (_, i) => bytes[i] = dec.charCodeAt(i)) - buf = SeaArray.from(bytes) - } else if (enc === 'binary') { - buf = SeaArray.from(input) + buf = SeaArray.from(bytes); + } else if (enc === "utf8") { + const length = input.length; + const words = new Uint16Array(length); + Array.from( + { length: length }, + (_, i) => (words[i] = input.charCodeAt(i)) + ); + buf = SeaArray.from(words); + } else if (enc === "base64") { + const dec = atob(input); + const length = dec.length; + const bytes = new Uint8Array(length); + Array.from( + { length: length }, + (_, i) => (bytes[i] = dec.charCodeAt(i)) + ); + buf = SeaArray.from(bytes); + } else if (enc === "binary") { + buf = SeaArray.from(input); } else { - console.info('SafeBuffer.from unknown encoding: '+enc) + console.info("SafeBuffer.from unknown encoding: " + enc); } - return buf + return buf; } - const byteLength = input.byteLength // what is going on here? FOR MARTTI - const length = input.byteLength ? input.byteLength : input.length + const byteLength = input.byteLength; // what is going on here? FOR MARTTI + const length = input.byteLength ? input.byteLength : input.length; if (length) { - let buf + let buf; if (input instanceof ArrayBuffer) { - buf = new Uint8Array(input) + buf = new Uint8Array(input); } - return SeaArray.from(buf || input) + return SeaArray.from(buf || input); } }, // This is 'safe-buffer.alloc' sans encoding support - alloc(length, fill = 0 /*, enc*/ ) { - return SeaArray.from(new Uint8Array(Array.from({ length: length }, () => fill))) + alloc(length, fill = 0 /*, enc*/) { + return SeaArray.from( + new Uint8Array(Array.from({ length: length }, () => fill)) + ); }, // This is normal UNSAFE 'buffer.alloc' or 'new Buffer(length)' - don't use! allocUnsafe(length) { - return SeaArray.from(new Uint8Array(Array.from({ length : length }))) + return SeaArray.from(new Uint8Array(Array.from({ length: length }))); }, // This puts together array of array like members - concat(arr) { // octet array + concat(arr) { + // octet array if (!Array.isArray(arr)) { - throw new TypeError('First argument must be Array containing ArrayBuffer or Uint8Array instances.') + throw new TypeError( + "First argument must be Array containing ArrayBuffer or Uint8Array instances." + ); } - return SeaArray.from(arr.reduce((ret, item) => ret.concat(Array.from(item)), [])) + return SeaArray.from( + arr.reduce((ret, item) => ret.concat(Array.from(item)), []) + ); } - }) - SafeBuffer.prototype.from = SafeBuffer.from - SafeBuffer.prototype.toString = SeaArray.prototype.toString + }); + SafeBuffer.prototype.from = SafeBuffer.from; + SafeBuffer.prototype.toString = SeaArray.prototype.toString; module.exports = SafeBuffer; - })(USE, './buffer'); + })(USE, "./buffer"); - ;USE(function(module){ - const SEA = USE('./root') - const Buffer = USE('./buffer') - const api = {Buffer: Buffer} + USE(function(module) { + const SEA = USE("./root"); + const Buffer = USE("./buffer"); + const api = { Buffer: Buffer }; var o = {}; - if(SEA.window){ + if (SEA.window) { api.crypto = window.crypto || window.msCrypto; - api.subtle = (api.crypto||o).subtle || (api.crypto||o).webkitSubtle; + api.subtle = (api.crypto || o).subtle || (api.crypto || o).webkitSubtle; api.TextEncoder = window.TextEncoder; api.TextDecoder = window.TextDecoder; - api.random = (len) => Buffer.from(api.crypto.getRandomValues(new Uint8Array(Buffer.alloc(len)))) + api.random = len => + Buffer.from( + api.crypto.getRandomValues(new Uint8Array(Buffer.alloc(len))) + ); } - if(!api.crypto){try{ - var crypto = USE('crypto', 1); - const { TextEncoder, TextDecoder } = USE('text-encoding', 1) - Object.assign(api, { - crypto, - //subtle, - TextEncoder, - TextDecoder, - random: (len) => Buffer.from(crypto.randomBytes(len)) - }); - //try{ - const WebCrypto = USE('node-webcrypto-ossl', 1); - api.ossl = api.subtle = new WebCrypto({directory: 'ossl'}).subtle // ECDH - //}catch(e){ + if (!api.crypto) { + try { + var crypto = USE("crypto", 1); + const { TextEncoder, TextDecoder } = USE("text-encoding", 1); + Object.assign(api, { + crypto, + //subtle, + TextEncoder, + TextDecoder, + random: len => Buffer.from(crypto.randomBytes(len)) + }); + //try{ + const WebCrypto = USE("node-webcrypto-ossl", 1); + api.ossl = api.subtle = new WebCrypto({ directory: "ossl" }).subtle; // ECDH + //}catch(e){ //console.log("node-webcrypto-ossl is optionally needed for ECDH, please install if needed."); - //} - }catch(e){ - console.log("node-webcrypto-ossl and text-encoding may not be included by default, please add it to your package.json!"); - OSSL_WEBCRYPTO_OR_TEXT_ENCODING_NOT_INSTALLED; - }} + //} + } catch (e) { + console.log( + "node-webcrypto-ossl and text-encoding may not be included by default, please add it to your package.json!" + ); + OSSL_WEBCRYPTO_OR_TEXT_ENCODING_NOT_INSTALLED; + } + } - module.exports = api - })(USE, './shim'); + module.exports = api; + })(USE, "./shim"); - ;USE(function(module){ - var SEA = USE('./root'); - var Buffer = USE('./buffer'); + USE(function(module) { + var SEA = USE("./root"); + var Buffer = USE("./buffer"); var s = {}; - s.pbkdf2 = {hash: 'SHA-256', iter: 100000, ks: 64}; + s.pbkdf2 = { hash: "SHA-256", iter: 100000, ks: 64 }; s.ecdsa = { - pair: {name: 'ECDSA', namedCurve: 'P-256'}, - sign: {name: 'ECDSA', hash: {name: 'SHA-256'}} + pair: { name: "ECDSA", namedCurve: "P-256" }, + sign: { name: "ECDSA", hash: { name: "SHA-256" } } }; - s.ecdh = {name: 'ECDH', namedCurve: 'P-256'}; + s.ecdh = { name: "ECDH", namedCurve: "P-256" }; // This creates Web Cryptography API compliant JWK for sign/verify purposes - s.jwk = function(pub, d){ // d === priv - pub = pub.split('.'); - var x = pub[0], y = pub[1]; - var jwk = {kty: "EC", crv: "P-256", x: x, y: y, ext: true}; - jwk.key_ops = d ? ['sign'] : ['verify']; - if(d){ jwk.d = d } + s.jwk = function(pub, d) { + // d === priv + pub = pub.split("."); + var x = pub[0], + y = pub[1]; + var jwk = { kty: "EC", crv: "P-256", x: x, y: y, ext: true }; + jwk.key_ops = d ? ["sign"] : ["verify"]; + if (d) { + jwk.d = d; + } return jwk; }; s.recall = { validity: 12 * 60 * 60, // internally in seconds : 12 hours - hook: function(props){ return props } // { iat, exp, alias, remember } // or return new Promise((resolve, reject) => resolve(props) + hook: function(props) { + return props; + } // { iat, exp, alias, remember } // or return new Promise((resolve, reject) => resolve(props) }; - s.check = function(t){ return (typeof t == 'string') && ('SEA{' === t.slice(0,4)) } - s.parse = 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; + s.check = function(t) { + return typeof t == "string" && "SEA{" === t.slice(0, 4); + }; + s.parse = 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; } catch (e) {} return t; - } + }; SEA.opt = s; - module.exports = s - })(USE, './settings'); + module.exports = s; + })(USE, "./settings"); - ;USE(function(module){ - var shim = USE('./shim'); - module.exports = async function(d, o){ - var t = (typeof d == 'string')? d : JSON.stringify(d); - var hash = await shim.subtle.digest({name: o||'SHA-256'}, new shim.TextEncoder().encode(t)); + USE(function(module) { + var shim = USE("./shim"); + module.exports = async function(d, o) { + var t = typeof d == "string" ? d : JSON.stringify(d); + var hash = await shim.subtle.digest( + { name: o || "SHA-256" }, + new shim.TextEncoder().encode(t) + ); return shim.Buffer.from(hash); - } - })(USE, './sha256'); + }; + })(USE, "./sha256"); - ;USE(function(module){ + USE(function(module) { // This internal func returns SHA-1 hashed data for KeyID generation - const __shim = USE('./shim') - const subtle = __shim.subtle - const ossl = __shim.ossl ? __shim.ossl : subtle - const sha1hash = (b) => ossl.digest({name: 'SHA-1'}, new ArrayBuffer(b)) - module.exports = sha1hash - })(USE, './sha1'); + const __shim = USE("./shim"); + const subtle = __shim.subtle; + const ossl = __shim.ossl ? __shim.ossl : subtle; + const sha1hash = b => ossl.digest({ name: "SHA-1" }, new ArrayBuffer(b)); + module.exports = sha1hash; + })(USE, "./sha1"); - ;USE(function(module){ - var SEA = USE('./root'); - var shim = USE('./shim'); - var S = USE('./settings'); - var sha = USE('./sha256'); + USE(function(module) { + var SEA = USE("./root"); + var shim = USE("./shim"); + var S = USE("./settings"); + var sha = USE("./sha256"); var u; - 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 || {}; - if(salt instanceof Function){ - cb = salt; - salt = u; - } - salt = salt || shim.random(9); - data = (typeof data == 'string')? data : JSON.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)} } - return rsha; - } - var key = await (shim.ossl || shim.subtle).importKey('raw', new shim.TextEncoder().encode(data), {name: opt.name || 'PBKDF2'}, false, ['deriveBits']); - var work = await (shim.ossl || shim.subtle).deriveBits({ - name: opt.name || 'PBKDF2', - iterations: opt.iterations || S.pbkdf2.iter, - salt: new shim.TextEncoder().encode(opt.salt || salt), - hash: opt.hash || S.pbkdf2.hash, - }, key, opt.length || (S.pbkdf2.ks * 8)) - data = shim.random(data.length) // Erase data in case of passphrase - var r = shim.Buffer.from(work, 'binary').toString(opt.encode || 'base64') - if(cb){ try{ cb(r) }catch(e){console.log(e)} } - return r; - } catch(e) { - console.log(e); - SEA.err = e; - if(SEA.throw){ throw e } - if(cb){ cb() } - return; - }}); + 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 || {}; + if (salt instanceof Function) { + cb = salt; + salt = u; + } + salt = salt || shim.random(9); + data = typeof data == "string" ? data : JSON.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); + } + } + return rsha; + } + var key = await (shim.ossl || shim.subtle).importKey( + "raw", + new shim.TextEncoder().encode(data), + { name: opt.name || "PBKDF2" }, + false, + ["deriveBits"] + ); + var work = await (shim.ossl || shim.subtle).deriveBits( + { + name: opt.name || "PBKDF2", + iterations: opt.iterations || S.pbkdf2.iter, + salt: new shim.TextEncoder().encode(opt.salt || salt), + hash: opt.hash || S.pbkdf2.hash + }, + key, + opt.length || S.pbkdf2.ks * 8 + ); + data = shim.random(data.length); // Erase data in case of passphrase + var r = shim.Buffer.from(work, "binary").toString( + opt.encode || "base64" + ); + if (cb) { + try { + cb(r); + } catch (e) { + console.log(e); + } + } + return r; + } catch (e) { + console.log(e); + SEA.err = e; + if (SEA.throw) { + throw e; + } + if (cb) { + cb(); + } + return; + } + }); module.exports = SEA.work; - })(USE, './work'); + })(USE, "./work"); - ;USE(function(module){ - var SEA = USE('./root'); - var shim = USE('./shim'); - var S = USE('./settings'); + USE(function(module) { + var SEA = USE("./root"); + var shim = USE("./shim"); + var S = USE("./settings"); - SEA.name = SEA.name || (async (cb, opt) => { try { - if(cb){ try{ cb() }catch(e){console.log(e)} } - return; - } catch(e) { - console.log(e); - SEA.err = e; - if(SEA.throw){ throw e } - if(cb){ cb() } - return; - }}); + SEA.name = + SEA.name || + (async (cb, opt) => { + try { + if (cb) { + try { + cb(); + } catch (e) { + console.log(e); + } + } + return; + } catch (e) { + console.log(e); + SEA.err = e; + if (SEA.throw) { + throw e; + } + if (cb) { + cb(); + } + return; + } + }); //SEA.pair = async (data, proof, cb) => { try { - SEA.pair = SEA.pair || (async (cb, opt) => { try { + SEA.pair = + SEA.pair || + (async (cb, opt) => { + try { + var ecdhSubtle = shim.ossl || shim.subtle; + // First: ECDSA keys for signing/verifying... + var sa = await shim.subtle + .generateKey(S.ecdsa.pair, true, ["sign", "verify"]) + .then(async keys => { + // privateKey scope doesn't leak out from here! + //const { d: priv } = await shim.subtle.exportKey('jwk', keys.privateKey) + var key = {}; + key.priv = (await shim.subtle.exportKey( + "jwk", + keys.privateKey + )).d; + var pub = await shim.subtle.exportKey("jwk", keys.publicKey); + //const pub = Buff.from([ x, y ].join(':')).toString('base64') // old + key.pub = pub.x + "." + pub.y; // new + // x and y are already base64 + // pub is UTF8 but filename/URL safe (https://www.ietf.org/rfc/rfc3986.txt) + // but split on a non-base64 letter. + return key; + }); - var ecdhSubtle = shim.ossl || shim.subtle; - // First: ECDSA keys for signing/verifying... - var sa = await shim.subtle.generateKey(S.ecdsa.pair, true, [ 'sign', 'verify' ]) - .then(async (keys) => { - // privateKey scope doesn't leak out from here! - //const { d: priv } = await shim.subtle.exportKey('jwk', keys.privateKey) - var key = {}; - key.priv = (await shim.subtle.exportKey('jwk', keys.privateKey)).d; - var pub = await shim.subtle.exportKey('jwk', keys.publicKey); - //const pub = Buff.from([ x, y ].join(':')).toString('base64') // old - key.pub = pub.x+'.'+pub.y; // new - // x and y are already base64 - // pub is UTF8 but filename/URL safe (https://www.ietf.org/rfc/rfc3986.txt) - // but split on a non-base64 letter. - return key; - }) - - // To include PGPv4 kind of keyId: - // const pubId = await SEA.keyid(keys.pub) - // Next: ECDH keys for encryption/decryption... + // To include PGPv4 kind of keyId: + // const pubId = await SEA.keyid(keys.pub) + // Next: ECDH keys for encryption/decryption... - try{ - var dh = await ecdhSubtle.generateKey(S.ecdh, true, ['deriveKey']) - .then(async (keys) => { - // privateKey scope doesn't leak out from here! - var key = {}; - key.epriv = (await ecdhSubtle.exportKey('jwk', keys.privateKey)).d; - var pub = await ecdhSubtle.exportKey('jwk', keys.publicKey); - //const epub = Buff.from([ ex, ey ].join(':')).toString('base64') // old - key.epub = pub.x+'.'+pub.y; // new - // ex and ey are already base64 - // epub is UTF8 but filename/URL safe (https://www.ietf.org/rfc/rfc3986.txt) - // but split on a non-base64 letter. - return key; - }) - }catch(e){ - if(SEA.window){ throw e } - if(e == 'Error: ECDH is not a supported algorithm'){ console.log('Ignoring ECDH...') } - else { throw e } - } dh = dh || {}; + try { + var dh = await ecdhSubtle + .generateKey(S.ecdh, true, ["deriveKey"]) + .then(async keys => { + // privateKey scope doesn't leak out from here! + var key = {}; + key.epriv = (await ecdhSubtle.exportKey( + "jwk", + keys.privateKey + )).d; + var pub = await ecdhSubtle.exportKey("jwk", keys.publicKey); + //const epub = Buff.from([ ex, ey ].join(':')).toString('base64') // old + key.epub = pub.x + "." + pub.y; // new + // ex and ey are already base64 + // epub is UTF8 but filename/URL safe (https://www.ietf.org/rfc/rfc3986.txt) + // but split on a non-base64 letter. + return key; + }); + } catch (e) { + if (SEA.window) { + throw e; + } + if (e == "Error: ECDH is not a supported algorithm") { + console.log("Ignoring ECDH..."); + } else { + throw e; + } + } + dh = dh || {}; - var r = { pub: sa.pub, priv: sa.priv, /* pubId, */ epub: dh.epub, epriv: dh.epriv } - if(cb){ try{ cb(r) }catch(e){console.log(e)} } - return r; - } catch(e) { - console.log(e); - SEA.err = e; - if(SEA.throw){ throw e } - if(cb){ cb() } - return; - }}); + var r = { + pub: sa.pub, + priv: sa.priv, + /* pubId, */ epub: dh.epub, + epriv: dh.epriv + }; + if (cb) { + try { + cb(r); + } catch (e) { + console.log(e); + } + } + return r; + } catch (e) { + console.log(e); + SEA.err = e; + if (SEA.throw) { + throw e; + } + if (cb) { + cb(); + } + return; + } + }); module.exports = SEA.pair; - })(USE, './pair'); + })(USE, "./pair"); - ;USE(function(module){ - var SEA = USE('./root'); - var shim = USE('./shim'); - var S = USE('./settings'); - var sha = USE('./sha256'); + USE(function(module) { + var SEA = USE("./root"); + var shim = USE("./shim"); + var S = USE("./settings"); + var sha = USE("./sha256"); var u; - SEA.sign = SEA.sign || (async (data, pair, cb, opt) => { try { - opt = opt || {}; - if(!(pair||opt).priv){ - 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 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) } - if(cb){ try{ cb(r) }catch(e){console.log(e)} } - return r; - } - var pub = pair.pub; - var priv = pair.priv; - var jwk = S.jwk(pub, priv); - var hash = await sha(json); - var 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! - var r = {m: json, s: shim.Buffer.from(sig, 'binary').toString(opt.encode || 'base64')} - if(!opt.raw){ r = 'SEA'+JSON.stringify(r) } + SEA.sign = + SEA.sign || + (async (data, pair, cb, opt) => { + try { + opt = opt || {}; + if (!(pair || opt).priv) { + 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 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); + } + if (cb) { + try { + cb(r); + } catch (e) { + console.log(e); + } + } + return r; + } + var pub = pair.pub; + var priv = pair.priv; + var jwk = S.jwk(pub, priv); + var hash = await sha(json); + var 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! + var r = { + m: json, + s: shim.Buffer.from(sig, "binary").toString(opt.encode || "base64") + }; + if (!opt.raw) { + r = "SEA" + JSON.stringify(r); + } - if(cb){ try{ cb(r) }catch(e){console.log(e)} } - return r; - } catch(e) { - console.log(e); - SEA.err = e; - if(SEA.throw){ throw e } - if(cb){ cb() } - return; - }}); + if (cb) { + try { + cb(r); + } catch (e) { + console.log(e); + } + } + return r; + } catch (e) { + console.log(e); + SEA.err = e; + if (SEA.throw) { + throw e; + } + if (cb) { + cb(); + } + return; + } + }); module.exports = SEA.sign; - })(USE, './sign'); + })(USE, "./sign"); - ;USE(function(module){ - var SEA = USE('./root'); - var shim = USE('./shim'); - var S = USE('./settings'); - var sha = USE('./sha256'); + USE(function(module) { + var SEA = USE("./root"); + var shim = USE("./shim"); + var S = USE("./settings"); + var sha = USE("./sha256"); var u; - SEA.verify = SEA.verify || (async (data, pair, cb, opt) => { try { - var json = S.parse(data); - if(false === pair){ // don't verify! - var raw = S.parse(json.m); - if(cb){ try{ cb(raw) }catch(e){console.log(e)} } - return raw; - } - opt = opt || {}; - // SEA.I // verify is free! Requires no user permission. - var pub = pair.pub || pair; - var key = SEA.opt.slow_leak? await SEA.opt.slow_leak(pub) : await (shim.ossl || shim.subtle).importKey('jwk', jwk, S.ecdsa.pair, false, ['verify']); - var hash = await sha(json.m); - var buf, sig, check, tmp; try{ - buf = shim.Buffer.from(json.s, opt.encode || 'base64'); // NEW DEFAULT! - sig = new Uint8Array(buf); - check = await (shim.ossl || shim.subtle).verify(S.ecdsa.sign, key, sig, new Uint8Array(hash)); - if(!check){ throw "Signature did not match." } - }catch(e){ - if(SEA.opt.fallback){ - return await SEA.opt.fall_verify(data, pair, cb, opt); - } - } - var r = check? S.parse(json.m) : u; + SEA.verify = + SEA.verify || + (async (data, pair, cb, opt) => { + try { + var json = S.parse(data); + if (false === pair) { + // don't verify! + var raw = S.parse(json.m); + if (cb) { + try { + cb(raw); + } catch (e) { + console.log(e); + } + } + return raw; + } + opt = opt || {}; + // SEA.I // verify is free! Requires no user permission. + var pub = pair.pub || pair; + var key = SEA.opt.slow_leak + ? await SEA.opt.slow_leak(pub) + : await (shim.ossl || shim.subtle).importKey( + "jwk", + jwk, + S.ecdsa.pair, + false, + ["verify"] + ); + var hash = await sha(json.m); + var buf, sig, check, tmp; + try { + buf = shim.Buffer.from(json.s, opt.encode || "base64"); // NEW DEFAULT! + sig = new Uint8Array(buf); + check = await (shim.ossl || shim.subtle).verify( + S.ecdsa.sign, + key, + sig, + new Uint8Array(hash) + ); + if (!check) { + throw "Signature did not match."; + } + } catch (e) { + if (SEA.opt.fallback) { + return await SEA.opt.fall_verify(data, pair, cb, opt); + } + } + var r = check ? S.parse(json.m) : u; - if(cb){ try{ cb(r) }catch(e){console.log(e)} } - return r; - } catch(e) { - console.log(e); // mismatched owner FOR MARTTI - SEA.err = e; - if(SEA.throw){ throw e } - if(cb){ cb() } - return; - }}); + if (cb) { + try { + cb(r); + } catch (e) { + console.log(e); + } + } + return r; + } catch (e) { + console.log(e); // mismatched owner FOR MARTTI + SEA.err = e; + if (SEA.throw) { + throw e; + } + if (cb) { + cb(); + } + return; + } + }); module.exports = SEA.verify; // legacy & ossl leak mitigation: var knownKeys = {}; - var keyForPair = SEA.opt.slow_leak = pair => { + var keyForPair = (SEA.opt.slow_leak = pair => { if (knownKeys[pair]) return knownKeys[pair]; var jwk = S.jwk(pair); - knownKeys[pair] = (shim.ossl || shim.subtle).importKey("jwk", jwk, S.ecdsa.pair, false, ["verify"]); + knownKeys[pair] = (shim.ossl || shim.subtle).importKey( + "jwk", + jwk, + S.ecdsa.pair, + false, + ["verify"] + ); return knownKeys[pair]; - }; + }); - - SEA.opt.fall_verify = async function(data, pair, cb, opt, f){ - if(f === SEA.opt.fallback){ throw "Signature did not match" } f = f || 1; - 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 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(S.ecdsa.sign, key, sig, new Uint8Array(hash)) - if(!check){ throw "Signature did not match." } - }catch(e){ - buf = shim.Buffer.from(json.s, 'utf8') // AUTO BACKWARD OLD UTF8 DATA! - sig = new Uint8Array(buf) - check = await (shim.ossl || shim.subtle).verify(S.ecdsa.sign, key, sig, new Uint8Array(hash)) - if(!check){ throw "Signature did not match." } + SEA.opt.fall_verify = async function(data, pair, cb, opt, f) { + if (f === SEA.opt.fallback) { + throw "Signature did not match"; + } + f = f || 1; + 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 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( + S.ecdsa.sign, + key, + sig, + new Uint8Array(hash) + ); + if (!check) { + throw "Signature did not match."; + } + } catch (e) { + buf = shim.Buffer.from(json.s, "utf8"); // AUTO BACKWARD OLD UTF8 DATA! + sig = new Uint8Array(buf); + check = await (shim.ossl || shim.subtle).verify( + S.ecdsa.sign, + key, + sig, + new Uint8Array(hash) + ); + if (!check) { + throw "Signature did not match."; + } + } + var r = check ? S.parse(json.m) : u; + if (cb) { + try { + cb(r); + } catch (e) { + console.log(e); + } } - var r = check? S.parse(json.m) : u; - if(cb){ try{ cb(r) }catch(e){console.log(e)} } return r; - } + }; SEA.opt.fallback = 2; + })(USE, "./verify"); - })(USE, './verify'); - - ;USE(function(module){ - var shim = USE('./shim'); - var sha256hash = USE('./sha256'); + USE(function(module) { + var shim = USE("./shim"); + var sha256hash = USE("./sha256"); 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 || {}; - const combo = key + (salt || shim.random(8)).toString('utf8'); // new - const hash = shim.Buffer.from(await sha256hash(combo), 'binary') - return await shim.subtle.importKey('raw', new Uint8Array(hash), opt.name || 'AES-GCM', false, ['encrypt', 'decrypt']) - } + const combo = key + (salt || shim.random(8)).toString("utf8"); // new + const hash = shim.Buffer.from(await sha256hash(combo), "binary"); + return await shim.subtle.importKey( + "raw", + new Uint8Array(hash), + opt.name || "AES-GCM", + false, + ["encrypt", "decrypt"] + ); + }; module.exports = importGen; - })(USE, './aeskey'); + })(USE, "./aeskey"); - ;USE(function(module){ - var SEA = USE('./root'); - var shim = USE('./shim'); - var S = USE('./settings'); - var aeskey = USE('./aeskey'); + USE(function(module) { + var SEA = USE("./root"); + var shim = USE("./shim"); + var S = USE("./settings"); + var aeskey = USE("./aeskey"); var u; - SEA.encrypt = SEA.encrypt || (async (data, pair, cb, opt) => { try { - opt = opt || {}; - var key = (pair||opt).epriv || pair; - if(u === data){ throw '`undefined` not allowed.' } - if(!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 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) - }, aes, new shim.TextEncoder().encode(msg))); - var r = { - 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(!opt.raw){ r = 'SEA'+JSON.stringify(r) } + SEA.encrypt = + SEA.encrypt || + (async (data, pair, cb, opt) => { + try { + opt = opt || {}; + var key = (pair || opt).epriv || pair; + if (u === data) { + throw "`undefined` not allowed."; + } + if (!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 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 /*shim.ossl ||*/.subtle + .encrypt( + { + // Keeping the AES key scope as private as possible... + name: opt.name || "AES-GCM", + iv: new Uint8Array(rand.iv) + }, + aes, + new shim.TextEncoder().encode(msg) + ) + ); + var r = { + 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 (!opt.raw) { + r = "SEA" + JSON.stringify(r); + } - if(cb){ try{ cb(r) }catch(e){console.log(e)} } - return r; - } catch(e) { - console.log(e); - SEA.err = e; - if(SEA.throw){ throw e } - if(cb){ cb() } - return; - }}); + if (cb) { + try { + cb(r); + } catch (e) { + console.log(e); + } + } + return r; + } catch (e) { + console.log(e); + SEA.err = e; + if (SEA.throw) { + throw e; + } + if (cb) { + cb(); + } + return; + } + }); module.exports = SEA.encrypt; - })(USE, './encrypt'); + })(USE, "./encrypt"); - ;USE(function(module){ - var SEA = USE('./root'); - var shim = USE('./shim'); - var S = USE('./settings'); - var aeskey = USE('./aeskey'); + USE(function(module) { + var SEA = USE("./root"); + var shim = USE("./shim"); + var S = USE("./settings"); + var aeskey = USE("./aeskey"); - SEA.decrypt = SEA.decrypt || (async (data, pair, cb, opt) => { try { - opt = opt || {}; - var key = (pair||opt).epriv || pair; - if(!key){ - pair = await SEA.I(null, {what: data, how: 'decrypt', why: opt.why}); - key = pair.epriv || pair; - } - var json = 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'); - bufct = shim.Buffer.from(json.ct, opt.encode || 'base64'); - var 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(bufiv) - }, aes, new Uint8Array(bufct))); - }catch(e){ - if('utf8' === opt.encode){ throw "Could not decrypt" } - if(SEA.opt.fallback){ - opt.encode = 'utf8'; - return await SEA.decrypt(data, pair, cb, opt); + SEA.decrypt = + SEA.decrypt || + (async (data, pair, cb, opt) => { + try { + opt = opt || {}; + var key = (pair || opt).epriv || pair; + if (!key) { + pair = await SEA.I(null, { + what: data, + how: "decrypt", + why: opt.why + }); + key = pair.epriv || pair; + } + var json = 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"); + bufct = shim.Buffer.from(json.ct, opt.encode || "base64"); + var ct = await aeskey(key, buf, opt).then(aes => + shim /*shim.ossl ||*/.subtle + .decrypt( + { + // Keeping aesKey scope as private as possible... + name: opt.name || "AES-GCM", + iv: new Uint8Array(bufiv) + }, + aes, + new Uint8Array(bufct) + ) + ); + } catch (e) { + if ("utf8" === opt.encode) { + throw "Could not decrypt"; + } + if (SEA.opt.fallback) { + opt.encode = "utf8"; + return await SEA.decrypt(data, pair, cb, opt); + } + } + var r = S.parse(new shim.TextDecoder("utf8").decode(ct)); + if (cb) { + try { + cb(r); + } catch (e) { + console.log(e); + } + } + return r; + } catch (e) { + console.log(e); + SEA.err = e; + if (SEA.throw) { + throw e; + } + if (cb) { + cb(); + } + return; } - } - var r = S.parse(new shim.TextDecoder('utf8').decode(ct)); - if(cb){ try{ cb(r) }catch(e){console.log(e)} } - return r; - } catch(e) { - console.log(e); - SEA.err = e; - if(SEA.throw){ throw e } - if(cb){ cb() } - return; - }}); + }); module.exports = SEA.decrypt; - })(USE, './decrypt'); + })(USE, "./decrypt"); - ;USE(function(module){ - var SEA = USE('./root'); - var shim = USE('./shim'); - var S = USE('./settings'); - // Derive shared secret from other's pub and my epub/epriv - SEA.secret = SEA.secret || (async (key, pair, cb, opt) => { try { - opt = opt || {}; - if(!pair || !pair.epriv || !pair.epub){ - pair = await SEA.I(null, {what: key, how: 'secret', why: opt.why}); - } - var pub = key.epub || key; - var epub = pair.epub; - var epriv = pair.epriv; - var ecdhSubtle = shim.ossl || shim.subtle; - var pubKeyData = keysToEcdhJwk(pub); - var props = Object.assign(S.ecdh, { public: await ecdhSubtle.importKey(...pubKeyData, true, []) }); - var privKeyData = keysToEcdhJwk(epub, epriv); - var derived = await ecdhSubtle.importKey(...privKeyData, false, ['deriveKey']).then(async (privKey) => { - // privateKey scope doesn't leak out from here! - var derivedKey = await ecdhSubtle.deriveKey(props, privKey, { name: 'AES-GCM', length: 256 }, true, [ 'encrypt', 'decrypt' ]); - return ecdhSubtle.exportKey('jwk', derivedKey).then(({ k }) => k); - }) - var r = derived; - if(cb){ try{ cb(r) }catch(e){console.log(e)} } - return r; - } catch(e) { - console.log(e); - SEA.err = e; - if(SEA.throw){ throw e } - if(cb){ cb() } - return; - }}); + USE(function(module) { + var SEA = USE("./root"); + var shim = USE("./shim"); + var S = USE("./settings"); + // Derive shared secret from other's pub and my epub/epriv + SEA.secret = + SEA.secret || + (async (key, pair, cb, opt) => { + try { + opt = opt || {}; + if (!pair || !pair.epriv || !pair.epub) { + pair = await SEA.I(null, { + what: key, + how: "secret", + why: opt.why + }); + } + var pub = key.epub || key; + var epub = pair.epub; + var epriv = pair.epriv; + var ecdhSubtle = shim.ossl || shim.subtle; + var pubKeyData = keysToEcdhJwk(pub); + var props = Object.assign( + { public: await ecdhSubtle.importKey(...pubKeyData, true, []) }, + S.ecdh + ); + var privKeyData = keysToEcdhJwk(epub, epriv); + var derived = await ecdhSubtle + .importKey(...privKeyData, false, ["deriveKey"]) + .then(async privKey => { + // privateKey scope doesn't leak out from here! + var derivedKey = await ecdhSubtle.deriveKey( + props, + privKey, + { name: "AES-GCM", length: 256 }, + true, + ["encrypt", "decrypt"] + ); + return ecdhSubtle.exportKey("jwk", derivedKey).then(({ k }) => k); + }); + var r = derived; + if (cb) { + try { + cb(r); + } catch (e) { + console.log(e); + } + } + return r; + } catch (e) { + console.log(e); + SEA.err = e; + if (SEA.throw) { + throw e; + } + if (cb) { + cb(); + } + return; + } + }); // can this be replaced with settings.jwk? - var keysToEcdhJwk = (pub, d) => { // d === priv + var keysToEcdhJwk = (pub, d) => { + // d === priv //var [ x, y ] = Buffer.from(pub, 'base64').toString('utf8').split(':') // old - var [ x, y ] = pub.split('.') // new - var jwk = d ? { d: d } : {} - return [ // Use with spread returned value... - 'jwk', - Object.assign( - jwk, - { x: x, y: y, kty: 'EC', crv: 'P-256', ext: true } - ), // ??? refactor + var [x, y] = pub.split("."); // new + var jwk = d ? { d: d } : {}; + return [ + // Use with spread returned value... + "jwk", + Object.assign(jwk, { x: x, y: y, kty: "EC", crv: "P-256", ext: true }), // ??? refactor S.ecdh - ] - } + ]; + }; module.exports = SEA.secret; - })(USE, './secret'); + })(USE, "./secret"); - ;USE(function(module){ - var shim = USE('./shim'); + USE(function(module) { + var shim = USE("./shim"); // Practical examples about usage found from ./test/common.js - var SEA = USE('./root'); - SEA.work = USE('./work'); - SEA.sign = USE('./sign'); - SEA.verify = USE('./verify'); - SEA.encrypt = USE('./encrypt'); - SEA.decrypt = USE('./decrypt'); + var SEA = USE("./root"); + SEA.work = USE("./work"); + SEA.sign = USE("./sign"); + SEA.verify = USE("./verify"); + SEA.encrypt = USE("./encrypt"); + SEA.decrypt = USE("./decrypt"); SEA.random = SEA.random || shim.random; // This is Buffer used in SEA and usable from Gun/SEA application also. // For documentation see https://nodejs.org/api/buffer.html - SEA.Buffer = SEA.Buffer || USE('./buffer'); + SEA.Buffer = SEA.Buffer || USE("./buffer"); // These SEA functions support now ony Promises or // async/await (compatible) code, use those like Promises. @@ -649,25 +1009,31 @@ // Creates a wrapper library around Web Crypto API // for various AES, ECDSA, PBKDF2 functions we called above. // Calculate public key KeyID aka PGPv4 (result: 8 bytes as hex string) - SEA.keyid = SEA.keyid || (async (pub) => { - try { - // base64('base64(x):base64(y)') => Buffer(xy) - const pb = Buffer.concat( - pub.replace(/-/g, '+').replace(/_/g, '/').split('.') - .map((t) => Buffer.from(t, 'base64')) - ) - // id is PGPv4 compliant raw key - const id = Buffer.concat([ - Buffer.from([0x99, pb.length / 0x100, pb.length % 0x100]), pb - ]) - const sha1 = await sha1hash(id) - const hash = Buffer.from(sha1, 'binary') - return hash.toString('hex', hash.length - 8) // 16-bit ID as hex - } catch (e) { - console.log(e) - throw e - } - }); + SEA.keyid = + SEA.keyid || + (async pub => { + try { + // base64('base64(x):base64(y)') => Buffer(xy) + const pb = Buffer.concat( + pub + .replace(/-/g, "+") + .replace(/_/g, "/") + .split(".") + .map(t => Buffer.from(t, "base64")) + ); + // id is PGPv4 compliant raw key + const id = Buffer.concat([ + Buffer.from([0x99, pb.length / 0x100, pb.length % 0x100]), + pb + ]); + const sha1 = await sha1hash(id); + const hash = Buffer.from(sha1, "binary"); + return hash.toString("hex", hash.length - 8); // 16-bit ID as hex + } catch (e) { + console.log(e); + throw e; + } + }); // all done! // Obviously it is missing MANY necessary features. This is only an alpha release. // Please experiment with it, audit what I've done so far, and complain about what needs to be added. @@ -677,82 +1043,108 @@ // 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 || USE((typeof common == "undefined"?'.':'')+'./gun', 1); + var Gun = + (SEA.window || {}).Gun || + USE((typeof common == "undefined" ? "." : "") + "./gun", 1); Gun.SEA = SEA; SEA.GUN = SEA.Gun = Gun; - module.exports = SEA - })(USE, './sea'); + module.exports = SEA; + })(USE, "./sea"); - ;USE(function(module){ - 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 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'); + USE(function(module) { + var SEA = USE("./sea"); var Gun = SEA.Gun; - var then = USE('./then'); + var then = USE("./then"); - function User(root){ - this._ = {$: this}; + function User(root) { + this._ = { $: this }; } - User.prototype = (function(){ function F(){}; F.prototype = Gun.chain; return new F() }()) // Object.create polyfill + 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), user; - if(pub){ return root.get('~'+pub) } - if(user = root.back('user')){ return user } - var root = (root._), at = root, uuid = at.opt.uuid || Gun.state.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 + '.'; - if(cb && cb.call){ cb(null, id) } - return id; + Gun.chain.user = function(pub) { + var gun = this, + root = gun.back(-1), + user; + if (pub) { + return root.get("~" + pub); } + if ((user = root.back("user"))) { + return user; + } + var root = root._, + at = root, + uuid = at.opt.uuid || Gun.state.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 + "."; + if (cb && cb.call) { + cb(null, id); + } + return id; + }; return user; - } + }; Gun.User = User; module.exports = User; - })(USE, './user'); + })(USE, "./user"); - ;USE(function(module){ + USE(function(module) { // TODO: This needs to be split into all separate functions. // Not just everything thrown into 'create'. - var SEA = USE('./sea'); - var User = USE('./user'); - var authsettings = USE('./settings'); + var SEA = USE("./sea"); + var User = USE("./user"); + var authsettings = USE("./settings"); var Gun = SEA.Gun; - var noop = function(){}; + var 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){ - var gun = this, cat = (gun._), root = gun.back(-1); + User.prototype.create = function(alias, pass, cb, opt) { + var gun = this, + cat = gun._, + root = gun.back(-1); cb = cb || noop; - if(cat.ing){ - cb({err: Gun.log("User is already being created or authenticated!"), wait: true}); + if (cat.ing) { + cb({ + 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){ + var act = {}, + u; + act.a = function(pubs) { act.pubs = pubs; - if(pubs && !opt.already){ + if (pubs && !opt.already) { // 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!')}; + var ack = { err: Gun.log("User already created!") }; cat.ing = false; cb(ack); gun.leave(); @@ -760,230 +1152,303 @@ } act.salt = Gun.text.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.b = function(proof) { 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){ var tmp; + }; + act.c = function(pair) { + var tmp; act.pair = pair || {}; - if(tmp = cat.root.user){ + if ((tmp = cat.root.user)) { tmp._.sea = pair; - tmp.is = {pub: pair.pub, epub: pair.epub, alias: alias}; + 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}; + act.data = { pub: pair.pub }; act.d(); - } - act.d = function(){ + }; + act.d = function() { act.data.alias = alias; act.e(); - } - act.e = function(){ - act.data.epub = act.pair.epub; - SEA.encrypt({priv: act.pair.priv, epriv: act.pair.epriv}, act.proof, act.f, {raw:1}); // to keep the private key safe, we AES encrypt it with the proof of work! - } - act.f = function(auth){ - act.data.auth = JSON.stringify({ek: auth, s: act.salt}); + }; + act.e = function() { + act.data.epub = act.pair.epub; + SEA.encrypt( + { priv: act.pair.priv, epriv: act.pair.epriv }, + act.proof, + act.f, + { raw: 1 } + ); // to keep the private key safe, we AES encrypt it with the proof of work! + }; + act.f = function(auth) { + act.data.auth = JSON.stringify({ ek: auth, s: act.salt }); act.g(act.data.auth); - } - act.g = function(auth){ var tmp; + }; + 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? - 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); - } - root.get('~@'+alias).once(act.a); + 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? + 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); + }; + 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}); + 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 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) }) + Gun.node.is(data, function(v) { + tmp.push(v); + }); return act.b(tmp); } - if(act.name){ return act.f(data) } + 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.') + }; + 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 + }; + 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){ + }; + 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'}; + }; + 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 + } + 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() } + }; + 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.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 user = root._.user, + at = user._; var tmp = at.tag; var upt = at.opt; - at = user._ = root.get('~'+pair.pub)._; + 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}; + 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){ + 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 { + 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. + 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){ + } catch (e) { Gun.log("Your 'auth' callback crashed with:", e); } - } - act.z = function(){ + }; + 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'); + }; + 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, '_'); + Gun.obj.del(tmp, "_"); tmp.auth = auth; - root.get('~'+act.pair.pub).put(tmp); + 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!')}; + 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() } + }; + act.plugin = function(name) { + if (!(act.name = name)) { + return act.err(); + } var tmp = [name]; - if('~' !== name[0]){ - tmp[1] = '~'+name; - tmp[2] = '~@'+name; + if ("~" !== name[0]) { + tmp[1] = "~" + name; + tmp[2] = "~@" + name; } act.b(tmp); - } - if(pair){ + }; + if (pair) { act.g(pair); - } else - if(alias){ - root.get('~@'+alias).once(act.a); - } else - if(!alias && !pass){ + } else if (alias) { + root.get("~@" + alias).once(act.a); + } else if (!alias && !pass) { SEA.name(act.plugin); } return gun; - } - User.prototype.pair = function(){ + }; + User.prototype.pair = function() { console.log("user.pair() IS DEPRECATED AND WILL BE DELETED!!!"); var user = this; - if(!user.is){ return false } + if (!user.is) { + return false; + } return user._.sea; - } - User.prototype.leave = function(opt, cb){ - var gun = this, user = (gun.back(-1)._).user; - if(user){ + }; + User.prototype.leave = function(opt, cb) { + var gun = this, + user = gun.back(-1)._.user; + if (user) { delete user.is; delete user._.is; delete user._.sea; } - if(SEA.window){ - try{var sS = {}; - sS = window.sessionStorage; - delete sS.alias; - delete sS.tmp; - delete sS.recall; - }catch(e){}; + if (SEA.window) { + try { + var sS = {}; + sS = window.sessionStorage; + delete sS.alias; + delete sS.tmp; + delete sS.recall; + } 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){ - var gun = this, root = gun.back(-1), user = gun.back('user'); + User.prototype.delete = async function(alias, pass, cb) { + var gun = this, + root = gun.back(-1), + user = gun.back("user"); try { - user.auth(alias, pass, function(ack){ - var pub = (user.is||{}).pub; + user.auth(alias, pass, function(ack) { + var pub = (user.is || {}).pub; // Delete user data - user.map().once(function(){ this.put(null) }); + user.map().once(function() { + this.put(null); + }); // Wipe user data from memory user.leave(); - (cb || noop)({ok: 0}); + (cb || noop)({ ok: 0 }); }); } catch (e) { - Gun.log('User.delete failed! Error:', e); + Gun.log("User.delete failed! Error:", e); } return gun; - } - User.prototype.recall = function(opt, cb){ - var gun = this, root = gun.back(-1), tmp; + }; + 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); + 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){} + } catch (e) {} } return gun; } @@ -993,81 +1458,125 @@ should expiry be core or a plugin? */ return gun; - } - User.prototype.alive = async function(){ - const gunRoot = this.back(-1) + }; + 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._ + await authRecall(gunRoot); + return gunRoot._.user._; } catch (e) { - const err = 'No session!' - Gun.log(err) - throw { err } + const err = "No session!"; + Gun.log(err); + throw { err }; } - } - User.prototype.trust = async 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)) { - user.get('pub').get((ctx, ev) => { - console.log(ctx, ev) - }) + user.get("pub").get((ctx, ev) => { + console.log(ctx, ev); + }); } - } - 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.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); - } - 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('trust').get(pub).get(path).put(enc, cb); - }()); + }; + 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.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); + } + 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("trust") + .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); - }()); + }; + 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; - } - module.exports = User - })(USE, './create'); + }; + module.exports = User; + })(USE, "./create"); - ;USE(function(module){ - const SEA = USE('./sea') + USE(function(module) { + const SEA = USE("./sea"); const Gun = SEA.Gun; // 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('node', each, at); + 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("node", each, at); } this.to.next(at); // make sure to call the "next" middleware adapter. }); @@ -1085,140 +1594,207 @@ // 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! + 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){ + 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[':']){ + if (u !== tmp[":"]) { node[key] = SEA.opt.unpack(tmp); return; } - if(!SEA.opt.check(val)){ 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) } + 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) } + 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){ + function signature(msg) { + if ((msg._ || noop).user) { return this.to.next(msg); } var ctx = this.as; - (msg._||(msg._=function(){})).user = ctx.user; + (msg._ || (msg._ = function() {})).user = ctx.user; security.call(this, msg); } // 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! + 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 (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? + 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? + } 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? + } else { + // Allow reading everything? return to.next(msg); // yes // TODO: No! Make this a callback/event that people can filter on. } } } - if(msg.put){ + if (msg.put) { // 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}); + 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}); + 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; } - SEA.verify(SEA.opt.pack(val,key,node,soul), pub, function(data){ var rel, tmp; + 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 (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)){ + 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}); + 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 + "'."}); + 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); + 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){ + 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 } }); @@ -1226,81 +1802,139 @@ 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}); + 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; + 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)){ + 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}); + 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 + }; + each.end = function(ctx) { + // TODO: Can't you just switch this to each.end = cb? + if (each.err) { 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. + 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}); + 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). } - SEA.opt.pub = function(s){ - if(!s){ return } - s = s.split('~'); - if(!s || !(s = s[1])){ return } - s = s.split('.'); - if(!s || 2 > s.length){ return } - 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.pack = function(d,k, n,s){ // pack for verifying - if(SEA.opt.check(d)){ return d } - var meta = (Gun.obj.ify(d)||noop), sig = meta['~']; - return sig? {m: {'#':s,'.':k,':':meta[':'],'>':Gun.state.is(n, k)}, s: sig} : d; - } - SEA.opt.unpack = function(d, k, n){ var tmp; - if(u === d){ return } - if(d && (u !== (tmp = d[':']))){ return tmp } - if(!k || !n){ return } - if(d === n[k]){ return d } - if(!SEA.opt.check(n[k])){ return d } - var soul = Gun.node.soul(n), s = Gun.state.is(n, k); - if(d && 4 === d.length && soul === d[0] && k === d[1] && fl(s) === fl(d[3])){ - return d[2]; + SEA.opt.pub = function(s) { + if (!s) { + return; } - if(s < SEA.opt.shuffle_attack){ + s = s.split("~"); + if (!s || !(s = s[1])) { + return; + } + s = s.split("."); + if (!s || 2 > s.length) { + return; + } + 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.pack = function(d, k, n, s) { + // pack for verifying + if (SEA.opt.check(d)) { return d; } - } + var meta = Gun.obj.ify(d) || noop, + sig = meta["~"]; + return sig + ? { + m: { "#": s, ".": k, ":": meta[":"], ">": Gun.state.is(n, k) }, + s: sig + } + : d; + }; + SEA.opt.unpack = function(d, k, n) { + var tmp; + if (u === d) { + return; + } + if (d && u !== (tmp = d[":"])) { + return tmp; + } + if (!k || !n) { + return; + } + if (d === n[k]) { + return d; + } + if (!SEA.opt.check(n[k])) { + return d; + } + var soul = Gun.node.soul(n), + s = Gun.state.is(n, k); + if ( + d && + 4 === d.length && + soul === d[0] && + k === d[1] && + fl(s) === fl(d[3]) + ) { + return d[2]; + } + if (s < SEA.opt.shuffle_attack) { + return d; + } + }; SEA.opt.shuffle_attack = 1546329600000; // Jan 1, 2019 - var noop = function(){}, u; + var noop = function() {}, + u; var fl = Math.floor; // TODO: Still need to fix inconsistent state issue. var rel_is = Gun.val.rel.is; // TODO: Potential bug? If pub/priv key starts with `-`? IDK how possible. - - })(USE, './index'); -}()); \ No newline at end of file + })(USE, "./index"); +})(); diff --git a/sea/secret.js b/sea/secret.js index 4009ee31..b52a6fc6 100644 --- a/sea/secret.js +++ b/sea/secret.js @@ -13,7 +13,7 @@ var epriv = pair.epriv; var ecdhSubtle = shim.ossl || shim.subtle; var pubKeyData = keysToEcdhJwk(pub); - var props = Object.assign(S.ecdh, { public: await ecdhSubtle.importKey(...pubKeyData, true, []) }); + var props = Object.assign({ public: await ecdhSubtle.importKey(...pubKeyData, true, []) },S.ecdh); var privKeyData = keysToEcdhJwk(epub, epriv); var derived = await ecdhSubtle.importKey(...privKeyData, false, ['deriveKey']).then(async (privKey) => { // privateKey scope doesn't leak out from here! @@ -47,4 +47,4 @@ } module.exports = SEA.secret; - \ No newline at end of file +