diff --git a/.gitignore b/.gitignore index 3c3629e6..f854f3fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ node_modules +.DS_Store +package-lock.json diff --git a/index.js b/index.js index 03944acf..a4ce98a6 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,7 @@ -const ethUtil = require('ethereumjs-util') -const ethAbi = require('ethereumjs-abi') +const ethUtil = require('ethereumjs-util'); +const ethAbi = require('ethereumjs-abi'); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); const TYPED_MESSAGE_SCHEMA = { type: 'object', @@ -225,6 +227,88 @@ module.exports = { return ethUtil.bufferToHex(sender) }, + encrypt: function(receiverPublicKey, msgParams, version) { + + switch(version) { + case 'x25519-xsalsa20-poly1305': + console.log(typeof msgParams.data ) + if( typeof msgParams.data == 'undefined'){ + throw new Error('Cannot detect secret message, message params should be of the form {data: "secret message"} ') + } + //generate ephemeral keypair + var ephemeralKeyPair = nacl.box.keyPair() + + // assemble encryption parameters - from string to UInt8 + try { + var pubKeyUInt8Array = nacl.util.decodeBase64(receiverPublicKey); + } catch (err){ + throw new Error('Bad public key') + } + + var msgParamsUInt8Array = nacl.util.decodeUTF8(msgParams.data); + var nonce = nacl.randomBytes(nacl.box.nonceLength); + + // encrypt + var encryptedMessage = nacl.box(msgParamsUInt8Array, nonce, pubKeyUInt8Array, ephemeralKeyPair.secretKey); + + // handle encrypted data + var output = { + version: 'x25519-xsalsa20-poly1305', + nonce: nacl.util.encodeBase64(nonce), + ephemPublicKey: nacl.util.encodeBase64(ephemeralKeyPair.publicKey), + ciphertext: nacl.util.encodeBase64(encryptedMessage) + }; + // return encrypted msg data + return output; + + default: + throw new Error('Encryption type/version not supported') + + } + }, + + decrypt: function(encryptedData, receiverPrivateKey) { + + switch(encryptedData.version) { + case 'x25519-xsalsa20-poly1305': + //string to buffer to UInt8Array + var recieverPrivateKeyUint8Array = nacl_decodeHex(receiverPrivateKey) + var recieverEncryptionPrivateKey = nacl.box.keyPair.fromSecretKey(recieverPrivateKeyUint8Array).secretKey + + // assemble decryption parameters + var nonce = nacl.util.decodeBase64(encryptedData.nonce); + var ciphertext = nacl.util.decodeBase64(encryptedData.ciphertext); + var ephemPublicKey = nacl.util.decodeBase64(encryptedData.ephemPublicKey); + + // decrypt + var decryptedMessage = nacl.box.open(ciphertext, nonce, ephemPublicKey, recieverEncryptionPrivateKey); + + // return decrypted msg data + try { + var output = nacl.util.encodeUTF8(decryptedMessage); + }catch(err) { + throw new Error('Decryption failed.') + } + + if (output){ + return output; + }else{ + throw new Error('Decryption failed.') + } + + + default: + throw new Error('Encryption type/version not supported.') + } + + }, + + getEncryptionPublicKey: function(privateKey){ + var privateKeyUint8Array = nacl_decodeHex(privateKey) + var encryptionPublicKey = nacl.box.keyPair.fromSecretKey(privateKeyUint8Array).publicKey + return nacl.util.encodeBase64(encryptionPublicKey) + } + signTypedData: function (privateKey, msgParams) { const message = TypedDataUtils.sign(msgParams.data) const sig = ethUtil.ecsign(message, privateKey) @@ -286,3 +370,12 @@ function padWithZeroes (number, length) { } return myString } + +//converts hex strings to the Uint8Array format used by nacl +function nacl_decodeHex(msgHex) { + var msgBase64 = (new Buffer(msgHex, 'hex')).toString('base64'); + return nacl.util.decodeBase64(msgBase64); +} + + + diff --git a/package.json b/package.json index cc162044..7d3890b6 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,11 @@ }, "homepage": "https://github.com/MetaMask/eth-sig-util#readme", "dependencies": { + "elliptic": "^6.4.0", + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "ethereumjs-util": "^5.1.1", + "tweetnacl": "^1.0.0", + "tweetnacl-util": "^0.15.0" "ethereumjs-abi": "0.6.5", "ethereumjs-util": "^5.1.1" }, diff --git a/test/index.js b/test/index.js index 03753cab..b5fd6396 100644 --- a/test/index.js +++ b/test/index.js @@ -310,6 +310,108 @@ function typedSignatureHashThrowsTest(opts) { }) } +const bob = { + ethereumPrivateKey: '7e5374ec2ef0d91761a6e72fdf8f6ac665519bfdf6da0a2329cf0d804514b816', + encryptionPrivateKey: 'flN07C7w2Rdhpucv349qxmVRm/322gojKc8NgEUUuBY=', + encryptionPublicKey: 'C5YMNdqE4kLgxQhJO1MfuQcHP5hjVSXzamzd/TxlR0U=' } + +const secretMessage = {data:'My name is Satoshi Buterin'}; + +const encryptedData = { version: 'x25519-xsalsa20-poly1305', +nonce: '1dvWO7uOnBnO7iNDJ9kO9pTasLuKNlej', +ephemPublicKey: 'FBH1/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', +ciphertext: 'f8kBcl/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy' }; + +test("Getting bob's encryptionPublicKey", async t => { + t.plan(1); + + const result = await sigUtil.getEncryptionPublicKey(bob.ethereumPrivateKey) + t.equal(result, bob.encryptionPublicKey); +}); + +//encryption test +test("Alice encrypts message with bob's encryptionPublicKey", async t => { + + + t.plan(4); + + const result = await sigUtil.encrypt( + bob.encryptionPublicKey, + secretMessage, + 'x25519-xsalsa20-poly1305' + ); + + console.log("RESULT", result) + + t.ok(result.version); + t.ok(result.nonce); + t.ok(result.ephemPublicKey); + t.ok(result.ciphertext); + +}); + +// decryption test +test("Bob decrypts message that Alice sent to him", t => { + t.plan(1); + + const result = sigUtil.decrypt(encryptedData, bob.ethereumPrivateKey); + t.equal(result, secretMessage.data); +}); + +test('Decryption failed because version is wrong or missing', t =>{ + t.plan(1) + + const badVersionData = { version: 'x256k1-aes256cbc', + nonce: '1dvWO7uOnBnO7iNDJ9kO9pTasLuKNlej', + ephemPublicKey: 'FBH1/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', + ciphertext: 'f8kBcl/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy' }; + + t.throws( function() { sigUtil.decrypt(badVersionData, bob.ethereumPrivateKey)}, 'Encryption type/version not supported.') +}); + +test('Decryption failed because nonce is wrong or missing', t => { + t.plan(1); + + //encrypted data + const badNonceData = { version: 'x25519-xsalsa20-poly1305', + nonce: '', + ephemPublicKey: 'FBH1/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', + ciphertext: 'f8kBcl/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy' }; + + t.throws(function() { sigUtil.decrypt(badNonceData, bob.ethereumPrivateKey)}, 'Decryption failed.') + +}); + +test('Decryption failed because ephemPublicKey is wrong or missing', t => { + t.plan(1); + + //encrypted data + const badEphemData = { version: 'x25519-xsalsa20-poly1305', + nonce: '1dvWO7uOnBnO7iNDJ9kO9pTasLuKNlej', + ephemPublicKey: 'FFFF/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', + ciphertext: 'f8kBcl/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy' }; + + t.throws(function() { sigUtil.decrypt(badEphemData, bob.ethereumPrivateKey)}, 'Decryption failed.') +}); + +test('Decryption failed because cyphertext is wrong or missing', async t => { + t.plan(1); + + //encrypted data + const badCypherData = { version: 'x25519-xsalsa20-poly1305', + nonce: '1dvWO7uOnBnO7iNDJ9kO9pTasLuKNlej', + ephemPublicKey: 'FBH1/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', + ciphertext: 'ffffff/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy' }; + + t.throws(function() { sigUtil.decrypt(badEphemData, bob.ethereumPrivateKey)}, 'Decryption failed.') +}); + +test("Decryption fails because you are not the recipient", t => { + t.plan(1); + + t.throws(function() { sigUtil.decrypt(encryptedData, alice.ethereumPrivateKey)}, 'Decryption failed.') +}); + test('signedTypeData', (t) => { t.plan(8) const utils = sigUtil.TypedDataUtils diff --git a/utils/eccrypto-lite.js b/utils/eccrypto-lite.js new file mode 100644 index 00000000..f8646bb0 --- /dev/null +++ b/utils/eccrypto-lite.js @@ -0,0 +1,167 @@ +"use strict"; + +var EC = require("elliptic").ec; + +var ec = new EC("secp256k1"); +var cryptoObj = global.crypto || global.msCrypto || {}; +var subtle = cryptoObj.subtle || cryptoObj.webkitSubtle; + +module.exports = { + decryptWithPrivateKey: async function(privateKey, encrypted) { + console.log(privateKey) + const twoStripped = privateKey.replace(/^.{2}/g, ''); + console.log(twoStripped) + const encryptedBuffer = { + iv: new Buffer(encrypted.iv, 'hex'), + ephemPublicKey: new Buffer(encrypted.ephemPublicKey, 'hex'), + ciphertext: new Buffer(encrypted.ciphertext, 'hex'), + mac: new Buffer(encrypted.mac, 'hex') + }; + + const decryptedBuffer = await decrypt( + new Buffer(twoStripped, 'hex'), + encryptedBuffer + ); + return decryptedBuffer.toString(); + + + }, + encryptWithPublicKey: async function(receiverPublicKey, payload) { + const pubString = '04' + receiverPublicKey; + const encryptedBuffers = await encrypt( + new Buffer(pubString, 'hex'), + Buffer(payload) + ); + const encrypted = { + iv: encryptedBuffers.iv.toString('hex'), + ephemPublicKey: encryptedBuffers.ephemPublicKey.toString('hex'), + ciphertext: encryptedBuffers.ciphertext.toString('hex'), + mac: encryptedBuffers.mac.toString('hex') + }; + return encrypted; + } +}; + +function assert(condition, message) { + if (!condition) { + throw new Error(message || "Assertion failed"); + } +} + +function randomBytes(size) { + var arr = new Uint8Array(size); + global.crypto.getRandomValues(arr); + return new Buffer(arr); +} + +function sha512(msg) { + return subtle.digest({name: "SHA-512"}, msg).then(function(hash) { + return new Buffer(new Uint8Array(hash)); + }); +} + +function getAes(op) { + return function(iv, key, data) { + var importAlgorithm = {name: "AES-CBC"}; + var keyp = subtle.importKey("raw", key, importAlgorithm, false, [op]); + return keyp.then(function(cryptoKey) { + var encAlgorithm = {name: "AES-CBC", iv: iv}; + return subtle[op](encAlgorithm, cryptoKey, data); + }).then(function(result) { + return new Buffer(new Uint8Array(result)); + }); + }; +} + +var aesCbcEncrypt = getAes("encrypt"); +var aesCbcDecrypt = getAes("decrypt"); + +function hmacSha256Sign(key, msg) { + var algorithm = {name: "HMAC", hash: {name: "SHA-256"}}; + var keyp = subtle.importKey("raw", key, algorithm, false, ["sign"]); + return keyp.then(function(cryptoKey) { + return subtle.sign(algorithm, cryptoKey, msg); + }).then(function(sig) { + return new Buffer(new Uint8Array(sig)); + }); +} + +function hmacSha256Verify(key, msg, sig) { + var algorithm = {name: "HMAC", hash: {name: "SHA-256"}}; + var keyp = subtle.importKey("raw", key, algorithm, false, ["verify"]); + return keyp.then(function(cryptoKey) { + return subtle.verify(algorithm, cryptoKey, sig, msg); + }); +} + +var getPublic = exports.getPublic = function(privateKey) { + assert(privateKey.length === 32, "Bad private key"); + return new Buffer(ec.keyFromPrivate(privateKey).getPublic("arr")); +}; + + +var derive = exports.derive = function(privateKeyA, publicKeyB) { + return new Promise(function(resolve) { + assert(Buffer.isBuffer(privateKeyA), "Bad input"); + assert(Buffer.isBuffer(publicKeyB), "Bad input"); + assert(privateKeyA.length === 32, "Bad private key"); + assert(publicKeyB.length === 65, "Bad public key"); + assert(publicKeyB[0] === 4, "Bad public key"); + var keyA = ec.keyFromPrivate(privateKeyA); + var keyB = ec.keyFromPublic(publicKeyB); + var Px = keyA.derive(keyB.getPublic()); // BN instance + resolve(new Buffer(Px.toArray())); + }); +}; + +const encrypt = function(publicKeyTo, msg, opts) { + assert(subtle, "WebCryptoAPI is not available"); + opts = opts || {}; + // Tmp variables to save context from flat promises; + var iv, ephemPublicKey, ciphertext, macKey; + return new Promise(function(resolve) { + var ephemPrivateKey = opts.ephemPrivateKey || randomBytes(32); + ephemPublicKey = getPublic(ephemPrivateKey); + resolve(derive(ephemPrivateKey, publicKeyTo)); + }).then(function(Px) { + return sha512(Px); + }).then(function(hash) { + iv = opts.iv || randomBytes(16); + var encryptionKey = hash.slice(0, 32); + macKey = hash.slice(32); + return aesCbcEncrypt(iv, encryptionKey, msg); + }).then(function(data) { + ciphertext = data; + var dataToMac = Buffer.concat([iv, ephemPublicKey, ciphertext]); + return hmacSha256Sign(macKey, dataToMac); + }).then(function(mac) { + return { + iv: iv, + ephemPublicKey: ephemPublicKey, + ciphertext: ciphertext, + mac: mac, + }; + }); +}; +const decrypt = function(privateKey, opts) { + assert(subtle, "WebCryptoAPI is not available"); + // Tmp variable to save context from flat promises; + var encryptionKey; + return derive(privateKey, opts.ephemPublicKey).then(function(Px) { + return sha512(Px); + }).then(function(hash) { + encryptionKey = hash.slice(0, 32); + var macKey = hash.slice(32); + var dataToMac = Buffer.concat([ + opts.iv, + opts.ephemPublicKey, + opts.ciphertext + ]); + return hmacSha256Verify(macKey, dataToMac, opts.mac); + }).then(function(macGood) { + assert(macGood, "Bad MAC"); + return aesCbcDecrypt(opts.iv, encryptionKey, opts.ciphertext); + }).then(function(msg) { + return new Buffer(new Uint8Array(msg)); + }); +}; \ No newline at end of file