Skip to content
This repository has been archived by the owner on Nov 17, 2023. It is now read-only.

Commit

Permalink
Merge pull request #2696 from korhaliv/feat/sign-verify-message
Browse files Browse the repository at this point in the history
feat: add ability to sign and verify messages
  • Loading branch information
mrfelton authored Aug 7, 2019
2 parents 44fd4c0 + 1c4ead1 commit e6cd7fb
Show file tree
Hide file tree
Showing 49 changed files with 1,184 additions and 16 deletions.
26 changes: 13 additions & 13 deletions renderer/components/Profile/ProfileMenu/ProfileMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@ import React from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage } from 'react-intl'
import { Menu } from 'components/UI'
import { PANE_NODEINFO, PANE_LNDCONNECT } from '../constants'
import { PANE_NODEINFO, PANE_LNDCONNECT, PANE_SIGNMESSAGE, PANE_VERIFYMESSAGE } from '../constants'
import messages from './messages'

const ProfileMenu = ({ group, setGroup, isLocalWallet, ...rest }) => {
const menuLink = (id, message) => ({
id,
title: <FormattedMessage {...message} />,
onClick: () => setGroup(id),
})

// Define all possible menu links.
const nodeInfoLink = {
id: PANE_NODEINFO,
title: <FormattedMessage {...messages.profile_pane_nodeinfo_title} />,
onClick: () => setGroup(PANE_NODEINFO),
}
const connectLink = {
id: PANE_LNDCONNECT,
title: <FormattedMessage {...messages.profile_pane_connect_title} />,
onClick: () => setGroup(PANE_LNDCONNECT),
}
const nodeInfoLink = menuLink(PANE_NODEINFO, messages.profile_pane_nodeinfo_title)
const connectLink = menuLink(PANE_LNDCONNECT, messages.profile_pane_connect_title)
const signMessageLink = menuLink(PANE_SIGNMESSAGE, messages.sign_message_title)
const verifyMessageLink = menuLink(PANE_VERIFYMESSAGE, messages.verify_message_title)

// Get set of menu links based on wallet type.
const getLocalLinks = () => [nodeInfoLink]
const getRemoteLinks = () => [nodeInfoLink, connectLink]
const getLocalLinks = () => [nodeInfoLink, signMessageLink, verifyMessageLink]
const getRemoteLinks = () => [nodeInfoLink, connectLink, signMessageLink, verifyMessageLink]
const items = isLocalWallet ? getLocalLinks() : getRemoteLinks()

return <Menu items={items} selectedItem={group} {...rest} />
Expand Down
2 changes: 2 additions & 0 deletions renderer/components/Profile/ProfileMenu/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ import { defineMessages } from 'react-intl'
export default defineMessages({
profile_pane_nodeinfo_title: 'Node Info',
profile_pane_connect_title: 'Connect',
sign_message_title: 'Sign Message',
verify_message_title: 'Verify Message',
})
14 changes: 13 additions & 1 deletion renderer/components/Profile/ProfilePage/ProfilePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,25 @@ import { Heading, MainContent, Panel, Sidebar } from 'components/UI'
import ZapLogo from 'components/Icon/ZapLogo'
import ProfilePaneConnect from 'containers/Profile/ProfilePaneConnect'
import ProfilePaneNodeInfo from 'containers/Profile/ProfilePaneNodeInfo'
import ProfilePaneSignMessage from 'containers/Profile/ProfilePaneSignMessage'
import ProfilePaneVerifyMessage from 'containers/Profile/ProfilePaneVerifyMessage'
import ProfileMenu from '../ProfileMenu'
import { PANE_NODEINFO, PANE_LNDCONNECT, DEFAULT_PANE } from '../constants'
import {
PANE_NODEINFO,
PANE_LNDCONNECT,
PANE_SIGNMESSAGE,
PANE_VERIFYMESSAGE,
DEFAULT_PANE,
} from '../constants'
import messages from './messages'

const ProfilePage = ({ activeWalletSettings }) => {
const [group, setGroup] = useState(DEFAULT_PANE)
const isLocalWallet = activeWalletSettings.type === 'local'

const hasNodeInfoPane = group === PANE_NODEINFO
const hasSignMessagePane = group === PANE_SIGNMESSAGE
const hasVerifyMessagePane = group === PANE_VERIFYMESSAGE
const hasConnectPane = group === PANE_LNDCONNECT && !isLocalWallet

return (
Expand All @@ -35,6 +45,8 @@ const ProfilePage = ({ activeWalletSettings }) => {
</Heading.h1>
{hasNodeInfoPane && <ProfilePaneNodeInfo />}
{hasConnectPane && <ProfilePaneConnect />}
{hasSignMessagePane && <ProfilePaneSignMessage />}
{hasVerifyMessagePane && <ProfilePaneVerifyMessage />}
</MainContent>
</>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, { useState, useRef } from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, injectIntl, intlShape } from 'react-intl'
import { Box, Flex } from 'rebass'
import { Bar, CopyBox, TextArea, Text, Form, Button } from 'components/UI'
import messages from './messages'

const ProfilePaneNodeInfo = ({ intl, signMessage, showNotification, ...rest }) => {
const [sig, setSig] = useState(null)

const formApiRef = useRef(null)
const notifyOfCopy = () =>
showNotification(intl.formatMessage({ ...messages.sig_copied_notification_description }))

const onSignMessage = async () => {
try {
const { current: formApi } = formApiRef
const message = formApi.getValue('message')
const { signature } = await signMessage(message)
setSig(signature)
} catch (e) {
setSig(intl.formatMessage({ ...messages.sign_error }))
}
}

return (
<Box as="section" {...rest}>
<Form
getApi={api => {
formApiRef.current = api
}}
onSubmit={onSignMessage}
>
<Flex alignItems="stretch" flexDirection="column">
<Text fontWeight="normal">
<FormattedMessage {...messages.sign_message_pane_title} />
</Text>
<Text color="gray" fontSize="s" mb={2} mt={2}>
<FormattedMessage {...messages.feature_desc} />
</Text>
<Bar mb={4} mt={2} />
<TextArea
css={`
word-break: break-all;
`}
description={intl.formatMessage({ ...messages.sign_message_desc })}
field="message"
isRequired
label={intl.formatMessage({ ...messages.sign_message_label })}
onChange={() => setSig(null)}
spellCheck="false"
/>
<Button alignSelf="flex-end" mt={3} type="submit" variant="normal">
<FormattedMessage {...messages.sign_message_action} />
</Button>
{sig && (
<>
<Text fontWeight="normal" mt={5}>
<FormattedMessage {...messages.signature} />
</Text>
<Bar />

<CopyBox
hint={intl.formatMessage({ ...messages.copy_signature })}
my={3}
onCopy={notifyOfCopy}
value={sig}
/>
</>
)}
</Flex>
</Form>
</Box>
)
}

ProfilePaneNodeInfo.propTypes = {
intl: intlShape.isRequired,
showNotification: PropTypes.func.isRequired,
signMessage: PropTypes.func.isRequired,
}

export default injectIntl(ProfilePaneNodeInfo)
3 changes: 3 additions & 0 deletions renderer/components/Profile/ProfilePaneSignMessage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ProfilePaneSignMessage from './ProfilePaneSignMessage'

export default ProfilePaneSignMessage
14 changes: 14 additions & 0 deletions renderer/components/Profile/ProfilePaneSignMessage/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineMessages } from 'react-intl'

/* eslint-disable max-len */
export default defineMessages({
sign_message_pane_title: 'Sign Message',
feature_desc: `Sign arbitrary messages to prove that you are in control of the node's public key. Signed message can be independently verified by the people you hand the message to.`,
sign_message_label: `Message`,
sign_message_desc: `Message to sign with node's private key.`,
sign_message_action: 'Sign',
copy_signature: 'Copy signature to clipboard',
signature: 'Signature',
sign_error: `Can't create signature`,
sig_copied_notification_description: 'Signature has been copied to your clipboard',
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, { useState, useRef } from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, injectIntl, intlShape } from 'react-intl'
import { Box, Flex } from 'rebass'
import { Bar, CopyBox, TextArea, Text, Form, Button, Input, Message } from 'components/UI'
import messages from './messages'

const VerificationStatus = ({ isValid }) => (
<Message mt={3} variant={isValid ? 'success' : 'error'}>
<FormattedMessage
{...messages[isValid ? 'signature_status_valid' : 'signature_status_error']}
/>
</Message>
)

VerificationStatus.propTypes = {
isValid: PropTypes.bool,
}

const ProfilePaneNodeInfo = ({ intl, verifyMessage, showNotification, ...rest }) => {
const [info, setInfo] = useState(null)
const formApiRef = useRef(null)

const notifyOfCopy = () =>
showNotification(intl.formatMessage({ ...messages.pubkey_copied_notification_description }))

const reset = () => setInfo(null)

const onSignMessage = async () => {
try {
const { current: formApi } = formApiRef
const message = formApi.getValue('message')
const signature = formApi.getValue('signature')
const { valid, pubkey } = await verifyMessage(message, signature)
setInfo({ valid, pubkey })
} catch (e) {
setInfo({ valid: false })
}
}

return (
<Box as="section" {...rest}>
<Form
getApi={api => {
formApiRef.current = api
}}
onSubmit={onSignMessage}
>
<Flex alignItems="stretch" flexDirection="column">
<Text fontWeight="normal">
<FormattedMessage {...messages.verify_pane_title} />
</Text>
<Text color="gray" fontSize="s" mb={2} mt={2}>
<FormattedMessage {...messages.feature_desc} />
</Text>
<Bar mb={4} mt={2} />

<TextArea
css={`
word-break: break-all;
height: 90px;
`}
description={intl.formatMessage({ ...messages.verify_message_desc })}
field="message"
isRequired
label={intl.formatMessage({ ...messages.verify_message_label })}
mb={3}
onChange={reset}
spellCheck="false"
/>
<Input
description={intl.formatMessage({ ...messages.verify_signature_desc })}
isRequired
label={intl.formatMessage({ ...messages.verify_signature_label })}
onChange={reset}
{...rest}
field="signature"
/>
<Button alignSelf="flex-end" mt={3} type="submit" variant="normal">
<FormattedMessage {...messages.verify_message_action} />
</Button>

{info && (
<>
<Text fontWeight="normal" mt={4}>
<FormattedMessage {...messages.verification} />
</Text>
<Bar />
<VerificationStatus isValid={info.valid} />
{info.valid && (
<CopyBox
hint={intl.formatMessage({ ...messages.pubkey_hint })}
my={3}
onCopy={notifyOfCopy}
value={info.pubkey}
/>
)}
</>
)}
</Flex>
</Form>
</Box>
)
}

ProfilePaneNodeInfo.propTypes = {
intl: intlShape.isRequired,
showNotification: PropTypes.func.isRequired,
verifyMessage: PropTypes.func.isRequired,
}

export default injectIntl(ProfilePaneNodeInfo)
3 changes: 3 additions & 0 deletions renderer/components/Profile/ProfilePaneVerifyMessage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ProfilePaneVerifyMessage from './ProfilePaneVerifyMessage'

export default ProfilePaneVerifyMessage
21 changes: 21 additions & 0 deletions renderer/components/Profile/ProfilePaneVerifyMessage/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineMessages } from 'react-intl'

/* eslint-disable max-len */
export default defineMessages({
verify_pane_title: 'Verify Message',
feature_desc: `Verify a signature over a message. The signature must be zbase32 encoded and signed
by an active node in the resident node's channel database. In addition to returning the
validity of the signature, recovered pubkey from the signature is displayed.`,
verify_message_label: 'Message',
verify_message_desc: 'Enter a message that you would like to verify the authenticity of.',
message_to_verify: 'Message to verify',
verify_message_action: 'Verify',
verify_signature_label: 'Signature',
verify_signature_desc: 'The signature to be verified over the given message.',
verification: 'Verification details',
sign_error: `Can't create signature`,
pubkey_hint: `Copy public key to clipboard`,
signature_status_valid: 'Signature is valid. Signer pubkey recovered from the signature:',
signature_status_error: `Unable to verify the signature. Signature may be incorrect or signer is not in the node's channel database.`,
pubkey_copied_notification_description: 'Pubkey has been copied to your clipboard',
})
2 changes: 2 additions & 0 deletions renderer/components/Profile/constants.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const PANE_NODEINFO = 'nodeinfo'
export const PANE_LNDCONNECT = 'lndconnect'
export const PANE_SIGNMESSAGE = 'signmessage'
export const PANE_VERIFYMESSAGE = 'verifymessage'
export const DEFAULT_PANE = PANE_NODEINFO
2 changes: 1 addition & 1 deletion renderer/components/UI/LightningInvoiceInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class LightningInvoiceInput extends React.Component {

return (
<InformedTextArea
placeholder={intl.formatMessage(
description={intl.formatMessage(
{
...messages.payreq_placeholder,
},
Expand Down
14 changes: 14 additions & 0 deletions renderer/containers/Profile/ProfilePaneSignMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { connect } from 'react-redux'
import { showNotification } from 'reducers/notification'
import { signMessage } from 'reducers/lnd'
import ProfilePaneSignMessage from 'components/Profile/ProfilePaneSignMessage'

const mapDispatchToProps = {
showNotification,
signMessage,
}

export default connect(
null,
mapDispatchToProps
)(ProfilePaneSignMessage)
14 changes: 14 additions & 0 deletions renderer/containers/Profile/ProfilePaneVerifyMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { connect } from 'react-redux'
import { showNotification } from 'reducers/notification'
import { verifyMessage } from 'reducers/lnd'
import ProfilePaneVerifyMessage from 'components/Profile/ProfilePaneVerifyMessage'

const mapDispatchToProps = {
showNotification,
verifyMessage,
}

export default connect(
null,
mapDispatchToProps
)(ProfilePaneVerifyMessage)
24 changes: 24 additions & 0 deletions renderer/reducers/lnd.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,30 @@ export const fetchSeedSuccess = cipher_seed_mnemonic => dispatch => {
dispatch(stopLnd())
}

/**
* signMessage - Sign a message with a node's private key.
*
* @param {string} message Message to sign
* @returns {Function} Thunk
*/
export const signMessage = message => () => {
return grpc.services.Lightning.signMessage({ msg: Buffer.from(message) })
}

/**
* verifyMessage - Verify `signature` over the given `message`.
*
* @param {string} message Message to verify
* @param {string} signature Signature
* @returns {Function} Thunk
*/
export const verifyMessage = (message, signature) => () => {
return grpc.services.Lightning.verifyMessage({
msg: Buffer.from(message),
signature: signature,
})
}

/**
* fetchSeedError - Fetch seed error callback.
*
Expand Down
Loading

0 comments on commit e6cd7fb

Please sign in to comment.