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:
@@ -159,6 +159,34 @@ static bool DecryptHDSeed(
|
||||
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)
|
||||
{
|
||||
CKeyingMaterial vchSecret;
|
||||
@@ -233,6 +261,19 @@ bool CCryptoKeyStore::Unlock(const CKeyingMaterial& vMasterKeyIn)
|
||||
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();
|
||||
for (; mi != mapCryptedKeys.end(); ++mi)
|
||||
{
|
||||
@@ -344,6 +385,82 @@ bool CCryptoKeyStore::GetHDSeed(HDSeed& seedOut) const
|
||||
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)
|
||||
{
|
||||
{
|
||||
@@ -505,6 +622,30 @@ bool CCryptoKeyStore::EncryptKeys(CKeyingMaterial& vMasterKeyIn)
|
||||
}
|
||||
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)
|
||||
{
|
||||
const CKey &key = mKey.second;
|
||||
|
||||
Reference in New Issue
Block a user