How Bitcoin Core Wallet Encryption and Key Derivation Works: A Technical Deep Dive
Bitcoin Core protects private keys using a two-tier encryption system where a user passphrase derives an AES-256-CBC encryption key via a SHA-512-based PBKDF-like routine to secure a random master key, which then encrypts all wallet private keys, with derivation iterations auto-tuned to approximately 100 milliseconds for optimal security.
Bitcoin Core's wallet encryption safeguards funds by ensuring private keys never exist on disk in plaintext once encryption is enabled. When users invoke the encryptwallet RPC, the implementation in src/wallet/wallet.cpp establishes a master key architecture that separates the passphrase protection layer from the actual private key storage. This design allows users to change passphrases without re-encrypting every private key, while utilizing computationally expensive key derivation to resist brute-force attacks.
The Two-Layer Encryption Architecture
Bitcoin Core implements hierarchical encryption rather than directly encrypting private keys with the user passphrase.
The first layer derives an encryption key from the user passphrase using a salt and iteration count, then uses that key to encrypt a randomly generated master key (32 bytes of CKeyingMaterial). The second layer uses this plain master key to encrypt every ScriptPubKeyMan (the components holding actual private keys) in the wallet. This architecture means:
- Private keys are encrypted with the master key
- The master key is encrypted with the passphrase-derived key
- Changing the passphrase only requires re-encrypting the master key, not every private key
Master Key Structure and Storage
The CMasterKey class defined in src/wallet/crypter.h stores all parameters needed to decrypt the master key:
class CMasterKey
{
public:
std::vector<unsigned char> vchCryptedKey; // Encrypted master key (ciphertext)
std::vector<unsigned char> vchSalt; // Random salt (WALLET_CRYPTO_SALT_SIZE bytes)
unsigned int nDerivationMethod; // Only 0 for EVP_sha512()
unsigned int nDeriveIterations; // PBKDF iteration count
// ... serialization methods
};
The wallet maintains a map of master keys in src/wallet/wallet.h:
typedef std::map<unsigned int, CMasterKey> MasterKeyMap;
mapMasterKeys;
This supports multiple master keys (for example, after passphrase changes), though only the most recent is typically used for new encryption operations.
Key Derivation with SHA-512
The critical derivation logic resides in CCrypter::SetKeyFromPassphrase within src/wallet/crypter.cpp:
bool CCrypter::SetKeyFromPassphrase(const SecureString& key_data,
const std::span<const unsigned char> salt,
const unsigned int rounds,
const unsigned int derivation_method)
{
if (rounds < 1 || salt.size() != WALLET_CRYPTO_SALT_SIZE) return false;
int i = 0;
if (derivation_method == 0) { // EVP_sha512()
i = BytesToKeySHA512AES(salt, key_data, rounds,
vchKey.data(), vchIV.data());
}
if (i != (int)WALLET_CRYPTO_KEY_SIZE) {
memory_cleanse(vchKey.data(), vchKey.size());
memory_cleanse(vchIV.data(), vchIV.size());
return false;
}
fKeySet = true;
return true;
}
The function BytesToKeySHA512AES implements OpenSSL's EVP_BytesToKey algorithm using SHA-512 as the digest function. It derives:
- A 256-bit encryption key (32 bytes) stored in
vchKey - A 128-bit IV (16 bytes) stored in
vchIV
Auto-Tuning the Derivation Cost
To balance security against brute-force attacks with usability, Bitcoin Core automatically calibrates the iteration count to consume approximately 100 milliseconds of CPU time. The static helper EncryptMasterKey in src/wallet/wallet.cpp implements this timing logic:
static bool EncryptMasterKey(const SecureString& wallet_passphrase,
const CKeyingMaterial& plain_master_key,
CMasterKey& master_key)
{
constexpr MillisecondsDouble target{100}; // target ~100 ms
auto start{SteadyClock::now()};
CCrypter crypter;
// Initial measurement to calibrate iterations
crypter.SetKeyFromPassphrase(wallet_passphrase,
master_key.vchSalt,
master_key.nDeriveIterations,
master_key.nDerivationMethod);
master_key.nDeriveIterations = static_cast<unsigned int>(
master_key.nDeriveIterations * target / (SteadyClock::now() - start));
// Refinement pass (averaging two measurements)
start = SteadyClock::now();
crypter.SetKeyFromPassphrase(wallet_passphrase,
master_key.vchSalt,
master_key.nDeriveIterations,
master_key.nDerivationMethod);
master_key.nDeriveIterations = (master_key.nDeriveIterations +
static_cast<unsigned int>(master_key.nDeriveIterations * target /
(SteadyClock::now() - start))) / 2;
// Enforce minimum security threshold
if (master_key.nDeriveIterations < CMasterKey::DEFAULT_DERIVE_ITERATIONS) {
master_key.nDeriveIterations = CMasterKey::DEFAULT_DERIVE_ITERATIONS;
}
// Final key derivation and encryption
if (!crypter.SetKeyFromPassphrase(wallet_passphrase,
master_key.vchSalt,
master_key.nDeriveIterations,
master_key.nDerivationMethod))
return false;
if (!crypter.Encrypt(plain_master_key, master_key.vchCryptedKey))
return false;
return true;
}
This measurement ensures that dictionary attacks against the wallet remain computationally expensive regardless of the host machine's speed.
Encrypting the Wallet
The high-level encryption flow in CWallet::EncryptWallet (src/wallet/wallet.cpp) orchestrates the complete process:
bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
{
// Only descriptor wallets support encryption
Assert(IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
if (HasEncryptionKeys()) return false;
// Generate random master key material
CKeyingMaterial plain_master_key(WALLET_CRYPTO_KEY_SIZE);
GetStrongRandBytes(plain_master_key);
CMasterKey master_key;
master_key.vchSalt.resize(WALLET_CRYPTO_SALT_SIZE);
GetStrongRandBytes(master_key.vchSalt);
// Encrypt the master key with passphrase-derived key
if (!EncryptMasterKey(strWalletPassphrase, plain_master_key, master_key))
return false;
// Persist and encrypt all ScriptPubKey managers
{
LOCK2(m_relock_mutex, cs_wallet);
mapMasterKeys[++nMasterKeyMaxID] = master_key;
WalletBatch* encrypted_batch = new WalletBatch(GetDatabase());
encrypted_batch->TxnBegin();
encrypted_batch->WriteMasterKey(nMasterKeyMaxID, master_key);
for (const auto& spk_man_pair : m_spk_managers) {
auto spk_man = spk_man_pair.second.get();
if (!spk_man->Encrypt(plain_master_key, encrypted_batch))
assert(false); // Inconsistent state - critical error
}
encrypted_batch->TxnCommit();
delete encrypted_batch;
Lock();
Unlock(strWalletPassphrase);
GetDatabase().Rewrite(); // Scrub plaintext remnants
}
return true;
}
Critical security steps include:
- Atomic database transaction writing the master key and encrypted private keys
- Immediate unlock after encryption to verify correctness
- Database rewrite (
Rewrite()) to eliminate any plaintext key remnants from the wallet file
Unlocking and Passphrase Changes
Unlocking reverses the encryption process. CWallet::Unlock attempts to decrypt each stored master key until finding one that works:
bool CWallet::Unlock(const SecureString& strWalletPassphrase)
{
CKeyingMaterial plain_master_key;
LOCK(cs_wallet);
for (const auto& [_, master_key] : mapMasterKeys) {
if (!DecryptMasterKey(strWalletPassphrase, master_key, plain_master_key))
continue;
if (Unlock(plain_master_key)) {
UpgradeDescriptorCache();
return true;
}
}
return false;
}
Changing the passphrase utilizes the same master key but re-encrypts it with a new passphrase-derived key. CWallet::ChangeWalletPassphrase decrypts the master key using the old passphrase, then calls EncryptMasterKey with the new passphrase (which re-tunes the iteration count), and writes the updated CMasterKey record to the database.
RPC Interface Commands
Users interact with the encryption system through three primary RPC commands implemented in src/wallet/rpc/encrypt.cpp and src/wallet/rpc/wallet.cpp:
encryptwallet "passphrase"– InitiatesCWallet::EncryptWallet, generating the master key and encrypting all private keyswalletpassphrase "passphrase" timeout– CallsCWallet::Unlockto decrypt the master key and enable spending for the specified duration (in seconds)walletpassphrasechange "oldpassphrase" "newpassphrase"– InvokesCWallet::ChangeWalletPassphraseto re-encrypt the master key without touching the underlying private keys
Summary
- Bitcoin Core uses a master key architecture where the user passphrase protects a random 32-byte master key, which then encrypts all private keys via AES-256-CBC
- Key derivation employs SHA-512 through
BytesToKeySHA512AESinCCrypter::SetKeyFromPassphrase, with iteration counts automatically tuned to approximately 100 milliseconds - Wallet encryption is atomic –
CWallet::EncryptWalletuses database transactions to ensure consistency, followed by a file rewrite to eliminate plaintext remnants - Multiple master keys are supported in
mapMasterKeysto handle passphrase changes while maintaining backward compatibility - Only descriptor wallets (those with
WALLET_FLAG_DESCRIPTORSset) support encryption in modern Bitcoin Core versions
Frequently Asked Questions
What encryption algorithm does Bitcoin Core use for wallet encryption?
Bitcoin Core uses AES-256-CBC for symmetric encryption of keys. The encryption keys and initialization vectors (IVs) are derived using a PBKDF-like routine based on SHA-512, implemented in the BytesToKeySHA512AES function within src/wallet/crypter.cpp. This provides 256-bit encryption strength for protecting the master key.
How does Bitcoin Core determine the number of KDF iterations?
The iteration count is auto-tuned during the encryption process to consume approximately 100 milliseconds of CPU time. The EncryptMasterKey function in src/wallet/wallet.cpp measures the time required for an initial iteration count, calculates the ratio needed to reach the 100ms target, and refines this through a second measurement. The final count is enforced to be at least DEFAULT_DERIVE_ITERATIONS.
Can funds be recovered if the wallet passphrase is forgotten?
No. Bitcoin Core's encryption is designed such that forgetting the passphrase results in permanent loss of funds. The passphrase is the only source material capable of deriving the key that decrypts the master key stored in CMasterKey. Without this master key, the encrypted private keys in the wallet database cannot be decrypted, and there is no recovery mechanism or backdoor.
What is the difference between the master key and private keys in Bitcoin Core?
The master key is a random 32-byte value generated when encrypting the wallet, stored encrypted in CMasterKey, and used solely to encrypt the wallet's private keys. The private keys are the actual ECDSA secp256k1 keys controlling Bitcoin outputs, stored in ScriptPubKeyMan objects. This separation allows users to change passphrases (which re-encrypts only the master key) without having to re-encrypt every individual private key in the wallet.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →