on master help sign in
Merge pull request #431 from urbit/lf/auth-token

Auth Token support

commit2c3486ce7fdd83210205016a6d9f07a760d88259 authorFang <git@fang.io> dateWed 15 Apr 2020, 14:35 (7 years ago) parent7a3cdd1bfbafde
11 files changed+214−32
package.json+1−1 src/lib/authToken.js+44 src/lib/ledger.js+19−2 src/lib/network.js+12−1 src/lib/trezor.js+15 src/lib/wallet.js+6 src/store/network.js+22−10 src/store/wallet.js+34 src/views/Login/Ledger.js+17−5 src/views/Login/Metamask.js+28−8 src/views/Login/Trezor.js+16−5
package.json+1−1
@@ -81,7 +81,7 @@
8181 "checksum": "cd build && find . -exec shasum -a256 {} ';' > ../checksums.txt && cd ..",
8282 "zip": "zip -ur bridge-$npm_package_version.zip README.md bridge-https.py checksums.txt && cd build && zip -ur ../bridge-$npm_package_version.zip ./* && cd ..",
8383 "release": "npm install && npm run build && npm run checksum && npm run zip",
84− "pilot:ganache": "ganache-cli --blockTime 1 --networkId 1 --host '0.0.0.0' -m 'benefit crew supreme gesture quantum web media hazard theory mercy wing kitten' > /dev/null &",
84+ "pilot:ganache": "ganache-cli --blockTime 1 --networkId 5 --host '0.0.0.0' -m 'benefit crew supreme gesture quantum web media hazard theory mercy wing kitten' > /dev/null &",
8585 "pilot:setup": "npm run pilot:ganache && truffle deploy",
8686 "pilot": "HTTPS=true npm-run-all pilot:setup start pilot:cleanup --continue-on-error",
8787 "pilot-invites": "WITH_TEST_STATE=INVITES npm run pilot",
src/lib/authToken.jsnew file+44
@@ -0,0 +1,44 @@
1+import * as secp256k1 from 'secp256k1';
2+
3+import { keccak256, WALLET_TYPES } from './wallet';
4+import { ledgerSignMessage } from './ledger';
5+import { trezorSignMessage } from './trezor';
6+
7+const MESSAGE = 'Bridge Authentication Token';
8+
9+function signMessage(privateKey) {
10+ const { signature } = secp256k1.sign(
11+ keccak256('\x19Ethereum Signed Message:\n' + MESSAGE.length + MESSAGE),
12+ privateKey
13+ );
14+ // add key recovery parameter
15+ const ethSignature = new Uint8Array(65);
16+ ethSignature.set(signature);
17+ const v = (ethSignature[32] & 1) + 27;
18+ ethSignature[64] = v;
19+
20+ return ethSignature;
21+}
22+
23+export const getAuthToken = async ({
24+ wallet,
25+ walletType,
26+ walletHdPath,
27+ web3,
28+}) => {
29+ if (walletType === WALLET_TYPES.METAMASK) {
30+ return web3.eth.personal.sign(MESSAGE, wallet.address, '');
31+ }
32+ if (walletType === WALLET_TYPES.LEDGER) {
33+ return ledgerSignMessage(MESSAGE, walletHdPath);
34+ }
35+ if (walletType === WALLET_TYPES.TREZOR) {
36+ return trezorSignMessage(MESSAGE, walletHdPath);
37+ }
38+
39+ const signature = signMessage(wallet.privateKey);
40+
41+ const token = `0x${Buffer.from(signature).toString('hex')}`;
42+
43+ return token;
44+};
src/lib/ledger.js+19−2
@@ -8,11 +8,11 @@ export const chopHdPrefix = str =>
88 str.slice(0, 2) === 'm/' ? str.slice(2) : str;
99export const addHdPrefix = str => (str.slice(0, 2) === 'm/' ? str : 'm/' + str);
1010
11−export const ledgerSignTransaction = async (txn, hdpath) => {
11+export const ledgerSignTransaction = async (txn, hdPath) => {
1212 const transport = await Transport.create();
1313 const eth = new Eth(transport);
1414
15− const path = chopHdPrefix(hdpath);
15+ const path = chopHdPrefix(hdPath);
1616 const serializedTx = txn.serialize().toString('hex');
1717 const sig = await eth.signTransaction(path, serializedTx);
1818
@@ -22,3 +22,20 @@ export const ledgerSignTransaction = async (txn, hdpath) => {
2222
2323 return txn;
2424};
25+
26+export const ledgerSignMessage = async (message, hdPath) => {
27+ const transport = await Transport.create();
28+ const eth = new Eth(transport);
29+
30+ const path = chopHdPrefix(hdPath);
31+ const { r, s, v } = await eth.signPersonalMessage(
32+ path,
33+ Buffer.from(message).toString('hex')
34+ );
35+ const ethSignature = new Uint8Array(65);
36+ ethSignature.set(Buffer.from(r, 'hex'));
37+ ethSignature.set(Buffer.from(s, 'hex'), 32);
38+ ethSignature[64] = v;
39+
40+ return `0x${Buffer.from(ethSignature).toString('hex')}`;
41+};
src/lib/network.js+12−1
@@ -16,4 +16,15 @@ const renderNetworkType = network =>
1616 ? 'Local Node'
1717 : 'Offline';
1818
19−export { NETWORK_TYPES, renderNetworkType };
19+const chainIdToNetworkType = chainId => {
20+ switch (chainId) {
21+ case '0x1':
22+ return NETWORK_TYPES.MAINNET;
23+ case '0x3':
24+ return NETWORK_TYPES.ROPSTEN;
25+ default:
26+ return NETWORK_TYPES.LOCAL;
27+ }
28+};
29+
30+export { NETWORK_TYPES, renderNetworkType, chainIdToNetworkType };
src/lib/trezor.js+15
@@ -36,4 +36,19 @@ const trezorSignTransaction = async (txn, hdpath) => {
3636 return txn;
3737};
3838
39+export const trezorSignMessage = async (message, hdPath) => {
40+ const sig = await TrezorConnect.signMessage({
41+ path: hdPath,
42+ message,
43+ });
44+
45+ if (!sig.success) {
46+ throw new Error(sig.payload.error);
47+ }
48+
49+ const { signature } = sig.payload;
50+
51+ return signature;
52+};
53+
3954export { TREZOR_PATH, trezorSignTransaction };
src/lib/wallet.js+6
@@ -22,6 +22,12 @@ export const WALLET_TYPES = {
2222 METAMASK: Symbol('METAMASK'),
2323};
2424
25+export const NONCUSTODIAL_WALLETS = new Set([
26+ WALLET_TYPES.METAMASK,
27+ WALLET_TYPES.TREZOR,
28+ WALLET_TYPES.LEDGER,
29+]);
30+
2531export function EthereumWallet(privateKey) {
2632 this.privateKey = privateKey;
2733 this.publicKey = secp256k1.publicKeyCreate(this.privateKey);
src/store/network.js+22−10
@@ -4,6 +4,7 @@ import React, {
44 useContext,
55 useMemo,
66 useState,
7+ useEffect,
78} from 'react';
89import { Just, Nothing } from 'folktale/maybe';
910import * as azimuth from 'azimuth-js';
@@ -11,25 +12,36 @@ import Web3 from 'web3';
1112import { includes } from 'lodash';
1213
1314import { CONTRACT_ADDRESSES } from '../lib/contracts';
14−import { NETWORK_TYPES } from '../lib/network';
15+import { NETWORK_TYPES, chainIdToNetworkType } from '../lib/network';
1516import { isDevelopment } from '../lib/flags';
1617import { BRIDGE_ERROR } from '../lib/error';
1718
1819function _useNetwork(initialNetworkType = null) {
19− const [networkType, _setNetworkType] = useState(initialNetworkType);
20+ const [networkType, setNetworkType] = useState(initialNetworkType);
2021
21− const setNetworkType = networkType => {
22− if (!includes(NETWORK_TYPES, networkType)) {
23− throw new Error(BRIDGE_ERROR.INVALID_NETWORK_TYPE);
24− }
25− _setNetworkType(networkType);
26− };
22+ const [metamask, setMetamask] = useState(false);
23+
24+ useEffect(() => {
25+ (async () => {
26+ if (window.ethereum) {
27+ try {
28+ await window.ethereum.enable();
29+ setMetamask(true);
30+ setNetworkType(chainIdToNetworkType(window.ethereum.chainId));
31+ } catch (e) {
32+ console.log('Metamask denied');
33+ }
34+ }
35+ })();
36+ }, []);
2737
2838 const { web3, contracts } = useMemo(() => {
2939 // given a web3 provider and contract addresses,
3040 // build the web3 and contracts objects
3141 const initWeb3 = (provider, contractAddresses) => {
32− const web3 = new Web3(provider);
42+ // use an in-window eth provider if possible
43+ const _provider = metamask ? window.ethereum : provider;
44+ const web3 = new Web3(_provider);
3345 const contracts = azimuth.initContracts(web3, contractAddresses);
3446
3547 return {
@@ -88,7 +100,7 @@ function _useNetwork(initialNetworkType = null) {
88100 };
89101 }
90102 }
91− }, [networkType]);
103+ }, [networkType, metamask]);
92104
93105 return { networkType, setNetworkType, web3, contracts };
94106}
src/store/wallet.js+34
@@ -4,6 +4,7 @@ import React, {
44 useCallback,
55 useContext,
66 useState,
7+ useEffect,
78} from 'react';
89import { Just, Nothing } from 'folktale/maybe';
910import { includes } from 'lodash';
@@ -13,9 +14,13 @@ import {
1314 DEFAULT_HD_PATH,
1415 addressFromSecp256k1Public,
1516 walletFromMnemonic,
17+ NONCUSTODIAL_WALLETS,
1618} from 'lib/wallet';
19+import { getAuthToken } from 'lib/authToken';
1720import { BRIDGE_ERROR } from 'lib/error';
1821
22+import { useNetwork } from 'store/network';
23+
1924export const WalletContext = createContext(null);
2025
2126const DEFAULT_WALLET_TYPE = WALLET_TYPES.TICKET;
@@ -46,6 +51,32 @@ function _useWallet(initialWallet = Nothing(), initialMnemonic = Nothing()) {
4651 const [networkSeed, setNetworkSeed] = useState(Nothing());
4752 const [networkRevision, setNetworkRevision] = useState(Nothing());
4853
54+ const [authToken, setAuthToken] = useState(Nothing());
55+
56+ const { web3 } = useNetwork();
57+
58+ useEffect(() => {
59+ (async () => {
60+ if (
61+ !Just.hasInstance(wallet) ||
62+ !Just.hasInstance(web3) ||
63+ NONCUSTODIAL_WALLETS.has(walletType)
64+ ) {
65+ return;
66+ }
67+ const _wallet = wallet.value;
68+ const _web3 = web3.value;
69+ const token = await getAuthToken({
70+ wallet: _wallet,
71+ walletType,
72+ walletHdPath,
73+ web3: _web3,
74+ });
75+
76+ setAuthToken(token);
77+ })();
78+ }, [wallet, walletType, walletHdPath, web3]);
79+
4980 const setWalletType = useCallback(
5081 walletType => {
5182 if (!includes(WALLET_TYPES, walletType)) {
@@ -132,6 +163,9 @@ function _useWallet(initialWallet = Nothing(), initialMnemonic = Nothing()) {
132163 setNetworkRevision,
133164 //
134165 resetWallet,
166+ //
167+ authToken,
168+ setAuthToken,
135169 };
136170}
137171
src/views/Login/Ledger.js+17−5
@@ -19,6 +19,7 @@ import {
1919import { WALLET_TYPES } from 'lib/wallet';
2020import useLoginView from 'lib/useLoginView';
2121import useBreakpoints from 'lib/useBreakpoints';
22+
2223import {
2324 composeValidator,
2425 buildCheckboxValidator,
@@ -31,6 +32,7 @@ import SubmitButton from 'form/SubmitButton';
3132import Condition from 'form/Condition';
3233import { FORM_ERROR } from 'final-form';
3334import { HdPathInput } from 'form/Inputs';
35+import { getAuthToken } from 'lib/authToken';
3436
3537const PATH_OPTIONS = [
3638 { text: 'Ledger Live', value: LEDGER_LIVE_PATH },
@@ -45,7 +47,7 @@ const ACCOUNT_OPTIONS = times(20, i => ({
4547export default function Ledger({ className, goHome }) {
4648 useLoginView(WALLET_TYPES.LEDGER);
4749
48− const { setWallet, setWalletHdPath } = useWallet();
50+ const { setWallet, setWalletHdPath, setAuthToken } = useWallet();
4951
5052 const validate = useMemo(
5153 () =>
@@ -70,14 +72,21 @@ export default function Ledger({ className, goHome }) {
7072 const chainCode = Buffer.from(info.chainCode, 'hex');
7173 const pub = secp256k1.publicKeyConvert(publicKey, true);
7274 const hd = bip32.fromPublicKey(pub, chainCode);
75+ const authToken = await getAuthToken({
76+ wallet: hd,
77+ walletType: WALLET_TYPES.LEDGER,
78+ walletHdPath: addHdPrefix(values.hdpath),
79+ });
7380
81+ setAuthToken(authToken);
7482 setWallet(Just(hd));
7583 setWalletHdPath(addHdPrefix(values.hdpath));
7684 } catch (error) {
85+ console.error(error);
7786 return { [FORM_ERROR]: error.message };
7887 }
7988 },
80− [setWallet, setWalletHdPath]
89+ [setAuthToken, setWallet, setWalletHdPath]
8190 );
8291
8392 const onValues = useCallback(({ valid, values, form }) => {
@@ -154,7 +163,9 @@ export default function Ledger({ className, goHome }) {
154163 Connect and authenticate to your Ledger, and then open the "Ethereum"
155164 application. If you're running on older firmware, make sure the "browser
156165 support" option is turned on. To sign transactions, you'll also need to
157− enable the "contract data" option.
166+ enable the "contract data" option. You will be prompted to sign a
167+ message. This allows Bridge to operate correctly. Never sign this
168+ message outside of Bridge.
158169 </Grid.Item>
159170
160171 <BridgeForm
@@ -163,7 +174,7 @@ export default function Ledger({ className, goHome }) {
163174 onSubmit={onSubmit}
164175 afterSubmit={goHome}
165176 initialValues={initialValues}>
166− {({ handleSubmit }) => (
177+ {({ handleSubmit, submitting }) => (
167178 <>
168179 <Condition when="useCustomPath" is={true}>
169180 <Grid.Item full as={HdPathInput} name="hdpath" label="HD Path" />
@@ -204,7 +215,8 @@ export default function Ledger({ className, goHome }) {
204215 <Grid.Item full as={FormError} />
205216
206217 <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
207− Authenticate
218+ {!submitting && 'Authenticate'}
219+ {submitting && 'Please check your device'}
208220 </Grid.Item>
209221 </>
210222 )}
src/views/Login/Metamask.js+28−8
@@ -1,26 +1,46 @@
11import React, { useCallback } from 'react';
22import { Grid, Text, LinkButton, H5 } from 'indigo-react';
33import { Just } from 'folktale/maybe';
4+import { FORM_ERROR } from 'final-form';
45
56import { useWallet } from 'store/wallet';
7+import { useNetwork } from 'store/network';
68
7−import { WALLET_TYPES } from 'lib/wallet';
9+import { WALLET_TYPES, abbreviateAddress } from 'lib/wallet';
810import { MetamaskWallet } from 'lib/metamask';
11+import { getAuthToken } from 'lib/authToken';
912import useLoginView from 'lib/useLoginView';
13+import * as need from 'lib/need';
1014
1115import SubmitButton from 'form/SubmitButton';
1216import BridgeForm from 'form/BridgeForm';
1317
1418export default function Metamask({ className, goHome }) {
1519 useLoginView(WALLET_TYPES.METAMASK);
16− const { setWallet, setWalletType } = useWallet();
20+ const { setWallet, setWalletType, setAuthToken } = useWallet();
21+
22+ const { web3 } = useNetwork();
23+ const _web3 = need.web3(web3);
1724
1825 const onSubmit = useCallback(async () => {
19− const accounts = await window.ethereum.enable();
20− const wallet = new MetamaskWallet(accounts[0]);
21− setWallet(Just(wallet));
22− setWalletType(WALLET_TYPES.METAMASK);
23− }, [setWallet, setWalletType]);
26+ try {
27+ const accounts = await window.ethereum.enable();
28+ const wallet = new MetamaskWallet(accounts[0]);
29+
30+ const authToken = await getAuthToken({
31+ wallet,
32+ walletType: WALLET_TYPES.METAMASK,
33+ web3: _web3,
34+ });
35+
36+ setAuthToken(authToken);
37+ setWallet(Just(wallet));
38+ setWalletType(WALLET_TYPES.METAMASK);
39+ } catch (e) {
40+ console.error(e);
41+ return { [FORM_ERROR]: e.message };
42+ }
43+ }, [_web3, setAuthToken, setWallet, setWalletType]);
2444
2545 return (
2646 <Grid className={className}>
@@ -56,7 +76,7 @@ function Login({ onSubmit, goHome }) {
5676 <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
5777 Login
5878 {window.ethereum.selectedAddress &&
59− ' as ' + window.ethereum.selectedAddress}
79+ ' as ' + abbreviateAddress(window.ethereum.selectedAddress)}
6080 </Grid.Item>
6181 )}
6282 </BridgeForm>
src/views/Login/Trezor.js+16−5
@@ -11,6 +11,7 @@ import { useWallet } from 'store/wallet';
1111import { TREZOR_PATH } from 'lib/trezor';
1212import { WALLET_TYPES } from 'lib/wallet';
1313import useLoginView from 'lib/useLoginView';
14+import { getAuthToken } from 'lib/authToken';
1415
1516import {
1617 composeValidator,
@@ -35,7 +36,7 @@ const ACCOUNT_OPTIONS = times(20, i => ({
3536export default function Trezor({ className, goHome }) {
3637 useLoginView(WALLET_TYPES.TREZOR);
3738
38− const { setWallet, setWalletHdPath } = useWallet();
39+ const { setWallet, setWalletHdPath, setAuthToken } = useWallet();
3940
4041 const validate = useMemo(
4142 () =>
@@ -67,10 +68,17 @@ export default function Trezor({ className, goHome }) {
6768 const pub = secp256k1.publicKeyConvert(publicKey, true);
6869 const hd = bip32.fromPublicKey(pub, chainCode);
6970
71+ const authToken = await getAuthToken({
72+ wallet: hd,
73+ walletType: WALLET_TYPES.TREZOR,
74+ walletHdPath: values.hdPath,
75+ });
76+
77+ setAuthToken(authToken);
7078 setWallet(Just(hd));
7179 setWalletHdPath(values.hdPath);
7280 },
73− [setWallet, setWalletHdPath]
81+ [setAuthToken, setWallet, setWalletHdPath]
7482 );
7583
7684 const onValues = useCallback(({ valid, values, form }) => {
@@ -96,7 +104,9 @@ export default function Trezor({ className, goHome }) {
96104 <Grid className={className}>
97105 <Grid.Item full as={Text} className="f6 gray4 mb3">
98106 Connect and authenticate to your Trezor. If you'd like to use a custom
99− derivation path, you may enter it below.
107+ derivation path, you may enter it below. Upon login, you will prompted
108+ to sign the message "Bridge Authentication Token". This allows Bridge to
109+ operate correctly. Never sign this message outside of Bridge.
100110 </Grid.Item>
101111
102112 <BridgeForm
@@ -105,7 +115,7 @@ export default function Trezor({ className, goHome }) {
105115 onSubmit={onSubmit}
106116 afterSubmit={goHome}
107117 initialValues={initialValues}>
108− {({ handleSubmit }) => (
118+ {({ handleSubmit, submitting }) => (
109119 <>
110120 <Condition when="useCustomPath" is={true}>
111121 <Grid.Item full as={HdPathInput} name="hdPath" label="HD Path" />
@@ -133,7 +143,8 @@ export default function Trezor({ className, goHome }) {
133143 <Grid.Item full as={FormError} />
134144
135145 <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
136− Authenticate
146+ {!submitting && 'Authenticate'}
147+ {submitting && 'Please check your device'}
137148 </Grid.Item>
138149 </>
139150 )}