Skip to content

Commit

Permalink
[Wallet] Restore mnemonics in any language if default fails (#5825)
Browse files Browse the repository at this point in the history
### Description

Use the words in the mnemonic to tell which language the mnemonic is actually in and use that to restore the account instead of using the app's language.

### Tested

Manually and there's a unit test as well

### Related issues

- Fixes #5204

### Backwards compatibility

N/A
  • Loading branch information
gnardini committed Nov 18, 2020
1 parent 63d917d commit 70334b3
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 16 deletions.
11 changes: 3 additions & 8 deletions packages/mobile/src/import/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ const mockValidSpanishPhrase =
'tajo fiera asunto tono aroma palma toro caos lobo espada número rato hacha largo pedir cemento urbe tejado volcán mimo grueso juvenil pueblo desvío'

describe('Import wallet saga', () => {
const expectSuccessfulSagaWithPhrase = async (phrase: string, language: string) => {
const expectSuccessfulSagaWithPhrase = async (phrase: string) => {
// @ts-ignore
await expectSaga(importBackupPhraseSaga, { phrase, useEmptyWallet: false })
.provide([
[call(waitWeb3LastBlock), true],
[select(currentLanguageSelector), language],
[matchers.call.fn(fetchTokenBalanceInWeiWithRetry), new BigNumber(10)],
[matchers.call.fn(assignAccountFromPrivateKey), mockAccount],
[call(storeMnemonic, phrase, mockAccount), true],
Expand All @@ -41,11 +40,11 @@ describe('Import wallet saga', () => {
}

it('imports a valid phrase', async () => {
await expectSuccessfulSagaWithPhrase(mockPhraseValid, 'english')
await expectSuccessfulSagaWithPhrase(mockPhraseValid)
})

it('imports a valid spanish phrase', async () => {
await expectSuccessfulSagaWithPhrase(mockValidSpanishPhrase, 'español')
await expectSuccessfulSagaWithPhrase(mockValidSpanishPhrase)
})

const expectFailedSagaWithPhrase = async (phrase: string) => {
Expand All @@ -64,10 +63,6 @@ describe('Import wallet saga', () => {
await expectFailedSagaWithPhrase(mockPhraseInvalid)
})

it('fails for a phrase in another language', async () => {
await expectFailedSagaWithPhrase(mockValidSpanishPhrase)
})

it('prevents import of an empty phrase', async () => {
// @ts-ignore
await expectSaga(importBackupPhraseSaga, { phrase: mockPhraseValid, useEmptyWallet: false })
Expand Down
8 changes: 3 additions & 5 deletions packages/mobile/src/import/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import { generateKeys, validateMnemonic } from '@celo/utils/src/account'
import { privateKeyToAddress } from '@celo/utils/src/address'
import BigNumber from 'bignumber.js'
import * as bip39 from 'react-native-bip39'
import { call, put, select, spawn, takeLeading } from 'redux-saga/effects'
import { call, put, spawn, takeLeading } from 'redux-saga/effects'
import { setBackupCompleted } from 'src/account/actions'
import { showError } from 'src/alert/actions'
import { ErrorMessages } from 'src/app/ErrorMessages'
import { currentLanguageSelector } from 'src/app/reducers'
import { getWordlist, storeMnemonic } from 'src/backup/utils'
import { storeMnemonic } from 'src/backup/utils'
import { CURRENCY_ENUM } from 'src/geth/consts'
import { refreshAllBalances } from 'src/home/actions'
import { setHasSeenVerificationNux } from 'src/identity/actions'
Expand All @@ -30,8 +29,7 @@ export function* importBackupPhraseSaga({ phrase, useEmptyWallet }: ImportBackup
Logger.debug(TAG + '@importBackupPhraseSaga', 'Importing backup phrase')
yield call(waitWeb3LastBlock)
try {
const wordlist = getWordlist(yield select(currentLanguageSelector))
if (!validateMnemonic(phrase, wordlist, bip39)) {
if (!validateMnemonic(phrase, undefined, bip39)) {
Logger.error(TAG + '@importBackupPhraseSaga', 'Invalid mnemonic')
yield put(showError(ErrorMessages.INVALID_BACKUP_PHRASE))
yield put(importBackupPhraseFailure())
Expand Down
12 changes: 11 additions & 1 deletion packages/utils/src/account.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { generateKeys, generateMnemonic, MnemonicStrength, validateMnemonic } from './account'
import {
generateKeys,
generateMnemonic,
MnemonicLanguages,
MnemonicStrength,
validateMnemonic,
} from './account'

describe('Mnemonic validation', () => {
it('should generate 24 word mnemonic', async () => {
Expand All @@ -25,6 +31,8 @@ describe('Mnemonic validation', () => {
'love regular blood road latin uncle shuffle hill aerobic cushion robust million elder gather clip unique pupil escape frost myth glove gadget pitch february',
'gasp eyebrow sibling dash armed guess excuse ball whip thunder insane pause lizard excuse air catalog tail control raise test dutch permit magic under',
]
const spanishMnemonic =
'avance colmo poema momia cofre pata res verso secta cinco tubería yacer eterno observar ojo tabaco seta ruina bebé oral miembro gato suelo violín'
const expectedPrivateKeys = [
{
derivation0: {
Expand Down Expand Up @@ -205,5 +213,7 @@ describe('Mnemonic validation', () => {
const password = await generateKeys(mnemonics[i], 'password')
expect({ derivation0, derivation1, password }).toEqual(expectedPrivateKeys[i])
}
expect(validateMnemonic(spanishMnemonic, MnemonicLanguages.english)).toBeFalsy()
expect(validateMnemonic(spanishMnemonic)).toBeTruthy()
})
})
27 changes: 25 additions & 2 deletions packages/utils/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,20 @@ export async function generateMnemonic(

export function validateMnemonic(
mnemonic: string,
language?: MnemonicLanguages,
defaultLanguage?: MnemonicLanguages,
bip39ToUse: Bip39 = bip39Wrapper
) {
return bip39ToUse.validateMnemonic(mnemonic, getWordList(language))
const mnemonicWords = mnemonic.trim().split(' ')
const languages = defaultLanguage
? [defaultLanguage]
: getAllLanguages().filter((lang) => lang !== defaultLanguage)
for (const language of languages) {
const wordList = getWordList(language)
if (mnemonicWords.every((word) => wordList.includes(word))) {
return bip39ToUse.validateMnemonic(mnemonic, getWordList(language))
}
}
return false
}

export async function generateKeys(
Expand Down Expand Up @@ -145,6 +155,19 @@ function getWordList(language?: MnemonicLanguages) {
}
}

function getAllLanguages() {
return [
MnemonicLanguages.chinese_simplified,
MnemonicLanguages.chinese_traditional,
MnemonicLanguages.english,
MnemonicLanguages.french,
MnemonicLanguages.italian,
MnemonicLanguages.japanese,
MnemonicLanguages.korean,
MnemonicLanguages.spanish,
]
}

export const AccountUtils = {
generateMnemonic,
validateMnemonic,
Expand Down

0 comments on commit 70334b3

Please sign in to comment.