Conversation
|
Hey @topealabi, this looks great, thanks for all the hard work, coordinating with other groups and building this! I can't wait to integrate it into MetaMask! Looks like you found a solution to the encryption curve solution that @chrislundkvist raised (pinging him to verify). Is this compatible with the parity encrypt/decrypt methods? If we have buy-in from a couple of the other A few nit-picky apprehensions with this PR are formatting/stylistic things:
I don't want to block on any of those, because I'm very grateful for the work, but I would appreciate if you clean up as much of those issues as possible before I merge. |
|
Sweet For reference, here's Parity's implementation: They prepend a version byte to the ciphertext which would be useful down the road for versioning and expanding the capability. Even if 1:1 parity with parity isn't feasible, it would be nice to have the ciphertext versioned in some way in my opinion |
utils/eccrypto-lite.js
Outdated
| }); | ||
| }; | ||
|
|
||
| // decrypt(e, pk); |
There was a problem hiding this comment.
consider cleaning up // decrypt(e, pk); or adding a clarifying comment.
|
Right now Also, this module should work in the browser, so |
test/index.js
Outdated
| t.ok(result.mac); | ||
| }); | ||
|
|
||
| //decryption test |
There was a problem hiding this comment.
what are your thoughts on adding a test that proves decryption fails with an invalid privateKey?
There was a problem hiding this comment.
That would be a good improvement, I agree we should have that.
|
I'd like to see this paired with an EIP, to get the larger community's opinion. Ideally the EIP would propose a cross-client method for requesting encryption/decryption. I would prefer if this method included a type/version parameter, so that different encryption methods can be added under the same method name. We're seeing the downside of hard-coded signature methods right now in the signTypedData discussion, where the exact implementation has changed as the discussion has gone on, and now there's no easy way for clients to iterate without breaking existing Dapps. Example: /**
* Decrypts some encrypted data.
* @param {Account} account - The account to decrypt with
* @param {string} version - A unique string identifying the decryption strategy.
* @param {Object} data - The data to decrypt
* @param {Function} callback - The function to call back when decryption is complete.
*/
web3.eth.decrypt = function decrypt (account, version, data, callback) { /* implementation */ }
web3.eth.decrypt(account, 'encrypt-eip-1024-v1', data, callback)By adding this I also would really like @christianlundkvist 's take on whether this encryption strategy is sound and secure. |
|
@danfinlay I agree. Cross-client compatibility would be great for the ecosystem. Parity currently prepends a ‘0x04’ to the ciphertext for versioning. I think we could do something similar. Kudos to @kjbbb for pointing that out. @christianlundkvist any thoughts on the ECIES implementation described above? EC(25519) for shared secret and aes256Cbc to generate cyphertext? |
General thoughtsSome of my thoughts are summarized in this comment. In the Ethereum community people really want to use the secp256k1 curve for encryption since this curve is already used for signing and wallets mostly support handling the private keys and generating public keys. This curve is not well suited for this task according to cryptographers, so most people use modern crypto libraries like nacl which uses curve25519 which is explicitly designed with encryption in mind. The result of this is that Ethereum devs need to use ECIES which theoretically works for any curve but has no solid/audited implementations (since most people are just using nacl anyway). So the consequence is that people end up just rolling their own ECIES implementations - Parity did this, the Whisper implementers did this and this PR is another hand-rolled implementation. This proliferation of hand-rolled encryption libraries seems a bit worrying. For instance I don’t see any of these libraries worrying about protection against small-subgroup (“twist”) attacks which is one of the subtle gotchas of ECIES (and one of the reasons cryptographers are not fans of secp256k1 for encryption): http://safecurves.cr.yp.to/twist.html Specific points about this PR
In general the code seems to be well written and I don’t see any obvious problems apart from the twist protection (which I believe all of these implementations lack). I’m not comfortable giving a thorough cryptographic review or “sign off” since I’m not a cryptographer. There may be other cryptography gotchas that I’ve missed (another reason I prefer complete, audited libraries like
What does this mean? As far as I can tell you are using a standard ECDH algorithm (pure EC multiplication from the
I personally much prefer the https://tonyarcieri.com/all-the-crypto-code-youve-ever-written-is-probably-broken That being said as far as I see this code follows best practices around AES (i.e. CBC with random IV, MAC using sha256). What I like about Possible alternative encryption/decryption implementationsAnother way to implement encryption/decryption is the following: Add a method like Upsides to this method:
Downsides:
ConclusionsThe code looks ok and I don’t see any major issues but I’m in general wary of the proliferation of hand-rolled secp256k1 ECIES implementations in the Ethereum space, and I personally much prefer the curve25519 based encryption algorithms. I agree with @FlySwatter that it’s important to have these discussions occur in an EIP so we can have input from professional cryptographers, and ideally to have client independent protocols. I also fully support the ability to support different versions or types. That way other encryption methods can be supported like the curve25519 one described above. I don’t think it’s enough to just have a version byte in the cipher text. The whole structure needs to have a version or type field to distinguish between different encryption methods. |
…g ethereum keys/addresses to their nacl compatible versions
…eypair() to use in nacl.box encryption
coder5876
left a comment
There was a problem hiding this comment.
Looks good! A couple comments;
-
Remove the dummy ‘some-version’ case, use a real case
-
In decrypt function, the encrypted data package already contains the Version so you can take it from there. Alternatively if you want to support versions that don’t contain this you should compare the passed in version with the one in the encrypted data package.
-
Add some error handling in the case where you fail to encrypt/decrypt (for instance if you try to decrypt something not meant for you)
|
Made some updates! |
coder5876
left a comment
There was a problem hiding this comment.
Good job! Added a couple comments!
index.js
Outdated
|
|
||
| // return decrypted msg data | ||
| return nacl.util.encodeUTF8(decryptedMessage); | ||
| var output = nacl.util.encodeUTF8(decryptedMessage); |
There was a problem hiding this comment.
Will this conversion succeed when the decryptedMessage is empty (i.e. when the decryption failed)?
index.js
Outdated
| if (output){ | ||
| return output; | ||
| }else{ | ||
| const error = new Error('Decrypt authentication failed. ') |
There was a problem hiding this comment.
Maybe just "decryption failed"? Not clear if it's the authentication or if we just don't have the right decryption key.
|
Also maybe add a couple tests to check that the error handling works as expected? |
coder5876
left a comment
There was a problem hiding this comment.
Ok great, it's getting pretty close now! :) Nice addition of tests etc. Main question is about the format of message, I feel that UTF8 is not the right format.
| @@ -127,18 +127,21 @@ module.exports = { | |||
| var decryptedMessage = nacl.box.open(ciphertext, nonce, ephemPublicKey, recieverEncryptionPrivateKey); | |||
There was a problem hiding this comment.
I would probably add a check directly here too to see if decryptedMessage is null.
| // return decrypted msg data | ||
| var output = nacl.util.encodeUTF8(decryptedMessage); | ||
| try { | ||
| var output = nacl.util.encodeUTF8(decryptedMessage); |
There was a problem hiding this comment.
This is a question we should dive into: What should the format of the message be? I don't see why it should always be UTF8. I think a lot of times we would rather use a binary format, for instance when encrypting keys etc. Since we are using nacl anyway we should probably stick to the UInt8Array format for the message and then leave it up to the implementer to encode it into the proper format.
I don't know though if we want to have an interface that accept hex-strings etc to conform to web3 norms, so that's something to keep in mind.
There was a problem hiding this comment.
Ok after discussions with @danfinlay I’m now ok with using UTF8 as a format for the cleartext message. The reason is that the RPC endpoint will always use stringified JSON as a format, so we can assume that the messages passed in are of this format as well.
|
Another thing that we need to discuss is padding. If you don't take padding into account it could break the whole security of the system being built. Imagine a voting app where a user submits an encrypted string which is either "Yes" or "No". If the implementer forgets about padding anyone can compute if the length of the cleartext string is either 3 or 2 and thus figure out which string is the cleartext one. So in this example the implementer would need to have the user sign either "Yes" or "No" which are both of length 3. Another example is encrypting a mnemonic seed. If you reveal the length of the seed you reveal a lot about which words may be included in the seed. So you'd need to add padding in this case as well, see for instance here in lightwallet: https://github.com/ConsenSys/eth-lightwallet/blob/master/lib/keystore.js#L78 |
|
Any comments on padding? Add a padding parameter or just don’t do padding and leave it to implementers? I’m ok with either but like some other inputs/opinions. |
|
Discussed this with Christian. The padding is used to obscure inputs that can be different lengths. Since inputs can vary widely in their lengths, we are going to leave padding outside of this API, and simply make it the responsibility of documentation to communicate the importance of properly padding inputs. (ie, a two-letter ciphertext that can be either yes or no is obviously yes). Aside from that documentation caveat, we're ready to merge! |
|
Closing in favor of #29 which can run the tests & fixes merge conflicts. |
MetaMask encrypt/decrypt
How does it work
To Encrypt:
-Alice requests Bob's publicEncryptionKey
-Bob generates his encryptionKeypair using nacl.box.keyPair.fromSecretKey(bob.ethereumPrivateKey)
-Bob sends Alice his encryptionKeyPair.publicKey
-Alice generates a random ephemeralKeyPair
-Alice uses her ephemeralKeypair.secretKey and Bob's encryptionPublicKey to encrypt the data using nacl.box. She sends him an encrypted blob of the form:
{ version: 'x25519-xsalsa20-poly1305', nonce: '1dvWO7uOnBnO7iNDJ9kO9pTasLuKNlej', ephemPublicKey: 'FBH1/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=', ciphertext: 'f8kBcl/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy' }To Decrypt:
-Bob generates his encryptionPrivatekey using nacl.box.keyPair.fromSecretKey(bob.ethereumPrivateKey).secretKey
-Bob passes his encryptionPrivateKey along with the encrypted blob to nacl.box.open(ciphertext, nonce, ephemPublicKey, myencryptionPrivatekey)
Thanks to @zubairnahmed and @gleim