Skip to content
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

Merged
merged 26 commits into from
Dec 30, 2018
Merged

feat(encrypt): add functions to allow encryption and decryption of arbitrary data #241

merged 26 commits into from
Dec 30, 2018

Conversation

trueinsider
Copy link
Contributor

@trueinsider trueinsider commented Jun 17, 2018

Description

There is three new API functions:
getPublicKey -- provides the public key of the currently authenticated account
encrypt -- allows to encrypt arbitrary data
decrypt -- allows to decrypt previously encrypted data

Motivation 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

  • Chore (tests, refactors, and fixes)
  • New feature (adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist:

  • My code follows the code style of this project.
  • The commit message follows our guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)
  • I have read the CONTRIBUTING document.

@mhuggins mhuggins self-assigned this Jun 17, 2018
@mhuggins mhuggins added the PR: needs review Pull request label Jun 17, 2018
Copy link
Contributor

@mhuggins mhuggins left a 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.
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. fixed
  2. 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.
Copy link
Contributor

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.

Copy link
Contributor Author

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.
Copy link
Contributor

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".

Copy link
Contributor Author

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.
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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');
Copy link
Contributor

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?

Copy link
Contributor Author

@trueinsider trueinsider Jun 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. AES-256-CBC: no particular reason, it's good enough
  2. 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 }) => {
Copy link
Contributor

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')
Copy link
Contributor

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?

Copy link
Contributor Author

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', () => {
Copy link
Contributor

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?

Copy link
Contributor Author

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', () => {
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@mhuggins mhuggins added PR: reviewed w/ comments Reviewed but needs to be looked at and removed PR: needs review Pull request labels Jun 17, 2018
@mhuggins
Copy link
Contributor

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.

@trueinsider
Copy link
Contributor Author

I believe I've fixed all points of concern.
Also there is no Buffers in documentation for return values. But input parameters can take string or Buffer.

docs/api.md Outdated
* `config.data` **string** | **Buffer** - The data to decrypt.

#### Returns
**Buffer** - decrypted data
Copy link
Contributor

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.

Copy link
Contributor Author

@trueinsider trueinsider Jun 18, 2018

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.

@jeroenptrs jeroenptrs added this to the v0.3.0 milestone Jun 18, 2018
@mhuggins mhuggins added PR: needs review Pull request and removed PR: reviewed w/ comments Reviewed but needs to be looked at labels Jun 18, 2018
@trueinsider
Copy link
Contributor Author

trueinsider commented Jun 18, 2018

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
Or change API in a way that if no public key is provided in the args then encryption will be using only private key
What do you think?

@trueinsider
Copy link
Contributor Author

trueinsider commented Jun 23, 2018

I was thinking, maybe I should do only getSharedSecret(publicKey) which will generate shared secret from provided public key and user's private key. That way we won't compromise private key and at the same time dApp developer could implement encryption/decryption that he wants/needs, not only hardcoded aes-256-cbc. And I can add encrypt/decrypt example in documentation on how to implement it using getSharedSecret() and aes-256-cbc.

And for encryption of user's own data I could implement getPrivateSecret() which will return sha512 hash of user's private key.

@codecov
Copy link

codecov bot commented Jul 1, 2018

Codecov Report

Merging #241 into develop will increase coverage by 1.87%.
The diff coverage is 100%.

@@             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

@mhuggins
Copy link
Contributor

mhuggins commented Jul 1, 2018

@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.

@DalderupMaurice
Copy link
Member

DalderupMaurice commented Jul 5, 2018

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. :)
cc FYI @mhuggins

Copy link
Contributor

@mhuggins mhuggins left a 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.

@mhuggins mhuggins added PR: good to merge Reviewed and approved PR: don't merge This doesn't seem right and removed PR: needs review Pull request PR: good to merge Reviewed and approved labels Jul 29, 2018
@mhuggins
Copy link
Contributor

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.

@mhuggins mhuggins removed this from the v0.3.0 milestone Sep 8, 2018
@DalderupMaurice
Copy link
Member

DalderupMaurice commented Sep 15, 2018

no more 0.3.0? 🙈

@mhuggins
Copy link
Contributor

@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.

@knaps
Copy link

knaps commented Sep 15, 2018

@mhuggins - I think you have the wrong person! I'm not a contributor to this project

@mhuggins
Copy link
Contributor

Sorry about that!


withData(authActions, mapAuthDataToProps),

withCall(decryptActions, ({ senderPublicKey, wif, iv, mac, data }) => ({
Copy link
Member

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

Copy link
Contributor

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);
Copy link
Member

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);

@DalderupMaurice DalderupMaurice added PR: reviewed w/ comments Reviewed but needs to be looked at and removed PR: don't merge This doesn't seem right labels Dec 28, 2018

withData(authActions, mapAuthDataToProps),

withCall(encryptActions, ({ recipientPublicKey, wif, data }) => ({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
withCall(encryptActions, ({ recipientPublicKey, wif, data }) => ({
withInitialCall(encryptActions, ({ recipientPublicKey, wif, data }) => ({

@@ -0,0 +1,34 @@
import { withCall, withData } from 'spunky';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { withCall, withData } from 'spunky';
import { withData } from 'spunky';

import { pick } from 'lodash';

import authActions from 'login/actions/authActions';

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import withInitialCall from 'shared/hocs/withInitialCall';

import { pick } from 'lodash';

import authActions from 'login/actions/authActions';

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import withInitialCall from 'shared/hocs/withInitialCall';

import { withCall, withData } from 'spunky';
import { compose } from 'recompose';

import authActions from 'login/actions/authActions';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 })),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
withCall(publicKeyActions, ({ wif }) => ({ wif })),
withInitialCall(publicKeyActions, ({ wif }) => ({ wif })),

@@ -0,0 +1,23 @@
import { withCall, withData } from 'spunky';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { withCall, withData } from 'spunky';
import { withData } from 'spunky';


withData(authActions, mapAuthDataToProps),

withCall(decryptActions, ({ senderPublicKey, wif, iv, mac, data }) => ({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
withCall(decryptActions, ({ senderPublicKey, wif, iv, mac, data }) => ({
withInitialCall(decryptActions, ({ senderPublicKey, wif, iv, mac, data }) => ({

@@ -0,0 +1,36 @@
import { withCall, withData } from 'spunky';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { withCall, withData } from 'spunky';
import { withData } from 'spunky';

@DalderupMaurice DalderupMaurice merged commit 4939abf into nos:develop Dec 30, 2018
@DalderupMaurice
Copy link
Member

I'll take these changes in a followup PR

@DalderupMaurice DalderupMaurice added PR: good to merge Reviewed and approved and removed PR: reviewed w/ comments Reviewed but needs to be looked at labels Dec 30, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR: good to merge Reviewed and approved
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants