Skip to content

Commit

Permalink
feat(Bitcoin): Add "Verify Message" modal to validate the signature o…
Browse files Browse the repository at this point in the history
…f a Bitcoin message.

fix #WEB4-500
  • Loading branch information
Empowerful committed Dec 10, 2018
1 parent 0d96e1c commit 525574d
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,12 @@ class Tooltips extends React.PureComponent {
defaultMessage='Refresh'
/>
</Tooltip>
<Tooltip id='verifyMessage'>
<FormattedMessage
id='modals.verifymessage.tooltip'
defaultMessage='Verify a message signed by the owner of a particular Bitcoin address.'
/>
</Tooltip>
<Tooltip id='whatsnew.tooltip'>
<FormattedMessage
id='whatsnew.tooltip.description'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import BitcoinMessage from 'bitcoinjs-message'

export const showResult = ({ address, message, signature }) =>
Boolean(address && message && signature)

export const verifySignature = ({ address, message, signature }) => {
// Exceptions are thrown if the signature is malformed or doesn't match the
// address.
try {
return BitcoinMessage.verify(message, address, signature)
} catch (exception) {
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as core from './core'

describe(`showResult`, () => {
it(`none`, () => {
expect(
core.showResult({ address: ``, message: ``, signature: `` })
).toEqual(false)
})

it(`some`, () => {
expect(
core.showResult({
address: `address`,
message: `message`,
signature: ``
})
).toEqual(false)
})

it(`all`, () => {
expect(
core.showResult({
address: `address`,
message: `message`,
signature: `signature`
})
).toEqual(true)
})
})

describe(`verifySignature`, () => {
it(`happy`, () => {
expect(
core.verifySignature({
address: `15WJg3bnKkuzeLxCKUGCKjRM42d3LgWg9u`,
message: `Fun is nuf spelled backwards!`,
signature: `HxB1FMYnYLda3SDhFVXpfyUfxfGOUOuCkrY0rwxIFaXzWgv/y7Wlijd9C/2t6ydoVOpi4v5XedgvDDOb8a6ZCfo=`
})
).toEqual(true)
})

it(`bad signature`, () => {
expect(
core.verifySignature({
address: `15WJg3bnKkuzeLxCKUGCKjRM42d3LgWg9u`,
message: `Fun is nuf spelled backwards!`,
signature: `bad signature`
})
).toEqual(false)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Verify the signature of a message signed with a Bitcoin address.

import {
Banner,
Button,
Modal,
ModalHeader,
ModalBody,
ModalFooter,
TooltipIcon,
TooltipHost
} from 'blockchain-info-components'

import { FormItem, FormLabel, TextArea, TextBox } from 'components/Form'
import * as core from './core'
import modalEnhancer from 'providers/ModalEnhancer'
import React from 'react'
import { FormattedMessage } from 'react-intl'
import { validBitcoinAddress } from 'services/FormHelper'

const item = (label, input) => (
<FormItem style={{ marginBottom: `1rem` }}>
<FormLabel>
<div style={{ marginBottom: `0.5rem` }}>{label}</div>
{input}
</FormLabel>
</FormItem>
)

const ItemAddress = ({ address, network, onChange }) =>
item(
<FormattedMessage
id='modals.verifyMessage.address'
defaultMessage='Bitcoin Address:'
/>,
<TextBox
input={{
onChange,
name: 'address'
}}
meta={{
error: validBitcoinAddress(address, null, { network }),
touched: address !== ``
}}
/>
)

const ItemMessage = ({ onChange }) =>
item(
<FormattedMessage
id='modals.verifyMessage.message'
defaultMessage='Message:'
/>,
<TextArea
input={{
name: 'message',
onChange
}}
meta={{}}
/>
)

const ItemSignature = ({ onChange }) =>
item(
<FormattedMessage
id='modals.verifyMessage.signature'
defaultMessage='Signature:'
/>,
<TextArea
input={{
name: 'signature',
onChange
}}
meta={{}}
/>
)

class VerifyMessage extends React.PureComponent {
constructor (props) {
super(props)
this.state = { address: ``, message: ``, signature: `` }
}

render () {
const { close, network } = this.props

const onChange = ({ target: { name, value } }) => {
this.setState({ [name]: value })
}

return (
<Modal>
<ModalHeader onClose={close}>
<FormattedMessage
id='modals.verifyMessage.title'
defaultMessage='Verify Message'
/>
<TooltipHost id='verifyMessage'>
<TooltipIcon name='question-in-circle' />
</TooltipHost>
</ModalHeader>
<ModalBody>
<ItemAddress
address={this.state.address}
network={network}
onChange={onChange}
/>
<ItemMessage onChange={onChange} />
<ItemSignature onChange={onChange} />
<div
style={{
visibility: core.showResult(this.state) ? `visible` : `hidden`
}}
>
{core.verifySignature(this.state) ? (
<Banner type='success'>
<FormattedMessage
id='modals.verifyMessage.success'
defaultMessage='The message has a valid signature from the address.'
/>
</Banner>
) : (
<Banner type='caution'>
<FormattedMessage
id='modals.verifyMessage.failure'
defaultMessage='The signature does not match the message.'
/>
</Banner>
)}
</div>
</ModalBody>
<ModalFooter align='right'>
<Button onClick={close} nature='primary'>
<FormattedMessage id='close' defaultMessage='Close' />
</Button>
</ModalFooter>
</Modal>
)
}
}

export default modalEnhancer('VerifyMessage')(VerifyMessage)
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import ImportBtcAddress from './ImportBtcAddress'
import RequestBtc from './RequestBtc'
import SendBtc from './SendBtc'
import ShowBtcPrivateKey from './ShowBtcPrivateKey'
import VerifyMessage from './VerifyMessage'

export {
AddBtcWallet,
ImportBtcAddress,
RequestBtc,
SendBtc,
ShowBtcPrivateKey
ShowBtcPrivateKey,
VerifyMessage
}
4 changes: 3 additions & 1 deletion packages/blockchain-wallet-v4-frontend/src/modals/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
ImportBtcAddress,
RequestBtc,
SendBtc,
ShowBtcPrivateKey
ShowBtcPrivateKey,
VerifyMessage
} from './Btc'
import {
CoinifyDeleteBank,
Expand Down Expand Up @@ -122,6 +123,7 @@ const Modals = props => (
<XlmCreateAccountLearn />
<XlmReserveLearn />
<SunRiverWelcome disableOutsideClose />
<VerifyMessage />
</div>
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class ImportedAddressesContainer extends React.Component {
constructor (props) {
super(props)
this.handleClickImport = this.handleClickImport.bind(this)
this.handleClickVerify = this.handleClickVerify.bind(this)
this.handleToggleArchived = this.handleToggleArchived.bind(this)
this.handleShowPriv = this.handleShowPriv.bind(this)
this.handleSignMessage = this.handleSignMessage.bind(this)
Expand All @@ -24,6 +25,10 @@ class ImportedAddressesContainer extends React.Component {
this.props.modalsActions.showModal('ImportBtcAddress')
}

handleClickVerify () {
this.props.modalsActions.showModal('VerifyMessage')
}

handleShowPriv (address) {
this.props.modalsActions.showModal('ShowBtcPrivateKey', {
addr: address.addr,
Expand All @@ -49,6 +54,7 @@ class ImportedAddressesContainer extends React.Component {
<Success
importedAddresses={value}
onClickImport={this.handleClickImport}
onClickVerify={this.handleClickVerify}
search={search && search.toLowerCase()}
onToggleArchived={this.handleToggleArchived}
onShowPriv={this.handleShowPriv}
Expand All @@ -60,6 +66,7 @@ class ImportedAddressesContainer extends React.Component {
failure
importedAddresses={values(addressesWithoutRemoteData)}
onClickImport={this.handleClickImport}
onClickVerify={this.handleClickVerify}
search={search && search.toLowerCase()}
onToggleArchived={this.handleToggleArchived}
onShowSignMessage={this.handleSignMessage}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import styled from 'styled-components'
import { FormattedMessage } from 'react-intl'
import { SettingDescription, SettingHeader } from 'components/Setting'
import {
Button,
Icon,
IconButton,
Table,
Expand All @@ -18,7 +19,8 @@ const Wrapper = styled.section`
box-sizing: border-box;
`
const ImportedAddressesSettingHeader = SettingHeader.extend`
justify-content: flex-start;
align-items: flex-start;
justify-content: space-between;
margin-top: 30px;
`

Expand All @@ -36,6 +38,7 @@ const ClickableText = styled(Text)`
const Success = ({
importedAddresses,
onClickImport,
onClickVerify,
onToggleArchived,
onShowPriv,
onShowSignMessage,
Expand Down Expand Up @@ -94,25 +97,33 @@ const Success = ({
return (
<Wrapper>
<ImportedAddressesSettingHeader>
<FormattedMessage
id='scenes.settings.addresses.btc.importedaddresses.success.title'
defaultMessage='Imported Bitcoin Addresses'
/>
</ImportedAddressesSettingHeader>
<SettingDescription style={spacing('mb-10')}>
<WarningWrapper>
<Icon
name='alert-filled'
size='20px'
className={'warning-icon'}
color='brand-yellow'
<div>
<FormattedMessage
id='scenes.settings.addresses.btc.importedaddresses.success.title'
defaultMessage='Imported Bitcoin Addresses'
/>
<SettingDescription style={spacing('mb-10')}>
<WarningWrapper>
<Icon
name='alert-filled'
size='20px'
className={'warning-icon'}
color='brand-yellow'
/>
<FormattedMessage
id='scenes.settings.addresses.btc.importedaddresses.success.description'
defaultMessage='Imported funds are not protected by your backup phrase. To ensure these funds are secured, please transfer them directly into your wallet.'
/>
</WarningWrapper>
</SettingDescription>
</div>
<Button onClick={onClickVerify}>
<FormattedMessage
id='scenes.settings.addresses.btc.importedaddresses.success.description'
defaultMessage='Imported funds are not protected by your backup phrase. To ensure these funds are secured, please transfer them directly into your wallet.'
id='scenes.settings.addresses.btc.importedaddresses.success.verifymessage'
defaultMessage='Verify Message'
/>
</WarningWrapper>
</SettingDescription>
</Button>
</ImportedAddressesSettingHeader>
{importedAddressesTableRows.length > 0 && (
<Table>
<TableHeader>
Expand Down

0 comments on commit 525574d

Please sign in to comment.