const Gun = (typeof window !== 'undefined' ? window : global).Gun || require('gun/gun') var wc = require('./webcrypto'); var subtle = wc.subtle; var getRandomBytes = wc.random; var EasyIndexedDB = require('./indexed'); var SafeBuffer = require('./buffer'); var settings = require('./settings'); var pbKdf2 = settings.pbkdf2; var ecdsaKeyProps = settings.ecdsa.pair; var ecdhKeyProps = settings.ecdh; var keysToEcdsaJwk = settings.jwk; var ecdsaSignProps = settings.ecdsa.sign; var sha256hash = require('./sha256'); var recallCryptoKey = require('./remember'); var parseProps = require('./parse'); // Practical examples about usage found from ./test/common.js const SEA = { // This is easy way to use IndexedDB, all methods are Promises EasyIndexedDB, // This is Buffer used in SEA and usable from Gun/SEA application also. // For documentation see https://nodejs.org/api/buffer.html Buffer: SafeBuffer, // These SEA functions support now ony Promises or // async/await (compatible) code, use those like Promises. // // Creates a wrapper library around Web Crypto API // for various AES, ECDSA, PBKDF2 functions we called above. async proof(pass, salt) { try { if (typeof window !== 'undefined') { // For browser subtle works fine const key = await subtle.importKey( 'raw', new TextEncoder().encode(pass), { name: 'PBKDF2' }, false, ['deriveBits'] ) const result = await subtle.deriveBits({ name: 'PBKDF2', iterations: pbKdf2.iter, salt: new TextEncoder().encode(salt), hash: pbKdf2.hash, }, key, pbKdf2.ks * 8) pass = getRandomBytes(pass.length) // Erase passphrase for app return Buffer.from(result, 'binary').toString('base64') } // For NodeJS crypto.pkdf2 rocks const hash = crypto.pbkdf2Sync( pass, new TextEncoder().encode(salt), pbKdf2.iter, pbKdf2.ks, pbKdf2.hash.replace('-', '').toLowerCase() ) pass = getRandomBytes(pass.length) // Erase passphrase for app return hash && hash.toString('base64') } catch (e) { Gun.log(e) throw e } }, // Calculate public key KeyID aka PGPv4 (result: 8 bytes as hex string) async keyid(pub) { try { // base64('base64(x):base64(y)') => Buffer(xy) const pb = Buffer.concat( Buffer.from(pub, 'base64').toString('utf8').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) { Gun.log(e) throw e } }, async pair() { try { const ecdhSubtle = wc.ossl || subtle // First: ECDSA keys for signing/verifying... const { pub, priv } = await subtle.generateKey(ecdsaKeyProps, true, [ 'sign', 'verify' ]) .then(async ({ publicKey, privateKey }) => { const { d: priv } = await subtle.exportKey('jwk', privateKey) // privateKey scope doesn't leak out from here! const { x, y } = await subtle.exportKey('jwk', publicKey) const pub = Buffer.from([ x, y ].join(':')).toString('base64') return { pub, priv } }) // To include PGPv4 kind of keyId: // const pubId = await SEA.keyid(keys.pub) // Next: ECDH keys for encryption/decryption... const { epub, epriv } = await ecdhSubtle.generateKey(ecdhKeyProps, true, ['deriveKey']) .then(async ({ publicKey, privateKey }) => { // privateKey scope doesn't leak out from here! const { d: epriv } = await ecdhSubtle.exportKey('jwk', privateKey) const { x, y } = await ecdhSubtle.exportKey('jwk', publicKey) const epub = Buffer.from([ x, y ].join(':')).toString('base64') return { epub, epriv } }) return { pub, priv, /* pubId, */ epub, epriv } } catch (e) { Gun.log(e) throw e } }, // Derive shared secret from other's pub and my epub/epriv async derive(pub, { epub, epriv }) { try { const { importKey, deriveKey, exportKey } = subtleossl || subtle const keystoecdhjwk = (pub, priv) => { const [ x, y ] = Buffer.from(pub, 'base64').toString('utf8').split(':') const jwk = priv ? { d: priv, key_ops: ['decrypt'] } : { key_ops: ['encrypt'] } return Object.assign(jwk, { kty: 'EC', crv: 'P-256', ext: false, x, y }) } const pubLic = await importKey('jwk', keystoecdhjwk(pub), ecdhKeyProps, false, ['deriveKey']) const props = Object.assign({}, ecdhKeyProps, { public: pubLic }) const derived = await importKey('jwk', keystoecdhjwk(epub, epriv), ecdhKeyProps, false, ['deriveKey']) .then(async (privKey) => { // privateKey scope doesn't leak out from here! const derivedKey = await deriveKey(props, privKey, { name: 'AES-CBC', length: 256 }, true, [ 'encrypt', 'decrypt' ]) return exportKey('jwk', derivedKey).then(({ k }) => k) }) return derived } catch (e) { Gun.log(e) throw e } }, async sign(data, { pub, priv }) { try { const jwk = keysToEcdsaJwk(pub, priv) const hash = await sha256hash(data) // privateKey scope doesn't leak out from here! const binSig = await subtle.importKey(...jwk, ecdsaKeyProps, false, ['sign']) .then((privKey) => subtle.sign(ecdsaSignProps, privKey, new Uint8Array(hash))) return Buffer.from(binSig, 'binary').toString('base64') } catch (e) { Gun.log(e) throw e } }, async verify(data, pub, sig) { try { const jwk = keysToEcdsaJwk(pub) const key = await subtle.importKey(...jwk, ecdsaKeyProps, false, ['verify']) const hash = await sha256hash(data) const ss = new Uint8Array(Buffer.from(sig, 'base64')) return await subtle.verify(ecdsaSignProps, key, ss, new Uint8Array(hash)) } catch (e) { Gun.log(e) throw e } }, async enc(data, priv) { try { const rands = { s: getRandomBytes(8), iv: getRandomBytes(16) } const r = Object.keys(rands) .reduce((obj, key) => Object.assign(obj, { [key]: rands[key].toString('hex') }), {}) try { data = (data.slice && data) || JSON.stringify(data) } catch(e) {} //eslint-disable-line no-empty const ct = await recallCryptoKey(priv, rands.s) .then((aesKey) => subtle.encrypt({ // Keeping aesKey scope as private as possible... name: 'AES-CBC', iv: new Uint8Array(rands.iv) }, aesKey, new TextEncoder().encode(data))) Object.assign(r, { ct: Buffer.from(ct, 'binary').toString('base64') }) return JSON.stringify(r) } catch (e) { Gun.log(e) throw e } }, async dec(data, priv) { try { const { s, iv, ct } = parseProps(data) const mm = { s, iv, ct } const rands = [ 'iv', 's' ].reduce((obj, key) => Object.assign(obj, { [key]: new Uint8Array(Buffer.from(mm[key], 'hex')) }), {}) const binCt = await recallCryptoKey(priv, rands.s) .then((aesKey) => subtle.decrypt({ // Keeping aesKey scope as private as possible... name: 'AES-CBC', iv: rands.iv }, aesKey, new Uint8Array(Buffer.from(mm.ct, 'base64')))) return parseProps(new TextDecoder('utf8').decode(binCt)) } catch (e) { Gun.log(e) throw e } }, async write(data, keys) { try { // TODO: something's bugging double 'SEA[]' treatment to mm... let m = data if (m && m.slice && 'SEA[' === m.slice(0, 4)) { return m } if (data && data.slice) { // Needs to remove previous signature envelope while ('SEA[' === m.slice(0, 4)) { try { m = JSON.parse(m.slice(3))[0] } catch (e){ break } } } m = (m && m.slice) ? m : JSON.stringify(m) const signature = await SEA.sign(m, keys) return `SEA${JSON.stringify([ m, signature ])}` } catch (e) { Gun.log(e) throw e } }, async read(data, pub) { try { let d if (!data) { return false === pub ? data : undefined } if (!data.slice || 'SEA[' !== data.slice(0, 4)) { return false === pub ? data : undefined } let m = parseProps(data.slice(3)) || '' d = parseProps(m[0]) if (false === pub) { return d } return (await SEA.verify(m[0], pub, m[1])) ? d : undefined } catch (e) { Gun.log(e) throw e } } } // Usage of the SEA object changed! Now use like this: // const gun = new Gun() // const SEA = gun.SEA() //Gun.SEA = () => SEA Gun.SEA = SEA // 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. // SEA should be a full suite that is easy and seamless to use. // Again, scroll naer the top, where I provide an EXAMPLE of how to create a user and sign in. // Once logged in, the rest of the code you just read handled automatically signing/validating data. // 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. try { module.exports = SEA } catch (e) {} //eslint-disable-line no-empty