wallet: add an optional mnemonic-entropy secret to the key stores
Additive plumbing for storing a BIP39 entropy alongside the HD seed, mirroring how the seed itself is handled through both key store layers: a plaintext member on CBasicKeyStore, an encrypted pair on CCryptoKeyStore, encryption during the unencrypted-to-encrypted conversion in EncryptKeys with the plaintext cleared, and decryption on unlock. RawHDSeed and CKeyingMaterial are the same secure_allocator vector type, so the entropy passes through EncryptSecret/DecryptSecret with no adaptation. Nothing calls this yet; there is no behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,42 @@ bool CBasicKeyStore::GetHDSeed(HDSeed& seedOut) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CBasicKeyStore::SetMnemonicEntropy(const RawHDSeed& entropy)
|
||||||
|
{
|
||||||
|
LOCK(cs_SpendingKeyStore);
|
||||||
|
if (entropy.empty()) {
|
||||||
|
// Never "install" nothing: HaveMnemonicEntropy() would stay false while
|
||||||
|
// the caller was told the call succeeded.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!mnemonicEntropy.empty()) {
|
||||||
|
// Same refuse-to-replace rule as SetHDSeed above, for a sharper reason:
|
||||||
|
// this is the printable form of the seed. If it could be swapped while
|
||||||
|
// hdSeed stayed put, the wallet would print a seed phrase that does not
|
||||||
|
// restore it -- strictly worse than printing none.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mnemonicEntropy = entropy;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CBasicKeyStore::HaveMnemonicEntropy() const
|
||||||
|
{
|
||||||
|
LOCK(cs_SpendingKeyStore);
|
||||||
|
return !mnemonicEntropy.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CBasicKeyStore::GetMnemonicEntropy(RawHDSeed& entropyOut) const
|
||||||
|
{
|
||||||
|
LOCK(cs_SpendingKeyStore);
|
||||||
|
if (mnemonicEntropy.empty()) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
entropyOut = mnemonicEntropy;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool CBasicKeyStore::AddKeyPubKey(const CKey& key, const CPubKey &pubkey)
|
bool CBasicKeyStore::AddKeyPubKey(const CKey& key, const CPubKey &pubkey)
|
||||||
{
|
{
|
||||||
LOCK(cs_KeyStore);
|
LOCK(cs_KeyStore);
|
||||||
|
|||||||
@@ -117,6 +117,12 @@ class CBasicKeyStore : public CKeyStore
|
|||||||
{
|
{
|
||||||
protected:
|
protected:
|
||||||
HDSeed hdSeed;
|
HDSeed hdSeed;
|
||||||
|
// BIP39 entropy for a mnemonic-recoverable wallet, kept BESIDE hdSeed, never
|
||||||
|
// instead of it. hdSeed holds the bytes actually fed to derivation (the
|
||||||
|
// expanded 64-byte BIP39 seed on new wallets); this record exists only so the
|
||||||
|
// seed phrase can be reprinted. Empty on legacy and hex-restored wallets,
|
||||||
|
// which is a normal state, not an error.
|
||||||
|
RawHDSeed mnemonicEntropy;
|
||||||
KeyMap mapKeys;
|
KeyMap mapKeys;
|
||||||
ScriptMap mapScripts;
|
ScriptMap mapScripts;
|
||||||
WatchOnlySet setWatchOnly;
|
WatchOnlySet setWatchOnly;
|
||||||
@@ -129,6 +135,18 @@ public:
|
|||||||
bool SetHDSeed(const HDSeed& seed);
|
bool SetHDSeed(const HDSeed& seed);
|
||||||
bool HaveHDSeed() const;
|
bool HaveHDSeed() const;
|
||||||
bool GetHDSeed(HDSeed& seedOut) const;
|
bool GetHDSeed(HDSeed& seedOut) const;
|
||||||
|
//! Mnemonic entropy: optional, present only on phrase-recoverable wallets.
|
||||||
|
//! Unlike the three seed accessors above -- which override pure virtuals on
|
||||||
|
//! CKeyStore (keystore.h:48-51) and therefore dispatch dynamically -- these
|
||||||
|
//! are plain non-virtual members: CKeyStore declares nothing for them and
|
||||||
|
//! nothing reaches the entropy through a base pointer. Every caller holds a
|
||||||
|
//! CWallet*, whose static type resolves to the CCryptoKeyStore overloads.
|
||||||
|
//! Do NOT add them to CKeyStore: that would force all four subclasses
|
||||||
|
//! (CBasicKeyStore, CCryptoKeyStore, CWallet, gtest's TestCCryptoKeyStore)
|
||||||
|
//! to implement them for zero call sites.
|
||||||
|
bool SetMnemonicEntropy(const RawHDSeed& entropy);
|
||||||
|
bool HaveMnemonicEntropy() const;
|
||||||
|
bool GetMnemonicEntropy(RawHDSeed& entropyOut) const;
|
||||||
|
|
||||||
bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey);
|
bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey);
|
||||||
bool HaveKey(const CKeyID &address) const
|
bool HaveKey(const CKeyID &address) const
|
||||||
|
|||||||
@@ -159,6 +159,34 @@ static bool DecryptHDSeed(
|
|||||||
return seed.Fingerprint() == seedFp;
|
return seed.Fingerprint() == seedFp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint256 MnemonicEntropyFingerprint(const RawHDSeed& entropy)
|
||||||
|
{
|
||||||
|
// The local copy is not gratuitous -- see the declaration in crypter.h.
|
||||||
|
// It is secure_allocator-backed, so it is memory_cleanse()d on destruction
|
||||||
|
// (support/allocators/secure.h:45-52).
|
||||||
|
RawHDSeed tmp(entropy);
|
||||||
|
return HDSeed(tmp).Fingerprint();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool DecryptMnemonicEntropy(
|
||||||
|
const CKeyingMaterial& vMasterKey,
|
||||||
|
const std::vector<unsigned char>& vchCryptedSecret,
|
||||||
|
const uint256& entropyFp,
|
||||||
|
RawHDSeed& entropyOut)
|
||||||
|
{
|
||||||
|
CKeyingMaterial vchSecret;
|
||||||
|
|
||||||
|
// Use the entropy's fingerprint as IV, mirroring DecryptHDSeed above.
|
||||||
|
if (!DecryptSecret(vMasterKey, vchCryptedSecret, entropyFp, vchSecret))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// RawHDSeed and CKeyingMaterial are the SAME type (both are
|
||||||
|
// std::vector<unsigned char, secure_allocator<unsigned char>>), so this is a
|
||||||
|
// plain copy of the same bytes, not a reinterpretation.
|
||||||
|
entropyOut = vchSecret;
|
||||||
|
return MnemonicEntropyFingerprint(entropyOut) == entropyFp;
|
||||||
|
}
|
||||||
|
|
||||||
static bool DecryptKey(const CKeyingMaterial& vMasterKey, const std::vector<unsigned char>& vchCryptedSecret, const CPubKey& vchPubKey, CKey& key)
|
static bool DecryptKey(const CKeyingMaterial& vMasterKey, const std::vector<unsigned char>& vchCryptedSecret, const CPubKey& vchPubKey, CKey& key)
|
||||||
{
|
{
|
||||||
CKeyingMaterial vchSecret;
|
CKeyingMaterial vchSecret;
|
||||||
@@ -233,6 +261,19 @@ bool CCryptoKeyStore::Unlock(const CKeyingMaterial& vMasterKeyIn)
|
|||||||
keyPass = true;
|
keyPass = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Deliberately NO arm here for cryptedMnemonicEntropy. This function is
|
||||||
|
// the "some keys decrypt but not all" corruption detector and a keyFail
|
||||||
|
// ends at the assert(false) below. The mnemonic entropy is an optional,
|
||||||
|
// non-spending, display-only record: legacy wallets, hex-restored
|
||||||
|
// wallets and every wallet predating this feature legitimately have a
|
||||||
|
// seed and no entropy, and a wallet whose every key decrypts while its
|
||||||
|
// entropy does not is not corrupt in any sense that should abort the
|
||||||
|
// process -- it simply cannot print its seed phrase. It is decrypted
|
||||||
|
// lazily in GetMnemonicEntropy() instead, so that case becomes a false
|
||||||
|
// return from one RPC while derivation and spending (which read the
|
||||||
|
// seed, not the entropy) carry on. Note the arm above caches nothing
|
||||||
|
// either -- `seed` is discarded; it only votes keyPass/keyFail -- so
|
||||||
|
// nothing is lost by omitting one here.
|
||||||
CryptedKeyMap::const_iterator mi = mapCryptedKeys.begin();
|
CryptedKeyMap::const_iterator mi = mapCryptedKeys.begin();
|
||||||
for (; mi != mapCryptedKeys.end(); ++mi)
|
for (; mi != mapCryptedKeys.end(); ++mi)
|
||||||
{
|
{
|
||||||
@@ -344,6 +385,82 @@ bool CCryptoKeyStore::GetHDSeed(HDSeed& seedOut) const
|
|||||||
return DecryptHDSeed(vMasterKey, cryptedHDSeed.second, cryptedHDSeed.first, seedOut);
|
return DecryptHDSeed(vMasterKey, cryptedHDSeed.second, cryptedHDSeed.first, seedOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CCryptoKeyStore::SetMnemonicEntropy(const RawHDSeed& entropy)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
LOCK(cs_SpendingKeyStore);
|
||||||
|
if (!IsCrypted()) {
|
||||||
|
return CBasicKeyStore::SetMnemonicEntropy(entropy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsLocked())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (entropy.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::vector<unsigned char> vchCryptedSecret;
|
||||||
|
// Use the entropy's fingerprint as IV
|
||||||
|
// TODO: Handle this properly when we make encryption a supported feature
|
||||||
|
auto entropyFp = MnemonicEntropyFingerprint(entropy);
|
||||||
|
// RawHDSeed IS CKeyingMaterial, so `entropy` binds directly here.
|
||||||
|
if (!EncryptSecret(vMasterKey, entropy, entropyFp, vchCryptedSecret))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Virtual: this calls into CWallet to store the crypted entropy to disk.
|
||||||
|
if (!SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CCryptoKeyStore::SetCryptedMnemonicEntropy(
|
||||||
|
const uint256& entropyFp,
|
||||||
|
const std::vector<unsigned char>& vchCryptedSecret)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
LOCK(cs_SpendingKeyStore);
|
||||||
|
if (!IsCrypted()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cryptedMnemonicEntropy.first.IsNull()) {
|
||||||
|
// Don't allow existing entropy to be changed, mirroring
|
||||||
|
// SetCryptedHDSeed: a phrase that no longer matches the installed
|
||||||
|
// seed is worse than no phrase at all.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptedMnemonicEntropy = std::make_pair(entropyFp, vchCryptedSecret);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CCryptoKeyStore::HaveMnemonicEntropy() const
|
||||||
|
{
|
||||||
|
LOCK(cs_SpendingKeyStore);
|
||||||
|
if (!IsCrypted())
|
||||||
|
return CBasicKeyStore::HaveMnemonicEntropy();
|
||||||
|
|
||||||
|
return !cryptedMnemonicEntropy.second.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CCryptoKeyStore::GetMnemonicEntropy(RawHDSeed& entropyOut) const
|
||||||
|
{
|
||||||
|
LOCK(cs_SpendingKeyStore);
|
||||||
|
if (!IsCrypted())
|
||||||
|
return CBasicKeyStore::GetMnemonicEntropy(entropyOut);
|
||||||
|
|
||||||
|
if (cryptedMnemonicEntropy.second.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Decrypted lazily, on demand, and deliberately NOT in Unlock(): see the
|
||||||
|
// comment there for why the entropy must not vote in the keyPass/keyFail
|
||||||
|
// corruption detector.
|
||||||
|
return DecryptMnemonicEntropy(vMasterKey, cryptedMnemonicEntropy.second,
|
||||||
|
cryptedMnemonicEntropy.first, entropyOut);
|
||||||
|
}
|
||||||
|
|
||||||
bool CCryptoKeyStore::AddKeyPubKey(const CKey& key, const CPubKey &pubkey)
|
bool CCryptoKeyStore::AddKeyPubKey(const CKey& key, const CPubKey &pubkey)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
@@ -505,6 +622,30 @@ bool CCryptoKeyStore::EncryptKeys(CKeyingMaterial& vMasterKeyIn)
|
|||||||
}
|
}
|
||||||
hdSeed = HDSeed();
|
hdSeed = HDSeed();
|
||||||
}
|
}
|
||||||
|
if (!mnemonicEntropy.empty()) {
|
||||||
|
{
|
||||||
|
std::vector<unsigned char> vchCryptedSecret;
|
||||||
|
// Use the entropy's fingerprint as IV
|
||||||
|
// TODO: Handle this properly when we make encryption a supported feature
|
||||||
|
auto entropyFp = MnemonicEntropyFingerprint(mnemonicEntropy);
|
||||||
|
if (!EncryptSecret(vMasterKeyIn, mnemonicEntropy, entropyFp, vchCryptedSecret)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Virtual: calls into CWallet to store the crypted entropy to disk.
|
||||||
|
if (!SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Drop the plaintext. swap() rather than `= RawHDSeed()`: assigning a
|
||||||
|
// shorter vector destroys the elements but KEEPS the capacity, so the
|
||||||
|
// old bytes would linger in the locked buffer. swap() hands the buffer
|
||||||
|
// to a temporary whose destructor deallocates it, and
|
||||||
|
// secure_allocator::deallocate memory_cleanse()s
|
||||||
|
// (support/allocators/secure.h:45-52). The `hdSeed = HDSeed();` above
|
||||||
|
// has the same weakness but cannot be fixed here: HDSeed's raw vector
|
||||||
|
// is private with no swap accessor (zip32.h:23-33).
|
||||||
|
RawHDSeed().swap(mnemonicEntropy);
|
||||||
|
}
|
||||||
BOOST_FOREACH(KeyMap::value_type& mKey, mapKeys)
|
BOOST_FOREACH(KeyMap::value_type& mKey, mapKeys)
|
||||||
{
|
{
|
||||||
const CKey &key = mKey.second;
|
const CKey &key = mKey.second;
|
||||||
|
|||||||
@@ -138,6 +138,21 @@ public:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Keystore which keeps the private keys encrypted.
|
||||||
|
* It derives from the basic key store, which is used if no encryption is active.
|
||||||
|
*/
|
||||||
|
//! Fingerprint of a BIP39 entropy blob, computed exactly as HDSeed::Fingerprint
|
||||||
|
//! does (BLAKE2b, ZCASH_HD_SEED_FP_PERSONAL). It is an IV / integrity tag and a
|
||||||
|
//! wallet.dat record key -- never a key-derivation input. Declared here rather
|
||||||
|
//! than duplicated because three call sites must produce identical bytes:
|
||||||
|
//! CCryptoKeyStore::SetMnemonicEntropy, CCryptoKeyStore::EncryptKeys, and
|
||||||
|
//! CWallet::SetMnemonicEntropy (which keys the plaintext record with it).
|
||||||
|
//!
|
||||||
|
//! It takes a copy internally on purpose: HDSeed's constructor takes a NON-const
|
||||||
|
//! RawHDSeed& (zip32.h:28), so HDSeed(entropy).Fingerprint() does not compile
|
||||||
|
//! against a const reference or a member read from a const method.
|
||||||
|
uint256 MnemonicEntropyFingerprint(const RawHDSeed& entropy);
|
||||||
|
|
||||||
/** Keystore which keeps the private keys encrypted.
|
/** Keystore which keeps the private keys encrypted.
|
||||||
* It derives from the basic key store, which is used if no encryption is active.
|
* It derives from the basic key store, which is used if no encryption is active.
|
||||||
*/
|
*/
|
||||||
@@ -145,6 +160,10 @@ class CCryptoKeyStore : public CBasicKeyStore
|
|||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
std::pair<uint256, std::vector<unsigned char>> cryptedHDSeed;
|
std::pair<uint256, std::vector<unsigned char>> cryptedHDSeed;
|
||||||
|
// Encrypted mnemonic entropy, shaped exactly like cryptedHDSeed above:
|
||||||
|
// .first is the entropy's fingerprint (AES IV + integrity tag on decrypt),
|
||||||
|
// .second is the ciphertext.
|
||||||
|
std::pair<uint256, std::vector<unsigned char>> cryptedMnemonicEntropy;
|
||||||
CryptedKeyMap mapCryptedKeys;
|
CryptedKeyMap mapCryptedKeys;
|
||||||
//CryptedSproutSpendingKeyMap mapCryptedSproutSpendingKeys;
|
//CryptedSproutSpendingKeyMap mapCryptedSproutSpendingKeys;
|
||||||
CryptedSaplingSpendingKeyMap mapCryptedSaplingSpendingKeys;
|
CryptedSaplingSpendingKeyMap mapCryptedSaplingSpendingKeys;
|
||||||
@@ -194,6 +213,14 @@ public:
|
|||||||
bool SetHDSeed(const HDSeed& seed);
|
bool SetHDSeed(const HDSeed& seed);
|
||||||
bool HaveHDSeed() const;
|
bool HaveHDSeed() const;
|
||||||
bool GetHDSeed(HDSeed& seedOut) const;
|
bool GetHDSeed(HDSeed& seedOut) const;
|
||||||
|
//! Mnemonic entropy, mirroring the four HD-seed members above.
|
||||||
|
//! SetCryptedMnemonicEntropy MUST stay virtual for the same reason
|
||||||
|
//! SetCryptedHDSeed is: CWallet overrides it to persist the record, and
|
||||||
|
//! SetMnemonicEntropy() below reaches that override through the vtable.
|
||||||
|
virtual bool SetCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char> &vchCryptedSecret);
|
||||||
|
bool SetMnemonicEntropy(const RawHDSeed& entropy);
|
||||||
|
bool HaveMnemonicEntropy() const;
|
||||||
|
bool GetMnemonicEntropy(RawHDSeed& entropyOut) const;
|
||||||
|
|
||||||
virtual bool AddCryptedKey(const CPubKey &vchPubKey, const std::vector<unsigned char> &vchCryptedSecret);
|
virtual bool AddCryptedKey(const CPubKey &vchPubKey, const std::vector<unsigned char> &vchCryptedSecret);
|
||||||
bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey);
|
bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey);
|
||||||
|
|||||||
Reference in New Issue
Block a user