-
Notifications
You must be signed in to change notification settings - Fork 73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(encrypt): add functions to allow encryption and decryption of arbitrary data #241
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, this looks like a great addition. I also appreciate the thoroughness of the tests added, thank you for that!
I added a few comments that I'd like to have addressed before merging, I'm always open to discussion around any comments I left as well.
I'd like to verify some of the questions I left regarding passing a Buffer
via IPC too. I'll circle back on that unless you beat me to it.
docs/api.md
Outdated
|
||
#### Parameters | ||
* `recipientPublicKey` **string** - The public key of the recipient account. | ||
* `data` **string** | **Buffer** - The data to encrypt. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This documentation should be restructured a bit to clarify that it's a single object argument, and that recipientPublicKey
and data
are keys on the object. i.e.:
* `config` **object** - The config options to perform this operation.
* `config.recipientPublicKey` **string** - The public key of the recipient account.
* `config.data` **string | Buffer** - The data to encrypt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thing worth mentioning is that I'm not confident passing a Buffer
data type will work due to the way IPC is implemented. Per the electron documentation:
Arguments will be serialized in JSON internally and hence no functions or prototype chain will be included.
There is a JSON representation of a Buffer, but I doubt that electron elegantly parses and converts it back to a Buffer on the other end.
const buffer = Buffer.from("foo");
// <Buffer 66 6f 6f>
const serializedBuffer = JSON.stringify(buffer);
// '{"type":"Buffer","data":[102,111,111]}'
const deserializedBuffer = JSON.parse(serializedBuffer);
// { type: 'Buffer', data: [ 102, 111, 111 ] }
I will try to verify if this is accurate, but something that will need to be verified before accepting as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- fixed
- I believe this is not a concern anymore as electron seems to support Buffers
docs/api.md
Outdated
The `encrypt` function allows you to encrypt arbitrary data for another user (you will need his public key, please see `getPublicKey`). It does not require the user to grant permission. | ||
|
||
#### Parameters | ||
* `recipientPublicKey` **string** - The public key of the recipient account. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just call this publicKey
? I think you're making an assumption that the data being serialized will be sent to another address, when it could just be for storing something pertaining to the current account's address.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I called it recipientPublicKey to clarify that it's not your public key that you need to pass to the function.
The way ECIES/ECDH work is that when encrypting I take my private key and your public key
and when decrypting you take your private key and my public key.
That way we'll get same shared secret without exchanging our private keys.
If you want to just encrypt some data for yourself you don't need shared secret or public key, you can just use hash of your private key as encryption key. Of course, then you will need private key for decryption too.
docs/api.md
Outdated
The `decrypt` function allows you to decrypt previously encrypted data for this user (you will need public key of the sender account, please see `getPublicKey`). It does not require the user to grant permission. | ||
|
||
#### Parameters | ||
* `senderPublicKey` **string** - The public key of the sender account. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here with calling this publicKey
. The description for this field could be clarify which key by stating something like "the public key used to encrypt the data".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Answered above
docs/api.md
Outdated
* `senderPublicKey` **string** - The public key of the sender account. | ||
* `iv` **string** - The IV received during encryption. | ||
* `mac` **string** - The MAC received during encryption. | ||
* `data` **string** | **Buffer** - The data to decrypt. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same concern here regarding Buffer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is not a concern anymore as electron seems to support Buffers
docs/api.md
Outdated
* `data` **string** | **Buffer** - The data to decrypt. | ||
|
||
#### Returns | ||
**Buffer** - decrypted data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't believe we'll be able to return a Buffer
for the same reason, but will verify. If not, then we should just make this a string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is not a concern anymore as electron seems to support Buffers
} | ||
|
||
const decrypt = ({ senderPublicKey, wif, iv, mac, data }) => { | ||
const ecdh = ECDH('prime256v1'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What factored into the decision to use AES-256-CBC above and prime256v1 here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- AES-256-CBC: no particular reason, it's good enough
- prime256v1 = secp256r1: this is what used by NEO itself according to this: https://github.com/neo-project/neo/blob/master/neo/Wallets/KeyPair.cs#L26
return Buffer.concat([firstChunk, secondChunk]); | ||
} | ||
|
||
const decrypt = ({ senderPublicKey, wif, iv, mac, data }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't taken a thorough look through the encryption/decryption logic yet. Adding a note here to ensure that happens before this PR is merged.
return { | ||
iv: iv.toString('hex'), | ||
mac: mac.toString('hex'), | ||
data: encrypted.toString('hex') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like data
is actually a string, not a Buffer
. Is the documentation just incorrect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
documentation for result doesn't mention Buffer anywhere
}); | ||
}); | ||
|
||
describe('when decrypted data has loaded', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All these tests show the happy path, which is great. Can you add a test for when decryption fails to ensure that onReject
is called and onResolve
is not called?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
}); | ||
}); | ||
|
||
describe('when encrypted data has loaded', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, can a test be added for the unhappy path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
Did a little digging on the electron side, and buffers may actually be supported. I haven't tested yet. Also, I think the documentation you added for the return value is still wrong because it's returning a string, not a buffer. |
I believe I've fixed all points of concern. |
docs/api.md
Outdated
* `config.data` **string** | **Buffer** - The data to decrypt. | ||
|
||
#### Returns | ||
**Buffer** - decrypted data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@trueinsider This is the return value of type Buffer
I was referring to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, you right. but it actually returns Buffer.
inside decrypt()
:
return aes256CbcDecrypt(ivBuffer, encryptionKey, dataBuffer);
and inside aes256CbcDecrypt()
:
return Buffer.concat([firstChunk, secondChunk]);
it's encrypt()
that returns hex-encoded string and documentation of encrypt result doesn't mention Buffers. maybe I should return Buffers here too.
I think maybe I should rename these functions to encryptFor/decryptFrom and then make another pair to just encrypt/decrypt stuff for myself using my private key |
I was thinking, maybe I should do only And for encryption of user's own data I could implement |
Codecov Report
@@ Coverage Diff @@
## develop #241 +/- ##
===========================================
+ Coverage 53.44% 55.32% +1.87%
===========================================
Files 207 220 +13
Lines 1783 1858 +75
Branches 249 250 +1
===========================================
+ Hits 953 1028 +75
Misses 691 691
Partials 139 139 |
@trueinsider I'm communicating with the team regarding your last question here. I'll circle back with you and let you know what we settle on. |
Had a small discussing concluding that for the moment being - the additional feature is a too specific use-case. It's also no high risk of implementing it on a later stage, so we're happy to finalize this PR and potentially add in the additional feature later and keeping it simple to begin with. :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a few commits at the end here as well as moving the documentation to nos/documentation via nos/documentation#10. I think this is good, but I want to check with @deanpress how he feels about merging this before/after having a security expert review it.
This change looks good to me, but I'm labeling with "don't merge" until we get a more in depth security review. At this point, no code changes are deemed needed prior to merging. |
no more 0.3.0? 🙈 |
@DalderupMaurice @deanpress since we're waiting on a review from @knaps, I can't guarantee it will happen before we're ready to release 0.3.0. |
@mhuggins - I think you have the wrong person! I'm not a contributor to this project |
Sorry about that! |
|
||
withData(authActions, mapAuthDataToProps), | ||
|
||
withCall(decryptActions, ({ senderPublicKey, wif, iv, mac, data }) => ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We currently use withInitialCall
everywhere for the API, is this something we should address before merging? @mhuggins
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would potentially be safer here to ensure we don't inadvertently call the action twice (in case of a re-render for any reason).
import { randomBytes } from 'crypto'; | ||
|
||
export default function getRandomIV() { | ||
return randomBytes(16); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would we maybe want the ability to choose the strength?
Having an optional parameter with 128 as default, and having return randomBytes(strength /8);
|
||
withData(authActions, mapAuthDataToProps), | ||
|
||
withCall(encryptActions, ({ recipientPublicKey, wif, data }) => ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
withCall(encryptActions, ({ recipientPublicKey, wif, data }) => ({ | |
withInitialCall(encryptActions, ({ recipientPublicKey, wif, data }) => ({ |
@@ -0,0 +1,34 @@ | |||
import { withCall, withData } from 'spunky'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import { withCall, withData } from 'spunky'; | |
import { withData } from 'spunky'; |
import { pick } from 'lodash'; | ||
|
||
import authActions from 'login/actions/authActions'; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import withInitialCall from 'shared/hocs/withInitialCall'; |
import { pick } from 'lodash'; | ||
|
||
import authActions from 'login/actions/authActions'; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import withInitialCall from 'shared/hocs/withInitialCall'; |
import { withCall, withData } from 'spunky'; | ||
import { compose } from 'recompose'; | ||
|
||
import authActions from 'login/actions/authActions'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import authActions from 'login/actions/authActions'; | |
import authActions from 'login/actions/authActions'; | |
import withInitialCall from 'shared/hocs/withInitialCall'; |
|
||
withData(authActions, mapAuthDataToProps), | ||
|
||
withCall(publicKeyActions, ({ wif }) => ({ wif })), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
withCall(publicKeyActions, ({ wif }) => ({ wif })), | |
withInitialCall(publicKeyActions, ({ wif }) => ({ wif })), |
@@ -0,0 +1,23 @@ | |||
import { withCall, withData } from 'spunky'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import { withCall, withData } from 'spunky'; | |
import { withData } from 'spunky'; |
|
||
withData(authActions, mapAuthDataToProps), | ||
|
||
withCall(decryptActions, ({ senderPublicKey, wif, iv, mac, data }) => ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
withCall(decryptActions, ({ senderPublicKey, wif, iv, mac, data }) => ({ | |
withInitialCall(decryptActions, ({ senderPublicKey, wif, iv, mac, data }) => ({ |
@@ -0,0 +1,36 @@ | |||
import { withCall, withData } from 'spunky'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import { withCall, withData } from 'spunky'; | |
import { withData } from 'spunky'; |
I'll take these changes in a followup PR |
Description
There is three new API functions:
getPublicKey
-- provides the public key of the currently authenticated accountencrypt
-- allows to encrypt arbitrary datadecrypt
-- allows to decrypt previously encrypted dataMotivation and Context
To securely exchange messages in the decentralized applications context some kind of encryption is necessary.
How Has This Been Tested?
Tested in client connected to nos-local with the help of slightly modified template dApp. Also there is new tests added.
Types of changes
Checklist: