Skip to content

Commit 35bf426

Browse files
committed
Merge bitcoin#28724: wallet: Cleanup accidental encryption keys in watchonly wallets
69e95c2 tests: Test cleanup of mkeys from wallets without privkeys (Andrew Chow) 2b9279b wallet: Remove unused encryption keys from watchonly wallets (Andrew Chow) 813a16a wallet: Add HasCryptedKeys (Andrew Chow) Pull request description: An earlier version allowed users to create watchonly wallets (wallets without private keys) that were "encrypted". Such wallets would have a stored encryption keys, but nothing would actually be encrypted with them. This can cause unexpected behavior such as bitcoin-core/gui#772. We can detect such wallets as they will have the disable private keys flag set, no encrypted keys, and encryption keys. For such wallets, we can remove those encryption keys thereby avoiding any issues that may result from this unexpected situation. ACKs for top commit: sipa: utACK 69e95c2. laanwj: Code review re-ACK 69e95c2 furszy: Code review ACK 69e95c2 Tree-SHA512: 901932cd709c57e66c598f011f0105a243b5a8b539db2ef3fcf370dca4cf35ae09bc1110e8fca8353be470f159468855a4dd96b99bc9c1112adc86ccc50e1b9d
2 parents 2168406 + 69e95c2 commit 35bf426

File tree

7 files changed

+106
-0
lines changed

7 files changed

+106
-0
lines changed

src/wallet/scriptpubkeyman.cpp

+12
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,12 @@ bool LegacyScriptPubKeyMan::HavePrivateKeys() const
525525
return !mapKeys.empty() || !mapCryptedKeys.empty();
526526
}
527527

528+
bool LegacyScriptPubKeyMan::HaveCryptedKeys() const
529+
{
530+
LOCK(cs_KeyStore);
531+
return !mapCryptedKeys.empty();
532+
}
533+
528534
void LegacyScriptPubKeyMan::RewriteDB()
529535
{
530536
LOCK(cs_KeyStore);
@@ -2411,6 +2417,12 @@ bool DescriptorScriptPubKeyMan::HavePrivateKeys() const
24112417
return m_map_keys.size() > 0 || m_map_crypted_keys.size() > 0;
24122418
}
24132419

2420+
bool DescriptorScriptPubKeyMan::HaveCryptedKeys() const
2421+
{
2422+
LOCK(cs_desc_man);
2423+
return !m_map_crypted_keys.empty();
2424+
}
2425+
24142426
std::optional<int64_t> DescriptorScriptPubKeyMan::GetOldestKeyPoolTime() const
24152427
{
24162428
// This is only used for getwalletinfo output and isn't relevant to descriptor wallets.

src/wallet/scriptpubkeyman.h

+3
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ class ScriptPubKeyMan
221221
virtual bool Upgrade(int prev_version, int new_version, bilingual_str& error) { return true; }
222222

223223
virtual bool HavePrivateKeys() const { return false; }
224+
virtual bool HaveCryptedKeys() const { return false; }
224225

225226
//! The action to do when the DB needs rewrite
226227
virtual void RewriteDB() {}
@@ -473,6 +474,7 @@ class LegacyScriptPubKeyMan : public LegacyDataSPKM
473474
bool Upgrade(int prev_version, int new_version, bilingual_str& error) override;
474475

475476
bool HavePrivateKeys() const override;
477+
bool HaveCryptedKeys() const override;
476478

477479
void RewriteDB() override;
478480

@@ -661,6 +663,7 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
661663
bool HasPrivKey(const CKeyID& keyid) const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
662664
//! Retrieve the particular key if it is available. Returns nullopt if the key is not in the wallet, or if the wallet is locked.
663665
std::optional<CKey> GetKey(const CKeyID& keyid) const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
666+
bool HaveCryptedKeys() const override;
664667

665668
std::optional<int64_t> GetOldestKeyPoolTime() const override;
666669
unsigned int GetKeyPoolSize() const override;

src/wallet/wallet.cpp

+8
Original file line numberDiff line numberDiff line change
@@ -3712,6 +3712,14 @@ bool CWallet::HasEncryptionKeys() const
37123712
return !mapMasterKeys.empty();
37133713
}
37143714

3715+
bool CWallet::HaveCryptedKeys() const
3716+
{
3717+
for (const auto& spkm : GetAllScriptPubKeyMans()) {
3718+
if (spkm->HaveCryptedKeys()) return true;
3719+
}
3720+
return false;
3721+
}
3722+
37153723
void CWallet::ConnectScriptPubKeyManNotifiers()
37163724
{
37173725
for (const auto& spk_man : GetActiveScriptPubKeyMans()) {

src/wallet/wallet.h

+1
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
973973
bool WithEncryptionKey(std::function<bool (const CKeyingMaterial&)> cb) const override;
974974

975975
bool HasEncryptionKeys() const override;
976+
bool HaveCryptedKeys() const;
976977

977978
/** Get last block processed height */
978979
int GetLastBlockHeight() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet)

src/wallet/walletdb.cpp

+20
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,11 @@ bool WalletBatch::WriteMasterKey(unsigned int nID, const CMasterKey& kMasterKey)
154154
return WriteIC(std::make_pair(DBKeys::MASTER_KEY, nID), kMasterKey, true);
155155
}
156156

157+
bool WalletBatch::EraseMasterKey(unsigned int id)
158+
{
159+
return EraseIC(std::make_pair(DBKeys::MASTER_KEY, id));
160+
}
161+
157162
bool WalletBatch::WriteCScript(const uint160& hash, const CScript& redeemScript)
158163
{
159164
return WriteIC(std::make_pair(DBKeys::CSCRIPT, hash), redeemScript, false);
@@ -1241,6 +1246,21 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
12411246
result = DBErrors::CORRUPT;
12421247
}
12431248

1249+
// Since it was accidentally possible to "encrypt" a wallet with private keys disabled, we should check if this is
1250+
// such a wallet and remove the encryption key records to avoid any future issues.
1251+
// Although wallets without private keys should not have *ckey records, we should double check that.
1252+
// Removing the mkey records is only safe if there are no *ckey records.
1253+
if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && pwallet->HasEncryptionKeys() && !pwallet->HaveCryptedKeys()) {
1254+
pwallet->WalletLogPrintf("Detected extraneous encryption keys in this wallet without private keys. Removing extraneous encryption keys.\n");
1255+
for (const auto& [id, _] : pwallet->mapMasterKeys) {
1256+
if (!EraseMasterKey(id)) {
1257+
pwallet->WalletLogPrintf("Error: Unable to remove extraneous encryption key '%u'. Wallet corrupt.\n", id);
1258+
return DBErrors::CORRUPT;
1259+
}
1260+
}
1261+
pwallet->mapMasterKeys.clear();
1262+
}
1263+
12441264
return result;
12451265
}
12461266

src/wallet/walletdb.h

+1
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ class WalletBatch
243243
bool WriteKey(const CPubKey& vchPubKey, const CPrivKey& vchPrivKey, const CKeyMetadata &keyMeta);
244244
bool WriteCryptedKey(const CPubKey& vchPubKey, const std::vector<unsigned char>& vchCryptedSecret, const CKeyMetadata &keyMeta);
245245
bool WriteMasterKey(unsigned int nID, const CMasterKey& kMasterKey);
246+
bool EraseMasterKey(unsigned int id);
246247

247248
bool WriteCScript(const uint160& hash, const CScript& redeemScript);
248249

test/functional/wallet_encryption.py

+61
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"""Test Wallet encryption"""
66

77
import time
8+
import subprocess
89

10+
from test_framework.messages import hash256
911
from test_framework.test_framework import BitcoinTestFramework
1012
from test_framework.util import (
1113
assert_raises_rpc_error,
@@ -100,6 +102,65 @@ def run_test(self):
100102
sig = self.nodes[0].signmessage(address, msg)
101103
assert self.nodes[0].verifymessage(address, sig, msg)
102104

105+
self.log.info("Test that wallets without private keys cannot be encrypted")
106+
self.nodes[0].createwallet(wallet_name="noprivs", disable_private_keys=True)
107+
noprivs_wallet = self.nodes[0].get_wallet_rpc("noprivs")
108+
assert_raises_rpc_error(-16, "Error: wallet does not contain private keys, nothing to encrypt.", noprivs_wallet.encryptwallet, "pass")
109+
110+
if self.is_wallet_tool_compiled():
111+
self.log.info("Test that encryption keys in wallets without privkeys are removed")
112+
113+
def do_wallet_tool(*args):
114+
proc = subprocess.Popen(
115+
[self.options.bitcoinwallet, f"-datadir={self.nodes[0].datadir_path}", f"-chain={self.chain}"] + list(args),
116+
stdin=subprocess.PIPE,
117+
stdout=subprocess.PIPE,
118+
stderr=subprocess.PIPE,
119+
text=True
120+
)
121+
stdout, stderr = proc.communicate()
122+
assert_equal(proc.poll(), 0)
123+
assert_equal(stderr, "")
124+
125+
# Since it is no longer possible to encrypt a wallet without privkeys, we need to force one into the wallet
126+
# 1. Make a dump of the wallet
127+
# 2. Add mkey record to the dump
128+
# 3. Create a new wallet from the dump
129+
130+
# Make the dump
131+
noprivs_wallet.unloadwallet()
132+
dumpfile_path = self.nodes[0].datadir_path / "noprivs.dump"
133+
do_wallet_tool("-wallet=noprivs", f"-dumpfile={dumpfile_path}", "dump")
134+
135+
# Modify the dump
136+
with open(dumpfile_path, "r", encoding="utf-8") as f:
137+
dump_content = f.readlines()
138+
# Drop the checksum line
139+
dump_content = dump_content[:-1]
140+
# Insert a valid mkey line. This corresponds to a passphrase of "pass".
141+
dump_content.append("046d6b657901000000,300dc926f3b3887aad3d5d5f5a0fc1b1a4a1722f9284bd5c6ff93b64a83902765953939c58fe144013c8b819f42cf698b208e9911e5f0c544fa300000000cc52050000\n")
142+
with open(dumpfile_path, "w", encoding="utf-8") as f:
143+
contents = "".join(dump_content)
144+
f.write(contents)
145+
checksum = hash256(contents.encode())
146+
f.write(f"checksum,{checksum.hex()}\n")
147+
148+
# Load the dump into a new wallet
149+
do_wallet_tool("-wallet=noprivs_enc", f"-dumpfile={dumpfile_path}", "createfromdump")
150+
# Load the wallet and make sure it is no longer encrypted
151+
with self.nodes[0].assert_debug_log(["Detected extraneous encryption keys in this wallet without private keys. Removing extraneous encryption keys."]):
152+
self.nodes[0].loadwallet("noprivs_enc")
153+
noprivs_wallet = self.nodes[0].get_wallet_rpc("noprivs_enc")
154+
assert_raises_rpc_error(-15, "Error: running with an unencrypted wallet, but walletpassphrase was called.", noprivs_wallet.walletpassphrase, "pass", 1)
155+
noprivs_wallet.unloadwallet()
156+
157+
# Make a new dump and check that there are no mkeys
158+
dumpfile_path = self.nodes[0].datadir_path / "noprivs_enc.dump"
159+
do_wallet_tool("-wallet=noprivs_enc", f"-dumpfile={dumpfile_path}", "dump")
160+
with open(dumpfile_path, "r", encoding="utf-8") as f:
161+
# Check theres nothing with an 'mkey' prefix
162+
assert_equal(all([not line.startswith("046d6b6579") for line in f]), True)
163+
103164

104165
if __name__ == '__main__':
105166
WalletEncryptionTest(__file__).main()

0 commit comments

Comments
 (0)