Skip to content

Commit c4ac53a

Browse files
Merge #6945: fix: improve wallet encryption robustness
116fca6 refactor: drop redundant `!hdchain.IsNull()` checks (UdjinM6) 24670c7 fix: return encryption failure when database rewrite fails (UdjinM6) 98d7a6b fix: improve wallet encryption robustness (UdjinM6) Pull request description: ## Issue being fixed or feature implemented Add defensive checks to prevent double-encryption of HD chains and improve wallet encryption robustness: - Check IsCrypted() status to prevent re-encryption attempts - Add TOCTOU protection by re-checking IsCrypted() under lock - Validate decryption keys before TopUp operations - Only decrypt HD chains when actually encrypted - Remove irrelevant HasEncryptionKeys() checks (keys passed as parameters) - Fail and log an error when db rewrite fails after encryption ## What was done? ## How Has This Been Tested? run tests ## Breaking Changes n/a ## Checklist: - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ ACKs for top commit: PastaPastaPasta: utACK 116fca6 Tree-SHA512: 5659b133ac46d425b1018d8a5e585a1702aeb452f9cb145f3659bdfbf57021b90bc9d52aabcac0dc2406598ee5afa1fee1c2ab29201e49e2c7412321b25aa22c
2 parents 8bff900 + 116fca6 commit c4ac53a

File tree

2 files changed

+45
-25
lines changed

2 files changed

+45
-25
lines changed

src/wallet/scriptpubkeyman.cpp

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,14 @@ bool LegacyScriptPubKeyMan::Encrypt(const CKeyingMaterial& master_key, WalletBat
244244
return false;
245245
}
246246

247-
// must get current HD chain before EncryptKeys
248247
CHDChain hdChainCurrent;
249248
GetHDChain(hdChainCurrent);
250249

250+
if (!hdChainCurrent.IsNull() && hdChainCurrent.IsCrypted()) {
251+
encrypted_batch = nullptr;
252+
return false;
253+
}
254+
251255
KeyMap keys_to_encrypt;
252256
keys_to_encrypt.swap(mapKeys); // Clear mapKeys so AddCryptedKeyInner will succeed.
253257
for (const KeyMap::value_type& mKey : keys_to_encrypt)
@@ -340,10 +344,13 @@ void LegacyScriptPubKeyMan::UpgradeKeyMetadata()
340344
CHDChain hdChainCurrent;
341345
if (!GetHDChain(hdChainCurrent))
342346
throw std::runtime_error(std::string(__func__) + ": GetHDChain failed");
343-
if (!m_storage.WithEncryptionKey([&](const CKeyingMaterial& encryption_key) {
344-
return DecryptHDChain(encryption_key, hdChainCurrent);
345-
})) {
346-
throw std::runtime_error(std::string(__func__) + ": DecryptHDChain failed");
347+
348+
if (hdChainCurrent.IsCrypted()) {
349+
if (!m_storage.WithEncryptionKey([&](const CKeyingMaterial& encryption_key) {
350+
return DecryptHDChain(encryption_key, hdChainCurrent);
351+
})) {
352+
throw std::runtime_error(std::string(__func__) + ": DecryptHDChain failed");
353+
}
347354
}
348355

349356
CExtKey masterKey;
@@ -475,10 +482,12 @@ bool LegacyScriptPubKeyMan::GetDecryptedHDChain(CHDChain& hdChainRet) const
475482
return false;
476483
}
477484

478-
if (!m_storage.WithEncryptionKey([&](const CKeyingMaterial& encryption_key) {
479-
return DecryptHDChain(encryption_key, hdChainTmp);
480-
})) {
481-
return false;
485+
if (hdChainTmp.IsCrypted()) {
486+
if (!m_storage.WithEncryptionKey([&](const CKeyingMaterial& encryption_key) {
487+
return DecryptHDChain(encryption_key, hdChainTmp);
488+
})) {
489+
return false;
490+
}
482491
}
483492

484493
// make sure seed matches this chain
@@ -493,12 +502,9 @@ bool LegacyScriptPubKeyMan::GetDecryptedHDChain(CHDChain& hdChainRet) const
493502
bool LegacyScriptPubKeyMan::EncryptHDChain(const CKeyingMaterial& vMasterKeyIn, CHDChain& chain)
494503
{
495504
LOCK(cs_KeyStore);
496-
// should call EncryptKeys first
497-
if (!m_storage.HasEncryptionKeys())
498-
return false;
499505

500506
if (chain.IsCrypted())
501-
return true;
507+
return false;
502508

503509
// make sure seed matches this chain
504510
if (chain.GetID() != chain.GetSeedHash())
@@ -541,8 +547,6 @@ bool LegacyScriptPubKeyMan::EncryptHDChain(const CKeyingMaterial& vMasterKeyIn,
541547
bool LegacyScriptPubKeyMan::DecryptHDChain(const CKeyingMaterial& vMasterKeyIn, CHDChain& hdChainRet) const
542548
{
543549
LOCK(cs_KeyStore);
544-
if (!m_storage.HasEncryptionKeys())
545-
return true;
546550

547551
if (m_hd_chain.IsNull()) {
548552
WalletLogPrintf("%s: ERROR: no HD chain\n", __func__);
@@ -1188,10 +1192,12 @@ bool LegacyScriptPubKeyMan::GetKey(const CKeyID &address, CKey& keyOut) const
11881192
CHDChain hdChainCurrent;
11891193
if (!GetHDChain(hdChainCurrent))
11901194
throw std::runtime_error(std::string(__func__) + ": GetHDChain failed");
1191-
if (!m_storage.WithEncryptionKey([&](const CKeyingMaterial& encryption_key) {
1192-
return DecryptHDChain(encryption_key, hdChainCurrent);
1193-
})) {
1194-
throw std::runtime_error(std::string(__func__) + ": DecryptHDChain failed");
1195+
if (hdChainCurrent.IsCrypted()) {
1196+
if (!m_storage.WithEncryptionKey([&](const CKeyingMaterial& encryption_key) {
1197+
return DecryptHDChain(encryption_key, hdChainCurrent);
1198+
})) {
1199+
throw std::runtime_error(std::string(__func__) + ": DecryptHDChain failed");
1200+
}
11951201
}
11961202
// make sure seed matches this chain
11971203
if (hdChainCurrent.GetID() != hdChainCurrent.GetSeedHash())
@@ -1303,10 +1309,12 @@ void LegacyScriptPubKeyMan::DeriveNewChildKey(WalletBatch &batch, CKeyMetadata&
13031309
throw std::runtime_error(std::string(__func__) + ": GetHDChain failed");
13041310
}
13051311

1306-
if (!m_storage.WithEncryptionKey([&](const CKeyingMaterial& encryption_key) {
1307-
return DecryptHDChain(encryption_key, hdChainTmp);
1308-
})) {
1309-
throw std::runtime_error(std::string(__func__) + ": DecryptHDChain failed");
1312+
if (hdChainTmp.IsCrypted()) {
1313+
if (!m_storage.WithEncryptionKey([&](const CKeyingMaterial& encryption_key) {
1314+
return DecryptHDChain(encryption_key, hdChainTmp);
1315+
})) {
1316+
throw std::runtime_error(std::string(__func__) + ": DecryptHDChain failed");
1317+
}
13101318
}
13111319
// make sure seed matches this chain
13121320
if (hdChainTmp.GetID() != hdChainTmp.GetSeedHash())

src/wallet/wallet.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,12 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
737737

738738
{
739739
LOCK(cs_wallet);
740+
741+
if (IsCrypted()) {
742+
// verify again now that cs_wallet lock is held
743+
return false;
744+
}
745+
740746
mapMasterKeys[++nMasterKeyMaxID] = kMasterKey;
741747
WalletBatch* encrypted_batch = new WalletBatch(GetDatabase());
742748
if (!encrypted_batch->TxnBegin()) {
@@ -783,6 +789,10 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
783789
} else if (auto spk_man = GetLegacyScriptPubKeyMan()) {
784790
// if we are not using HD, generate new keypool
785791
if (spk_man->IsHDEnabled()) {
792+
// NOTE: using internal master key which should be populated on Unlock()
793+
if (!spk_man->CheckDecryptionKey(vMasterKey)) {
794+
return false;
795+
}
786796
if (!spk_man->TopUp()) {
787797
return false;
788798
}
@@ -796,8 +806,10 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
796806

797807
// Need to completely rewrite the wallet file; if we don't, bdb might keep
798808
// bits of the unencrypted private key in slack space in the database file.
799-
GetDatabase().Rewrite();
800-
809+
if (!GetDatabase().Rewrite()) {
810+
WalletLogPrintf("ERROR: Rewrite failed - wallet is in dangerous state!\n");
811+
return false;
812+
}
801813
// BDB seems to have a bad habit of writing old data into
802814
// slack space in .dat files; that is bad if the old data is
803815
// unencrypted private keys. So:

0 commit comments

Comments
 (0)