diff --git a/.gitignore b/.gitignore index c5272f8..3f0fc86 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ coverage test lib dist/*.js +.DS_Store diff --git a/package.json b/package.json index 72215b7..a99debc 100644 --- a/package.json +++ b/package.json @@ -39,13 +39,12 @@ "dependencies": { "@babel/runtime": "7.4.4", "bigi": "1.4.2", - "browserify-aes": "1.0.6", "bs58": "4.0.1", - "bytebuffer": "5.0.1", "create-hash": "1.1.3", "create-hmac": "1.1.6", "ecurve": "1.0.5", - "randombytes": "2.0.5" + "randombytes": "2.0.5", + "tweetnacl": "1.0.1" }, "license": "MIT", "devDependencies": { diff --git a/src/aes.js b/src/aes.js index 7e4307c..d9ce143 100644 --- a/src/aes.js +++ b/src/aes.js @@ -1,18 +1,18 @@ const randomBytes = require('randombytes') -const ByteBuffer = require('bytebuffer') -const crypto = require('browserify-aes') const assert = require('assert') const PublicKey = require('./key_public') const PrivateKey = require('./key_private') const hash = require('./hash') - -const Long = ByteBuffer.Long; +const nacl = require("tweetnacl/nacl-fast") module.exports = { encrypt, - decrypt + decrypt, + decrypt_shared_secret, + encrypt_shared_secret, } +const nonceLength = 24 /** Spec: http://localhost:3002/steem/@dantheman/how-to-encrypt-a-memo-when-transferring-steem @@ -20,17 +20,17 @@ module.exports = { @arg {PrivateKey} private_key - required and used for decryption @arg {PublicKey} public_key - required and used to calcualte the shared secret - @arg {string} [nonce = uniqueNonce()] - assigned a random unique uint64 @return {object} - @property {string} nonce - random or unique uint64, provides entropy when re-using the same private/public keys. - @property {Buffer} message - Plain text message - @property {number} checksum - shared secret checksum + @property {Buffer} message - Secret */ -function encrypt(private_key, public_key, message, nonce = uniqueNonce()) { - return crypt(private_key, public_key, nonce, message) +function encrypt(private_key, public_key, message) { + return crypt(private_key, public_key, message, true) } +function encrypt_shared_secret(shared_secret, message) { + return crypt_shared_secret(shared_secret, message, true) +} /** Spec: http://localhost:3002/steem/@dantheman/how-to-encrypt-a-memo-when-transferring-steem @@ -44,8 +44,12 @@ function encrypt(private_key, public_key, message, nonce = uniqueNonce()) { @return {Buffer} - message */ -function decrypt(private_key, public_key, nonce, message, checksum) { - return crypt(private_key, public_key, nonce, message, checksum).message +function decrypt(private_key, public_key, box) { + return crypt(private_key, public_key, box, false) +} + +function decrypt_shared_secret(shared_secret, box) { + return crypt_shared_secret(shared_secret, box, false) } /** @@ -53,114 +57,97 @@ function decrypt(private_key, public_key, nonce, message, checksum) { @arg {number} checksum - shared secret checksum (null to encrypt, non-null to decrypt) @private */ -function crypt(private_key, public_key, nonce, message, checksum) { +function crypt(private_key, public_key, box, encrypt) { + let nonce, message private_key = PrivateKey(private_key) if (!private_key) throw new TypeError('private_key is required') public_key = PublicKey(public_key) if (!public_key) - throw new TypeError('public_key is required') + throw new TypeError('public_key is required') - nonce = toLongObj(nonce) - if (!nonce) - throw new TypeError('nonce is required') + const S = private_key.getSharedSecret(public_key); + return crypt_shared_secret(S, box, encrypt); + +} - if (!Buffer.isBuffer(message)) { - if (typeof message !== 'string') - throw new TypeError('message should be buffer or string') - message = new Buffer(message, 'binary') - } - if (checksum && typeof checksum !== 'number') - throw new TypeError('checksum should be a number') +function crypt_shared_secret(S, box, encrypt) { + let nonce, message + if(encrypt) { + nonce = uniqueNonce() + message = box + } else { + ({nonce, message} = deserialize(box)) + } + if (!Buffer.isBuffer(message)) { + if (typeof message !== 'string') + throw new TypeError('message should be buffer or string') + message = new Buffer(message, 'binary') + } + assert(Buffer.isBuffer(S), "S is not a buffer") + assert(Buffer.isBuffer(nonce), "nonce is not a buffer") + + const ekey_length = S.length + nonce.length + let ebuf = Buffer.concat([nonce, S], ekey_length) + const encryption_key = hash.sha512(ebuf) + + const iv = encryption_key.slice(32, 56) + const key = encryption_key.slice(0, 32) + + if (encrypt) { + message = cryptoJsEncrypt(message, key, iv) + return serialize(nonce, message) + } else { + return cryptoJsDecrypt(message, key, iv) + } +} - const S = private_key.getSharedSecret(public_key); - let ebuf = new ByteBuffer(ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN) - ebuf.writeUint64(nonce) - ebuf.append(S.toString('binary'), 'binary') - ebuf = new Buffer(ebuf.copy(0, ebuf.offset).toBinary(), 'binary') - const encryption_key = hash.sha512(ebuf) - - // D E B U G - // console.log('crypt', { - // priv_to_pub: private_key.toPublic().toString(), - // pub: public_key.toString(), - // nonce: nonce.toString(), - // message: message.length, - // checksum, - // S: S.toString('hex'), - // encryption_key: encryption_key.toString('hex'), - // }) - - const iv = encryption_key.slice(32, 48) - const key = encryption_key.slice(0, 32) - - // check is first 64 bit of sha256 hash treated as uint64_t truncated to 32 bits. - let check = hash.sha256(encryption_key) - check = check.slice(0, 4) - const cbuf = ByteBuffer.fromBinary(check.toString('binary'), ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN) - check = cbuf.readUint32() - - if (checksum) { - if (check !== checksum) - throw new Error('Invalid key') - message = cryptoJsDecrypt(message, key, iv) - } else { - message = cryptoJsEncrypt(message, key, iv) - } - return {nonce, message, checksum: check} +function serialize(nonce, message) { + const len = nonceLength + message.length + return Buffer.concat([nonce, message], len) } -/** This method does not use a checksum, the returned data must be validated some other way. +function deserialize(buf) { + const nonce = buf.slice(0, nonceLength) + const message = buf.slice(nonceLength) + return {nonce, message} +} +/** This method both decrypts and checks the authenticity of the messsage. @arg {string|Buffer} message - ciphertext binary format @arg {string|Buffer} key - 256bit - @arg {string|Buffer} iv - 128bit + @arg {string|Buffer} iv - 192bit @return {Buffer} */ -function cryptoJsDecrypt(message, key, iv) { - assert(message, "Missing cipher text") - message = toBinaryBuffer(message) - const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv) - // decipher.setAutoPadding(true) - message = Buffer.concat([decipher.update(message), decipher.final()]) - return message +function cryptoJsDecrypt(box, key, nonce) { + assert(box, "Missing cipher text") + box = toBinaryBuffer(box) + const decrypted = nacl.secretbox.open(box, nonce, key) + if(decrypted === null) { + throw new Error('Secretbox refused to open (wrong key or corrupted or tampered message)') + } + return Buffer.from(decrypted) } -/** This method does not use a checksum, the returned data must be validated some other way. +/** This method both encrypts and authenticates the message. @arg {string|Buffer} message - plaintext binary format @arg {string|Buffer} key - 256bit - @arg {string|Buffer} iv - 128bit + @arg {string|Buffer} iv - 192bit @return {Buffer} */ -function cryptoJsEncrypt(message, key, iv) { +function cryptoJsEncrypt(message, key, nonce) { assert(message, "Missing plain text") message = toBinaryBuffer(message) - const cipher = crypto.createCipheriv('aes-256-cbc', key, iv) - // cipher.setAutoPadding(true) - message = Buffer.concat([cipher.update(message), cipher.final()]) - return message + return Buffer.from(nacl.secretbox(message, nonce, key)) } -/** @return {string} unique 64 bit unsigned number string. Being time based, this is careful to never choose the same nonce twice. This value could be recorded in the blockchain for a long time. +/** @return {string} 192bit random nonce. Long enough to be unique. This value could be recorded in the blockchain for a long time. */ function uniqueNonce() { - if(unique_nonce_entropy === null) { - const b = new Uint8Array(randomBytes(2)) - unique_nonce_entropy = parseInt(b[0] << 8 | b[1], 10) - } - let long = Long.fromNumber(Date.now()) - const entropy = ++unique_nonce_entropy % 0xFFFF - // console.log('uniqueNonce date\t', ByteBuffer.allocate(8).writeUint64(long).toHex(0)) - // console.log('uniqueNonce entropy\t', ByteBuffer.allocate(8).writeUint64(Long.fromNumber(entropy)).toHex(0)) - long = long.shiftLeft(16).or(Long.fromNumber(entropy)); - // console.log('uniqueNonce final\t', ByteBuffer.allocate(8).writeUint64(long).toHex(0)) - return long.toString() + return randomBytes(nonceLength) } -let unique_nonce_entropy = null -// for(let i=1; i < 10; i++) key.uniqueNonce() -const toLongObj = o => (o ? Long.isLong(o) ? o : Long.fromString(o) : o) const toBinaryBuffer = o => (o ? Buffer.isBuffer(o) ? o : new Buffer(o, 'binary') : o) diff --git a/src/aes.test.js b/src/aes.test.js new file mode 100644 index 0000000..9e0828a --- /dev/null +++ b/src/aes.test.js @@ -0,0 +1,44 @@ +/* eslint-env mocha */ +const assert = require('assert') +const ecc = require('.') + +const alice = { + public_key: 'EOS81xEWcDyZCxACZcYQekiWXLjuSoPMwmRv16nZMuqm2BtQMvXbg', + private_key: '5JxhzyqYERz5MRSswNnDUXL1gFyM2m5Zxde9gGWfMkndbnjB8kD', +} +const bob = { + public_key: 'EOS7jAEWX9d4nZJWNckkaxBsHyqbe6yrVH6VUoCzP6DLxHAEvsBKM', + private_key: '5HrR1D5UbeeMETVR6Ud3Xc6PchVKbtAHmHiPmkmMQDqXY53bQKZ', +} + +describe('encrypt/decrypt', () => { + it('Decrypt should recover the original message', async function() { + const message = Buffer.from("My first message") + let box = ecc.Aes.encrypt(alice.private_key, bob.public_key, message) + const decrypted = ecc.Aes.decrypt(bob.private_key, alice.public_key, box) + assert.deepEqual(decrypted, message) + }) + + /* The following test fails with the normal eosjs-ecc */ + it('Tampered message should throw', async function() { + const message = Buffer.from("My first message") + let box = ecc.Aes.encrypt(alice.private_key, bob.public_key, message) + + // a little tampering + box = Buffer.concat([box, box]) + + assert.throws(function() { + ecc.Aes.decrypt(bob.private_key, alice.public_key, box) + }) + }) + + it("encryption with pre-existing shared secret", async function() { + const shared_secret = Buffer.from("1234") + const message = Buffer.from("My first message") + const box = ecc.Aes.encrypt_shared_secret(shared_secret, message) + const decrypted =ecc.Aes.decrypt_shared_secret(shared_secret, box) + assert.deepEqual(decrypted, message) + }) + +}) + diff --git a/src/common.test.js b/src/common.test.js index 04778a3..0636260 100644 --- a/src/common.test.js +++ b/src/common.test.js @@ -81,16 +81,3 @@ describe('Common API', () => { }) }) -describe('Common API (initialized)', () => { - it('initialize', () => ecc.initialize()) - - it('randomKey', () => { - const cpuEntropyBits = 1 - ecc.key_utils.addEntropy(1, 2, 3) - const pvt = ecc.unsafeRandomKey().then(pvt => { - assert.equal(typeof pvt, 'string', 'pvt') - assert(/^5[HJK]/.test(wif)) - // assert(/^PVT_K1_/.test(pvt)) - }) - }) -}) diff --git a/src/key_private.js b/src/key_private.js index 1ca8c71..5938bd3 100644 --- a/src/key_private.js +++ b/src/key_private.js @@ -76,7 +76,7 @@ function PrivateKey(d) { /** ECIES @arg {string|Object} pubkey wif, PublicKey object - @return {Buffer} 64 byte shared secret + @return {Buffer} 32 byte shared secret */ function getSharedSecret(public_key) { public_key = PublicKey(public_key) @@ -89,8 +89,7 @@ function PrivateKey(d) { let r = toBuffer() let P = KBP.multiply(BigInteger.fromBuffer(r)) let S = P.affineX.toBuffer({size: 32}) - // SHA512 used in ECIES - return hash.sha512(S) + return S } // /** ECIES TODO unit test @@ -268,9 +267,9 @@ function initialize() { return } - unitTest() - keyUtils.addEntropy(...keyUtils.cpuEntropy()) - assert(keyUtils.entropyCount() >= 128, 'insufficient entropy') + // unitTest() + // keyUtils.addEntropy(...keyUtils.cpuEntropy()) + // assert(keyUtils.entropyCount() >= 128, 'insufficient entropy') initialized = true } diff --git a/src/key_utils.js b/src/key_utils.js index 82ac5ff..4ae62a9 100644 --- a/src/key_utils.js +++ b/src/key_utils.js @@ -33,9 +33,6 @@ function random32ByteBuffer({cpuEntropyBits = 0, safe = true} = {}) { assert.equal(typeof cpuEntropyBits, 'number', 'cpuEntropyBits') assert.equal(typeof safe, 'boolean', 'boolean') - if(safe) { - assert(entropyCount >= 128, 'Call initialize() to add entropy') - } // if(entropyCount > 0) { // console.log(`Additional private key entropy: ${entropyCount} events`) diff --git a/yarn.lock b/yarn.lock index 37124ea..02ffe40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -502,7 +502,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-runtime@^7.4.4": +"@babel/plugin-transform-runtime@7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.4.4.tgz#a50f5d16e9c3a4ac18a1a9f9803c107c380bce08" integrity sha512-aMVojEjPszvau3NRg+TIH14ynZLvPewH4xhlCW1w6A3rkxTS1m4uwzRclYR9oS+rl/dr+kT+pzbfHuAWP/lc7Q== @@ -612,7 +612,7 @@ js-levenshtein "^1.1.3" semver "^5.5.0" -"@babel/runtime@^7.4.4": +"@babel/runtime@7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.4.tgz#dc2e34982eb236803aa27a07fea6857af1b9171d" integrity sha512-w0+uT71b6Yi7i5SE0co4NioIpSYS6lLiXvCzWzGSKvpK5vdQtCbICHMj+gbAKAOtxiV6HsVh/MBdaF9EQ6faSg== @@ -6295,6 +6295,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tweetnacl@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.1.tgz#2594d42da73cd036bd0d2a54683dd35a6b55ca17" + integrity sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"