diff --git a/app/scripts/lib/seed-phrase-verifier.js b/app/scripts/lib/seed-phrase-verifier.js deleted file mode 100644 index 28c0b856d..000000000 --- a/app/scripts/lib/seed-phrase-verifier.js +++ /dev/null @@ -1,56 +0,0 @@ -import { KeyringController } from '@metamask/eth-keyring-controller'; -import log from 'loglevel'; - -import { KeyringType } from '../../../shared/constants/keyring'; - -const seedPhraseVerifier = { - /** - * Verifies if the seed words can restore the accounts. - * - * Key notes: - * - The seed words can recreate the primary keyring and the accounts belonging to it. - * - The created accounts in the primary keyring are always the same. - * - The keyring always creates the accounts in the same sequence. - * - * @param {Array} createdAccounts - The accounts to restore - * @param {Buffer} seedPhrase - The seed words to verify, encoded as a Buffer - * @returns {Promise} - */ - async verifyAccounts(createdAccounts, seedPhrase) { - if (!createdAccounts || createdAccounts.length < 1) { - throw new Error('No created accounts defined.'); - } - - const keyringController = new KeyringController({}); - const keyringBuilder = keyringController.getKeyringBuilderForType( - KeyringType.hdKeyTree, - ); - const keyring = keyringBuilder(); - const opts = { - mnemonic: seedPhrase, - numberOfAccounts: createdAccounts.length, - }; - - await keyring.deserialize(opts); - const restoredAccounts = await keyring.getAccounts(); - log.debug(`Created accounts: ${JSON.stringify(createdAccounts)}`); - log.debug(`Restored accounts: ${JSON.stringify(restoredAccounts)}`); - - if (restoredAccounts.length !== createdAccounts.length) { - // this should not happen... - throw new Error('Wrong number of accounts'); - } - - for (let i = 0; i < restoredAccounts.length; i++) { - if ( - restoredAccounts[i].toLowerCase() !== createdAccounts[i].toLowerCase() - ) { - throw new Error( - `Not identical accounts! Original: ${createdAccounts[i]}, Restored: ${restoredAccounts[i]}`, - ); - } - } - }, -}; - -export default seedPhraseVerifier; diff --git a/app/scripts/lib/seed-phrase-verifier.test.js b/app/scripts/lib/seed-phrase-verifier.test.js deleted file mode 100644 index be549034c..000000000 --- a/app/scripts/lib/seed-phrase-verifier.test.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * @jest-environment node - * https://github.com/facebook/jest/issues/7780 - */ -import { cloneDeep } from 'lodash'; -import { KeyringController } from '@metamask/eth-keyring-controller'; -import firstTimeState from '../first-time-state'; -import mockEncryptor from '../../../test/lib/mock-encryptor'; -import { KeyringType } from '../../../shared/constants/keyring'; -import seedPhraseVerifier from './seed-phrase-verifier'; - -describe('SeedPhraseVerifier', () => { - describe('verifyAccounts', () => { - const password = 'passw0rd1'; - const { hdKeyTree } = KeyringType; - - let keyringController; - let primaryKeyring; - - beforeEach(async () => { - keyringController = new KeyringController({ - initState: cloneDeep(firstTimeState), - encryptor: mockEncryptor, - }); - - expect.any(keyringController); - - await keyringController.createNewVaultAndKeychain(password); - primaryKeyring = keyringController.getKeyringsByType(hdKeyTree)[0]; - }); - - it('should be able to verify created account with seed words', async () => { - const createdAccounts = await primaryKeyring.getAccounts(); - expect(createdAccounts).toHaveLength(1); - - const serialized = await primaryKeyring.serialize(); - const seedWords = serialized.mnemonic; - expect(seedWords).not.toHaveLength(0); - - await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords); - }); - - it('should be able to verify created account (upper case) with seed words', async () => { - const createdAccounts = await primaryKeyring.getAccounts(); - expect(createdAccounts).toHaveLength(1); - - const upperCaseAccounts = [createdAccounts[0].toUpperCase()]; - - const serialized = await primaryKeyring.serialize(); - const seedWords = serialized.mnemonic; - expect(seedWords).not.toHaveLength(0); - - await seedPhraseVerifier.verifyAccounts(upperCaseAccounts, seedWords); - }); - - it('should be able to verify created account (lower case) with seed words', async () => { - const createdAccounts = await primaryKeyring.getAccounts(); - expect(createdAccounts).toHaveLength(1); - const lowerCaseAccounts = [createdAccounts[0].toLowerCase()]; - - const serialized = await primaryKeyring.serialize(); - const seedWords = serialized.mnemonic; - expect(seedWords).not.toHaveLength(0); - - await seedPhraseVerifier.verifyAccounts(lowerCaseAccounts, seedWords); - }); - - it('should return error with good but different seed words', async () => { - const createdAccounts = await primaryKeyring.getAccounts(); - expect(createdAccounts).toHaveLength(1); - - await primaryKeyring.serialize(); - const seedWords = - 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'; - - await expect(async () => { - await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords); - }).rejects.toThrow('Not identical accounts!'); - }); - - it('should return error with undefined existing accounts', async () => { - const createdAccounts = await primaryKeyring.getAccounts(); - expect(createdAccounts).toHaveLength(1); - - await primaryKeyring.serialize(); - const seedWords = - 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'; - - await expect(async () => { - await seedPhraseVerifier.verifyAccounts(undefined, seedWords); - }).rejects.toThrow('No created accounts defined.'); - }); - - it('should return error with empty accounts array', async () => { - const createdAccounts = await primaryKeyring.getAccounts(); - expect(createdAccounts).toHaveLength(1); - - await primaryKeyring.serialize(); - const seedWords = - 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'; - - await expect(async () => { - await seedPhraseVerifier.verifyAccounts([], seedWords); - }).rejects.toThrow('No created accounts defined.'); - }); - - it('should be able to verify more than one created account with seed words', async () => { - await keyringController.addNewAccount(primaryKeyring); - await keyringController.addNewAccount(primaryKeyring); - - const createdAccounts = await primaryKeyring.getAccounts(); - expect(createdAccounts).toHaveLength(3); - - const serialized = await primaryKeyring.serialize(); - const seedWords = serialized.mnemonic; - expect(seedWords).not.toHaveLength(0); - - await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords); - }); - }); -}); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 5a52dae61..7aa9e66f8 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -194,7 +194,6 @@ import DecryptMessageController from './controllers/decrypt-message'; import TransactionController from './controllers/transactions'; import DetectTokensController from './controllers/detect-tokens'; import SwapsController from './controllers/swaps'; -import seedPhraseVerifier from './lib/seed-phrase-verifier'; import MetaMetricsController from './controllers/metametrics'; import { segment } from './lib/segment'; import createMetaRPCHandler from './lib/createMetaRPCHandler'; @@ -3021,6 +3020,20 @@ export default class MetamaskController extends EventEmitter { return new Uint8Array(new Uint16Array(indices).buffer); } + /** + * Converts a BIP-39 mnemonic stored as indices of words in the English wordlist to a buffer of Unicode code points. + * + * @param {Uint8Array} wordlistIndices - Indices to specific words in the BIP-39 English wordlist. + * @returns {Buffer} The BIP-39 mnemonic formed from the words in the English wordlist, encoded as a list of Unicode code points. + */ + _convertEnglishWordlistIndicesToCodepoints(wordlistIndices) { + return Buffer.from( + Array.from(new Uint16Array(wordlistIndices.buffer)) + .map((i) => wordlist[i]) + .join(' '), + ); + } + /** * Get an account balance from the AccountTracker or request it directly from the network. * @@ -3461,28 +3474,9 @@ export default class MetamaskController extends EventEmitter { * encoded as an array of UTF-8 bytes. */ async verifySeedPhrase() { - const [primaryKeyring] = this.coreKeyringController.getKeyringsByType( - KeyringType.hdKeyTree, + return this._convertEnglishWordlistIndicesToCodepoints( + await this.coreKeyringController.verifySeedPhrase(), ); - if (!primaryKeyring) { - throw new Error('MetamaskController - No HD Key Tree found'); - } - - const serialized = await primaryKeyring.serialize(); - const seedPhraseAsBuffer = Buffer.from(serialized.mnemonic); - - const accounts = await primaryKeyring.getAccounts(); - if (accounts.length < 1) { - throw new Error('MetamaskController - No accounts found'); - } - - try { - await seedPhraseVerifier.verifyAccounts(accounts, seedPhraseAsBuffer); - return Array.from(seedPhraseAsBuffer.values()); - } catch (err) { - log.error(err.message); - throw err; - } } /** diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index a3ff411ce..315adc00c 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -871,10 +871,7 @@ describe('MetaMaskController', function () { try { await metamaskController.verifySeedPhrase(); } catch (error) { - assert.equal( - error.message, - 'MetamaskController - No HD Key Tree found', - ); + assert.equal(error.message, 'No HD keyring found.'); } });