From Mnemonic Phrase to Private Key: Everything You Need to Know

Written by rolaman | Published 2023/06/22
Tech Story Tags: web3 | cryptography | rust | cryptocurrency | programming | crypto-wallet | crypto-wallet-security | seed-phrase

TLDRThis article explains how to get your private key for any blockchain. It is the best way to store all your keys. This knowledge contributes to a better understanding of the underlying mechanisms. All examples are provided in Rust, so you can try to run them on your local machine.via the TL;DR App

When creating a wallet for any blockchain-based application(TrustWallet, MetaMask, Phantom) or cryptocurrency, one of the first things you encounter is a mnemonic phrase - a set of words(usually 12 or 24) that holds the power to recover your wallet. But how does this collection of words translate into the private key, which is the real guardian of your digital assets?

This article provides an explanation of how to obtain your private key for any blockchain and highlights why it is considered the optimal method for storing all your keys. Understanding this process not only enhances your comprehension of the underlying mechanisms within the blockchain world but also enables you to handle your digital assets more securely. All examples in this article are demonstrated using Rust, allowing you to execute them on your local machine for hands-on experience.

The transition consists of 4 main steps:

  • Mnemonic generation

  • Mnemonic to seed conversion

  • Seed to master key conversion

  • Master key to child private keys conversion

Mnemonic generation

First, we need to get the words for the mnemonic phrase. In this example, I will show how to get a 12-word phrase, but it is also applicable to a 24-word phrase.

Let’s begin by generating random 128 bits (16 bytes):

let mut bytes = [0u8; 16];
rand::thread_rng().fill(&mut bytes[..]);

It is crucial to generate them randomly, so nobody can regenerate them.

Next, we need to calculate the SHA256 hash of our bytes:

let mut hasher = Sha256::new();
hasher.update(bytes);
let result = hasher.finalize();

This will be a checksum for our initial bytes, enabling us to always validate our phrase.

After these steps, we can easily fetch words for the mnemonic phrase. We need to concatenate generated bytes and checksum bytes. This will total 132 bits. Each group of 11 bits(12 groups in total) represents a number from 0 to 2047.

Map these numbers to the BIP-39 English word list, which contains 2048 words:

// word_list contains all BIP-39 words
let chunk_size = 11;
let bits = BitVec::from_bytes(&bytes);
let mut mnemonic = String::new();

for i in (0..bits.len()).step_by(chunk_size) {
    let end = std::cmp::min(i + chunk_size, bits.len());
    let chunk = bits.get(i..end).unwrap();
    let mut value: usize = 0;
    for bit in chunk.iter() {
        value = (value << 1) | (bit as usize);
    }
    mnemonic.push_str(word_list[value]);
    mnemonic.push(' ');
}
mnemonic.pop();

Congratulations, you just generated your unique mnemonic phrase and can use it in blockchain wallets.

You might be wondering about the choice of words. Why do we use this particular list? Can you use your own list of words to store your private keys? Theoretically, yes, you could use any list of 2047 words, or even use your favorite book as a source of words. However, the BIP-39 word list is a worldwide standard, and all crypto wallets map the words to their indices according to this list. You can remember the indices from any word list, but in this case, every time you would need to map words from your list to the standard one.

Mnemonic to seed conversion

Now we need to convert our 12-word phrase to the seed. According to BIP-39 PBKDF2 function with HMAC-SHA512 should be applied. Additionally, it is common practice to add salt passphrase to this algorithm. If the passphrase is empty, then the salt value is just ‘mnemonic’. Using a passphrase provides an extra layer of security. If used, the passphrase should be kept secret, just like the mnemonic.

Getting seed from mnemonic and passphrase:

let mut pbkdf2_hash = [0u8; 64];
let salt = format!("mnemonic{}", passphrase);
pbkdf2::derive(
	pbkdf2::PBKDF2_HMAC_SHA512,
	std::num::NonZeroU32::new(2048).unwrap(),
	&salt.as_bytes(),
	mnemonic.as_bytes(),
	&mut pbkdf2_hash
);

The result of the PBKDF2 function is a 64-byte hash that contains all the information needed to derive the master key.

Seed to master key conversion

Once you have derived the seed(stored in pbkdf2_hash) from the mnemonic using the PBKDF2 HMAC-SHA512 function, you can use it to generate a master private key and chain code for a Hierarchical Deterministic (HD) Wallet according to the BIP-32 standard.

The BIP-32 standard describes how you can build a general hierarchical tree structure of keys, which allows you to derive child keys from parent keys in a deterministic manner. This means you can produce an entire family of key pairs (public key, private key) from a single master seed.

The BIP-32 standard specifies that the seed should be processed with HMAC-SHA512, using "Bitcoin seed" as the key, to produce the master private key and master chain code.

Getting master key:

let key = b"Bitcoin seed";

type HmacSha512 = Hmac<Sha512>;
let mut mac = HmacSha512::new_from_slice(key).expect("HmacSha512 error");
mac.update(&pbkdf2_hash);

let result = mac.finalize();
let bytes = result.into_bytes();

let master_private_key = &bytes[0..32];
let chain_code = &bytes[32..64];

The key value "Bitcoin seed" is defined in the BIP-32 (Bitcoin Improvement Proposal 32) standard for creating Hierarchical Deterministic wallets.

It may seem confusing that "Bitcoin seed" is used even when deriving keys for other cryptocurrencies, like Ethereum. However, it's important to remember that BIP-32 is a protocol that originated in the Bitcoin community and its standards are followed by many other cryptocurrencies. The use of "Bitcoin seed" doesn't restrict the usage to Bitcoin only -- it's simply a value used by the cryptographic function to generate the master private key and chain code.

A chain code, in the context of Bitcoin and other cryptocurrencies, is part of the Hierarchical Deterministic (HD) wallet structure specified in BIP-32 (Bitcoin Improvement Proposal 32). In a HD wallet, each node in the tree structure (each account, and each address within each account) is defined by a private/public key pair and a chain code. The chain code is a 32-byte value that, together with a private key, is used to securely derive child keys in a deterministic way, meaning the same parent key and chain code will always generate the same set of child keys.

Master key to child private keys conversion

Finally, we can fetch private keys for any blockchain from master_key.

Bitcoin example:

let secp = bitcoin::secp256k1::Secp256k1::new();
let master_privkey = ExtendedPrivKey::new_master(Network::Bitcoin, &master_key).unwrap();

let child_number = ChildNumber::from_normal_idx(0).unwrap();
let child_privkey = master_privkey.ckd_priv(&secp, child_number).unwrap();

Ethereum example:

let mut child_privkey_index = 0;
let derivation_path = format!("m/44'/60'/0'/0/{}", child_privkey_index);
let master_key = ExtendedPrivKey::derive(&seed, &derivation_path).unwrap();
let child_privkey = SecretKey::parse_slice(&child_key.secret()).unwrap();

In Ethereum, each master_key is directly mapped to child keys.

Summary

Congratulations, we generated a mnemonic and can store private keys for many blockchains via this. We've shown this process step-by-step using the Rust programming language. It's crucial to keep this mnemonic safe because it's used to access your digital assets.


Written by rolaman | Blockchain Developer
Published by HackerNoon on 2023/06/22