diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 5c63a319a..b73e5c3a9 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2587,6 +2587,72 @@ bool CWallet::LoadCryptedHDSeed(const uint256& seedFp, const std::vector& vchCryptedSecret) +{ + if (!CCryptoKeyStore::SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret)) { + return false; + } + + if (!fFileBacked) { + return true; + } + + { + LOCK(cs_wallet); + // Same write-then-erase discipline as SetCryptedHDSeed: CDB::Rewrite at + // the end of EncryptWallet copies every surviving record, so a leftover + // plaintext "mnementropy" would keep the seed phrase recoverable from an + // encrypted wallet.dat. The erase is best-effort for the same reason: a + // hard failure would propagate into EncryptKeys -> assert(false). + if (pwalletdbEncryption) { + if (!pwalletdbEncryption->WriteCryptedMnemonicEntropy(entropyFp, vchCryptedSecret)) + return false; + if (!pwalletdbEncryption->EraseMnemonicEntropy(entropyFp)) + LogPrintf("%s: WARNING: could not erase the plaintext mnementropy record\n", __func__); + return true; + } else { + CWalletDB walletdb(strWalletFile); + if (!walletdb.WriteCryptedMnemonicEntropy(entropyFp, vchCryptedSecret)) + return false; + if (!walletdb.EraseMnemonicEntropy(entropyFp)) + LogPrintf("%s: WARNING: could not erase the plaintext mnementropy record\n", __func__); + return true; + } + } + return false; +} + +bool CWallet::LoadMnemonicEntropy(const RawHDSeed& entropy) +{ + return CBasicKeyStore::SetMnemonicEntropy(entropy); +} + +bool CWallet::LoadCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector& vchCryptedSecret) +{ + return CCryptoKeyStore::SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret); +} bool CWallet::InstallHDSeed(const HDSeed& seed, bool fMnemonic, int64_t nCreateTime) { @@ -2675,6 +2741,36 @@ bool CWallet::GetHDSeedForDerivation(HDSeed& seedOut) const bool CWallet::GetMnemonicPhrase(std::string& phraseOut) const { + // Preferred form: the HD seed is the EXPANDED 64-byte BIP39 seed and the + // entropy sits in its own record. + RawHDSeed entropy; + if (GetMnemonicEntropy(entropy)) { // false on an encrypted+locked wallet + // NEVER hand out a phrase that does not restore THIS wallet. Prove the + // entropy expands to the exact bytes derivation consumes; if it does + // not (a torn install, a wallet.dat edited by hand, an entropy record + // paired with a different seed), refuse rather than print a phrase that + // silently restores someone else's key tree. Costs one PBKDF2 on a + // user-initiated RPC. + RawHDSeed seed64; + if (!Bip39SeedFromEntropy(entropy, seed64)) + return false; + + HDSeed derivationSeed; + if (!GetHDSeedForDerivation(derivationSeed)) + return false; + + if (derivationSeed.RawSeed() != seed64) { + LogPrintf("%s: refusing to export a seed phrase: the stored mnemonic entropy does not " + "expand to this wallet's HD seed\n", __func__); + return false; + } + + return EntropyToMnemonic(entropy, phraseOut); + } + + // Legacy form (earlier builds of this branch): the stored HD seed IS the + // 32-byte BIP39 entropy, expanded on every derivation. Consistent by + // construction, so no cross-check is possible or needed. if (!hdChain.fMnemonicSeed) return false; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 697823878..1b77ca291 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1345,6 +1345,14 @@ public: bool SetHDSeed(const HDSeed& seed); bool SetCryptedHDSeed(const uint256& seedFp, const std::vector &vchCryptedSecret); + /* Record this wallet's BIP39 entropy so its seed phrase can be reprinted. + Display-only: derivation never reads it (the HD seed holds the bytes that + are actually derived from). Refuses to replace an existing record. + SetCryptedMnemonicEntropy overrides the CCryptoKeyStore virtual so the + record reaches disk; SetMnemonicEntropy merely hides the base version, + which is safe because no call site holds a base pointer. */ + bool SetMnemonicEntropy(const RawHDSeed& entropy); + bool SetCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector &vchCryptedSecret); /* Restore a wallet's HD seed from a hex string (as exported in the z_exportwallet "# HDSeed=" comment): 32 bytes for a legacy raw seed, or @@ -1363,8 +1371,14 @@ public: wallet and the seed is available (unlocked). Returns false otherwise. */ bool GetMnemonicPhrase(std::string& phraseOut) const; - /* True if the HD seed was derived from a BIP39 mnemonic (stored as entropy). */ - bool IsMnemonicSeed() const { return hdChain.fMnemonicSeed; } + /* True if this wallet has a BIP39 seed phrase available. Two storage forms + qualify: + - current: the HD seed is the EXPANDED 64-byte BIP39 seed and the + entropy lives in its own record (HaveMnemonicEntropy()); + - legacy: hdChain.fMnemonicSeed, where the stored HD seed IS the + 32-byte entropy and is expanded on every derivation. + Gates z_exportmnemonic (rpcdump.cpp). */ + bool IsMnemonicSeed() const { return hdChain.fMnemonicSeed || HaveMnemonicEntropy(); } /* Return the seed to feed into HD derivation. For mnemonic wallets this expands the stored 32-byte entropy into the 64-byte BIP39 seed; for legacy @@ -1396,6 +1410,11 @@ public: /* Set the current encrypted HD seed, without saving it to disk (used by LoadWallet) */ bool LoadCryptedHDSeed(const uint256& seedFp, const std::vector& seed); + /* Set the mnemonic entropy, without saving it to disk (used by LoadWallet) */ + bool LoadMnemonicEntropy(const RawHDSeed& entropy); + + /* Set the encrypted mnemonic entropy, without saving it to disk (used by LoadWallet) */ + bool LoadCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector& vchCryptedSecret); /* Find notes filtered by payment address, min depth, ability to spend */ void GetFilteredNotes(std::vector& saplingEntries, diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index eb144bd25..abac194ba 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -860,6 +860,38 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, ssValue >> nOrigin; pwallet->hdSeedOrigin = (int)nOrigin; } + else if (strType == "mnementropy") + { + uint256 entropyFp; + RawHDSeed entropy; + ssKey >> entropyFp; + ssValue >> entropy; + + if (MnemonicEntropyFingerprint(entropy) != entropyFp) + { + strErr = "Error reading wallet database: mnemonic entropy corrupt"; + return false; + } + + if (!pwallet->LoadMnemonicEntropy(entropy)) + { + strErr = "Error reading wallet database: LoadMnemonicEntropy failed"; + return false; + } + } + else if (strType == "cmnementropy") + { + uint256 entropyFp; + vector vchCryptedSecret; + ssKey >> entropyFp; + ssValue >> vchCryptedSecret; + if (!pwallet->LoadCryptedMnemonicEntropy(entropyFp, vchCryptedSecret)) + { + strErr = "Error reading wallet database: LoadCryptedMnemonicEntropy failed"; + return false; + } + wss.fIsEncrypted = true; + } } catch (...) { return false;