diff --git a/src/Bridge.js b/src/Bridge.js index 00d78e021..6701b20c5 100644 --- a/src/Bridge.js +++ b/src/Bridge.js @@ -40,6 +40,7 @@ class Bridge extends React.Component { this.state = { // routes routeCrumbs: Stack([ ROUTE_NAMES.LANDING ]), + routeData: {}, // network networkType: networkType, web3: Maybe.Nothing(), @@ -51,6 +52,7 @@ class Bridge extends React.Component { // urbit wallet-related urbitWallet: Maybe.Nothing(), authMnemonic: Maybe.Nothing(), + networkSeedCache: null, // point pointCursor: Maybe.Nothing(), pointCache: {}, @@ -70,6 +72,7 @@ class Bridge extends React.Component { this.setAuthMnemonic = this.setAuthMnemonic.bind(this) this.setPointCursor = this.setPointCursor.bind(this) this.setTxnHashCursor = this.setTxnHashCursor.bind(this) + this.setNetworkSeedCache = this.setNetworkSeedCache.bind(this) this.addToPointCache = this.addToPointCache.bind(this) } @@ -88,7 +91,7 @@ class Bridge extends React.Component { // if (process.env.NODE_ENV === 'development') { // - // const socket = 'wss://localhost:8545' + // const socket = 'ws://localhost:8545' // const provider = new Web3.providers.WebsocketProvider(socket) // const web3 = new Web3(provider) // const contracts = azimuth.initContracts(web3, CONTRACT_ADDRESSES.DEV) @@ -98,7 +101,8 @@ class Bridge extends React.Component { // // this.setState({ // routeCrumbs: Stack([ - // ROUTE_NAMES.CREATE_GALAXY, + // // ROUTE_NAMES.CREATE_GALAXY, + // // ROUTE_NAMES.SHIP, // ROUTE_NAMES.SHIPS, // // ROUTE_NAMES.MNEMONIC, // ROUTE_NAMES.WALLET, @@ -106,20 +110,22 @@ class Bridge extends React.Component { // ROUTE_NAMES.LANDING // ]), // networkType: NETWORK_NAMES.LOCAL, + // pointCursor: Maybe.Just(0), // web3: Maybe.Just(web3), // contracts: Maybe.Just(contracts), // walletType: WALLET_NAMES.MNEMONIC, // wallet: walletFromMnemonic(mnemonic, hdpath), // urbitWallet: Maybe.Nothing(), - // authMnemonic: Maybe.Nothing() + // authMnemonic: Maybe.Just('benefit crew supreme gesture quantum web media hazard theory mercy wing kitten') // }) // } } - pushRoute(symbol) { + pushRoute(symbol, routeData) { if (lodash.includes(ROUTE_NAMES, symbol)) { this.setState((state, _) => ({ routeCrumbs: state.routeCrumbs.push(symbol), + routeData: routeData })); // Scroll to top of page with each route transition. @@ -152,6 +158,12 @@ class Bridge extends React.Component { } } + setNetworkSeedCache(networkSeed) { + this.setState({ + networkSeedCache: networkSeed + }) + } + setNetwork(web3, contracts) { this.setState({ web3: web3, @@ -202,12 +214,14 @@ class Bridge extends React.Component { render() { const { routeCrumbs, + routeData, networkType, walletType, walletHdPath, wallet, urbitWallet, authMnemonic, + networkSeedCache, pointCursor, pointCache, txnHashCursor, @@ -235,6 +249,7 @@ class Bridge extends React.Component { // router pushRoute={ this.pushRoute } popRoute={ this.popRoute } + routeData={ routeData } // network setNetworkType={ this.setNetworkType } setNetwork={ this.setNetwork } @@ -258,6 +273,8 @@ class Bridge extends React.Component { addToPointCache={ this.addToPointCache } pointCursor={ pointCursor } pointCache={ pointCache } + networkSeedCache= { networkSeedCache } + setNetworkSeedCache= { this.setNetworkSeedCache } // txn setTxnHashCursor={ this.setTxnHashCursor } txnHashCursor={ txnHashCursor } /> diff --git a/src/style/extend.css b/src/style/extend.css index f1512dd96..eda824074 100644 --- a/src/style/extend.css +++ b/src/style/extend.css @@ -463,3 +463,10 @@ code { .clickable { cursor: pointer; } + +.keyfile { + white-space: pre-wrap; + word-break: break-word; + padding: 1rem 2rem; + margin-bottom: 4rem; +} diff --git a/src/views/GenKeyfile.js b/src/views/GenKeyfile.js index ea3999b0d..6cd50a710 100644 --- a/src/views/GenKeyfile.js +++ b/src/views/GenKeyfile.js @@ -1,4 +1,5 @@ import React from 'react' +import Maybe from 'folktale/maybe' import { Button } from '../components/Base' import { RequiredInput, InnerLabel } from '../components/Base' @@ -21,39 +22,14 @@ class GenKeyfile extends React.Component { this.state = { keyfile: '', - networkSeed: '' + loaded: false } - - this.handleNetworkSeedInput = this.handleNetworkSeedInput.bind(this) - this.handleKeyfileChange = this.handleKeyfileChange.bind(this) - } - - componentDidMount() { - this.deriveSeed() - } - - async deriveSeed() { - const next = false - const seed = await attemptSeedDerivation(next, this.props) - this.setState({ - networkSeed: seed.getOrElse('') - }) - } - - handleNetworkSeedInput = (networkSeed) => { - this.setState({ networkSeed }) } - handleKeyfileChange = (keyfile) => { - this.setState({ keyfile }) - } - - render() { + getPointDetails() { const { pointCache } = this.props const { pointCursor } = this.props - const { keyfile, networkSeed } = this.state - const point = pointCursor.matchWith({ Just: (pt) => pt.value, Nothing: () => { @@ -68,6 +44,34 @@ class GenKeyfile extends React.Component { const revision = parseInt(pointDetails.keyRevisionNumber) + return { + point, + pointDetails, + revision + } + } + + async componentDidMount() { + const { point, pointDetails, revision } = this.getPointDetails(); + let keyfile = '' + + const hexRegExp = /[0-9A-Fa-f]{64}/g + const networkSeed = await this.deriveSeed() + + const keysmatch = this.checkKeysMatch(networkSeed, pointDetails) + const seedValid = hexRegExp.test(networkSeed) + + if (keysmatch && seedValid) { + keyfile = genKey(networkSeed, point, revision) + } + + this.setState({ + keyfile: keyfile, + loaded: true + }); + } + + checkKeysMatch(networkSeed, pointDetails) { const crypub = pointDetails.encryptionKey const sgnpub = pointDetails.authenticationKey @@ -77,29 +81,23 @@ class GenKeyfile extends React.Component { crypub === addHexPrefix(crypt.public) && sgnpub === addHexPrefix(auth.public) - const warning = - keyfile !== '' && keysmatch === false - ? - - { 'WARNING' }{ ": derived key doesn't match Azimuth keys!" } - - - :
+ return keysmatch; + } - const hexRegExp = /[0-9A-Fa-f]{64}/g + async deriveSeed() { + const next = false + let seed = await attemptSeedDerivation(next, this.props) - const download = - keyfile !== '' - ? - : '' + if (seed.getOrElse('') === '' && this.props.networkSeedCache) { + seed = Maybe.Just(this.props.networkSeedCache) + } + + return seed.getOrElse('') + } + + render() { + const { point, pointDetails, revision } = this.getPointDetails(); + const { keyfile, loaded } = this.state return ( @@ -107,55 +105,40 @@ class GenKeyfile extends React.Component {

{ 'Generate keyfile' }

- { "Generate a private key file for booting this point in Arvo." } + { "Download a private key file for booting this point in Arvo." }

-

- { - `Enter a network seed below for generating your key file. Your - network seed must be a string of 64 characters (containing 0-9, A-Z, a-z)` + { keyfile === '' && !loaded && +

+ { "Generating keyfile..." } +

} -

-

- { - `If you've authenticated with either a master ticket or a - management proxy mnemonic, a seed will be generated for you - automatically.` + + { keyfile === '' && loaded && +

+ Warning: + { `No valid network seed detected. To generate a keyfile, you + must reset your networking keys, or try logging in with your + master ticket or management mnemonic` } +

} -

- - { 'Network seed' } - - - - - - { keyfile } - - - { warning } - - { download } + { keyfile !== '' && + +
+ { keyfile } +
+ +
+ }
diff --git a/src/views/SentTransaction.js b/src/views/SentTransaction.js index 73a7fc888..4a82b334d 100644 --- a/src/views/SentTransaction.js +++ b/src/views/SentTransaction.js @@ -2,6 +2,7 @@ import React from 'react' import { Row, Col, H1, H3, P, Warning, Anchor } from '../components/Base' import { Button } from '../components/Base' +import { ROUTE_NAMES } from '../lib/router' import { BRIDGE_ERROR, renderTxnError } from '../lib/error' import { NETWORK_NAMES } from '../lib/network' @@ -75,9 +76,11 @@ const Failure = (props) => const SentTransaction = (props) => { - const { web3, txnHashCursor, networkType, popRoute } = props + const { web3, txnHashCursor, networkType, popRoute, pushRoute } = props const { setPointCursor, pointCursor } = props + const promptKeyfile = props.routeData && props.routeData.promptKeyfile + const w3 = web3.matchWith({ Nothing: _ => { throw BRIDGE_ERROR.MISSING_WEB3 }, Just: res => res.value @@ -113,10 +116,32 @@ const SentTransaction = (props) => { + let keyfile; + + if (promptKeyfile) { + keyfile = ( + + + + + + ) + } + return (
{ body } { ok } + { keyfile }
) } diff --git a/src/views/SetKeys.js b/src/views/SetKeys.js index 7513e8772..be9f8c44c 100644 --- a/src/views/SetKeys.js +++ b/src/views/SetKeys.js @@ -8,7 +8,9 @@ import { RequiredInput, InnerLabel } from '../components/Base' import StatelessTransaction from '../components/StatelessTransaction' import { BRIDGE_ERROR } from '../lib/error' import { ROUTE_NAMES } from '../lib/router' -import { attemptSeedDerivation } from '../lib/keys' +import { attemptSeedDerivation, genKey } from '../lib/keys' + +import saveAs from 'file-saver' import { WALLET_NAMES } from '../lib/wallet' @@ -39,6 +41,7 @@ class SetKeys extends React.Component { auth: '', encr: '', networkSeed: '', + nondeterministicSeed: false, point: point, cryptoSuiteVersion: 1, continuity: false, @@ -122,22 +125,38 @@ class SetKeys extends React.Component { }) } + //TODO use web3.utils.randomHex when it gets fixed, see web3.js#1490 + randomHex(len) { + let hex = ""; + + for (var i = 0; i < len; i++) { + hex = hex + ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', + 'C', 'D', 'E', 'F'][Math.floor(Math.random() * 16)] + } + + return hex; + } + async deriveSeed() { const next = true - const seed = await attemptSeedDerivation(next, this.props) + let seed = await attemptSeedDerivation(next, this.props) + let nondeterministicSeed = false; + + if (seed.getOrElse('') === '') { + seed = Maybe.Just(this.randomHex(64)); + nondeterministicSeed = true; + } + this.setState({ - networkSeed: seed.getOrElse('') + networkSeed: seed.getOrElse(''), + nondeterministicSeed: nondeterministicSeed }) } - - handleNetworkSeedInput(networkSeed) { this.setState({ networkSeed }) } - - handleCreateUnsignedTxn() { const txn = this.createUnsignedTxn() this.setState({ txn }) @@ -222,23 +241,42 @@ class SetKeys extends React.Component { }) } + downloadKeyfile(networkSeed) { + const { pointCache } = this.props + const { pointCursor } = this.props + const point = pointCursor.matchWith({ + Just: (pt) => pt.value, + Nothing: () => { + throw BRIDGE_ERROR.MISSING_POINT + } + }) + + const pointDetails = + point in pointCache + ? pointCache[point] + : (() => { throw BRIDGE_ERROR.MISSING_POINT })() + + const revision = parseInt(pointDetails.keyRevisionNumber) + const keyfile = genKey(networkSeed, point, revision) + let blob = new Blob([keyfile], {type:"text/plain;charset=utf-8"}); + saveAs(blob, `${ob.patp(point).slice(1)}-${revision}.key`) + } handleSubmit(){ const { props, state } = this sendSignedTransaction(props.web3.value, state.stx) .then(sent => { + props.setNetworkSeedCache(this.state.networkSeed) props.setTxnHashCursor(sent) props.popRoute() - props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION) + props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION, {promptKeyfile: true}) }) .catch(err => { this.setState({ txError: err.map(val => val.merge()) }) }) } - - createUnsignedTxn() { const { state, props } = this @@ -299,16 +337,6 @@ class SetKeys extends React.Component { ? props.pointCache[state.point] : (() => { throw BRIDGE_ERROR.MISSING_POINT })() - const isManagementMnemonic = - this.state.isManagementSeed && - props.walletType === WALLET_NAMES.MNEMONIC - - const isMasterTicket = - props.walletType === WALLET_NAMES.TICKET || - props.walletType === WALLET_NAMES.SHARD - - const networkSeedDisabled = isManagementMnemonic || isMasterTicket; - return ( @@ -316,19 +344,22 @@ class SetKeys extends React.Component { { 'Set Network Keys For ' } { `${ob.patp(state.point)}` } -

+

{ - `Please enter a network seed for generating and setting your public - network authentication and encryption keys. Your network seed - must be a string of 64 characters (containing 0-9, A-Z, a-z).` + `Set new authentication and encryption keys for your Arvo ship.` }

-

- { - `If you've authenticated with a master ticket or management proxy - mnemonic, a seed will be generated for you automatically.` + + { state.nondeterministicSeed && + +

{'Warning'}

+ { + `Your network seed could not be derived automatically. We've + generated a random one for you, so you must download your Arvo + keyfile during this session after setting your keys.` + } + } -

{ pointDetails.keyRevisionNumber === '0' ? @@ -340,17 +371,6 @@ class SetKeys extends React.Component { :
} - - { 'Network seed' } - -