Commit add2d45b authored by Alexey's avatar Alexey Committed by Maciej Hirsz
Browse files

Ethereum message sign (#212)

* Ethereum message sign implementation
parent a2ec719b
......@@ -19,7 +19,12 @@
'use strict';
import React, { Component } from 'react';
import { StatusBar } from 'react-native';
import { createBottomTabNavigator, createStackNavigator, HeaderBackButton, withNavigation } from 'react-navigation';
import {
createBottomTabNavigator,
createStackNavigator,
HeaderBackButton,
withNavigation
} from 'react-navigation';
import { Provider as UnstatedProvider } from 'unstated';
import '../ReactotronConfig';
import colors from './colors';
......@@ -39,14 +44,15 @@ import AccountPin from './screens/AccountPin';
import AccountRecover from './screens/AccountRecover';
import { AccountUnlock, AccountUnlockAndSign } from './screens/AccountUnlock';
import Loading from './screens/Loading';
import MessageDetails from './screens/MessageDetails';
import PrivacyPolicy from './screens/PrivacyPolicy';
import QrScanner from './screens/QrScanner';
import Security from './screens/Security';
import SignedMessage from './screens/SignedMessage';
import SignedTx from './screens/SignedTx';
import TermsAndConditions from './screens/TermsAndConditions';
import TxDetails from './screens/TxDetails';
export default class App extends Component {
render() {
return (
......@@ -196,6 +202,12 @@ const Screens = createStackNavigator(
},
SignedTx: {
screen: SignedTx
},
SignedMessage: {
screen: SignedMessage
},
MessageDetails: {
screen: MessageDetails
}
},
{
......
......@@ -28,35 +28,36 @@ import AccountsStore from '../stores/AccountsStore';
import ScannerStore from '../stores/ScannerStore';
export class AccountUnlockAndSign extends React.PureComponent {
render() {
const { navigation } = this.props;
const next = navigation.getParam('next', 'SignedTx');
return (
<Subscribe to={[AccountsStore, ScannerStore]}>
{(accounts, scannerStore) => (
<AccountUnlockView
{...this.props}
accounts={accounts}
checkPin={async (pin) => {
checkPin={async pin => {
try {
scannerStore.getTXRequest();
await scannerStore.signData(pin);
return true
return true;
} catch (e) {
return false
return false;
}
}}
navigate={
() => {
const resetAction = StackActions.reset({
index: 2,
actions: [
NavigationActions.navigate({ routeName: 'QrScanner' }),
NavigationActions.navigate({ routeName: 'TxDetails' }),
NavigationActions.navigate({ routeName: 'SignedTx' })
]
});
this.props.navigation.dispatch(resetAction);
}
}
navigate={() => {
const resetAction = StackActions.reset({
index: 2,
actions: [
NavigationActions.navigate({ routeName: 'QrScanner' }),
NavigationActions.navigate({ routeName: 'TxDetails' }),
NavigationActions.navigate({ routeName: next })
]
});
navigation.dispatch(resetAction);
}}
/>
)}
</Subscribe>
......@@ -71,9 +72,8 @@ export class AccountUnlock extends React.Component {
{accounts => (
<AccountUnlockView
{...this.props}
checkPin={async (pin) => {
console.log('checkPin')
return await accounts.unlockAccount(accounts.getSelected(), pin)
checkPin={async pin => {
return await accounts.unlockAccount(accounts.getSelected(), pin);
}}
navigate={() => {
const resetAction = StackActions.reset({
......@@ -95,7 +95,6 @@ export class AccountUnlock extends React.Component {
}
class AccountUnlockView extends React.PureComponent {
static propTypes = {
checkPin: PropTypes.func.isRequired,
hasWrongPin: PropTypes.bool
......@@ -106,10 +105,9 @@ class AccountUnlockView extends React.PureComponent {
hasWrongPin: false
};
showErrorMessage = () => {
return this.state.hasWrongPin ? 'Wrong pin, please try again' : '';
}
};
render() {
return (
......@@ -119,18 +117,17 @@ class AccountUnlockView extends React.PureComponent {
<Text style={styles.errorText}>{this.showErrorMessage()}</Text>
<Text style={styles.title}>PIN</Text>
<PinInput
onChangeText={async (pin) => {
this.setState({ pin: pin })
onChangeText={async pin => {
this.setState({ pin: pin });
if (pin.length < 4) {
return;
}
if (await this.props.checkPin(pin)) {
this.props.navigate()
this.props.navigate();
} else if (pin.length > 5) {
this.setState({ hasWrongPin: true })
this.setState({ hasWrongPin: true });
}
}}
value={this.state.pin}
/>
</View>
......
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
'use strict';
import PropTypes from 'prop-types';
import React from 'react';
import { ScrollView, StyleSheet, Text } from 'react-native';
import { Subscribe } from 'unstated';
import colors from '../colors';
import AccountCard from '../components/AccountCard';
import Background from '../components/Background';
import Button from '../components/Button';
import AccountsStore from '../stores/AccountsStore';
import ScannerStore from '../stores/ScannerStore';
import { hexToAscii, isAscii } from '../util/message';
const orUnknown = (value = 'Unknown') => value;
export default class MessageDetails extends React.PureComponent {
static navigationOptions = {
title: 'Transaction Details',
headerBackTitle: 'Transaction details'
};
render() {
return (
<Subscribe to={[ScannerStore, AccountsStore]}>
{(scannerStore, accounts) => {
const dataToSign = scannerStore.getDataToSign();
if (dataToSign) {
const tx = scannerStore.getTx();
return (
<MessageDetailsView
{...this.props}
scannerStore={scannerStore}
sender={scannerStore.getSender()}
message={scannerStore.getMessage()}
dataToSign={dataToSign}
onPressAccount={async account => {
await accounts.select(account);
this.props.navigation.navigate('AccountDetails');
}}
onNext={async () => {
try {
this.props.navigation.navigate('AccountUnlockAndSign', {
next: 'SignedMessage'
});
} catch (e) {
scannerStore.setErrorMsg(e.message);
}
}}
/>
);
} else {
return null;
}
}}
</Subscribe>
);
}
}
export class MessageDetailsView extends React.PureComponent {
static propTypes = {
onNext: PropTypes.func.isRequired,
dataToSign: PropTypes.string.isRequired,
sender: PropTypes.object.isRequired,
message: PropTypes.string.isRequired
};
render() {
return (
<ScrollView
contentContainerStyle={styles.bodyContent}
style={styles.body}
>
<Background />
<Text style={styles.topTitle}>SIGN MESSAGE</Text>
<Text style={styles.title}>FROM ACCOUNT</Text>
<AccountCard
title={this.props.sender.name}
address={this.props.sender.address}
chainId={this.props.sender.chainId}
onPress={() => {
this.props.onPressAccount(this.props.sender);
}}
/>
<Text style={styles.title}>MESSAGE</Text>
<Text style={styles.message}>
{isAscii(this.props.message)
? hexToAscii(this.props.message)
: this.props.data}
</Text>
<Button
buttonStyles={{ backgroundColor: colors.bg_positive, height: 60 }}
title="Sign Message"
textStyles={{ color: colors.card_text }}
onPress={() => this.props.onNext()}
/>
</ScrollView>
);
}
}
const styles = StyleSheet.create({
body: {
backgroundColor: colors.bg,
flex: 1,
flexDirection: 'column',
padding: 20,
overflow: 'hidden'
},
bodyContent: {
paddingBottom: 40
},
transactionDetails: {
flex: 1,
backgroundColor: colors.card_bg
},
topTitle: {
textAlign: 'center',
color: colors.bg_text_sec,
fontSize: 24,
fontFamily: 'Manifold CF',
fontWeight: 'bold',
paddingBottom: 20
},
title: {
color: colors.bg_text_sec,
fontSize: 18,
fontFamily: 'Manifold CF',
fontWeight: 'bold',
paddingBottom: 20
},
message: {
marginBottom: 20,
padding: 10,
height: 120,
lineHeight: 26,
fontSize: 20,
backgroundColor: colors.card_bg
},
wrapper: {
borderRadius: 5
},
address: {
flex: 1
},
deleteText: {
textAlign: 'right'
},
changePinText: {
textAlign: 'left',
color: 'green'
},
actionsContainer: {
flex: 1,
flexDirection: 'row'
},
actionButtonContainer: {
flex: 1
}
});
......@@ -45,13 +45,17 @@ export default class Scanner extends React.PureComponent {
}
try {
const data = JSON.parse(txRequestData.data);
if (data.data.rlp === undefined) {
if (data.action === undefined) {
return;
}
if (!(await scannerStore.setTXRequest(data, accountsStore))) {
if (!(await scannerStore.setData(data, accountsStore))) {
return;
} else {
this.props.navigation.navigate('TxDetails');
if ('transaction' === scannerStore.getType()) {
this.props.navigation.navigate('TxDetails');
} else { // message
this.props.navigation.navigate('MessageDetails');
}
}
} catch (e) {
scannerStore.setBusy();
......
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
'use strict';
import PropTypes from 'prop-types';
import React from 'react';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import { Subscribe } from 'unstated';
import colors from '../colors';
import QrView from '../components/QrView';
import AccountsStore from '../stores/AccountsStore';
import ScannerStore from '../stores/ScannerStore';
import { hexToAscii, isAscii } from '../util/message';
export default class SignedMessage extends React.PureComponent {
render() {
return (
<Subscribe to={[ScannerStore, AccountsStore]}>
{(scanner, accounts) => {
return (
<SignedMessageView
data={scanner.getSignedTxData()}
message={scanner.getMessage()}
onPressAccount={async account => {
await accounts.select(account);
this.props.navigation.navigate('AccountDetails');
}}
/>
);
}}
</Subscribe>
);
}
}
export class SignedMessageView extends React.PureComponent {
static propTypes = {
data: PropTypes.string.isRequired
};
render() {
return (
<ScrollView style={styles.body} contentContainerStyle={{ padding: 20 }}>
<Text style={styles.topTitle}>SCAN SIGNATURE</Text>
<View style={styles.qr}>
<QrView text={this.props.data} />
</View>
<Text style={styles.title}>MESSAGE</Text>
<Text style={styles.message}>
{isAscii(this.props.message)
? hexToAscii(this.props.message)
: this.props.data}
</Text>
</ScrollView>
);
}
}
const styles = StyleSheet.create({
body: {
backgroundColor: colors.bg,
flex: 1,
flexDirection: 'column',
overflow: 'hidden'
},
qr: {
marginBottom: 20,
backgroundColor: colors.card_bg
},
topTitle: {
textAlign: 'center',
color: colors.bg_text_sec,
fontSize: 24,
fontFamily: 'Manifold CF',
fontWeight: 'bold',
paddingBottom: 20
},
title: {
color: colors.bg_text_sec,
fontSize: 18,
fontFamily: 'Manifold CF',
fontWeight: 'bold',
paddingBottom: 20
},
message: {
marginBottom: 20,
padding: 10,
height: 120,
lineHeight: 26,
fontSize: 20,
backgroundColor: colors.card_bg
}
});
......@@ -116,7 +116,7 @@ export default class AccountsStore extends Container<AccountsState> {
});
}
async loadAccountTxs() { }
async loadAccountTxs() {}
async save(account, pin = null) {
try {
......@@ -181,6 +181,12 @@ export default class AccountsStore extends Container<AccountsState> {
return this.state.accounts.get(accountId(account)) || empty(account);
}
getByAddress(address): ?Account {
return this.getAccounts().find(
a => a.address.toLowerCase() === address.toLowerCase()
);
}
getSelected(): ?Account {
return this.state.accounts.get(this.state.selected);
}
......
......@@ -18,7 +18,7 @@
import { Container } from 'unstated';
import { NETWORK_TITLES } from '../constants';
import { saveTx } from '../util/db';
import { brainWalletSign, decryptData, keccak } from '../util/native';
import { brainWalletSign, decryptData, keccak, ethSign } from '../util/native';
import transaction from '../util/transaction';
import { Account } from './AccountsStore';
......@@ -31,7 +31,9 @@ type SignedTX = {
};
type ScannerState = {
type: 'transaction' | 'message',
txRequest: TXRequest | null,
message: string,
tx: Object,
sender: Account,
recipient: Account,
......@@ -42,8 +44,10 @@ type ScannerState = {
};
const defaultState = {
type: null,
busy: false,
txRequest: null,
message: null,
sender: null,
recipient: null,
tx: '',
......@@ -55,12 +59,44 @@ const defaultState = {
export default class ScannerStore extends Container<ScannerState> {
state = defaultState;
async setData(data, accountsStore) {
console.log(data);
switch (data.action) {
case 'signTransaction':
return await this.setTXRequest(data, accountsStore);
case 'signData':
return await this.setDataToSign(data, accountsStore);
default:
throw new Error(
`Scanned QR should contain either transaction or a message to sign`
);
}
}
async setDataToSign(signRequest, accountsStore) {
const message = signRequest.data.data;
const address = signRequest.data.account;
const dataToSign = await ethSign(message);
const sender = accountsStore.getByAddress(address);
if (!sender || !sender.encryptedSeed) {
throw new Error(
`No private key found for ${address} found in your signer key storage.`
);
}
this.setState({
type: 'message',
sender,
message,
dataToSign
});
return true;
}
async setTXRequest(txRequest, accountsStore) {
this.setBusy();
console.log(txRequest);
if (!(txRequest.data && txRequest.data.rlp && txRequest.data.account)) {
throw new Error(
`Scanned QR contains no valid transaction`
);
throw new Error(`Scanned QR contains no valid transaction`);
}
const tx = await transaction(txRequest.data.rlp);
const { chainId = '1' } = tx;
......@@ -72,10 +108,10 @@ export default class ScannerStore extends Container<ScannerState> {
});
const networkTitle = NETWORK_TITLES[chainId];
if (!sender.encryptedSeed) {
if (!sender || !sender.encryptedSeed) {
throw new Error(
`No private key found for account ${
txRequest.data.account
txRequest.data.account
} found in your signer key storage for the ${networkTitle} chain.`
);
}
......@@ -87,6 +123,7 @@ export default class ScannerStore extends Container<ScannerState> {
});
const dataToSign = await keccak(txRequest.data.rlp);
this.setState({
type: 'transaction',
sender,
recipient,
txRequest,
......@@ -97,18 +134,24 @@ export default class ScannerStore extends Container<ScannerState> {
}
async signData(pin = '1') {
const sender = this.state.sender;
const { type, sender } = this.state;
const seed = await decryptData(sender.encryptedSeed, pin);
const signedData = await brainWalletSign(seed, this.state.dataToSign);
this.setState({ signedData });
await saveTx({
hash: this.state.dataToSign,
tx: this.state.tx,
sender: this.state.sender,
recipient: this.state.recipient,
signature: signedData,
createdAt: new Date().getTime()
});
if (type == 'transaction') {
await saveTx({
hash: this.state.dataToSign,
tx: this.state.tx,
sender: this.state.sender,
recipient: this.state.recipient,
signature: signedData,
createdAt: new Date().getTime()
});
}
}
getType() {
return this.state.type;
}
setBusy() {
......@@ -143,6 +186,10 @@ export default class ScannerStore extends Container<ScannerState> {
return this.state.txRequest;