diff --git a/src/components/decryptMessage/decryptMessage.js b/src/components/decryptMessage/decryptMessage.js new file mode 100644 index 000000000..8ca91ea13 --- /dev/null +++ b/src/components/decryptMessage/decryptMessage.js @@ -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 ( +
+
+
+ + + + +
+ {this.state.resultIsShown ? + : + + } + +
+ ); + } +} + +export default translate()(DecryptMessage); diff --git a/src/components/decryptMessage/decryptMessage.test.js b/src/components/decryptMessage/decryptMessage.test.js new file mode 100644 index 000000000..121c1844e --- /dev/null +++ b/src/components/decryptMessage/decryptMessage.test.js @@ -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( + + + + ); + }); + + 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(); + }); +}); diff --git a/src/components/decryptMessage/index.js b/src/components/decryptMessage/index.js new file mode 100644 index 000000000..5b95b0bf1 --- /dev/null +++ b/src/components/decryptMessage/index.js @@ -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)); diff --git a/src/components/decryptMessage/index.test.js b/src/components/decryptMessage/index.test.js new file mode 100644 index 000000000..91cbd542d --- /dev/null +++ b/src/components/decryptMessage/index.test.js @@ -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(); + 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(); + }); +}); diff --git a/src/components/dialog/dialogs.js b/src/components/dialog/dialogs.js index aeeb78683..95f1ea0fd 100644 --- a/src/components/dialog/dialogs.js +++ b/src/components/dialog/dialogs.js @@ -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: { @@ -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, + }, }); diff --git a/src/components/encryptMessage/encryptMessage.js b/src/components/encryptMessage/encryptMessage.js new file mode 100644 index 000000000..032c3a2f2 --- /dev/null +++ b/src/components/encryptMessage/encryptMessage.js @@ -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 ( +
+
+
+ +

+ {this.props.t('Public key : ')} +

+ {this.props.account.publicKey} +
+ + +
+ {this.state.resultIsShown ? + : + + } + +
+ ); + } +} + +export default translate()(EncryptMessage); diff --git a/src/components/encryptMessage/encryptMessage.test.js b/src/components/encryptMessage/encryptMessage.test.js new file mode 100644 index 000000000..f377984c7 --- /dev/null +++ b/src/components/encryptMessage/encryptMessage.test.js @@ -0,0 +1,65 @@ +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 EncryptMessage from './encryptMessage'; + + +describe('EncryptMessage', () => { + let wrapper; + let successToastSpy; + let copyMock; + let encryptMessageSpy; + const recipientPublicKey = '164a0580cd2b430bc3496f82adf51b799546a3a4658bb9dca550a0e20cb579c8'; + const message = 'Hello world'; + const publicKey = '164a0580cd2b430bc3496f82adf51b799546a3a4658bb9dca550a0e20cb579c8'; + const account = { + passphrase: 'wagon stock borrow episode laundry kitten salute link globe zero feed marble', + publicKey, + }; + + beforeEach(() => { + successToastSpy = sinon.spy(); + copyMock = sinon.mock(); + encryptMessageSpy = sinon.spy(Lisk.crypto, 'encryptMessageWithSecret'); + const props = { + account, + successToast: successToastSpy, + copyToClipboard: copyMock, + t: key => key, + }; + + wrapper = mount( + + + + ); + }); + + afterEach(() => { + encryptMessageSpy.restore(); + }); + + it('allows to encrypt a message, copies encrypted message result to clipboard and shows success toast', () => { + copyMock.returns(true); + wrapper.find('.message textarea').simulate('change', { target: { value: message } }); + wrapper.find('.recipientPublicKey input').simulate('change', { target: { value: recipientPublicKey } }); + wrapper.find('form').simulate('submit'); + expect(encryptMessageSpy).to.have.been + .calledWith(message, account.passphrase, recipientPublicKey); + expect(successToastSpy).to.have.been.calledWith({ label: 'Result copied to clipboard' }); + }); + + 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('.recipientPublicKey input').simulate('change', { target: { value: recipientPublicKey } }); + wrapper.find('.primary-button').simulate('click'); + expect(successToastSpy).to.have.not.been.calledWith(); + }); +}); diff --git a/src/components/encryptMessage/index.js b/src/components/encryptMessage/index.js new file mode 100644 index 000000000..9522e2848 --- /dev/null +++ b/src/components/encryptMessage/index.js @@ -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 EncryptMessage from './encryptMessage'; + +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()(EncryptMessage)); diff --git a/src/components/encryptMessage/index.test.js b/src/components/encryptMessage/index.test.js new file mode 100644 index 000000000..6c6867e98 --- /dev/null +++ b/src/components/encryptMessage/index.test.js @@ -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 EncryptMessageHOC from './index'; +import EncryptMessage from './encryptMessage'; + +describe('EncryptMessageHOC', () => { + let props; + let wrapper; + + beforeEach(() => { + wrapper = mount(); + props = wrapper.find(EncryptMessage).props(); + }); + + it('should render the encryptMessage with props.successToast and props.copyToClipboard and props.errorToast', () => { + expect(wrapper.find(EncryptMessage).exists()).to.equal(true); + expect(typeof wrapper.find(EncryptMessage).props().successToast).to.equal('function'); + expect(typeof wrapper.find(EncryptMessage).props().copyToClipboard).to.equal('function'); + }); + + it('should bind successToastDisplayed action to EncryptMessageComponent props.successToast', () => { + const actionsSpy = sinon.spy(toasterActions, 'successToastDisplayed'); + props.successToast({}); + expect(actionsSpy).to.be.calledWith(); + actionsSpy.restore(); + }); + + it('should bind errorToastDisplayed action to EncryptMessageComponent props.errorToast', () => { + const actionsSpy = sinon.spy(toasterActions, 'errorToastDisplayed'); + props.errorToast({}); + expect(actionsSpy).to.be.calledWith(); + actionsSpy.restore(); + }); +}); diff --git a/src/components/header/header.js b/src/components/header/header.js index d0c471acd..c1a161ca1 100644 --- a/src/components/header/header.js +++ b/src/components/header/header.js @@ -44,6 +44,14 @@ const Header = props => ( {props.t('Verify message')} + + {props.t('Encrypt message')} + + + {props.t('Decrypt message')} + diff --git a/src/components/header/header.test.js b/src/components/header/header.test.js index 3e9e846e4..3fd74d72b 100644 --- a/src/components/header/header.test.js +++ b/src/components/header/header.test.js @@ -45,8 +45,8 @@ describe('Header', () => { expect(wrapper.find(Button)).to.have.length(1); }); - it('renders 7 RelativeLink components', () => { - expect(wrapper.find(RelativeLink)).to.have.length(7); + it('renders 9 RelativeLink components', () => { + expect(wrapper.find(RelativeLink)).to.have.length(9); }); it('should have an image with srouce of "logo"', () => { diff --git a/src/locales/en/common.json b/src/locales/en/common.json index 486ca6b6f..5a5cf16dc 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -17,11 +17,13 @@ "Connection re-established": "Connection re-established", "Copy address to clipboard": "Copy address to clipboard", "Custom Node": "Custom Node", + "Decrypt message": "Decrypt message", "Delegate": "Delegate", "Delegate Registration": "Delegate Registration", "Delegate name": "Delegate name", "Delegate registration was successfully submitted with username: \"{{username}}\". It can take several seconds before it is processed.": "Delegate registration was successfully submitted with username: \"{{username}}\". It can take several seconds before it is processed.", "Downvotes:": "Downvotes:", + "Encrypt message": "Encrypt message", "Enter the missing word": "Enter the missing word", "Enter your passphrase": "Enter your passphrase", "Entered passphrase does not belong to the active account": "Entered passphrase does not belong to the active account", @@ -58,6 +60,7 @@ "No delegates found": "No delegates found", "No matches": "No matches", "Node address": "Node address", + "Nonce": "Nonce", "Note: After registration completes,": "Note: After registration completes,", "Note: Digital Signatures and signed messages are not encrypted!": "Note: Digital Signatures and signed messages are not encrypted!", "Ok": "Ok", @@ -69,9 +72,11 @@ "Please keep it safe!": "Please keep it safe!", "Press #{key} to copy": "Press #{key} to copy", "Public Key": "Public Key", + "Public key : ": "Public key : ", "Rank": "Rank", "Receive LSK": "Receive LSK", "Recipient Address": "Recipient Address", + "Recipient PublicKey": "Recipient PublicKey", "Register": "Register", "Register Second Passphrase": "Register Second Passphrase", "Register as delegate": "Register as delegate", @@ -95,6 +100,7 @@ "Send Lisk from Blockchain Application": "Send Lisk from Blockchain Application", "Send Lisk to Blockchain Application": "Send Lisk to Blockchain Application", "Send to this address": "Send to this address", + "Sender PublicKey": "Sender PublicKey", "Set maximum amount": "Set maximum amount", "Settings": "Settings", "Show passphrase": "Show passphrase", @@ -137,6 +143,8 @@ "Zero not allowed": "Zero not allowed", "confirmation": "confirmation", "confirmations": "confirmations", + "decrypt": "decrypt", + "encrypt": "encrypt", "logout": "logout", "my votes": "my votes", "send": "send",