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:
parent
9d58329450
commit
68e5972a03
|
@ -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 />
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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')
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@ describe('wallet reducer', () => {
|
|||
|
||||
const walletInstance = {
|
||||
getAddress: () => doSomething,
|
||||
signRawTransaction: () => doSomething
|
||||
signRawTransaction: () => doSomething,
|
||||
signMessage: () => doSomething
|
||||
};
|
||||
|
||||
expect(wallet(undefined, walletActions.setWallet(walletInstance))).toEqual({
|
||||
|
|
Loading…
Reference in New Issue