How MetaMask🦊 stores your wallet secret?

As a security engineer/pentester in the blockchain space, I have tested many crypto wallet applications during research and client engagements. After seeing different ways of handing secrets in different wallets, I was curious about how MetaMask, one of the most famous crypto wallets in the space, does it. Well, if you ask me why I didn’t look at MetaMask earlier, because I just assume it’s secure, and the chance for me to find any issue is very low.

Both their Extension and Mobile app along with many other modules create by MetaMask used in the application are open source. The codebase is by far the biggest among all crypto wallets I have seen. But it’s not too difficult to follow, as the code is well written and full of comments.

The article goes by explaining different components in the app that relate to the secret storage. At the end of the article, I include a very rough code path that describes how the extension to create a new wallet. Note that Wallet secrets in this article refer to the seed phrase(mnemonic) and private keys.

🦊Keyring

Keyring is the core concept of the secret storing and account management system in MetaMask. KeyringController is the implementation of the keyring. Cited from the KeyringController README:

A module for managing groups of Ethereum accounts called "Keyrings", defined originally for MetaMask's multiple-account-type feature.

The KeyringController has three main responsibilities:
    *Initializing & using (signing with) groups of Ethereum accounts ("keyrings").
    *Keeping track of local nicknames for those individual accounts.
    *Providing password-encryption persisting & restoring of secret information.

Here is the visual reference of the keyring structure:

The circular ring represents the seed phrase that is used to generate public key-private key pairs. Each key hangs on the ring is an individual wallet account with its private key drives from the seed phase. The seed phrase and all accounts data get bundled together, encrypted with an encryption key generated from the user password, and stores in the extension.

The KeyringController uses the “obs-store” class to store data. “obs-store” stands for ObservableStore, which is a synchronous in-memory store for a single value. The code also references the obs-store as a “Vault“. Let’s take a closer look at how it’s implemented: Two ObservableStore objects are created inside the KeyringController constructor, one named “this.store” and  another one named “this.memStore“:  

this.store stores the encrypted wallet secret. Data in this.store will be put into the chrome extension local store for persistence data storage. Users can access the data by entering the following code in the extension development console. 

chrome.storage.local.get('data', result => {
    var vault = result.data.KeyringController.vault
    console.log(vault)
})

this.memStore, on the other hand, stores the decrypted wallet secrets. Data in this object stays in memory and will not be put into the browser’s persistent storage. When you open the MetaMask extension and enter your password, your decrypted account private key will be stored in the this.memStore object for any future use. Reference 1, Reference 2

🦊encryptor

Inside the KeyringController class, the encryption and decryption operation is performed by the encryptor object.

Ex1. Encrypt keyring data:
return this.encryptor.encrypt(this.password, serializedKeyrings)

Ex2. Decrypt the encryptedVault
const vault = await this.encryptor.decrypt(password, encryptedVault)

The encryptor object is assigned in the KeyringController constructor

The extension and the mobile app uses a different encryptor. The extension uses the browser-passworder module, its source code is available on Github. The mobile app has its own encryptor class. They work pretty much the same, except for the PBKDF2 iteration and AES mode.

browser-passworder: 
Generate enc_key from password: PBKDF2, 10000 iteration
AES mode: AES-GCM

Mobile app encryptor: 
Generate enc_key from password: PBKDF2, 5000 iteration
AES mode: AES-CBC

🦊Mobile App

Similar to extension, the mobile app adopts the Keyring structure to store secrets and manage accounts. For persistent storage, the app stores encrypted data with the async-storage module that define in the persistConfig. The only official documentation I can find that describe how “async-storage” work says:

On iOS, AsyncStorage is backed by native code that stores small values in a serialized dictionary and larger values in separate files. 

On Android, AsyncStorage will use either RocksDB or SQLite based on what is available.

The mobile wallet provides the “remember me” and ‘unlock with touch ID/device passcode” options. Users don’t need to enter their password every time they open the app on their mobile devices.













To achieve this, the MetaMask mobile app stores the user password in the device with the SecureKeychain module, which builds upon the “react-native-keychain“. The user password is used to generate the key to decrypt the encrypted wallet secret in persistent storage.

Quick note on how “react-native-keychain” stores data in mobile devices:

🦊SecureKeychain

Comment in the code explain what the SecureKeychain does:

/**
* Class that wraps Keychain from react-native-keychain
* abstracting metamask specific functionality and settings
* and also adding an extra layer of encryption before writing into
* the phone's keychain
*/
class SecureKeychain {
...
}

“abstracting metamask specific functionality and settings” refers to the “remember me” and “sign in with touch ID/device passcode” feature. Check out the resetGenericPassword, getGenericPassword and setGenericPassword function to see how it’s implemented.

As for “adding an extra layer of encryption”, I am curious about what’s the encryption key used here to perform the encryption? Here is the relevant code path I found:

  1. The code in the constructor is used as the encryption key. (Source)
  2. In the init(salt) function, the SecureKeychain object is created with the argument ‘salt’ (Source)
  3. The init function is called with the argument props.foxCode (Source)

What is the foxCode? I searched in the mobile wallet repository and all public repositories under the MetaMask organization, but no result. Hmm🤔, how about the app binary?  

I download the Metamask APK with this tool, de-package with jadx, search for the string “foxCode” and found this:

Wait, so the foxCode equal to the string “encrypt”? What’s the point of encrypting something with a hardcoded string? 🤔

I send an email to the Metamask security team and ask for why does the user password encrypted with the hardcoded string(the foxCode) “encrypt”, the team responded: 

I pretty much agree with him. But I still wonder why such code gets put in the codebase in the first place.

🦊The end

This is the end of the article. I hope it explains the basics of how MetaMask wallet stores your wallet secret. If you want to know more details, you can read its source code in their GitHub repo https://github.com/MetaMask.

==========================🦊Appendix🦊==========================

A very rough code path for the new wallet creation:

1. metamask-extension/ui/app/pages/first-time-flow/first-time-flow.component.js  <-- Click Me
const seedPhrase = await createNewAccount(password)

2. metamask-extension/ui/app/pages/first-time-flow/first-time-flow.container.js
createNewAccount: (password) =>
dispatch(createNewVaultAndGetSeedPhrase(password))

3. metamask-extension/ui/app/store/actions.js
export function createNewVaultAndGetSeedPhrase(password){
    ....
    await createNewVault(password)
    const seedWords = await verifySeedPhrase()
    ....
}

4. metamask-extension/app/scripts/metamask-controller.js
async createNewVaultAndKeychain(password){
    vault = await this.keyringController.createNewVaultAndKeychain(password)
}

5. KeyringController/blob/master/index.js
createNewVaultAndKeychain (password) {
    return this.persistAllKeyrings(password)
        .then(this.createFirstKeyTree.bind(this))
        .then(this.persistAllKeyrings.bind(this, password))
        .then(this.setUnlocked.bind(this))
        .then(this.fullUpdate.bind(this))
}

6. MetaMask/KeyringController/blob/master/index.js createFirstKeyTree () {
    ... 
    return this.addNewKeyring('HD Key Tree', { numberOfAccounts: 1 })
    ... 
} 

7. MetaMask/KeyringController/blob/master/index.js addNewKeyring (type, opts) { 
    ... 
    const Keyring = this.getKeyringClassForType(type)
    const keyring = new Keyring(opts)
    ... 
}

8. MetaMask/eth-hd-keyring/blob/master/index.js
addAccounts (numberOfAccounts = 1) {
    ...
    this._initFromMnemonic(bip39.generateMnemonic())
    ...
}