diff --git a/src/keystore.cpp b/src/keystore.cpp index 0e78cbdc9..b0d30bd56 100644 --- a/src/keystore.cpp +++ b/src/keystore.cpp @@ -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) { LOCK(cs_KeyStore); diff --git a/src/keystore.h b/src/keystore.h index 666d38984..827fc2bcd 100644 --- a/src/keystore.h +++ b/src/keystore.h @@ -117,6 +117,12 @@ class CBasicKeyStore : public CKeyStore { protected: 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; ScriptMap mapScripts; WatchOnlySet setWatchOnly; @@ -129,6 +135,18 @@ public: bool SetHDSeed(const HDSeed& seed); bool HaveHDSeed() 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 HaveKey(const CKeyID &address) const diff --git a/src/wallet/crypter.cpp b/src/wallet/crypter.cpp index f3674c776..fe2e9ee55 100644 --- a/src/wallet/crypter.cpp +++ b/src/wallet/crypter.cpp @@ -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& 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>), 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& 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 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& 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 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; diff --git a/src/wallet/crypter.h b/src/wallet/crypter.h index cb459fe07..d72a8a1b6 100644 --- a/src/wallet/crypter.h +++ b/src/wallet/crypter.h @@ -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. * It derives from the basic key store, which is used if no encryption is active. */ @@ -145,6 +160,10 @@ class CCryptoKeyStore : public CBasicKeyStore { private: std::pair> 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> cryptedMnemonicEntropy; CryptedKeyMap mapCryptedKeys; //CryptedSproutSpendingKeyMap mapCryptedSproutSpendingKeys; CryptedSaplingSpendingKeyMap mapCryptedSaplingSpendingKeys; @@ -194,6 +213,14 @@ public: bool SetHDSeed(const HDSeed& seed); bool HaveHDSeed() 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 &vchCryptedSecret); + bool SetMnemonicEntropy(const RawHDSeed& entropy); + bool HaveMnemonicEntropy() const; + bool GetMnemonicEntropy(RawHDSeed& entropyOut) const; virtual bool AddCryptedKey(const CPubKey &vchPubKey, const std::vector &vchCryptedSecret); bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey);