Sign & Verify Message (#315)

* add route and nav tab for new module

* add new module to tabs index

* add signMessage to wallet interface

* add signed message verification, normalize pkey sign

* init Sign & Verify Message tab

* reorder imports

* mock out Trezor

* cast to bool instead of length check

* normalize ledger sign message

* fix broken this context

* add commented message signing to trezor wallet

* correct var to start on sign tab

* remove unused state var

* clean up SignMessage classes

* clean up VerifyMessage classes, remove unnecessary log

* correct event variable types

* remove unnecessary exports

* remove empty classname

* use implicit return

* shorten signMessage method

* remove unnecessary disable

* tweak variable name

* make better use of destructuring, remove console log

* use destructured var

* flatten if statement

* add signMessage method to wallet reducer test
This commit is contained in:
skubakdj 2017-10-30 15:10:25 -04:00 committed by Daniel Ternyak
parent 9d58329450
commit 68e5972a03
16 changed files with 473 additions and 41 deletions

View File

@ -9,6 +9,7 @@ import Help from 'containers/Tabs/Help';
import SendTransaction from 'containers/Tabs/SendTransaction';
import Swap from 'containers/Tabs/Swap';
import ViewWallet from 'containers/Tabs/ViewWallet';
import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage';
import BroadcastTx from 'containers/Tabs/BroadcastTx';
// TODO: fix this
@ -32,6 +33,10 @@ export default class Root extends Component<Props, {}> {
<Route path="/send-transaction" component={SendTransaction} />
<Route path="/contracts" component={Contracts} />
<Route path="/ens" component={ENS} />
<Route
path="/sign-and-verify-message"
component={SignAndVerifyMessage}
/>
<Route path="/pushTx" component={BroadcastTx} />
<LegacyRoutes />

View File

@ -28,6 +28,10 @@ const tabs = [
name: 'NAV_ENS',
to: 'ens'
},
{
name: 'Sign & Verify Message',
to: 'sign-and-verify-message'
},
{
name: 'Broadcast Transaction',
to: 'pushTx'

View File

@ -125,7 +125,5 @@ export default class TrezorDecrypt extends Component<Props, State> {
this.props.onUnlock(new TrezorWallet(address, this.state.dPath, index));
};
private handleNullConnect = (): void => {
return this.handleConnect();
}
private handleNullConnect = (): void => this.handleConnect();
}

View File

@ -0,0 +1,31 @@
.SignMessage {
text-align: center;
padding-top: 30px;
&-sign {
width: 100%;
}
&-help {
margin-top: 10px;
font-size: 13px;
font-style: italic;
}
&-inputBox {
min-height: 180px;
}
&-error {
opacity: 0;
transition: none;
&.is-showing {
opacity: 1;
}
}
&-buy {
margin-top: 10px;
}
}

View File

@ -0,0 +1,131 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
import { IWallet } from 'libs/wallet/IWallet';
import WalletDecrypt from 'components/WalletDecrypt';
import translate from 'translations';
import { showNotification, TShowNotification } from 'actions/notifications';
import { ISignedMessage } from 'libs/signing';
import { AppState } from 'reducers';
import './index.scss';
interface Props {
wallet: IWallet;
showNotification: TShowNotification;
}
interface State {
message: string;
signMessageError: string;
signedMessage: ISignedMessage | null;
}
const initialState: State = {
message: '',
signMessageError: '',
signedMessage: null
};
const messagePlaceholder =
'This is a sweet message that you are signing to prove that you own the address you say you own.';
export class SignMessage extends Component<Props, State> {
public state: State = initialState;
public render() {
const { wallet } = this.props;
const { message, signedMessage } = this.state;
const messageBoxClass = classnames([
'SignMessage-inputBox',
'form-control',
message ? 'is-valid' : 'is-invalid'
]);
return (
<div>
<div className="Tab-content-pane">
<h4>{translate('MSG_message')}</h4>
<div className="form-group">
<textarea
className={messageBoxClass}
placeholder={messagePlaceholder}
value={message}
onChange={this.handleMessageChange}
/>
<div className="SignMessage-help">{translate('MSG_info2')}</div>
</div>
{!!wallet && (
<button
className="SignMessage-sign btn btn-primary btn-lg"
onClick={this.handleSignMessage}
>
{translate('NAV_SignMsg')}
</button>
)}
{!!signedMessage && (
<div>
<h4>{translate('MSG_signature')}</h4>
<div className="form-group">
<textarea
className="SignMessage-inputBox form-control"
value={JSON.stringify(signedMessage, null, 2)}
disabled={true}
onChange={this.handleMessageChange}
/>
</div>
</div>
)}
</div>
{!wallet && <WalletDecrypt />}
</div>
);
}
private handleSignMessage = async () => {
const { wallet } = this.props;
const { message } = this.state;
if (!wallet) {
return;
}
try {
const signedMessage: ISignedMessage = {
address: await wallet.getAddress(),
message,
signature: await wallet.signMessage(message),
version: '2'
};
this.setState({ signedMessage });
this.props.showNotification(
'success',
`Successfully signed message with address ${signedMessage.address}.`
);
} catch (err) {
this.props.showNotification(
'danger',
`Error signing message: ${err.message}`
);
}
};
private handleMessageChange = (e: React.FormEvent<HTMLTextAreaElement>) => {
const message = e.currentTarget.value;
this.setState({ message });
};
}
function mapStateToProps(state: AppState) {
return {
wallet: state.wallet.inst
};
}
export default connect(mapStateToProps, {
showNotification
})(SignMessage);

View File

@ -0,0 +1,28 @@
.VerifyMessage {
text-align: center;
padding-top: 30px;
&-sign {
width: 100%;
}
&-help {
margin-top: 10px;
font-size: 13px;
font-style: italic;
}
&-inputBox {
min-height: 180px;
}
&-success {
opacity: 1;
transition: none;
margin-top: 10px;
}
&-buy {
margin-top: 10px;
}
}

View File

@ -0,0 +1,105 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
import translate from 'translations';
import { showNotification, TShowNotification } from 'actions/notifications';
import { verifySignedMessage, ISignedMessage } from 'libs/signing';
import './index.scss';
interface Props {
showNotification: TShowNotification;
}
interface State {
signature: string;
verifiedAddress?: string;
verifiedMessage?: string;
}
const initialState: State = {
signature: ''
};
const signaturePlaceholder =
'{"address":"0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8","message":"asdfasdfasdf","signature":"0x4771d78f13ba8abf608457f12471f427ca8f2fb046c1acb3f5969eefdfe452a10c9154136449f595a654b44b3b0163e86dd099beaca83bfd52d64c21da2221bb1c","version":"2"}';
export class VerifyMessage extends Component<Props, State> {
public state: State = initialState;
public render() {
const { verifiedAddress, verifiedMessage, signature } = this.state;
const signatureBoxClass = classnames([
'VerifyMessage-inputBox',
'form-control',
signature ? 'is-valid' : 'is-invalid'
]);
return (
<div>
<div className="Tab-content-pane">
<h4>{translate('MSG_signature')}</h4>
<div className="form-group">
<textarea
className={signatureBoxClass}
placeholder={signaturePlaceholder}
value={signature}
onChange={this.handleSignatureChange}
/>
</div>
<button
className="VerifyMessage-sign btn btn-primary btn-lg"
onClick={this.handleVerifySignedMessage}
disabled={false}
>
{translate('MSG_verify')}
</button>
{!!verifiedAddress &&
!!verifiedMessage && (
<div className="VerifyMessage-success alert alert-success">
<strong>{verifiedAddress}</strong> did sign the message{' '}
<strong>{verifiedMessage}</strong>.
</div>
)}
</div>
</div>
);
}
private clearVerifiedData = () =>
this.setState({
verifiedAddress: '',
verifiedMessage: ''
});
private handleVerifySignedMessage = () => {
try {
const parsedSignature: ISignedMessage = JSON.parse(this.state.signature);
if (!verifySignedMessage(parsedSignature)) {
throw Error();
}
const { address, message } = parsedSignature;
this.setState({
verifiedAddress: address,
verifiedMessage: message
});
this.props.showNotification('success', translate('SUCCESS_7'));
} catch (err) {
this.clearVerifiedData();
this.props.showNotification('danger', translate('ERROR_12'));
}
};
private handleSignatureChange = (e: React.FormEvent<HTMLTextAreaElement>) => {
const signature = e.currentTarget.value;
this.setState({ signature });
};
}
export default connect(null, {
showNotification
})(VerifyMessage);

View File

@ -0,0 +1,30 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
.SignAndVerifyMsg {
&-header {
margin: 0;
text-align: center;
&-tab {
@include reset-button;
color: $ether-blue;
&:hover,
&:active {
opacity: 0.8;
}
&.is-active {
&,
&:hover,
&:active {
color: $text-color;
cursor: default;
opacity: 1;
font-weight: 500;
}
}
}
}
}

View File

@ -0,0 +1,59 @@
import React, { Component } from 'react';
import translate from 'translations';
import SignMessage from './components/SignMessage';
import VerifyMessage from './components/VerifyMessage';
import TabSection from 'containers/TabSection';
import './index.scss';
interface State {
activeTab: string;
}
export default class SignAndVerifyMessage extends Component<{}, State> {
public state: State = {
activeTab: 'sign'
};
public changeTab = activeTab => () => this.setState({ activeTab });
public render() {
const { activeTab } = this.state;
let content;
let signActive = '';
let verifyActive = '';
if (activeTab === 'sign') {
content = <SignMessage />;
signActive = 'is-active';
} else {
content = <VerifyMessage />;
verifyActive = 'is-active';
}
return (
<TabSection>
<section className="Tab-content SignAndVerifyMsg">
<div className="Tab-content-pane">
<h1 className="SignAndVerifyMsg-header">
<button
className={`SignAndVerifyMsg-header-tab ${signActive}`}
onClick={this.changeTab('sign')}
>
{translate('Sign Message')}
</button>{' '}
<span>or</span>{' '}
<button
className={`SignAndVerifyMsg-header-tab ${verifyActive}`}
onClick={this.changeTab('verify')}
>
{translate('Verify Message')}
</button>
</h1>
</div>
<main role="main">{content}</main>
</section>
</TabSection>
);
}
}

View File

@ -4,6 +4,7 @@ import { default as Help } from './Help';
import { default as SendTransaction } from './SendTransaction';
import { default as Swap } from './Swap';
import { default as ViewWallet } from './ViewWallet';
import { default as SignAndVerifyMessage } from './SignAndVerifyMessage';
export default {
ENS,
@ -11,5 +12,6 @@ export default {
Help,
SendTransaction,
Swap,
ViewWallet
ViewWallet,
SignAndVerifyMessage
};

View File

@ -1,7 +1,16 @@
import EthTx from 'ethereumjs-tx';
import { ecsign, sha3 } from 'ethereumjs-util';
import {
addHexPrefix,
ecsign,
ecrecover,
sha3,
hashPersonalMessage,
toBuffer,
pubToAddress
} from 'ethereumjs-util';
import { RawTransaction } from 'libs/transaction';
import { isValidRawTx } from 'libs/validators';
import { stripHexPrefixAndLower } from 'libs/values';
export function signRawTxWithPrivKey(
privKey: Buffer,
@ -16,15 +25,10 @@ export function signRawTxWithPrivKey(
return '0x' + eTx.serialize().toString('hex');
}
export function signMessageWithPrivKey(
privKey: Buffer,
msg: string,
address: string,
date: string
): string {
const spacer = msg.length > 0 && date.length > 0 ? ' ' : '';
const fullMessage = msg + spacer + date;
const hash = sha3(fullMessage);
// adapted from:
// https://github.com/kvhnuke/etherwallet/blob/2a5bc0db1c65906b14d8c33ce9101788c70d3774/app/scripts/controllers/signMsgCtrl.js#L95
export function signMessageWithPrivKeyV2(privKey: Buffer, msg: string): string {
const hash = hashPersonalMessage(toBuffer(msg));
const signed = ecsign(hash, privKey);
const combined = Buffer.concat([
Buffer.from(signed.r),
@ -33,9 +37,35 @@ export function signMessageWithPrivKey(
]);
const combinedHex = combined.toString('hex');
return JSON.stringify({
address,
msg: fullMessage,
sig: '0x' + combinedHex
});
return addHexPrefix(combinedHex);
}
export interface ISignedMessage {
address: string;
message: string;
signature: string;
version: string;
}
// adapted from:
// https://github.com/kvhnuke/etherwallet/blob/2a5bc0db1c65906b14d8c33ce9101788c70d3774/app/scripts/controllers/signMsgCtrl.js#L118
export function verifySignedMessage({
address,
message,
signature,
version
}: ISignedMessage) {
const sig = new Buffer(stripHexPrefixAndLower(signature), 'hex');
if (sig.length !== 65) {
return false;
}
//TODO: explain what's going on here
sig[64] = sig[64] === 0 || sig[64] === 1 ? sig[64] + 27 : sig[64];
const hash =
version === '2' ? hashPersonalMessage(toBuffer(message)) : sha3(message);
const pubKey = ecrecover(hash, sig[64], sig.slice(0, 32), sig.slice(32, 64));
return (
stripHexPrefixAndLower(address) === pubToAddress(pubKey).toString('hex')
);
}

View File

@ -3,4 +3,5 @@ import { RawTransaction } from 'libs/transaction';
export interface IWallet {
getAddress(): Promise<string>;
signRawTransaction(tx: RawTransaction): Promise<string>;
signMessage(msg: string): Promise<string>;
}

View File

@ -70,17 +70,8 @@ export default class LedgerWallet extends DeterministicWallet
try {
const combined = signed.r + signed.s + signed.v;
const combinedHex = combined.toString('hex');
const signedMsg = JSON.stringify(
{
address: await this.getAddress(),
msg,
sig: addHexPrefix(combinedHex),
version: '2'
},
null,
2
);
resolve(signedMsg);
const signature = addHexPrefix(combinedHex);
resolve(signature);
} catch (err) {
reject(err);
}

View File

@ -5,7 +5,7 @@ import {
toChecksumAddress
} from 'ethereumjs-util';
import { pkeyToKeystore, UtcKeystore } from 'libs/keystore';
import { signMessageWithPrivKey, signRawTxWithPrivKey } from 'libs/signing';
import { signMessageWithPrivKeyV2, signRawTxWithPrivKey } from 'libs/signing';
import { RawTransaction } from 'libs/transaction';
import { isValidPrivKey } from 'libs/validators';
import { stripHexPrefixAndLower } from 'libs/values';
@ -69,13 +69,6 @@ export default class PrivKeyWallet implements IWallet {
});
}
public signMessage(msg: string, address: string, date: string): Promise<any> {
return new Promise((resolve, reject) => {
try {
resolve(signMessageWithPrivKey(this.privKey, msg, address, date));
} catch (err) {
reject(err);
}
});
}
public signMessage = async (msg: string) =>
signMessageWithPrivKeyV2(this.privKey, msg);
}

View File

@ -42,4 +42,27 @@ export default class TrezorWallet extends DeterministicWallet
);
});
}
public signMessage = () =>
Promise.reject(new Error('Signing via Trezor not yet supported.'));
// works, but returns a signature that can only be verified with a Trezor device
/*
public signMessage = (message: string): Promise<string> => {
return new Promise((resolve, reject) => {
(TrezorConnect as any).ethereumSignMessage(
this.getPath(),
message,
response => {
if (response.success) {
resolve(addHexPrefix(response.signature))
} else{
console.error(response.error)
reject(response.error)
}
}
)
})
}
*/
}

View File

@ -10,7 +10,8 @@ describe('wallet reducer', () => {
const walletInstance = {
getAddress: () => doSomething,
signRawTransaction: () => doSomething
signRawTransaction: () => doSomething,
signMessage: () => doSomething
};
expect(wallet(undefined, walletActions.setWallet(walletInstance))).toEqual({