on master help sign in
Merge pull request #85 from urbit/gen-keyfile-improvements
153508bChristopher Johnson
src/Bridge.js+21−4
@@ -40,6 +40,7 @@ class Bridge extends React.Component {
40 this.state = {
40 this.state = {
41 // routes
41 // routes
42 routeCrumbs: Stack([ ROUTE_NAMES.LANDING ]),
42 routeCrumbs: Stack([ ROUTE_NAMES.LANDING ]),
43+ routeData: {},
43 // network
44 // network
44 networkType: networkType,
45 networkType: networkType,
45 web3: Maybe.Nothing(),
46 web3: Maybe.Nothing(),
@@ -51,6 +52,7 @@ class Bridge extends React.Component {
51 // urbit wallet-related
52 // urbit wallet-related
52 urbitWallet: Maybe.Nothing(),
53 urbitWallet: Maybe.Nothing(),
53 authMnemonic: Maybe.Nothing(),
54 authMnemonic: Maybe.Nothing(),
55+ networkSeedCache: null,
54 // point
56 // point
55 pointCursor: Maybe.Nothing(),
57 pointCursor: Maybe.Nothing(),
56 pointCache: {},
58 pointCache: {},
@@ -70,6 +72,7 @@ class Bridge extends React.Component {
70 this.setAuthMnemonic = this.setAuthMnemonic.bind(this)
72 this.setAuthMnemonic = this.setAuthMnemonic.bind(this)
71 this.setPointCursor = this.setPointCursor.bind(this)
73 this.setPointCursor = this.setPointCursor.bind(this)
72 this.setTxnHashCursor = this.setTxnHashCursor.bind(this)
74 this.setTxnHashCursor = this.setTxnHashCursor.bind(this)
75+ this.setNetworkSeedCache = this.setNetworkSeedCache.bind(this)
73 this.addToPointCache = this.addToPointCache.bind(this)
76 this.addToPointCache = this.addToPointCache.bind(this)
74 }
77 }
75
78
@@ -88,7 +91,7 @@ class Bridge extends React.Component {
88
91
89 // if (process.env.NODE_ENV === 'development') {
92 // if (process.env.NODE_ENV === 'development') {
90 //
93 //
91− // const socket = 'wss://localhost:8545'
94+ // const socket = 'ws://localhost:8545'
92 // const provider = new Web3.providers.WebsocketProvider(socket)
95 // const provider = new Web3.providers.WebsocketProvider(socket)
93 // const web3 = new Web3(provider)
96 // const web3 = new Web3(provider)
94 // const contracts = azimuth.initContracts(web3, CONTRACT_ADDRESSES.DEV)
97 // const contracts = azimuth.initContracts(web3, CONTRACT_ADDRESSES.DEV)
@@ -98,7 +101,8 @@ class Bridge extends React.Component {
98 //
101 //
99 // this.setState({
102 // this.setState({
100 // routeCrumbs: Stack([
103 // routeCrumbs: Stack([
101− // ROUTE_NAMES.CREATE_GALAXY,
104+ // // ROUTE_NAMES.CREATE_GALAXY,
105+ // // ROUTE_NAMES.SHIP,
102 // ROUTE_NAMES.SHIPS,
106 // ROUTE_NAMES.SHIPS,
103 // // ROUTE_NAMES.MNEMONIC,
107 // // ROUTE_NAMES.MNEMONIC,
104 // ROUTE_NAMES.WALLET,
108 // ROUTE_NAMES.WALLET,
@@ -106,20 +110,22 @@ class Bridge extends React.Component {
106 // ROUTE_NAMES.LANDING
110 // ROUTE_NAMES.LANDING
107 // ]),
111 // ]),
108 // networkType: NETWORK_NAMES.LOCAL,
112 // networkType: NETWORK_NAMES.LOCAL,
113+ // pointCursor: Maybe.Just(0),
109 // web3: Maybe.Just(web3),
114 // web3: Maybe.Just(web3),
110 // contracts: Maybe.Just(contracts),
115 // contracts: Maybe.Just(contracts),
111 // walletType: WALLET_NAMES.MNEMONIC,
116 // walletType: WALLET_NAMES.MNEMONIC,
112 // wallet: walletFromMnemonic(mnemonic, hdpath),
117 // wallet: walletFromMnemonic(mnemonic, hdpath),
113 // urbitWallet: Maybe.Nothing(),
118 // urbitWallet: Maybe.Nothing(),
114− // authMnemonic: Maybe.Nothing()
119+ // authMnemonic: Maybe.Just('benefit crew supreme gesture quantum web media hazard theory mercy wing kitten')
115 // })
120 // })
116 // }
121 // }
117}
122}
118
123
119− pushRoute(symbol) {
124+ pushRoute(symbol, routeData) {
120 if (lodash.includes(ROUTE_NAMES, symbol)) {
125 if (lodash.includes(ROUTE_NAMES, symbol)) {
121 this.setState((state, _) => ({
126 this.setState((state, _) => ({
122 routeCrumbs: state.routeCrumbs.push(symbol),
127 routeCrumbs: state.routeCrumbs.push(symbol),
128+ routeData: routeData
123 }));
129 }));
124
130
125 // Scroll to top of page with each route transition.
131 // Scroll to top of page with each route transition.
@@ -152,6 +158,12 @@ class Bridge extends React.Component {
152 }
158 }
153 }
159 }
154
160
161+ setNetworkSeedCache(networkSeed) {
162+ this.setState({
163+ networkSeedCache: networkSeed
164+ })
165+ }
166+
155 setNetwork(web3, contracts) {
167 setNetwork(web3, contracts) {
156 this.setState({
168 this.setState({
157 web3: web3,
169 web3: web3,
@@ -202,12 +214,14 @@ class Bridge extends React.Component {
202 render() {
214 render() {
203 const {
215 const {
204 routeCrumbs,
216 routeCrumbs,
217+ routeData,
205 networkType,
218 networkType,
206 walletType,
219 walletType,
207 walletHdPath,
220 walletHdPath,
208 wallet,
221 wallet,
209 urbitWallet,
222 urbitWallet,
210 authMnemonic,
223 authMnemonic,
224+ networkSeedCache,
211 pointCursor,
225 pointCursor,
212 pointCache,
226 pointCache,
213 txnHashCursor,
227 txnHashCursor,
@@ -235,6 +249,7 @@ class Bridge extends React.Component {
235 // router
249 // router
236 pushRoute={ this.pushRoute }
250 pushRoute={ this.pushRoute }
237 popRoute={ this.popRoute }
251 popRoute={ this.popRoute }
252+ routeData={ routeData }
238 // network
253 // network
239 setNetworkType={ this.setNetworkType }
254 setNetworkType={ this.setNetworkType }
240 setNetwork={ this.setNetwork }
255 setNetwork={ this.setNetwork }
@@ -258,6 +273,8 @@ class Bridge extends React.Component {
258 addToPointCache={ this.addToPointCache }
273 addToPointCache={ this.addToPointCache }
259 pointCursor={ pointCursor }
274 pointCursor={ pointCursor }
260 pointCache={ pointCache }
275 pointCache={ pointCache }
276+ networkSeedCache= { networkSeedCache }
277+ setNetworkSeedCache= { this.setNetworkSeedCache }
261 // txn
278 // txn
262 setTxnHashCursor={ this.setTxnHashCursor }
279 setTxnHashCursor={ this.setTxnHashCursor }
263 txnHashCursor={ txnHashCursor } />
280 txnHashCursor={ txnHashCursor } />
src/style/extend.css+7
@@ -463,3 +463,10 @@ code {
463.clickable {
463.clickable {
464 cursor: pointer;
464 cursor: pointer;
465}
465}
466+
467+.keyfile {
468+ white-space: pre-wrap;
469+ word-break: break-word;
470+ padding: 1rem 2rem;
471+ margin-bottom: 4rem;
472+}
src/views/GenKeyfile.js+75−92
@@ -1,4 +1,5 @@
1import React from 'react'
1import React from 'react'
2+import Maybe from 'folktale/maybe'
2
3
3import { Button } from '../components/Base'
4import { Button } from '../components/Base'
4import { RequiredInput, InnerLabel } from '../components/Base'
5import { RequiredInput, InnerLabel } from '../components/Base'
@@ -21,39 +22,14 @@ class GenKeyfile extends React.Component {
21
22
22 this.state = {
23 this.state = {
23 keyfile: '',
24 keyfile: '',
24− networkSeed: ''
25+ loaded: false
25 }
26 }
26−
27− this.handleNetworkSeedInput = this.handleNetworkSeedInput.bind(this)
28− this.handleKeyfileChange = this.handleKeyfileChange.bind(this)
29− }
30−
31− componentDidMount() {
32− this.deriveSeed()
33− }
34−
35− async deriveSeed() {
36− const next = false
37− const seed = await attemptSeedDerivation(next, this.props)
38− this.setState({
39− networkSeed: seed.getOrElse('')
40− })
41− }
42−
43− handleNetworkSeedInput = (networkSeed) => {
44− this.setState({ networkSeed })
45 }
27 }
46
28
47− handleKeyfileChange = (keyfile) => {
29+ getPointDetails() {
48− this.setState({ keyfile })
49− }
50−
51− render() {
52 const { pointCache } = this.props
30 const { pointCache } = this.props
53 const { pointCursor } = this.props
31 const { pointCursor } = this.props
54
32
55− const { keyfile, networkSeed } = this.state
56−
57 const point = pointCursor.matchWith({
33 const point = pointCursor.matchWith({
58 Just: (pt) => pt.value,
34 Just: (pt) => pt.value,
59 Nothing: () => {
35 Nothing: () => {
@@ -68,6 +44,34 @@ class GenKeyfile extends React.Component {
68
44
69 const revision = parseInt(pointDetails.keyRevisionNumber)
45 const revision = parseInt(pointDetails.keyRevisionNumber)
70
46
47+ return {
48+ point,
49+ pointDetails,
50+ revision
51+ }
52+ }
53+
54+ async componentDidMount() {
55+ const { point, pointDetails, revision } = this.getPointDetails();
56+ let keyfile = ''
57+
58+ const hexRegExp = /[0-9A-Fa-f]{64}/g
59+ const networkSeed = await this.deriveSeed()
60+
61+ const keysmatch = this.checkKeysMatch(networkSeed, pointDetails)
62+ const seedValid = hexRegExp.test(networkSeed)
63+
64+ if (keysmatch && seedValid) {
65+ keyfile = genKey(networkSeed, point, revision)
66+ }
67+
68+ this.setState({
69+ keyfile: keyfile,
70+ loaded: true
71+ });
72+ }
73+
74+ checkKeysMatch(networkSeed, pointDetails) {
71 const crypub = pointDetails.encryptionKey
75 const crypub = pointDetails.encryptionKey
72 const sgnpub = pointDetails.authenticationKey
76 const sgnpub = pointDetails.authenticationKey
73
77
@@ -77,29 +81,23 @@ class GenKeyfile extends React.Component {
77 crypub === addHexPrefix(crypt.public)
81 crypub === addHexPrefix(crypt.public)
78 && sgnpub === addHexPrefix(auth.public)
82 && sgnpub === addHexPrefix(auth.public)
79
83
80− const warning =
81− keyfile !== '' && keysmatch === false
84+ return keysmatch;
82− ? <Row>
85+ }
83− <Col>
84− <b>{ 'WARNING' }</b>{ ": derived key doesn't match Azimuth keys!" }
85− </Col>
86− </Row>
87− : <div />
88
86
89− const hexRegExp = /[0-9A-Fa-f]{64}/g
87+ async deriveSeed() {
88+ const next = false
89+ let seed = await attemptSeedDerivation(next, this.props)
90
90
91− const download =
91+ if (seed.getOrElse('') === '' && this.props.networkSeedCache) {
92− keyfile !== ''
92+ seed = Maybe.Just(this.props.networkSeedCache)
93− ? <Button
93+ }
94− onClick={
94+
95− () => {
95+ return seed.getOrElse('')
96− let blob = new Blob([keyfile], {type:"text/plain;charset=utf-8"});
96+ }
97− saveAs(blob, `${ob.patp(point).slice(1)}-${revision}.key`)
97+
98− }
98+ render() {
99− }>
99+ const { point, pointDetails, revision } = this.getPointDetails();
100− Download →
100+ const { keyfile, loaded } = this.state
101− </Button>
102− : ''
103
101
104 return (
102 return (
105 <Row>
103 <Row>
@@ -107,55 +105,40 @@ class GenKeyfile extends React.Component {
107 <H1>{ 'Generate keyfile' }</H1>
105 <H1>{ 'Generate keyfile' }</H1>
108
106
109 <P>
107 <P>
110− { "Generate a private key file for booting this point in Arvo." }
108+ { "Download a private key file for booting this point in Arvo." }
111 </P>
109 </P>
112
110
111+ { keyfile === '' && !loaded &&
113− <P>
112+ <P>
114− {
113+ { "Generating keyfile..." }
115− `Enter a network seed below for generating your key file. Your
114+ </P>
116− network seed must be a string of 64 characters (containing 0-9, A-Z, a-z)`
117 }
115 }
116+
117+ { keyfile === '' && loaded &&
118− </P>
118+ <P>
119− <P>
119+ <b>Warning: </b>
120− {
120+ { `No valid network seed detected. To generate a keyfile, you
121− `If you've authenticated with either a master ticket or a
121+ must reset your networking keys, or try logging in with your
122− management proxy mnemonic, a seed will be generated for you
122+ master ticket or management mnemonic` }
123− automatically.`
123+ </P>
124 }
124 }
125− </P>
126
125
127− <RequiredInput
126+ { keyfile !== '' &&
128− className='mono'
127+ <React.Fragment>
129− prop-size='lg'
128+ <div className="pb-5 text-code keyfile">
130− prop-format='innerLabel'
129+ { keyfile }
131− autoFocus
130+ </div>
132− value={ networkSeed }
133− onChange={ this.handleNetworkSeedInput }>
134− <InnerLabel>{ 'Network seed' }</InnerLabel>
135− </RequiredInput>
136−
137− <Button
131+ <Button
138− className={'mt-8'}
139− color={'blue'}
140− onClick={
132+ onClick={
141− () => {
133+ () => {
142− if (hexRegExp.test(networkSeed)) {
134+ let blob = new Blob([keyfile], {type:"text/plain;charset=utf-8"});
143− const keyfile = genKey(networkSeed, point, revision)
135+ saveAs(blob, `${ob.patp(point).slice(1)}-${revision}.key`)
144− this.setState({ keyfile })
145− }
136+ }
146− }
147− }
137+ }>
148− >
138+ Download →
149− { 'Generate →' }
150− </Button>
139+ </Button>
151−
140+ </React.Fragment>
152− <Code>
141+ }
153− { keyfile }
154− </Code>
155−
156− { warning }
157−
158− { download }
159
142
160 </Col>
143 </Col>
161 </Row>
144 </Row>
src/views/SentTransaction.js+26−1
@@ -2,6 +2,7 @@ import React from 'react'
2import { Row, Col, H1, H3, P, Warning, Anchor } from '../components/Base'
2import { Row, Col, H1, H3, P, Warning, Anchor } from '../components/Base'
3import { Button } from '../components/Base'
3import { Button } from '../components/Base'
4
4
5+import { ROUTE_NAMES } from '../lib/router'
5import { BRIDGE_ERROR, renderTxnError } from '../lib/error'
6import { BRIDGE_ERROR, renderTxnError } from '../lib/error'
6import { NETWORK_NAMES } from '../lib/network'
7import { NETWORK_NAMES } from '../lib/network'
7
8
@@ -75,9 +76,11 @@ const Failure = (props) =>
75 </Row>
76 </Row>
76
77
77const SentTransaction = (props) => {
78const SentTransaction = (props) => {
78− const { web3, txnHashCursor, networkType, popRoute } = props
79+ const { web3, txnHashCursor, networkType, popRoute, pushRoute } = props
79 const { setPointCursor, pointCursor } = props
80 const { setPointCursor, pointCursor } = props
80
81
82+ const promptKeyfile = props.routeData && props.routeData.promptKeyfile
83+
81 const w3 = web3.matchWith({
84 const w3 = web3.matchWith({
82 Nothing: _ => { throw BRIDGE_ERROR.MISSING_WEB3 },
85 Nothing: _ => { throw BRIDGE_ERROR.MISSING_WEB3 },
83 Just: res => res.value
86 Just: res => res.value
@@ -113,10 +116,32 @@ const SentTransaction = (props) => {
113 </Col>
116 </Col>
114 </Row>
117 </Row>
115
118
119+ let keyfile;
120+
121+ if (promptKeyfile) {
122+ keyfile = (
123+ <Row>
124+ <Col>
125+ <Button
126+ prop-type={'link'}
127+ onClick={
128+ () => {
129+ popRoute()
130+ pushRoute(ROUTE_NAMES.GEN_KEYFILE)
131+ }
132+ }>
133+ { 'Download Keyfile →' }
134+ </Button>
135+ </Col>
136+ </Row>
137+ )
138+ }
139+
116 return (
140 return (
117 <div>
141 <div>
118 { body }
142 { body }
119 { ok }
143 { ok }
144+ { keyfile }
120 </div>
145 </div>
121 )
146 )
122}
147}
src/views/SetKeys.js+60−40
@@ -8,7 +8,9 @@ import { RequiredInput, InnerLabel } from '../components/Base'
8import StatelessTransaction from '../components/StatelessTransaction'
8import StatelessTransaction from '../components/StatelessTransaction'
9import { BRIDGE_ERROR } from '../lib/error'
9import { BRIDGE_ERROR } from '../lib/error'
10import { ROUTE_NAMES } from '../lib/router'
10import { ROUTE_NAMES } from '../lib/router'
11−import { attemptSeedDerivation } from '../lib/keys'
11+import { attemptSeedDerivation, genKey } from '../lib/keys'
12+
13+import saveAs from 'file-saver'
12
14
13import { WALLET_NAMES } from '../lib/wallet'
15import { WALLET_NAMES } from '../lib/wallet'
14
16
@@ -39,6 +41,7 @@ class SetKeys extends React.Component {
39 auth: '',
41 auth: '',
40 encr: '',
42 encr: '',
41 networkSeed: '',
43 networkSeed: '',
44+ nondeterministicSeed: false,
42 point: point,
45 point: point,
43 cryptoSuiteVersion: 1,
46 cryptoSuiteVersion: 1,
44 continuity: false,
47 continuity: false,
@@ -122,22 +125,38 @@ class SetKeys extends React.Component {
122 })
125 })
123 }
126 }
124
127
128+ //TODO use web3.utils.randomHex when it gets fixed, see web3.js#1490
129+ randomHex(len) {
130+ let hex = "";
131+
132+ for (var i = 0; i < len; i++) {
133+ hex = hex + ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B',
134+ 'C', 'D', 'E', 'F'][Math.floor(Math.random() * 16)]
135+ }
136+
137+ return hex;
138+ }
139+
125 async deriveSeed() {
140 async deriveSeed() {
126 const next = true
141 const next = true
127− const seed = await attemptSeedDerivation(next, this.props)
142+ let seed = await attemptSeedDerivation(next, this.props)
143+ let nondeterministicSeed = false;
144+
145+ if (seed.getOrElse('') === '') {
146+ seed = Maybe.Just(this.randomHex(64));
147+ nondeterministicSeed = true;
148+ }
149+
128 this.setState({
150 this.setState({
129− networkSeed: seed.getOrElse('')
151+ networkSeed: seed.getOrElse(''),
152+ nondeterministicSeed: nondeterministicSeed
130 })
153 })
131 }
154 }
132
155
133−
134−
135 handleNetworkSeedInput(networkSeed) {
156 handleNetworkSeedInput(networkSeed) {
136 this.setState({ networkSeed })
157 this.setState({ networkSeed })
137 }
158 }
138
159
139−
140−
141 handleCreateUnsignedTxn() {
160 handleCreateUnsignedTxn() {
142 const txn = this.createUnsignedTxn()
161 const txn = this.createUnsignedTxn()
143 this.setState({ txn })
162 this.setState({ txn })
@@ -222,23 +241,42 @@ class SetKeys extends React.Component {
222 })
241 })
223 }
242 }
224
243
244+ downloadKeyfile(networkSeed) {
245+ const { pointCache } = this.props
246+ const { pointCursor } = this.props
225
247
248+ const point = pointCursor.matchWith({
249+ Just: (pt) => pt.value,
250+ Nothing: () => {
251+ throw BRIDGE_ERROR.MISSING_POINT
252+ }
253+ })
254+
255+ const pointDetails =
256+ point in pointCache
257+ ? pointCache[point]
258+ : (() => { throw BRIDGE_ERROR.MISSING_POINT })()
259+
260+ const revision = parseInt(pointDetails.keyRevisionNumber)
261+ const keyfile = genKey(networkSeed, point, revision)
262+ let blob = new Blob([keyfile], {type:"text/plain;charset=utf-8"});
263+ saveAs(blob, `${ob.patp(point).slice(1)}-${revision}.key`)
264+ }
226
265
227 handleSubmit(){
266 handleSubmit(){
228 const { props, state } = this
267 const { props, state } = this
229 sendSignedTransaction(props.web3.value, state.stx)
268 sendSignedTransaction(props.web3.value, state.stx)
230 .then(sent => {
269 .then(sent => {
270+ props.setNetworkSeedCache(this.state.networkSeed)
231 props.setTxnHashCursor(sent)
271 props.setTxnHashCursor(sent)
232 props.popRoute()
272 props.popRoute()
233− props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION)
273+ props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION, {promptKeyfile: true})
234 })
274 })
235 .catch(err => {
275 .catch(err => {
236 this.setState({ txError: err.map(val => val.merge()) })
276 this.setState({ txError: err.map(val => val.merge()) })
237 })
277 })
238 }
278 }
239
279
240−
241−
242 createUnsignedTxn() {
280 createUnsignedTxn() {
243 const { state, props } = this
281 const { state, props } = this
244
282
@@ -299,16 +337,6 @@ class SetKeys extends React.Component {
299 ? props.pointCache[state.point]
337 ? props.pointCache[state.point]
300 : (() => { throw BRIDGE_ERROR.MISSING_POINT })()
338 : (() => { throw BRIDGE_ERROR.MISSING_POINT })()
301
339
302− const isManagementMnemonic =
303− this.state.isManagementSeed &&
304− props.walletType === WALLET_NAMES.MNEMONIC
305−
306− const isMasterTicket =
307− props.walletType === WALLET_NAMES.TICKET ||
308− props.walletType === WALLET_NAMES.SHARD
309−
310− const networkSeedDisabled = isManagementMnemonic || isMasterTicket;
311−
312 return (
340 return (
313 <Row>
341 <Row>
314 <Col>
342 <Col>
@@ -316,19 +344,22 @@ class SetKeys extends React.Component {
316 { 'Set Network Keys For ' } <code>{ `${ob.patp(state.point)}` }</code>
344 { 'Set Network Keys For ' } <code>{ `${ob.patp(state.point)}` }</code>
317 </H1>
345 </H1>
318
346
319− <P>
347+ <P className="mt-10">
320 {
348 {
321− `Please enter a network seed for generating and setting your public
322− network authentication and encryption keys. Your network seed
349+ `Set new authentication and encryption keys for your Arvo ship.`
323− must be a string of 64 characters (containing 0-9, A-Z, a-z).`
324 }
350 }
325 </P>
351 </P>
326− <P>
352+
327− {
353+ { state.nondeterministicSeed &&
328− `If you've authenticated with a master ticket or management proxy
354+ <Warning>
329− mnemonic, a seed will be generated for you automatically.`
355+ <h3 className={'mb-2'}>{'Warning'}</h3>
356+ {
357+ `Your network seed could not be derived automatically. We've
358+ generated a random one for you, so you must download your Arvo
359+ keyfile during this session after setting your keys.`
360+ }
361+ </Warning>
330 }
362 }
331− </P>
332
363
333 { pointDetails.keyRevisionNumber === '0'
364 { pointDetails.keyRevisionNumber === '0'
334 ? <Warning>
365 ? <Warning>
@@ -340,17 +371,6 @@ class SetKeys extends React.Component {
340 </Warning>
371 </Warning>
341 : <div /> }
372 : <div /> }
342
373
343− <RequiredInput
344− className='mono'
345− prop-size='lg'
346− prop-format='innerLabel'
347− autoFocus
348− disabled= { networkSeedDisabled }
349− value={ state.networkSeed }
350− onChange={ this.handleNetworkSeedInput }>
351− <InnerLabel>{ 'Network seed' }</InnerLabel>
352− </RequiredInput>
353−
354 <StatelessTransaction
374 <StatelessTransaction
355 // Upper scope
375 // Upper scope
356 web3={props.web3}
376 web3={props.web3}