Skip to content
This repository has been archived by the owner on Dec 11, 2019. It is now read-only.

Commit

Permalink
Add "Import recovery keys" button to Payments->Advanced->Recover
Browse files Browse the repository at this point in the history
(opens a file chooser dialog)

fixes #4806

Includes tests for Ledger backup and recovery:
- add advanced ledger panel tests file `test/components/ledgerPanelAdvancedPanelTest.js`
- add tests for backup and recovery of wallet
- add commands to Brave test client (`ipcOnce`, `ipcSendRendererSync`, and `translations`)
  client.translations: returns a map of all existing translations (current locale) to test client

Import recovery keys success closes modals
- successful import closes modals
- and closing file chooser dialog does not trigger error screen

fixes #6263

Import recovery keys shows error popover if keys are invalid or missing
- error popover is displayed if paymentId / passphrase are missing or not UUIDs
  (ledger-client#recoverWallet should probably do validation too)
- added tests for cases:
  one or both recovery keys missing from file
  a recovery key is not a UUID
  an empty recovery file
  • Loading branch information
Willy Bruns authored and bsclifton committed Jan 18, 2017
1 parent 96efdd6 commit a861abd
Show file tree
Hide file tree
Showing 11 changed files with 396 additions and 19 deletions.
1 change: 1 addition & 0 deletions app/extensions/brave/locales/en-US/preferences.properties
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ backupLedger=Backup your wallet
balanceRecovered={{balance}} BTC was recovered and transferred to your Brave wallet.
recoverLedger=Recover your wallet
recover=Recover
recoverFromFile=Import recovery keys
printKeys=Print keys
saveRecoveryFile=Save recovery file...
advancedPrivacySettings=Advanced Privacy Settings:
Expand Down
4 changes: 3 additions & 1 deletion app/filtering.js
Original file line number Diff line number Diff line change
Expand Up @@ -487,12 +487,14 @@ function registerForDownloadListener (session) {
}

const defaultPath = path.join(getSetting(settings.DEFAULT_DOWNLOAD_SAVE_PATH) || app.getPath('downloads'), itemFilename)
const savePath = dialog.showSaveDialog(win, { defaultPath })
const savePath = (process.env.SPECTRON ? defaultPath : dialog.showSaveDialog(win, { defaultPath }))

// User cancelled out of save dialog prompt
if (!savePath) {
event.preventDefault()
return
}

item.setSavePath(savePath)
appActions.changeSetting(settings.DEFAULT_DOWNLOAD_SAVE_PATH, path.dirname(savePath))

Expand Down
102 changes: 99 additions & 3 deletions app/ledger.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ var backupKeys = (appState, action) => {
const date = moment().format('L')
const paymentId = appState.getIn(['ledgerInfo', 'paymentId'])
const passphrase = appState.getIn(['ledgerInfo', 'passphrase'])

const messageLines = [
locale.translation('ledgerBackupText1'),
[locale.translation('ledgerBackupText2'), date].join(' '),
Expand All @@ -264,6 +265,7 @@ var backupKeys = (appState, action) => {
'',
locale.translation('ledgerBackupText5')
]

const message = messageLines.join(os.EOL)
const filePath = path.join(app.getPath('userData'), '/brave_wallet_recovery.txt')

Expand All @@ -284,14 +286,83 @@ var backupKeys = (appState, action) => {
return appState
}

var loadKeysFromBackupFile = (filePath) => {
let keys = null
let data = fs.readFileSync(filePath)

if (!data || !data.length || !(data.toString())) {
logError('No data in backup file', 'recoveryWallet')
} else {
try {
const recoveryFileContents = data.toString()

let messageLines = recoveryFileContents.split(os.EOL)

let paymentIdLine = '' || messageLines[3]
let passphraseLine = '' || messageLines[4]

const paymentIdPattern = new RegExp([locale.translation('ledgerBackupText3'), '([^ ]+)'].join(' '))
const paymentId = (paymentIdLine.match(paymentIdPattern) || [])[1]

const passphrasePattern = new RegExp([locale.translation('ledgerBackupText4'), '(.+)$'].join(' '))
const passphrase = (passphraseLine.match(passphrasePattern) || [])[1]

keys = {
paymentId,
passphrase
}
} catch (exc) {
logError(exc, 'recoveryWallet')
}
}

return keys
}

/*
* Recover Ledger Keys
*/

var recoverKeys = (appState, action) => {
client.recoverWallet(action.firstRecoveryKey, action.secondRecoveryKey, (err, body) => {
if (logError(err, 'recoveryWallet')) appActions.updateLedgerInfo(underscore.omit(ledgerInfo, [ '_internal' ]))
if (err) {
let firstRecoveryKey, secondRecoveryKey

if (action.useRecoveryKeyFile) {
let recoveryKeyFile = promptForRecoveryKeyFile()
if (!recoveryKeyFile) {
// user canceled from dialog, we abort without error
return appState
}

if (recoveryKeyFile) {
let keys = loadKeysFromBackupFile(recoveryKeyFile) || {}

if (keys) {
firstRecoveryKey = keys.paymentId
secondRecoveryKey = keys.passphrase
}
}
}

if (!firstRecoveryKey || !secondRecoveryKey) {
firstRecoveryKey = action.firstRecoveryKey
secondRecoveryKey = action.secondRecoveryKey
}

const UUID_REGEX = /^[0-9a-z]{8}\-[0-9a-z]{4}\-[0-9a-z]{4}\-[0-9a-z]{4}\-[0-9a-z]{12}$/
if (typeof firstRecoveryKey !== 'string' || !firstRecoveryKey.match(UUID_REGEX) || typeof secondRecoveryKey !== 'string' || !secondRecoveryKey.match(UUID_REGEX)) {
setImmediate(() => appActions.ledgerRecoveryFailed())
return appState
}

client.recoverWallet(firstRecoveryKey, secondRecoveryKey, (err, body) => {
let existingLedgerError = ledgerInfo.error

if (logError(err, 'recoveryWallet')) {
// we reset ledgerInfo.error to what it was before (likely null)
// if ledgerInfo.error is not null, the wallet info will not display in UI
// logError sets ledgerInfo.error, so we must we clear it or UI will show an error
ledgerInfo.error = existingLedgerError
appActions.updateLedgerInfo(underscore.omit(ledgerInfo, [ '_internal' ]))
setImmediate(() => appActions.ledgerRecoveryFailed())
} else {
setImmediate(() => appActions.ledgerRecoverySucceeded())
Expand All @@ -301,6 +372,31 @@ var recoverKeys = (appState, action) => {
return appState
}

const dialog = electron.dialog

var promptForRecoveryKeyFile = () => {
const defaultRecoveryKeyFilePath = path.join(app.getPath('userData'), '/brave_wallet_recovery.txt')

let files

if (process.env.SPECTRON) {
// skip the dialog for tests
console.log(`for test, trying to recover keys from path: ${defaultRecoveryKeyFilePath}`)
files = [defaultRecoveryKeyFilePath]
} else {
files = dialog.showOpenDialog({
properties: ['openFile'],
defaultPath: defaultRecoveryKeyFilePath,
filters: [{
name: 'TXT files',
extensions: ['txt']
}]
})
}

return (files && files.length ? files[0] : null)
}

/*
* IPC entry point
*/
Expand Down
4 changes: 2 additions & 2 deletions docs/appActions.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,13 @@ Dispatches a message to clear all completed downloads

### ledgerRecoverySucceeded()

Dispatches a message to clear all completed downloads
Dispatches a message indicating ledger recovery succeeded



### ledgerRecoveryFailed()

Dispatches a message to clear all completed downloads
Dispatches a message indicating ledger recovery failed



Expand Down
7 changes: 7 additions & 0 deletions js/about/aboutActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ const aboutActions = {
})
},

ledgerRecoverWalletFromFile: function () {
aboutActions.dispatchAction({
actionType: appConstants.APP_RECOVER_WALLET,
useRecoveryKeyFile: true
})
},

/**
* Clear wallet recovery status
*/
Expand Down
27 changes: 23 additions & 4 deletions js/about/preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,10 @@ class PaymentsTab extends ImmutableComponent {
aboutActions.ledgerRecoverWallet(this.state.FirstRecoveryKey, this.state.SecondRecoveryKey)
}

recoverWalletFromFile () {
aboutActions.ledgerRecoverWalletFromFile()
}

copyToClipboard (text) {
aboutActions.setClipboard(text)
}
Expand All @@ -911,6 +915,7 @@ class PaymentsTab extends ImmutableComponent {

clearRecoveryStatus () {
aboutActions.clearRecoveryStatus()
this.props.hideAdvancedOverlays()
}

printKeys () {
Expand Down Expand Up @@ -1143,23 +1148,26 @@ class PaymentsTab extends ImmutableComponent {
}

get ledgerRecoveryContent () {
let balance = this.props.ledgerData.get('balance')

const l10nDataArgs = {
balance: this.props.ledgerData.get('balance')
balance: (!balance ? '0.00' : balance)
}

return <div className='board'>
{
this.props.ledgerData.get('recoverySucceeded') === true
? <div className='recoveryOverlay'>
<h1>Success!</h1>
<p className='spaceAround' data-l10n-id='balanceRecovered' data-l10n-args={JSON.stringify(l10nDataArgs)} />
<Button l10nId='ok' className='whiteButton inlineButton' onClick={this.clearRecoveryStatus} />
<Button l10nId='ok' className='whiteButton inlineButton' onClick={this.clearRecoveryStatus.bind(this)} />
</div>
: null
}
{
this.props.ledgerData.get('recoverySucceeded') === false
? <div className='recoveryOverlay'>
<h1>Recovery failed</h1>
<h1 className='recoveryError'>Recovery failed</h1>
<p className='spaceAround'>Please re-enter keys or try different keys.</p>
<Button l10nId='ok' className='whiteButton inlineButton' onClick={this.clearRecoveryStatus} />
</div>
Expand All @@ -1184,6 +1192,7 @@ class PaymentsTab extends ImmutableComponent {
return <div className='panel advancedSettingsFooter'>
<div className='recoveryFooterButtons'>
<Button l10nId='recover' className='primaryButton' onClick={this.recoverWallet} />
<Button l10nId='recoverFromFile' className='primaryButton' onClick={this.recoverWalletFromFile} />
<Button l10nId='cancel' className='whiteButton' onClick={this.props.hideOverlay.bind(this, 'ledgerRecovery')} />
</div>
</div>
Expand Down Expand Up @@ -1841,6 +1850,15 @@ class AboutPreferences extends React.Component {
this.updateTabFromAnchor = this.updateTabFromAnchor.bind(this)
}

hideAdvancedOverlays () {
this.setState({
advancedSettingsOverlayVisible: false,
ledgerBackupOverlayVisible: false,
ledgerRecoveryOverlayVisible: false
})
this.forceUpdate()
}

componentDidMount () {
window.addEventListener('popstate', this.updateTabFromAnchor)
}
Expand Down Expand Up @@ -1966,7 +1984,8 @@ class AboutPreferences extends React.Component {
ledgerRecoveryOverlayVisible={this.state.ledgerRecoveryOverlayVisible}
addFundsOverlayVisible={this.state.addFundsOverlayVisible}
showOverlay={this.setOverlayVisible.bind(this, true)}
hideOverlay={this.setOverlayVisible.bind(this, false)} />
hideOverlay={this.setOverlayVisible.bind(this, false)}
hideAdvancedOverlays={this.hideAdvancedOverlays.bind(this)} />
break
case preferenceTabs.SECURITY:
tab = <SecurityTab settings={settings} siteSettings={siteSettings} braveryDefaults={braveryDefaults} onChangeSetting={this.onChangeSetting} />
Expand Down
4 changes: 2 additions & 2 deletions js/actions/appActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ const appActions = {
},

/**
* Dispatches a message to clear all completed downloads
* Dispatches a message indicating ledger recovery succeeded
*/
ledgerRecoverySucceeded: function () {
AppDispatcher.dispatch({
Expand All @@ -201,7 +201,7 @@ const appActions = {
},

/**
* Dispatches a message to clear all completed downloads
* Dispatches a message indicating ledger recovery failed
*/
ledgerRecoveryFailed: function () {
AppDispatcher.dispatch({
Expand Down
Loading

0 comments on commit a861abd

Please sign in to comment.