Skip to content

Commit

Permalink
Pin and Passphrase modals
Browse files Browse the repository at this point in the history
  • Loading branch information
matheusd committed Oct 10, 2018
1 parent 9fb082b commit 90f61d3
Show file tree
Hide file tree
Showing 12 changed files with 376 additions and 2 deletions.
135 changes: 133 additions & 2 deletions app/actions/TrezorActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const loadDeviceList = () => (dispatch, getState) => {
if (!currentDevice) {
// first device connected. Use it.
dispatch({ device, type: TRZ_SELECTEDDEVICE_CHANGED });
setDeviceListeners(device, dispatch);
}
});

Expand Down Expand Up @@ -111,6 +112,127 @@ export const selectDevice = (path) => async (dispatch, getState) => {
const devList = getState().trezor.deviceList;
if (!devList.devices[path]) return;
dispatch({ device: devList.devices[path], type: TRZ_SELECTEDDEVICE_CHANGED });
setDeviceListeners(devList.devices[path], dispatch);
};

export const TRZ_PIN_REQUESTED = "TRZ_PIN_REQUESTED";
export const TRZ_PIN_ENTERED = "TRZ_PIN_ENTERED";
export const TRZ_PIN_CANCELED = "TRZ_PIN_CANCELED";
export const TRZ_PASSPHRASE_REQUESTED = "TRZ_PASSPHRASE_REQUESTED";
export const TRZ_PASSPHRASE_ENTERED = "TRZ_PASSPHRASE_ENTERED";
export const TRZ_PASSPHRASE_CANCELED = "TRZ_PASSPHRASE_CANCELED";

function setDeviceListeners(device, dispatch) {
device.on("pin", (pinMessage, pinCallBack) => {
dispatch({ pinMessage, pinCallBack, type: TRZ_PIN_REQUESTED });
});

device.on("passphrase", (passPhraseCallBack) => {
dispatch({ passPhraseCallBack, type: TRZ_PASSPHRASE_REQUESTED });
});
}

// deviceRun is the main function for executing trezor operations. This handles
// cleanup for cancellations and device disconnections during mid-operation (eg:
// someone disconnected trezor while it was waiting for a pin input).
// In general, fn itself shouldn't handle errors, letting this function handle
// the common cases, which are then propagated up the call stack into fn's
// parent.
async function deviceRun(dispatch, getState, device, fn) {

const handleError = error => {
const { trezor: { waitingForPin, waitingForPassphrase } } = getState();
console.log("Handle error no deviceRun");
if (waitingForPin) dispatch({ error, type: TRZ_PIN_CANCELED });
if (waitingForPassphrase) dispatch({ error, type: TRZ_PASSPHRASE_CANCELED });
if (error instanceof Error) {
if (error.message.includes("Inconsistent state")) {
return "Device returned inconsistent state. Disconnect and reconnect the device.";
}
}
return error;
};

try {
return await device.run(async session => {
try {
return await fn(session);
} catch (err) {
// doesn't seem to be reachable by trezor interruptions, but might be
// caused by fn() failing in some other way (even though it's
// recommended not to do (non-trezor) lengthy operations inside fn())
throw handleError(err);
}
});
} catch (outerErr) {
throw handleError(outerErr);
}
}

export const TRZ_CANCELOPERATION_SUCCESS = "TRZ_CANCELOPERATION_SUCCESS";
export const TRZ_CANCELOPERATION_FAILED = "TRZ_CANCELOPERATION_FAILED";

// Note that calling this function while no pin/passphrase operation is running
// will attempt to steal the device, cancelling operations from apps *other
// than decrediton*.
export const cancelCurrentOperation = () => async (dispatch, getState) => {
const device = selectors.trezorDevice(getState());
const { trezor: { pinCallBack, passPhraseCallBack } } = getState();

if (!device) return;
try {
if (pinCallBack) await pinCallBack("cancelled", null);
else if (passPhraseCallBack) await passPhraseCallBack("cancelled", null);
else await device.steal();

dispatch({ type: TRZ_CANCELOPERATION_SUCCESS });
} catch (error) {
dispatch({ error, type: TRZ_CANCELOPERATION_FAILED });
}
};

export const submitPin = (pin) => (dispatch, getState) => {
const { trezor: { pinCallBack } } = getState();
dispatch({ type: TRZ_PIN_ENTERED });
if (!pinCallBack) return;
pinCallBack(null, pin);
};

export const submitPassPhrase = (passPhrase) => (dispatch, getState) => {
const { trezor: { passPhraseCallBack } } = getState();
dispatch({ type: TRZ_PASSPHRASE_ENTERED });
if (!passPhraseCallBack) return;
passPhraseCallBack(null, passPhrase);
};

// checkTrezorIsDcrwallet verifies whether the wallet currently running on
// dcrwallet (presumably a watch only wallet created from a trezor provided
// xpub) is the same wallet as the one of the currently connected trezor. This
// function throws an error if they are not the same.
// This is useful for making sure, prior to performing some wallet related
// function such as transaction signing, that trezor will correctly perform the
// operation.
// Note that this might trigger pin/passphrase modals, depending on the current
// trezor configuration.
// The way the check is performed is by generating the first address from the
// trezor wallet and then validating this address agains dcrwallet, ensuring
// this is an owned address at the appropriate branch/index.
// This check is only valid for a single session (ie, a single execution of
// `deviceRun`) as the physical device might change between sessions.
const checkTrezorIsDcrwallet = (session) => async (dispatch, getState) => {
const { grpc: { walletService } } = getState();
const chainParams = selectors.chainParams(getState());

const address_n = addressPath(0, 0, WALLET_ACCOUNT, chainParams.HDCoinType);
const resp = await session.getAddress(address_n, chainParams.trezorCoinName, false);
const addr = resp.message.address;

const addrValidResp = await wallet.validateAddress(walletService, addr);
if (!addrValidResp.getIsValid()) throw "Trezor provided an invalid address " + addr;

if (!addrValidResp.getIsMine()) throw "Trezor and dcrwallet not running from the same extended public key";

if (addrValidResp.getIndex() !== 0) throw "Wallet replied with wrong index.";
};

export const signTransactionAttemptTrezor = (rawUnsigTx, constructTxResponse) => async (dispatch, getState) => {
Expand All @@ -120,7 +242,10 @@ export const signTransactionAttemptTrezor = (rawUnsigTx, constructTxResponse) =>
const chainParams = selectors.chainParams(getState());

const device = selectors.trezorDevice(getState());
// TODO: handle not having device
if (!device) {
dispatch({ error: "Device not connected", type: SIGNTX_FAILED });
return;
}

console.log("construct tx response", constructTxResponse);

Expand All @@ -135,7 +260,9 @@ export const signTransactionAttemptTrezor = (rawUnsigTx, constructTxResponse) =>
const txInfo = await dispatch(walletTxToBtcjsTx(decodedUnsigTx,
changeIndex, inputTxs));

const signedRaw = await device.run(async session => {
const signedRaw = await deviceRun(dispatch, getState, device, async session => {
await dispatch(checkTrezorIsDcrwallet(session));

const signedResp = await session.signTx(txInfo.inputs, txInfo.outputs,
refTxs, chainParams.trezorCoinName, 0);
return signedResp.message.serialized.serialized_tx;
Expand Down Expand Up @@ -196,6 +323,7 @@ export const walletTxToBtcjsTx = (tx, changeIndex, inputTxs) => async (dispatch,
sequence: inp.getSequence(),
address_n: addressPath(addrIndex, addrBranch, WALLET_ACCOUNT,
chainParams.HDCoinType),
decred_tree: inp.getTree()

// FIXME: this needs to be supported on trezor.js.
// decredTree: inp.getTree(),
Expand Down Expand Up @@ -228,6 +356,7 @@ export const walletTxToBtcjsTx = (tx, changeIndex, inputTxs) => async (dispatch,
script_type: "PAYTOADDRESS", // needs to change on OP_RETURNs
address: addr,
address_n: address_n,
decred_script_version: outp.getVersion(),
});
}

Expand All @@ -249,6 +378,7 @@ export function walletTxToRefTx(tx) {
amount: inp.getAmountIn(),
prev_hash: rawHashToHex(inp.getPreviousTransactionHash()),
prev_index: inp.getPreviousTransactionIndex(),
decred_tree: inp.getTree()

// TODO: this needs to be supported on trezor.js
// decredTree: inp.getTree(),
Expand All @@ -258,6 +388,7 @@ export function walletTxToRefTx(tx) {
const bin_outputs = tx.getOutputsList().map(outp => ({
amount: outp.getValue(),
script_pubkey: rawToHex(outp.getScript()),
decred_script_version: outp.getVersion(),
}));

const txInfo = {
Expand Down
36 changes: 36 additions & 0 deletions app/components/modals/trezor/Modals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { trezor } from "connectors";
import PinModal from "./PinModal";
import PassPhraseModal from "./PassPhraseModal";
import "style/Trezor.less";

@autobind
class TrezorModals extends React.Component {
constructor(props) {
super(props);
}

onCancelModal() {
this.props.cancelCurrentOperation();
}

render() {
if (this.props.waitingForPin) {
return <PinModal
{...this.props}
onCancelModal={this.onCancelModal}
/>;
} else if (this.props.waitingForPassPhrase) {
return <PassPhraseModal
{...this.props}
onCancelModal={this.onCancelModal}
/>;
} else {
return null;
}
}
}

const TrezorModalsOrNone = ({ isTrezor, ...props }) =>
isTrezor ? <TrezorModals {...props} /> : null;

export default trezor(TrezorModalsOrNone);
38 changes: 38 additions & 0 deletions app/components/modals/trezor/PassPhraseModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { PassphraseModal } from "../PassphraseModal";
import { FormattedMessage as T } from "react-intl";

@autobind
class TrezorPassphraseModal extends React.Component {
constructor(props) {
super(props);
}

onSubmit(passPhrase) {
console.log("gonna submit", passPhrase);
this.props.submitPassPhrase(passPhrase);
}

render() {
const { onCancelModal } = this.props;
const { onSubmit } = this;

const trezorLabel = this.props.device ? this.props.device.features.label : "";

return (
<PassphraseModal
show={true}
modalTitle={<T id="trezor.passphraseModal.title" m="Confirm Trezor Passphrase" />}
className="trezor-passphrase-modal"
modalDescription={
<p>
<T id="trezor.passphraseModal.description" m="Type the secret passphrase for the wallet stored in trezor {label}"
values={{ label: <span className="trezor-label">'{trezorLabel}'</span> }} />
</p>
}
{...{ onCancelModal, onSubmit }}
/>
);
}
}

export default TrezorPassphraseModal;
75 changes: 75 additions & 0 deletions app/components/modals/trezor/PinModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Modal from "../Modal";
import { FormattedMessage as T } from "react-intl";
import { PasswordInput } from "inputs";
import { ButtonsToolbar } from "../PassphraseModal";
import { InvisibleButton } from "buttons";

const PinButton = ({ index, label, onClick }) =>
<div className="pin-pad-button" onClick={() => onClick(index)}>{label}</div>;

@autobind
class PinModal extends React.Component {
constructor(props) {
super(props);
this.state = { currentPin: "" };
}

onPinButtonClick(index) {
this.setState({ currentPin: this.state.currentPin + index });
}

onCancelModal() {
this.setState({ currentPin: "" });
this.props.onCancelModal();
}

onSubmit() {
this.props.submitPin(this.state.currentPin);
}

onClearPin() {
this.setState({ currentPin: "" });
}

render() {
const { onCancelModal, onSubmit, onPinButtonClick, onClearPin } = this;

const labels = "ABCDEFGHI";
const currentPin = this.state.currentPin.split("").map(v => labels[parseInt(v)-1]).join("");

const Button = ({ index }) =>
<PinButton label={labels[index-1]} index={index} onClick={onPinButtonClick} />;

const trezorLabel = this.props.device ? this.props.device.features.label : "";

return (
<Modal className="passphrase-modal trezor-pin-modal" {...{ onCancelModal }}>
<h1><T id="trezor.pinModal.title" m="Enter Pin" /></h1>
<p><T id="trezor.pinModal.description" m="Click button sequence that corresponds to your pin on trezor {label}"
values={{ label: <span className="trezor-label">'{trezorLabel}'</span> }} /></p>
<div className="pin-pad">
<Button index={7} />
<Button index={8} />
<Button index={9} />
<Button index={4} />
<Button index={5} />
<Button index={6} />
<Button index={1} />
<Button index={2} />
<Button index={3} />
</div>
<div>
<InvisibleButton onClick={onClearPin} className="pin-pad-clear-btn">
<T id="trezor.pinModal.clear" m="clear" />
</InvisibleButton>
</div>
<div className="password-field">
<PasswordInput value={currentPin} />
</div>
<ButtonsToolbar {... { onCancelModal, onSubmit }} />
</Modal>
);
}
}

export default PinModal;
1 change: 1 addition & 0 deletions app/components/modals/trezor/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as TrezorModals } from "./Modals";
1 change: 1 addition & 0 deletions app/connectors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ export { default as preVoteProposals } from "./preVoteProposals";
export { default as votedProposals } from "./votedProposals";
export { default as proposals } from "./proposals";
export { default as network } from "./network";
export { default as trezor } from "./trezor";
20 changes: 20 additions & 0 deletions app/connectors/trezor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { connect } from "react-redux";
import { selectorMap } from "../fp";
import { bindActionCreators } from "redux";
import * as sel from "../selectors";
import * as trza from "../actions/TrezorActions";

const mapStateToProps = selectorMap({
isTrezor: sel.isTrezor,
waitingForPin: sel.trezorWaitingForPin,
waitingForPassPhrase: sel.trezorWaitingForPassPhrase,
device: sel.trezorDevice,
});

const mapDispatchToProps = dispatch => bindActionCreators({
cancelCurrentOperation: trza.cancelCurrentOperation,
submitPin: trza.submitPin,
submitPassPhrase: trza.submitPassPhrase,
}, dispatch);

export default connect(mapStateToProps, mapDispatchToProps);
2 changes: 2 additions & 0 deletions app/containers/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ShutdownAppPage from "components/views/ShutdownAppPage";
import FatalErrorPage from "components/views/FatalErrorPage";
import Snackbar from "components/Snackbar";
import { log } from "wallet";
import { TrezorModals } from "components/modals/trezor";
import "style/Layout.less";
const topLevelAnimation = { atEnter: { opacity: 0 }, atLeave: { opacity: 0 }, atActive: { opacity: 1 } };

Expand Down Expand Up @@ -104,6 +105,7 @@ class App extends React.Component {
<Route path="/" component={WalletContainer} />
</MainSwitch>
<div id="modal-portal" />
<TrezorModals />
</Aux>
</IntlProvider>
);
Expand Down
6 changes: 6 additions & 0 deletions app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,12 @@ var initialState = {
deviceList: null,
transportError: false,
device: null,
waitingForPin: false,
waitingForPassPhrase: false,
pinCallBack: null,
passPhraseCallBack: null,
pinMessage: null,
passPhraseMessage: null,
},
locales: locales
};
Expand Down
Loading

0 comments on commit 90f61d3

Please sign in to comment.