2

Where does Metamask store the private keys of an EVM-account after one recovers an account with private keys? Does it cipher the private keys before writing to storage, e.g. encoding with the access password that one sets up once installation is ready?

Solved. See the updated below.

1 Answer 1

0

OK, I figured it out.

Thanks @rdymak for (https://ethereum.stackexchange.com/a/64038/120999)

  1. Physically the encrypted secret passphrase is stored at this location in a .log file: "C:\Users\Kamikaze([insert_your_user_name])\AppData\Local\Google\Chrome\User Data\Default\Local Extension Settings\nkbihfbeogaeaoehlefnkodbefgpgknn\[000333...].log";

  2. Prior to writing (saving) to physical storage, Metamask will encrypt the secret recovery passphrase (that it generates from an Ethereum private key) with a password that you set up during the onboarding process;


The [(000333)|(....)].log file partially has the following structure:

{
   "KeyringController":{
      "vault":{
         "data":"WUE8t2eFijsw9wgn4rO+mRlopix4saZUYX3UDHwgNTG7hkE8kv5VRBA7qbs0uVlQOlz+mtp/DTn+ieik0UA3IvJCz12c/90lhyOuR25qZqvhMtpmm6gKy/yMzTPHBwZircHvlf/VcuiyMUeSPsc/3YjfZpGEzWn75xpWaTT16jSMsa64ND4fctO4ucQwO0oGN50bIacwxb6bY5MgCnl8CxBKea4JtFogD3E5yoPWUwTmAYFoseXt4NPYDwRBCPK2SRmvc1o+FKWUVTRo/X1krxsNLlBNYO1Dwo1RfJvKGMgELL1OLfoqzgNV5mk7kGjuFjJ6Q6A1PRKXz4g+i9Sl2x/qK8cgEH8mVhKWJ1b+oFV+SBlR5wQjdNl5Kgfc5msaHdDWifEKS80sZN0O29N24Rgl5YP0/R6hqnGdTcU/YZE9dWckxtsx8mKe3g7wJbqUuMRUI4CJ7dlE7V2eHBIXEe3efU61d1E0G5fgZRR5N0eBaENo7gdLv8/5o88w",
         "iv":"zZb2oMm7tw+f5ZUKzEyx7A==",
         "keyMetadata":{
            "algorithm":"PBKDF2",
            "params":{
               "iterations":600000
            }
         },
         "salt":"6HywOxM7tGwP+BdZpTR8PC4qSBYYRdt6dotAjynKlZk="
      }
   }
}

As a test, I decided to create a new Metamask instance, and decrypt the data that was stored at "C:\Users\Kamikaze\AppData\Local\Google\Chrome\User Data\Profile 3\Local Extension Settings\nkbihfbeogaeaoehlefnkodbefgpgknn\000018.log".

  • Note that I set up my password as hello123!
  • The secret recovery phrase is winter point process warm provide draft top knee clever matter visa catch

You can see the packed JSON chunk in the code snippet above (it was prettyfied).


Now, diving deeper into the Metamask Chrome extension's Javascript code, it's evident that the app\scripts\metamask-controller.js file updates the state of the app via accessing this.extension.storage.session, a custom local store contract that references browser.storage "under the hood", which comes from the webextension-polyfill NPM library.

That's how the private key and salt data is stored to the physical device storage.


Finally, here's the code snippet of unlocking Metamask after you have already backed up your private key and set up a login password, for illustration:

/**
   * Submits a user's encryption key to log the user in via login token
   */
  
  async submitEncryptionKey() {
    try {
      const { loginToken, loginSalt } =
      
        await this.extension.storage.session.get(['loginToken', 'loginSalt']);
      if (loginToken && loginSalt) {
        const { vault } = this.keyringController.state;


        const jsonVault = JSON.parse(vault);

        if (jsonVault.salt !== loginSalt) {
          console.warn(
            'submitEncryptionKey: Stored salt and vault salt do not match',
          );
          await this.clearLoginArtifacts();
          return;
        }

        await this.keyringController.submitEncryptionKey(loginToken, loginSalt);
      }
    } catch (e) {
      // If somehow this login token doesn't work properly,
      // remove it and the user will get shown back to the unlock screen
      await this.clearLoginArtifacts();
      throw e;
    }
  }

  async clearLoginArtifacts() {
    await this.extension.storage.session.remove(['loginToken', 'loginSalt']);
  }

The submitEncryptionKey call is forwarded to the KeyringController from this Metamask Foundation package.

There it propagates to:

/**
   * Submit Encryption Key.
   *
   * Attempts to decrypt the current vault and load its keyrings
   * into memory based on the vault and CryptoKey information.
   *
   * @fires KeyringController#unlock
   * @param encryptionKey - The encrypted key information used to decrypt the vault.
   * @param encryptionSalt - The salt used to generate the last key.
   * @returns A promise that resolves to the state.
   */
  async submitEncryptionKey(
    encryptionKey: string,
    encryptionSalt: string,
  ): Promise<KeyringControllerState> {
    this.keyrings = await this.unlockKeyrings(
      undefined,
      encryptionKey,
      encryptionSalt,
    );
    this.#setUnlocked();
    return this.fullUpdate();
  }

... And:

/**
   * Unlock Keyrings.
   *
   * Attempts to unlock the persisted encrypted storage,
   * initializing the persisted keyrings to RAM.
   *
   * @param password - The keyring controller password.
   * @param encryptionKey - An exported key string to unlock keyrings with.
   * @param encryptionSalt - The salt used to encrypt the vault.
   * @returns The keyrings array.
   */
  async unlockKeyrings(
    password: string | undefined,
    encryptionKey?: string,
    encryptionSalt?: string,
  ): Promise<EthKeyring<Json>[]> {
    const encryptedVault = this.store.getState().vault;
    if (!encryptedVault) {
      throw new Error(KeyringControllerError.VaultError);
    }

    await this.#clearKeyrings();

    let vault;

    if (this.#cacheEncryptionKey) {
      assertIsExportableKeyEncryptor(this.#encryptor);

      if (password) {
        const result = await this.#encryptor.decryptWithDetail(
          password,
          encryptedVault,
        );
        vault = result.vault;
        this.password = password;

        this.memStore.updateState({
          encryptionKey: result.exportedKeyString,
          encryptionSalt: result.salt,
        });
      } else {
        const parsedEncryptedVault = JSON.parse(encryptedVault);

        if (encryptionSalt !== parsedEncryptedVault.salt) {
          throw new Error(KeyringControllerError.ExpiredCredentials);
        }

        if (typeof encryptionKey !== 'string') {
          throw new TypeError(KeyringControllerError.WrongPasswordType);
        }

        const key = await this.#encryptor.importKey(encryptionKey);
        vault = await this.#encryptor.decryptWithKey(key, parsedEncryptedVault);

        // This call is required on the first call because encryptionKey
        // is not yet inside the memStore
        this.memStore.updateState({
          encryptionKey,
          // we can safely assume that encryptionSalt is defined here
          // because we compare it with the salt from the vault
          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
          encryptionSalt: encryptionSalt!,
        });
      }
    } else {
      if (typeof password !== 'string') {
        throw new TypeError(KeyringControllerError.WrongPasswordType);
      }

      vault = await this.#encryptor.decrypt(password, encryptedVault);
      this.password = password;
    }

    if (!isSerializedKeyringsArray(vault)) {
      throw new Error(KeyringControllerError.VaultDataError);
    }

    await Promise.all(vault.map(this.#restoreKeyring.bind(this)));
    await this.updateMemStoreKeyrings();

    if (
      this.password &&
      (!this.#cacheEncryptionKey || !encryptionKey) &&
      this.#encryptor.isVaultUpdated &&
      !this.#encryptor.isVaultUpdated(encryptedVault)
    ) {
      // Re-encrypt the vault with safer method if one is available
      await this.persistAllKeyrings();
    }

    return this.keyrings;
  }

Now let's use the utility that Metamask provides for cases when you have lost access to the UI, but still have access to the 0000[....,l=~4-5].log file.

You cna find it @ https://metamask.github.io/vault-decryptor/

Check the "Paste text" option, and paste the formatted object, like this:

{
         "data":"WUE8t2eFijsw9wgn4rO+mRlopix4saZUYX3UDHwgNTG7hkE8kv5VRBA7qbs0uVlQOlz+mtp/DTn+ieik0UA3IvJCz12c/90lhyOuR25qZqvhMtpmm6gKy/yMzTPHBwZircHvlf/VcuiyMUeSPsc/3YjfZpGEzWn75xpWaTT16jSMsa64ND4fctO4ucQwO0oGN50bIacwxb6bY5MgCnl8CxBKea4JtFogD3E5yoPWUwTmAYFoseXt4NPYDwRBCPK2SRmvc1o+FKWUVTRo/X1krxsNLlBNYO1Dwo1RfJvKGMgELL1OLfoqzgNV5mk7kGjuFjJ6Q6A1PRKXz4g+i9Sl2x/qK8cgEH8mVhKWJ1b+oFV+SBlR5wQjdNl5Kgfc5msaHdDWifEKS80sZN0O29N24Rgl5YP0/R6hqnGdTcU/YZE9dWckxtsx8mKe3g7wJbqUuMRUI4CJ7dlE7V2eHBIXEe3efU61d1E0G5fgZRR5N0eBaENo7gdLv8/5o88w",
         "iv":"zZb2oMm7tw+f5ZUKzEyx7A==",
         "keyMetadata":{
            "algorithm":"PBKDF2",
            "params":{
               "iterations":600000
            }
         },
         "salt":"6HywOxM7tGwP+BdZpTR8PC4qSBYYRdt6dotAjynKlZk="
      }

In older version, you'd have something like this:

{"data":"wwpXXtFCqZkYsWfeEwItZjJ0Cc7mRVjG47Dqh+ztL1PiCG6Izhg+zG0mM+H2ykyjz3X0RNhAE6IVsWFZamcZ47B4sVi4SvUxrMhARm5L3yHPxr3UsyGrOXmthyVMgEGmjwlmnFCNd2nMZ2o8/sRMra8FupurqevnBv57FiYpEEs7gPpFHv6587aL44MmKD8Snv4JLFqiqmlK82Waq5F+Iv9mw2sFVAL9mgZBSgFgbWdB3TsKVB2k","iv":"rkUQlNcGTxBE0My7a/bCXw==","salt":"HcKyNfGzaRALRQ0DlKgcIe5Uk30iI/M//oG6w8vX8Nk="}

(Metamask official guide) metamask-vault-decryptor

Then enter the password

Finally click "Decrypt"

Result:

[{"type":"HD Key Tree","data":{"mnemonic":"winter point process warm provide draft top knee clever matter visa catch","numberOfAccounts":1,"hdPath":"m/44'/60'/0'/0"}}]

A curious point that I noticed is that the [0000...].log file that stores the encrypted vault and salt properties has changed its name numerous times during the debugging process: it was 000019 at the beginning, then, when I locked my Metamask popup in the browser, it became 000026, and finally it was 000028 all of the sudden.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.