Skip to content
This repository has been archived by the owner on Apr 15, 2019. It is now read-only.

Commit

Permalink
Merge pull request #855 from LiskHQ/536-add-support-for-message-encry…
Browse files Browse the repository at this point in the history
…ption-and-decryption

Add support for message encryption and decryption - Closes #536
  • Loading branch information
yasharAyari authored Oct 11, 2017
2 parents 8bc1ac2 + 19af65a commit e28052b
Show file tree
Hide file tree
Showing 12 changed files with 500 additions and 2 deletions.
98 changes: 98 additions & 0 deletions src/components/decryptMessage/decryptMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from 'react';
import Input from 'react-toolbox/lib/input';
import Lisk from 'lisk-js';
import { translate } from 'react-i18next';
import SignVerifyResult from '../signVerifyResult';
import ActionBar from '../actionBar';

class DecryptMessage extends React.Component {
constructor() {
super();
this.state = {
result: '',
nonce: {
value: '',
},
message: {
value: '',
},
senderPublicKey: {
value: '',
},
};
}

handleChange(name, value, error) {
this.setState({
[name]: {
value,
error,
},
});
}

showResult(event) {
event.preventDefault();
let decryptedMessage = null;
try {
decryptedMessage = Lisk.crypto.decryptMessageWithSecret(
this.state.message.value,
this.state.nonce.value,
this.props.account.passphrase,
this.state.senderPublicKey.value);
} catch (error) {
this.props.errorToast({ label: error.message });
}
if (decryptedMessage) {
const result = [
'-----DECRYPTED MESSAGE-----',
decryptedMessage,
].join('\n');
this.setState({ result, resultIsShown: false });
this.setState({ resultIsShown: true });
this.props.successToast({ label: this.props.t('Message is decrypted successfully') });
}
}

render() {
return (
<div className='sign-message'>
<form onSubmit={this.showResult.bind(this)}>
<section>
<Input className='senderPublicKey' label={this.props.t('Sender PublicKey')}
autoFocus={true}
value={this.state.senderPublicKey.value}
onChange={this.handleChange.bind(this, 'senderPublicKey')} />
<Input className='nonce' label={this.props.t('Nonce')}
autoFocus={true}
value={this.state.nonce.value}
onChange={this.handleChange.bind(this, 'nonce')} />
<Input className='message' multiline label={this.props.t('Message')}
autoFocus={true}
value={this.state.message.value}
onChange={this.handleChange.bind(this, 'message')} />

</section>
{this.state.resultIsShown ?
<SignVerifyResult result={this.state.result} title={this.props.t('Result')} /> :
<ActionBar
secondaryButton={{
onClick: this.props.closeDialog,
}}
primaryButton={{
label: this.props.t('decrypt'),
className: 'sign-button',
type: 'submit',
disabled: (this.state.message.value.length === 0 ||
this.state.senderPublicKey.value.length === 0 ||
this.state.nonce.value.length === 0
),
}} />
}
</form>
</div>
);
}
}

export default translate()(DecryptMessage);
83 changes: 83 additions & 0 deletions src/components/decryptMessage/decryptMessage.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React from 'react';
import { expect } from 'chai';
import { mount } from 'enzyme';
import sinon from 'sinon';
import { Provider } from 'react-redux';
import { I18nextProvider } from 'react-i18next';
import Lisk from 'lisk-js';
import i18n from '../../i18n';
import store from '../../store';
import DecryptMessage from './decryptMessage';


describe('DecryptMessage', () => {
let wrapper;
let successToastSpy;
let errorSpy;
let copyMock;
let decryptMessageMock;
const senderPublicKey = '164a0580cd2b430bc3496f82adf51b799546a3a4658bb9dca550a0e20cb579c8';
const message = 'Hello world';
const decryptedMessage = 'Decrypted Hello world';
const nonce = 'this is nonce';
const publicKey = '164a0580cd2b430bc3496f82adf51b799546a3a4658bb9dca550a0e20cb579c8';
const account = {
passphrase: 'wagon stock borrow episode laundry kitten salute link globe zero feed marble',
publicKey,
};

beforeEach(() => {
successToastSpy = sinon.spy();
errorSpy = sinon.spy();
copyMock = sinon.mock();
decryptMessageMock = sinon.stub(Lisk.crypto, 'decryptMessageWithSecret');
// decryptMessageSpy = sinon.spy(Lisk.crypto, 'decryptMessageWithSecret');
const props = {
account,
successToast: successToastSpy,
errorToast: sinon.spy(),
copyToClipboard: copyMock,
t: key => key,
};

wrapper = mount(<Provider store={store}>
<I18nextProvider i18n={ i18n }>
<DecryptMessage {...props} />
</I18nextProvider>
</Provider>);
});

afterEach(() => {
decryptMessageMock.restore();
});

// ToDo find the problem with this test
it.skip('shows error toast when couldn\'t decrypt a message', () => {
decryptMessageMock.returnsPromise().rejects({ message: 'couldn\'t decrypt the message' });
wrapper.find('.message textarea').simulate('change', { target: { value: message } });
wrapper.find('.senderPublicKey input').simulate('change', { target: { value: senderPublicKey } });
wrapper.find('.nonce input').simulate('change', { target: { value: nonce } });
wrapper.find('form').simulate('submit');
expect(errorSpy).to.have.been.calledOnce();
expect(errorSpy).to.have.been.calledWith({ label: 'couldn\'t decrypt the message' });
});

it('allows to decrypt a message, copies encrypted message result to clipboard and shows success toast', () => {
copyMock.returns(true);
decryptMessageMock.returnsPromise().resolves(decryptedMessage);
wrapper.find('.message textarea').simulate('change', { target: { value: message } });
wrapper.find('.senderPublicKey input').simulate('change', { target: { value: senderPublicKey } });
wrapper.find('.nonce input').simulate('change', { target: { value: nonce } });
wrapper.find('form').simulate('submit');
expect(successToastSpy).to.have.been.calledWith({ label: 'Message is decrypted successfully' });
});

it('does not show success toast if copy-to-clipboard failed', () => {
copyMock.returns(false);
wrapper.find('.message textarea').simulate('change', { target: { value: message } });
wrapper.find('.senderPublicKey input').simulate('change', { target: { value: senderPublicKey } });
wrapper.find('.nonce input').simulate('change', { target: { value: nonce } });
wrapper.find('.primary-button').simulate('click');
expect(successToastSpy).to.have.not.been.calledWith();
});
});
20 changes: 20 additions & 0 deletions src/components/decryptMessage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { connect } from 'react-redux';
import { translate } from 'react-i18next';
import copy from 'copy-to-clipboard';
import { successToastDisplayed, errorToastDisplayed } from '../../actions/toaster';
import DecryptMessage from './decryptMessage';

const mapStateToProps = state => ({
account: state.account,
});

const mapDispatchToProps = dispatch => ({
successToast: data => dispatch(successToastDisplayed(data)),
errorToast: data => dispatch(errorToastDisplayed(data)),
copyToClipboard: (...args) => copy(...args),
});

export default connect(
mapStateToProps,
mapDispatchToProps,
)(translate()(DecryptMessage));
40 changes: 40 additions & 0 deletions src/components/decryptMessage/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { expect } from 'chai';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import sinon from 'sinon';
import i18n from '../../i18n';
import * as toasterActions from '../../actions/toaster';
import store from '../../store';
import DecryptMessageHOC from './index';
import DecryptMessage from './decryptMessage';

describe('DecryptMessageHOC', () => {
let props;
let wrapper;

beforeEach(() => {
wrapper = mount(<Provider store={store}><DecryptMessageHOC i18n={i18n}/></Provider>);
props = wrapper.find(DecryptMessage).props();
});

it('should render the decryptMessage with props.successToast and props.copyToClipboard and props.errorToast', () => {
expect(wrapper.find(DecryptMessage).exists()).to.equal(true);
expect(typeof wrapper.find(DecryptMessage).props().successToast).to.equal('function');
expect(typeof wrapper.find(DecryptMessage).props().copyToClipboard).to.equal('function');
});

it('should bind successToastDisplayed action to DecryptMessageComponent props.successToast', () => {
const actionsSpy = sinon.spy(toasterActions, 'successToastDisplayed');
props.successToast({});
expect(actionsSpy).to.be.calledWith();
actionsSpy.restore();
});

it('should bind errorToastDisplayed action to DecryptMessageComponent props.errorToast', () => {
const actionsSpy = sinon.spy(toasterActions, 'errorToastDisplayed');
props.errorToast({});
expect(actionsSpy).to.be.calledWith();
actionsSpy.restore();
});
});
10 changes: 10 additions & 0 deletions src/components/dialog/dialogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Settings from '../settings';
import SignMessage from '../signMessage';
import VerifyMessage from '../verifyMessage';
import VoteDialog from '../voteDialog';
import EncryptMessage from '../encryptMessage';
import DecryptMessage from '../decryptMessage';

export default () => ({
send: {
Expand Down Expand Up @@ -51,4 +53,12 @@ export default () => ({
title: i18next.t('Settings'),
component: Settings,
},
'encrypt-message': {
title: i18next.t('Encrypt message'),
component: EncryptMessage,
},
'decrypt-message': {
title: i18next.t('Decrypt message'),
component: DecryptMessage,
},
});
106 changes: 106 additions & 0 deletions src/components/encryptMessage/encryptMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React from 'react';
import Input from 'react-toolbox/lib/input';
import Lisk from 'lisk-js';
import { translate } from 'react-i18next';
import InfoParagraph from '../infoParagraph';
import SignVerifyResult from '../signVerifyResult';
import ActionBar from '../actionBar';


class EncryptMessage extends React.Component {
constructor() {
super();
this.state = {
result: '',
recipientPublicKey: {
value: '',
},
message: {
value: '',
},
};
}

handleChange(name, value, error) {
this.setState({
[name]: {
value,
error,
},
});
}

encrypt(event) {
event.preventDefault();
let cryptoResult = null;
try {
cryptoResult = Lisk.crypto.encryptMessageWithSecret(
this.state.message.value,
this.props.account.passphrase,
this.state.recipientPublicKey.value);
} catch (error) {
this.props.errorToast({ label: error.message });
}
if (cryptoResult) {
const result = [
'-----ENCRYPTED MESSAGE-----',
cryptoResult.encryptedMessage,
'-----NONCE-----',
cryptoResult.nonce,
].join('\n');
this.setState({ result, resultIsShown: false });
this.showResult();
}
}

showResult() {
const copied = this.props.copyToClipboard(this.state.result, {
message: this.props.t('Press #{key} to copy'),
});
if (copied) {
this.props.successToast({ label: this.props.t('Result copied to clipboard') });
}
this.setState({ resultIsShown: true });
}

render() {
return (
<div className='sign-message'>
<form onSubmit={this.encrypt.bind(this)}>
<section>
<InfoParagraph>
<h3>
{this.props.t('Public key : ')}
</h3>
{this.props.account.publicKey}
</InfoParagraph>
<Input className='recipientPublicKey' label={this.props.t('Recipient PublicKey')}
autoFocus={true}
value={this.state.recipientPublicKey.value}
onChange={this.handleChange.bind(this, 'recipientPublicKey')} />
<Input className='message' multiline label={this.props.t('Message')}
autoFocus={true}
value={this.state.message.value}
onChange={this.handleChange.bind(this, 'message')} />
</section>
{this.state.resultIsShown ?
<SignVerifyResult id='encryptResult' result={this.state.result} title={this.props.t('Result')} /> :
<ActionBar
secondaryButton={{
onClick: this.props.closeDialog,
}}
primaryButton={{
label: this.props.t('encrypt'),
className: 'sign-button',
type: 'submit',
disabled: (this.state.message.value.length === 0 ||
this.state.recipientPublicKey.value.length === 0),
}} />
}
</form>
</div>
);
}
}

export default translate()(EncryptMessage);
Loading

0 comments on commit e28052b

Please sign in to comment.