brave-browser: crypto wallet: auto-generated mnemonic is not standard compatible

Description

Brave does not correctly derive the Ethereum address for 24-word mnemonics. The crypto wallet automatically creates 24 seed words upon first start which are also not yielding the correct address. Entering those seed words in another tool such as MyEtherWallet or MyCrypto derives another address.

As a consequence all funds that are sent to a Brave address cannot be recovered and might be lost!

Steps to Reproduce

  1. Create a new profile on brave (to have a blank wallet setup)
  2. Open the crypto wallet and follow the setup instructions
  3. The setup instructions will lead to a 24-word mnemonic, copy that one (in my case it was toward around purity endorse style assume seed discover beyond mosquito lend auto cattle shed smile pretty push slot grid hotel eagle govern defense element
  4. Note the address that is shown in the brave wallet - in my case it was 0xC5443F1948FF4273222E04E93A9365780990Bc1D
  5. Open e.g. MyEtherWallet (same result using MyCrypto desktop app) or any other standard Ethereum wallet and restore the address from the mnemonic.
  6. The resulting Ethereum address is a different one, in my case 0x815B52687Ff57Ae9a58B4894e66ad476f0ba224D

Actual result:

The Ethereum address generated in Brave does not match those generated with standard tools.

Expected result:

The Ethereum address generated in Brave should match those generated with standard tools.

Reproduces how often:

Easily reproduced

Brave version (brave://version info)

on Ubuntu 18.04

Brave | 1.8.96 Chromium: 81.0.4044.138 (Official Build) (64-bit)
-- | --
Revision | 8c6c7ba89cc9453625af54f11fd83179e23450fa-refs/branch-heads/4044@{#999}
OS | Linux
JavaScript | V8 8.1.307.32

Miscellaneous Information:

Generally, Brave seems to be unable to import 24-word mnemonics correctly. E.g. the test vector zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote yields the address 0x63D1ec5Cb2D2D2d3168266E88DA0F08c7455e6D9 (should be 0x1959f5f4979c5Cd87D5CB75c678c770515cb5E0E).

12-word mnemonics on the other hand do work fine. The test vector zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong yields the correct address 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 4
  • Comments: 45 (24 by maintainers)

Commits related to this issue

Most upvoted comments

Can this be re-opened, as it still is an issue? It is still not fixed as I have a 24-word phrase that cannot be used in Brave because Brave doesn’t follow the standard HD Wallet algorithm for key derivation from it.

IIUC, #13245 only fixed 12-word phrases, but 24-word phrases are still non-standards compliant. While I understand that this is a huge pain to fix, the pain of Brave mnemonics being standards non-compliant is only going to get worse with additional crypto adoption over time and the longer we wait to start on the road to standards compliance in brave the more painful this will be.

My recommendation on long-term migration strategy:

  1. Update the recover UI to have an option for the user to choose between “Externally Created Seed Phrase” and “Brave Created Seed Phrase” (exact wording and UX TBD).
  2. For new users start generating standards compliant 24-word seed phrases and at the same time change the recovery UI to prompt for “Externally Created Seed Phrase or Brave Created Seed Phrase after date X” and “Brave Created Seed Phrase before date X” (exact wording and UX TBD).
  3. Eventually (years from now) hide the option to recover from a legacy Brave seed phrase somewhere so most users don’t have to see/deal with it but users with ancient seed phrases can still derive.

Another option would be that when the user tries to recover a 24 word phrase, check to see which accounts have assets on the current network so we can helpfully hint to the user which one they probably have.

Okay, this can be the 3rd complaint. I expect most users of Brave Crypto Wallet would not realise the issue exists. If they had known of the issue they probably wouldn’t have choosen the Brave Crypto Wallet in the first place.

Is there a guide for deriving seed words that will restore the crypto wallet addresses to other wallets?

AFAICT we’ve only had 2 complaints of this so far, probably because this bug only occurs if you are going from Brave to another wallet implementation. I agree it would be ideal to be standards-compatible because that’s what users expect, but either way the funds are fully recoverable.

I think the 2 options are:

  1. Add lowercase key export option. In the codewords screen, add a disclaimer that restoring wallets from the codewords requires using Brave; for exporting to other wallets, use the private key export. This is my preferred solution since it’s easier.
  2. Add lowercase key export option, but also change the wallet derivation process to be standard-compatible; then on wallet restoration in Brave, we ask whether their wallet codewords are v1 or v2 and derive accordingly. This might cause some user confusion if someone doesn’t know whether they are using v1 or v2 codewords, which can also lead to perceived loss of funds.

i did some digging; findings here

how brave does wallet derivation/restoration

  1. generate 32 bytes of entropy and runs this through HKDF to generate a “seed” (see step 7 of https://github.com/brave/brave-browser/wiki/Brave-Ethereum-Remote-Client-Wallet-Seed-Information#protocol)
  2. calls bip39.entropyToMnemonic to encode it as a mnemonic
  3. calls hdkey.fromMasterSeed directly on the 32-byte seed (https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#master-key-generation)
  4. upon restoration, we call bip39.mnemonicToEntropy on the mnemonic to convert it back into the seed

how metamask does wallet derivation/restoration

as far as i understand:

  1. calls bip39.generateMnemonic() to generate 16 bytes of entropy encoded as a mnemonic. this follows https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic.
  2. calls mnemonicToSeed, which does some PBKDF2-HMAC-SHA512 on the mnemonic to generate a 64-byte seed: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed
  3. calls hdkey.fromMasterSeed with the 64-byte seed
  4. upon restoration, calls bip39.mnemonicToSeed

unfortunately, i think this will make it difficult for us to encode the brave wallet codewords as other-wallet-compatible code words, because we would need to find the PBKDF2 preimage. if other wallets are willing to take the seed directly as input, that could work.

After a quick skim of the code (way over my head here), is this line correct? https://github.com/brave/brave-core/blob/cd5a08090094fc1a67033e2760c3a067b27e001d/components/brave_sync/crypto/crypto.cc#L148

bool PassphraseToBytes32(const std::string& passphrase,
                         std::vector<uint8_t>* bytes) {
  DCHECK(bytes);
  size_t written;
  bytes->resize(DEFAULT_SEED_SIZE);
  if (bip39_mnemonic_to_bytes(nullptr, passphrase.c_str(), bytes->data(),
                              bytes->size(), &written) != WALLY_OK) {
    LOG(ERROR) << "bip39_mnemonic_to_bytes failed";
    return false;
  }
  return true;
}

bip39_mnemonic_to_bytes only converts the mnemonic into a binary representation, but it does not actually derive the BIP39 seed. That is done by bip39_mnemonic_to_seed, which doesn’t seem to be used anywhere in the code. Maybe someone mistook the result of PassphraseToBytes32 as the actual seed to derive keys with?