Enable Parity Signer Message Signing (#1663)

* Enable Parity Signer to sign messages

* Verify that message signature is correct

* Type systems are awesome :)
This commit is contained in:
Maciej Hirsz 2018-04-26 02:36:29 +02:00 committed by Daniel Ternyak
parent 5542791af8
commit cf59688896
24 changed files with 329 additions and 72 deletions

View File

@ -0,0 +1,28 @@
import * as interfaces from './actionTypes';
import { TypeKeys } from './constants';
import { ISignedMessage } from 'libs/signing';
export type TSignMessageRequested = typeof signMessageRequested;
export function signMessageRequested(payload: string): interfaces.SignMessageRequestedAction {
return {
type: TypeKeys.SIGN_MESSAGE_REQUESTED,
payload
};
}
export type TSignLocalMessageSucceeded = typeof signLocalMessageSucceeded;
export function signLocalMessageSucceeded(
payload: ISignedMessage
): interfaces.SignLocalMessageSucceededAction {
return {
type: TypeKeys.SIGN_LOCAL_MESSAGE_SUCCEEDED,
payload
};
}
export type TSignMessageFailed = typeof signMessageFailed;
export function signMessageFailed(): interfaces.SignMessageFailedAction {
return {
type: TypeKeys.SIGN_MESSAGE_FAILED
};
}

View File

@ -0,0 +1,22 @@
import { TypeKeys } from './constants';
import { ISignedMessage } from 'libs/signing';
export interface SignMessageRequestedAction {
type: TypeKeys.SIGN_MESSAGE_REQUESTED;
payload: string;
}
export interface SignLocalMessageSucceededAction {
type: TypeKeys.SIGN_LOCAL_MESSAGE_SUCCEEDED;
payload: ISignedMessage;
}
export interface SignMessageFailedAction {
type: TypeKeys.SIGN_MESSAGE_FAILED;
}
/*** Union Type ***/
export type MessageAction =
| SignMessageRequestedAction
| SignLocalMessageSucceededAction
| SignMessageFailedAction;

View File

@ -0,0 +1,5 @@
export enum TypeKeys {
SIGN_MESSAGE_REQUESTED = 'SIGN_MESSAGE_REQUESTED',
SIGN_LOCAL_MESSAGE_SUCCEEDED = 'SIGN_LOCAL_MESSAGE_SUCCEEDED',
SIGN_MESSAGE_FAILED = 'SIGN_MESSAGE_FAILED'
}

View File

@ -0,0 +1,3 @@
export * from './actionCreators';
export * from './constants';
export * from './actionTypes';

View File

@ -1,13 +1,32 @@
import * as types from './actionTypes';
import { TypeKeys } from './constants';
export type TRequestSignature = typeof requestSignature;
export function requestSignature(from: string, rlp: string): types.RequestSignatureAction {
export type TRequestTransactionSignature = typeof requestTransactionSignature;
export function requestTransactionSignature(
from: string,
data: string
): types.RequestTransactionSignatureAction {
return {
type: TypeKeys.PARITY_SIGNER_REQUEST_SIGNATURE,
type: TypeKeys.PARITY_SIGNER_REQUEST_TX_SIGNATURE,
payload: {
isMessage: false,
from,
rlp
data
}
};
}
export type TRequestMessageSignature = typeof requestMessageSignature;
export function requestMessageSignature(
from: string,
data: string
): types.RequestMessageSignatureAction {
return {
type: TypeKeys.PARITY_SIGNER_REQUEST_MSG_SIGNATURE,
payload: {
isMessage: true,
from,
data
}
};
}

View File

@ -1,9 +1,19 @@
import { TypeKeys } from './constants';
export interface RequestSignatureAction {
type: TypeKeys.PARITY_SIGNER_REQUEST_SIGNATURE;
export interface RequestTransactionSignatureAction {
type: TypeKeys.PARITY_SIGNER_REQUEST_TX_SIGNATURE;
payload: {
rlp: string;
isMessage: false;
data: string;
from: string;
};
}
export interface RequestMessageSignatureAction {
type: TypeKeys.PARITY_SIGNER_REQUEST_MSG_SIGNATURE;
payload: {
isMessage: true;
data: string;
from: string;
};
}
@ -14,4 +24,7 @@ export interface FinalizeSignatureAction {
}
/*** Union Type ***/
export type ParitySignerAction = RequestSignatureAction | FinalizeSignatureAction;
export type ParitySignerAction =
| RequestTransactionSignatureAction
| RequestMessageSignatureAction
| FinalizeSignatureAction;

View File

@ -1,4 +1,5 @@
export enum TypeKeys {
PARITY_SIGNER_REQUEST_SIGNATURE = 'PARITY_SIGNER_REQUEST_SIGNATURE',
PARITY_SIGNER_REQUEST_TX_SIGNATURE = 'PARITY_SIGNER_REQUEST_TX_SIGNATURE',
PARITY_SIGNER_REQUEST_MSG_SIGNATURE = 'PARITY_SIGNER_REQUEST_MSG_SIGNATURE',
PARITY_SIGNER_FINALIZE_SIGNATURE = 'PARITY_SIGNER_FINALIZE_SIGNATURE'
}

View File

@ -18,7 +18,8 @@ interface ScanProps {
interface ShowProps {
scan: false;
account: string;
rlp: string;
data?: string;
rlp?: string;
}
interface SharedProps {

View File

@ -22,10 +22,9 @@ export const DISABLE_WALLETS: { [key in WalletMode]: DisabledWallets } = {
}
},
[WalletMode.UNABLE_TO_SIGN]: {
wallets: [SecureWalletName.TREZOR, SecureWalletName.PARITY_SIGNER, MiscWalletName.VIEW_ONLY],
wallets: [SecureWalletName.TREZOR, MiscWalletName.VIEW_ONLY],
reasons: {
[SecureWalletName.TREZOR]: 'This wallet cant sign messages',
[SecureWalletName.PARITY_SIGNER]: 'This wallet cant sign messages',
[MiscWalletName.VIEW_ONLY]: 'This wallet cant sign messages'
}
}

View File

@ -16,8 +16,9 @@ interface PropsClosed {
interface PropsOpen {
isOpen: true;
rlp: string;
isMessage: boolean;
from: string;
data: string;
}
interface ActionProps {
@ -40,7 +41,7 @@ class QrSignerModal extends React.Component<Props, State> {
}
const { scan } = this.state;
const { from, rlp } = this.props;
const { from, data, isMessage } = this.props;
const buttons: IButton[] = [
{
@ -60,7 +61,7 @@ class QrSignerModal extends React.Component<Props, State> {
return (
<div className="QrSignerModal">
<Modal
title={translateRaw('DEP_SIGNTX')}
title={translateRaw(isMessage ? 'NAV_SIGNMSG' : 'DEP_SIGNTX')}
isOpen={true}
buttons={buttons}
handleClose={this.onClose}
@ -68,8 +69,10 @@ class QrSignerModal extends React.Component<Props, State> {
<div className="QrSignerModal-qr-bounds">
{scan ? (
<ParityQrSigner scan={true} onScan={this.onScan} />
) : isMessage ? (
<ParityQrSigner scan={false} account={from} data={data} />
) : (
<ParityQrSigner scan={false} account={from} rlp={rlp} />
<ParityQrSigner scan={false} account={from} rlp={data} />
)}
</div>
</Modal>
@ -103,12 +106,9 @@ function mapStateToProps(state: AppState): PropsClosed | PropsOpen {
return { isOpen: false };
}
const { from, rlp } = requested;
return {
isOpen: true,
from,
rlp
...requested
};
}

View File

@ -1,14 +1,10 @@
import React from 'react';
import translate from 'translations';
import { ISignedMessage } from 'libs/signing';
import { IFullWallet } from 'libs/wallet';
import { TShowNotification } from 'actions/notifications';
import { TSignMessageRequested } from 'actions/message';
interface Props {
wallet: IFullWallet;
message: string;
showNotification: TShowNotification;
onSignMessage(msg: ISignedMessage): any;
signMessageRequested: TSignMessageRequested;
}
export default class SignMessageButton extends React.Component<Props, {}> {
@ -20,24 +16,9 @@ export default class SignMessageButton extends React.Component<Props, {}> {
);
}
private handleSignMessage = async () => {
const { wallet, message, showNotification, onSignMessage } = this.props;
private handleSignMessage = () => {
const { signMessageRequested, message } = this.props;
try {
const signedMessage: ISignedMessage = {
address: wallet.getAddressString(),
msg: message,
sig: await wallet.signMessage(message),
version: '2'
};
onSignMessage(signedMessage);
showNotification(
'success',
translate('SIGN_MSG_SUCCESS', { $address: signedMessage.address })
);
} catch (err) {
showNotification('danger', translate('SIGN_MSG_FAIL', { $err: err.message }));
}
signMessageRequested(message);
};
}

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import WalletDecrypt, { DISABLE_WALLETS } from 'components/WalletDecrypt';
import translate, { translateRaw } from 'translations';
import { showNotification, TShowNotification } from 'actions/notifications';
import { signMessageRequested, TSignMessageRequested } from 'actions/message';
import { resetWallet, TResetWallet } from 'actions/wallet';
import { ISignedMessage } from 'libs/signing';
import { IFullWallet } from 'libs/wallet';
@ -15,18 +15,17 @@ import { TextArea, CodeBlock } from 'components/ui';
interface Props {
wallet: IFullWallet;
unlocked: boolean;
showNotification: TShowNotification;
signMessageRequested: TSignMessageRequested;
signedMessage: ISignedMessage | null;
resetWallet: TResetWallet;
}
interface State {
message: string;
signedMessage: ISignedMessage | null;
}
const initialState: State = {
message: '',
signedMessage: null
message: ''
};
const messagePlaceholder = translateRaw('SIGN_MSG_PLACEHOLDER');
@ -39,8 +38,8 @@ export class SignMessage extends Component<Props, State> {
}
public render() {
const { wallet, unlocked } = this.props;
const { message, signedMessage } = this.state;
const { unlocked, signedMessage } = this.props;
const { message } = this.state;
return (
<div>
@ -68,10 +67,8 @@ export class SignMessage extends Component<Props, State> {
</div>
<SignButton
wallet={wallet}
message={this.state.message}
showNotification={this.props.showNotification}
onSignMessage={this.onSignMessage}
signMessageRequested={this.props.signMessageRequested}
/>
{signedMessage && (
@ -97,21 +94,17 @@ export class SignMessage extends Component<Props, State> {
this.setState({ message });
};
private onSignMessage = (signedMessage: ISignedMessage) => {
this.setState({ signedMessage });
};
private changeWallet = () => {
this.props.resetWallet();
};
}
const mapStateToProps = (state: AppState) => ({
wallet: state.wallet.inst,
signedMessage: state.message.signed,
unlocked: isWalletFullyUnlocked(state)
});
export default connect(mapStateToProps, {
showNotification,
signMessageRequested,
resetWallet
})(SignMessage);

View File

@ -92,7 +92,7 @@ export class VerifyMessage extends Component<Props, State> {
this.props.showNotification('success', translate('SUCCESS_7'));
} catch (err) {
this.clearVerifiedData();
this.props.showNotification('danger', translate('ERROR_12'));
this.props.showNotification('danger', translate('ERROR_38'));
}
};

View File

@ -10,10 +10,9 @@ export default class ParitySignerWallet implements IFullWallet {
}
public signRawTransaction = () =>
Promise.reject(new Error('Web3 wallets cannot sign raw transactions.'));
Promise.reject(new Error('Parity Signer cannot sign raw transactions.'));
public signMessage = () =>
Promise.reject(new Error('Signing via Parity Signer not yet supported.'));
public signMessage = () => Promise.reject(new Error('Parity Signer cannot sign messages.'));
public getAddressString() {
return this.address;

View File

@ -9,6 +9,7 @@ import { rates, State as RatesState } from './rates';
import { State as SwapState, swap } from './swap';
import { State as WalletState, wallet } from './wallet';
import { State as TransactionState, transaction } from './transaction';
import { State as MessageState, message } from './message';
import { State as GasState, gas } from './gas';
import { onboardStatus, State as OnboardStatusState } from './onboardStatus';
import { State as TransactionsState, transactions } from './transactions';
@ -28,6 +29,7 @@ export interface AppState {
swap: SwapState;
transaction: TransactionState;
transactions: TransactionsState;
message: MessageState;
paritySigner: ParitySignerState;
gas: GasState;
schedule: ScheduleState;
@ -47,6 +49,7 @@ export default combineReducers<AppState>({
deterministicWallets,
transaction,
transactions,
message,
paritySigner,
gas,
schedule,

View File

@ -0,0 +1,35 @@
import { MessageAction, SignLocalMessageSucceededAction, TypeKeys } from 'actions/message';
import { ISignedMessage } from 'libs/signing';
export interface State {
signed?: ISignedMessage | null;
}
export const INITIAL_STATE: State = {
signed: null
};
function signLocalMessageSucceeded(state: State, action: SignLocalMessageSucceededAction): State {
return {
...state,
signed: action.payload
};
}
function signMessageFailed(state: State): State {
return {
...state,
signed: null
};
}
export function message(state: State = INITIAL_STATE, action: MessageAction): State {
switch (action.type) {
case TypeKeys.SIGN_LOCAL_MESSAGE_SUCCEEDED:
return signLocalMessageSucceeded(state, action);
case TypeKeys.SIGN_MESSAGE_FAILED:
return signMessageFailed(state);
default:
return state;
}
}

View File

@ -1,19 +1,35 @@
import { ParitySignerAction, RequestSignatureAction, TypeKeys } from 'actions/paritySigner';
import {
ParitySignerAction,
RequestTransactionSignatureAction,
RequestMessageSignatureAction,
TypeKeys
} from 'actions/paritySigner';
export interface State {
requested?: QrSignatureState | null;
}
interface QrSignatureState {
isMessage: boolean;
from: string;
rlp: string;
data: string;
}
export const INITIAL_STATE: State = {
requested: null
};
function requestSignature(state: State, action: RequestSignatureAction): State {
function requestTransactionSignature(
state: State,
action: RequestTransactionSignatureAction
): State {
return {
...state,
requested: action.payload
};
}
function requestMessageSignature(state: State, action: RequestMessageSignatureAction): State {
return {
...state,
requested: action.payload
@ -29,8 +45,10 @@ function finalizeSignature(state: State): State {
export function paritySigner(state: State = INITIAL_STATE, action: ParitySignerAction): State {
switch (action.type) {
case TypeKeys.PARITY_SIGNER_REQUEST_SIGNATURE:
return requestSignature(state, action);
case TypeKeys.PARITY_SIGNER_REQUEST_TX_SIGNATURE:
return requestTransactionSignature(state, action);
case TypeKeys.PARITY_SIGNER_REQUEST_MSG_SIGNATURE:
return requestMessageSignature(state, action);
case TypeKeys.PARITY_SIGNER_FINALIZE_SIGNATURE:
return finalizeSignature(state);
default:

View File

@ -8,6 +8,7 @@ import swapRates from './swap/rates';
import wallet from './wallet';
import { ens } from './ens';
import { transaction } from './transaction';
import { message } from './message';
import transactions from './transactions';
import gas from './gas';
import { schedule } from './schedule';
@ -21,6 +22,7 @@ export default {
notifications,
wallet,
transaction,
message,
deterministicWallets,
rates,
transactions,

View File

@ -0,0 +1,38 @@
import { SagaIterator } from 'redux-saga';
import { put, call, select } from 'redux-saga/effects';
import translate from 'translations';
import { padLeftEven } from 'libs/values';
import { showNotification } from 'actions/notifications';
import { getWalletInst } from 'selectors/wallet';
import { IFullWallet } from 'libs/wallet';
import { signMessageFailed, SignMessageRequestedAction } from 'actions/message';
export function* signingWrapper(
handler: (wallet: IFullWallet, message: string) => SagaIterator,
action: SignMessageRequestedAction
): SagaIterator {
const message = action.payload;
const wallet = yield select(getWalletInst);
try {
yield call(handler, wallet, message);
} catch (err) {
yield put(showNotification('danger', translate('SIGN_MSG_FAIL', { $err: err.message }), 5000));
yield put(signMessageFailed());
}
}
/**
* Turns a string into hex-encoded UTF-8 byte array, `0x` prefixed.
*
* @param {string} message to encode
* @return {string}
*/
export function messageToData(message: string): string {
return (
'0x' +
Array.from(Buffer.from(message, 'utf8'))
.map(n => padLeftEven(n.toString(16)))
.join('')
);
}

View File

@ -0,0 +1,7 @@
import { SagaIterator } from 'redux-saga';
import { all } from 'redux-saga/effects';
import { signing } from './signing';
export function* message(): SagaIterator {
yield all([...signing]);
}

View File

@ -0,0 +1,89 @@
import { SagaIterator } from 'redux-saga';
import { put, take, apply, takeEvery, call, select } from 'redux-saga/effects';
import translate, { translateRaw } from 'translations';
import { showNotification } from 'actions/notifications';
import { verifySignedMessage } from 'libs/signing';
import {
TypeKeys,
SignMessageRequestedAction,
signLocalMessageSucceeded,
SignLocalMessageSucceededAction,
signMessageFailed
} from 'actions/message';
import {
requestMessageSignature,
FinalizeSignatureAction,
TypeKeys as ParityKeys
} from 'actions/paritySigner';
import { IFullWallet } from 'libs/wallet';
import { getWalletType, IWalletType } from 'selectors/wallet';
import { messageToData, signingWrapper } from './helpers';
function* signLocalMessage(wallet: IFullWallet, msg: string): SagaIterator {
const address = yield apply(wallet, wallet.getAddressString);
const sig: string = yield apply(wallet, wallet.signMessage, [msg]);
yield put(
signLocalMessageSucceeded({
address,
msg,
sig,
version: '2'
})
);
}
function* signParitySignerMessage(wallet: IFullWallet, msg: string): SagaIterator {
const address = yield apply(wallet, wallet.getAddressString);
const data = yield call(messageToData, msg);
yield put(requestMessageSignature(address, data));
const { payload: sig }: FinalizeSignatureAction = yield take(
ParityKeys.PARITY_SIGNER_FINALIZE_SIGNATURE
);
if (!sig) {
throw new Error(translateRaw('ERROR_38'));
}
yield put(
signLocalMessageSucceeded({
address,
msg,
sig,
version: '2'
})
);
}
function* handleMessageRequest(action: SignMessageRequestedAction): SagaIterator {
const walletType: IWalletType = yield select(getWalletType);
const signingHandler = walletType.isParitySignerWallet
? signParitySignerMessage
: signLocalMessage;
return yield call(signingWrapper, signingHandler, action);
}
function* verifySignature(action: SignLocalMessageSucceededAction): SagaIterator {
const success = yield call(verifySignedMessage, action.payload);
if (success) {
yield put(
showNotification(
'success',
translate('SIGN_MSG_SUCCESS', { $address: action.payload.address })
)
);
} else {
yield put(signMessageFailed());
yield put(showNotification('danger', translate('ERROR_38')));
}
}
export const signing = [
takeEvery(TypeKeys.SIGN_MESSAGE_REQUESTED, handleMessageRequest),
takeEvery(TypeKeys.SIGN_LOCAL_MESSAGE_SUCCEEDED, verifySignature)
];

View File

@ -15,7 +15,7 @@ import { computeIndexingHash } from 'libs/transaction';
import { serializedAndTransactionFieldsMatch } from 'selectors/transaction';
import { showNotification } from 'actions/notifications';
import {
requestSignature,
requestTransactionSignature,
FinalizeSignatureAction,
TypeKeys as ParityKeys
} from 'actions/paritySigner';
@ -53,7 +53,7 @@ export function* signParitySignerTransactionHandler({
const from = yield apply(wallet, wallet.getAddressString);
const rlp = yield call(transactionToRLP, tx);
yield put(requestSignature(from, rlp));
yield put(requestTransactionSignature(from, rlp));
const { payload }: FinalizeSignatureAction = yield take(
ParityKeys.PARITY_SIGNER_FINALIZE_SIGNATURE

View File

@ -266,6 +266,7 @@
"ERROR_34": "The name you are attempting to reveal does not match the name you have entered. ",
"ERROR_36": "Enter valid TX hash",
"ERROR_37": "Enter valid hex string (0-9, a-f)",
"ERROR_38": "Invalid signed message. ",
"SUCCESS_1": "Valid address ",
"SUCCESS_2": "Wallet successfully decrypted ",
"SUCCESS_3": "Your TX has been broadcast to the network. It is waiting to be mined & confirmed. During ICOs, it may take 3+ hours to confirm. Use the Verify & Check buttons below to see. TX Hash: ",

View File

@ -10,7 +10,7 @@
"npm": ">= 5.0.0"
},
"dependencies": {
"@parity/qr-signer": "0.1.1",
"@parity/qr-signer": "0.2.0",
"babel-polyfill": "6.26.0",
"bip39": "2.5.0",
"bn.js": "4.11.8",