upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/main.cpp
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-08-04 10:01:36 -0300
committerGitHub <noreply@github.com>2026-08-04 10:01:36 -0300
commit1db5f372c36eb701602a70a358de2344726ed734 (patch)
tree9d028ed3097ac51368ca312ff439664c76f439b7 /src/main.cpp
parent9176d5b3f32b907e4bafde5152828b2ae04ae8a1 (diff)
wallet encryption: the stack from #130–#133, with the two blockers fixed (#134)
* wallet: refuse future encrypted wallet formats * wallet: add encrypted key unlock core * wallet: add encryptwallet rewrite command * wallet: read passphrases from stdin or files * wallet: keep the derivation across encryption, and take the logs with it Two things stopped the encryption stack from being safe to merge. Both were found against a wallet holding real coin, not in the self-tests. 1. Encrypting turned a BIP44 wallet into a legacy one. LoadWallet() ends by resetting the schema and the derivation counters when no seed is in memory. That was written when "no seed after loading" could only mean the wallet had none. An encrypted wallet has a seed -- it just has not been decrypted yet -- so the reset threw away metadata that had been read correctly a moment earlier. Unlocking later restores the seed and nothing restores the rest. Measured on the wallet from the #124 proof: after encrypting, index 1 moved off BAtgSgra2X8Dqo5PJzPCLJhPwrrC2thYVJ, the address actually holding 1.00 BTF, and the audit went from "1.00 covered by the phrase" to "0.00 covered, 1.00 wallet.dat-only". Encrypt your wallet and the program told you your recovery phrase no longer covered your money. Now the condition also accepts a wallet whose seed is present but encrypted. Same wallet, before and after: schema bip44, same four derived addresses, 1.00 BTF still reported as covered. 2. The plaintext keys stayed behind in the Berkeley DB logs. wallet.dat comes out clean -- verified by scanning the raw file for the 118 bytes of a known private key. database/log.0000000002 did not: it held the whole key, and still did after a full node restart, because every record this wallet ever wrote passed through the log. That defeats the feature for the case it exists for, since our own backup advice is to copy the whole directory rather than wallet.dat alone. log_archive() cannot fix it: it only drops logs Berkeley DB considers obsolete and never the one it is writing to, and with one 10 MB log per directory that is precisely where the keys are. They also did not get there at encryption time -- they accumulated over the wallet's whole life -- so nothing done during the rewrite can archive them out. So the encrypt command now closes the environment and deletes the logs on its way out, after the flush that makes every .dat self-contained. This is not secure erasure; the blocks are unlinked, not overwritten. It closes the case of someone reading a copied data directory. Getting there uncovered a third bug, older than both. DBFlush(true) is not safe to call twice. The first call closes the environment, and txn_checkpoint() on a closed environment segfaults instead of returning an error, so the try/catch around it never sees anything. EncryptWallet() already flushes before renaming the wallet, and the command flushes again on the way out -- that pair crashed every single time. Guarded now. The same function has been calling log_archive() with the address of an uninitialised pointer since 0.1.0, on every clean shutdown, in a path whose catch(...) cannot catch a structured fault on Windows. DB_ARCH_REMOVE requires that argument to be NULL. Fixed while here, because the encryption cleanup depends on that call not being a coin flip. --------- Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
Diffstat (limited to 'src/main.cpp')
-rw-r--r--src/main.cpp334
1 files changed, 332 insertions, 2 deletions
diff --git a/src/main.cpp b/src/main.cpp
index efece6c..0342650 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -72,7 +72,15 @@ vector<pair<uint256, bool> > vWalletUpdated;
72CCriticalSection cs_mapWallet; 72CCriticalSection cs_mapWallet;
73 73
74map<vector<unsigned char>, CPrivKey> mapKeys; 74map<vector<unsigned char>, CPrivKey> mapKeys;
75map<vector<unsigned char>, vector<unsigned char> > mapCryptedKeys;
75map<uint160, vector<unsigned char> > mapPubKeys; 76map<uint160, vector<unsigned char> > mapPubKeys;
77map<unsigned int, CWalletMasterKey> mapMasterKeys;
78unsigned int nWalletMasterKeyMaxID = 0;
79CKeyingMaterial vWalletMasterKey;
80vector<unsigned char> vchCryptedHDMaster;
81vector<unsigned char> vchCryptedHDChainCode;
82bool fWalletEncrypted = false;
83bool fWalletLocked = true;
76CCriticalSection cs_mapKeys; 84CCriticalSection cs_mapKeys;
77CKey keyUser; 85CKey keyUser;
78 86
@@ -159,6 +167,21 @@ int MinerThreadCount()
159 167
160bool AddKey(const CKey& key) 168bool AddKey(const CKey& key)
161{ 169{
170 if (IsWalletEncrypted())
171 {
172 string strError;
173 CPrivKey vchPrivKey = key.GetPrivKey();
174 vector<unsigned char> vchCryptedSecret;
175 if (IsWalletLocked())
176 return error("AddKey() : wallet is locked\n");
177 if (!EncryptSecret(vWalletMasterKey, vector<unsigned char>(vchPrivKey.begin(), vchPrivKey.end()),
178 WalletKeyIV(key.GetPubKey()), vchCryptedSecret))
179 return error("AddKey() : encrypting key failed\n");
180 if (!AddCryptedKey(key.GetPubKey(), vchCryptedSecret))
181 return false;
182 return CWalletDB().WriteCryptedKey(key.GetPubKey(), vchCryptedSecret);
183 }
184
162 CRITICAL_BLOCK(cs_mapKeys) 185 CRITICAL_BLOCK(cs_mapKeys)
163 { 186 {
164 mapKeys[key.GetPubKey()] = key.GetPrivKey(); 187 mapKeys[key.GetPubKey()] = key.GetPrivKey();
@@ -167,10 +190,287 @@ bool AddKey(const CKey& key)
167 return CWalletDB().WriteKey(key.GetPubKey(), key.GetPrivKey()); 190 return CWalletDB().WriteKey(key.GetPubKey(), key.GetPrivKey());
168} 191}
169 192
193bool IsWalletEncrypted()
194{
195 return fWalletEncrypted;
196}
197
198bool IsWalletLocked()
199{
200 return fWalletEncrypted && fWalletLocked;
201}
202
203void LockWallet()
204{
205 if (!fWalletEncrypted)
206 return;
207 CRITICAL_BLOCK(cs_mapKeys)
208 {
209 vWalletMasterKey.clear();
210 if (!vchCryptedHDMaster.empty())
211 vchHDMaster.clear();
212 if (!vchCryptedHDChainCode.empty())
213 vchHDChainCode.clear();
214 fWalletLocked = true;
215 }
216}
217
218bool DeriveWalletPassphraseKey(const string& strPassphrase,
219 const vector<unsigned char>& vchSalt,
220 unsigned int nDeriveIterations,
221 CKeyingMaterial& vchKeyRet,
222 vector<unsigned char>& vchIVRet)
223{
224 vchKeyRet.clear();
225 vchIVRet.clear();
226 if (vchSalt.size() != 8 || nDeriveIterations < 1)
227 return false;
228
229 unsigned char chKey[32];
230 unsigned char chIV[16];
231 int n = EVP_BytesToKey(EVP_aes_256_cbc(), EVP_sha512(), &vchSalt[0],
232 (const unsigned char*)strPassphrase.data(),
233 (int)strPassphrase.size(),
234 (int)nDeriveIterations,
235 chKey, chIV);
236 if (n != 32)
237 {
238 memset(chKey, 0, sizeof(chKey));
239 memset(chIV, 0, sizeof(chIV));
240 return false;
241 }
242 vchKeyRet.assign(chKey, chKey + sizeof(chKey));
243 vchIVRet.assign(chIV, chIV + sizeof(chIV));
244 memset(chKey, 0, sizeof(chKey));
245 memset(chIV, 0, sizeof(chIV));
246 return true;
247}
248
249bool EncryptSecret(const CKeyingMaterial& vchKey,
250 const vector<unsigned char>& vchPlaintext,
251 const vector<unsigned char>& vchIV,
252 vector<unsigned char>& vchCiphertextRet)
253{
254 vchCiphertextRet.clear();
255 if (vchKey.size() != 32 || vchIV.size() != 16)
256 return false;
257
258 EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
259 if (!ctx)
260 return false;
261
262 bool fOk = false;
263 int nLen = 0;
264 int nFinal = 0;
265 vector<unsigned char> vchOut(vchPlaintext.size() + EVP_CIPHER_block_size(EVP_aes_256_cbc()));
266 if (EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, &vchKey[0], &vchIV[0]) &&
267 EVP_EncryptUpdate(ctx, &vchOut[0], &nLen,
268 vchPlaintext.empty() ? NULL : &vchPlaintext[0],
269 (int)vchPlaintext.size()) &&
270 EVP_EncryptFinal_ex(ctx, &vchOut[0] + nLen, &nFinal))
271 {
272 vchOut.resize(nLen + nFinal);
273 vchCiphertextRet.swap(vchOut);
274 fOk = true;
275 }
276 EVP_CIPHER_CTX_free(ctx);
277 return fOk;
278}
279
280bool DecryptSecret(const CKeyingMaterial& vchKey,
281 const vector<unsigned char>& vchCiphertext,
282 const vector<unsigned char>& vchIV,
283 vector<unsigned char>& vchPlaintextRet)
284{
285 vchPlaintextRet.clear();
286 if (vchKey.size() != 32 || vchIV.size() != 16 || vchCiphertext.empty())
287 return false;
288
289 EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
290 if (!ctx)
291 return false;
292
293 bool fOk = false;
294 int nLen = 0;
295 int nFinal = 0;
296 vector<unsigned char> vchOut(vchCiphertext.size());
297 if (EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, &vchKey[0], &vchIV[0]) &&
298 EVP_DecryptUpdate(ctx, &vchOut[0], &nLen, &vchCiphertext[0],
299 (int)vchCiphertext.size()) &&
300 EVP_DecryptFinal_ex(ctx, &vchOut[0] + nLen, &nFinal))
301 {
302 vchOut.resize(nLen + nFinal);
303 vchPlaintextRet.swap(vchOut);
304 fOk = true;
305 }
306 EVP_CIPHER_CTX_free(ctx);
307 return fOk;
308}
309
310vector<unsigned char> WalletKeyIV(const vector<unsigned char>& vchPubKey)
311{
312 uint256 hash = Hash(vchPubKey.begin(), vchPubKey.end());
313 vector<unsigned char> vchIV(16);
314 memcpy(&vchIV[0], hash.begin(), vchIV.size());
315 return vchIV;
316}
317
318vector<unsigned char> WalletSecretIV(const string& strLabel)
319{
320 uint256 hash = Hash(strLabel.begin(), strLabel.end());
321 vector<unsigned char> vchIV(16);
322 memcpy(&vchIV[0], hash.begin(), vchIV.size());
323 return vchIV;
324}
325
326bool AddCryptedKey(const vector<unsigned char>& vchPubKey,
327 const vector<unsigned char>& vchCryptedSecret)
328{
329 if (vchPubKey.empty() || vchCryptedSecret.empty())
330 return false;
331 CRITICAL_BLOCK(cs_mapKeys)
332 {
333 fWalletEncrypted = true;
334 mapCryptedKeys[vchPubKey] = vchCryptedSecret;
335 mapPubKeys[Hash160(vchPubKey)] = vchPubKey;
336 }
337 return true;
338}
339
340bool WalletCanSpendKey(const vector<unsigned char>& vchPubKey)
341{
342 CRITICAL_BLOCK(cs_mapKeys)
343 return mapKeys.count(vchPubKey) > 0 || mapCryptedKeys.count(vchPubKey) > 0;
344 return false;
345}
346
347bool GetWalletPrivKey(const vector<unsigned char>& vchPubKey,
348 CPrivKey& vchPrivKeyRet,
349 string& strErrorRet)
350{
351 vchPrivKeyRet.clear();
352 CRITICAL_BLOCK(cs_mapKeys)
353 {
354 map<vector<unsigned char>, CPrivKey>::iterator mi = mapKeys.find(vchPubKey);
355 if (mi != mapKeys.end())
356 {
357 vchPrivKeyRet = (*mi).second;
358 return true;
359 }
360
361 map<vector<unsigned char>, vector<unsigned char> >::iterator mci = mapCryptedKeys.find(vchPubKey);
362 if (mci == mapCryptedKeys.end())
363 {
364 strErrorRet = "private key not found";
365 return false;
366 }
367 if (IsWalletLocked())
368 {
369 strErrorRet = "wallet is locked";
370 return false;
371 }
372
373 vector<unsigned char> vchPlaintext;
374 if (!DecryptSecret(vWalletMasterKey, (*mci).second, WalletKeyIV(vchPubKey), vchPlaintext))
375 {
376 strErrorRet = "could not decrypt private key";
377 return false;
378 }
379
380 CPrivKey vchPrivKey(vchPlaintext.begin(), vchPlaintext.end());
381 CKey key;
382 if (!key.SetPrivKey(vchPrivKey) || key.GetPubKey() != vchPubKey)
383 {
384 strErrorRet = "decrypted private key does not match its public key";
385 return false;
386 }
387 vchPrivKeyRet = vchPrivKey;
388 return true;
389 }
390 return false;
391}
392
393bool UnlockWallet(const string& strPassphrase, string& strErrorRet)
394{
395 strErrorRet.clear();
396 if (!IsWalletEncrypted())
397 return true;
398
399 CRITICAL_BLOCK(cs_mapKeys)
400 {
401 for (map<unsigned int, CWalletMasterKey>::iterator mi = mapMasterKeys.begin();
402 mi != mapMasterKeys.end(); ++mi)
403 {
404 const CWalletMasterKey& kMasterKey = (*mi).second;
405 CKeyingMaterial vchPassKey;
406 vector<unsigned char> vchPassIV;
407 if (!DeriveWalletPassphraseKey(strPassphrase, kMasterKey.vchSalt,
408 kMasterKey.nDeriveIterations,
409 vchPassKey, vchPassIV))
410 continue;
411
412 vector<unsigned char> vchMasterPlain;
413 if (!DecryptSecret(vchPassKey, kMasterKey.vchCryptedKey,
414 vchPassIV, vchMasterPlain))
415 continue;
416 if (vchMasterPlain.size() != 32)
417 continue;
418
419 CKeyingMaterial vchCandidate(vchMasterPlain.begin(), vchMasterPlain.end());
420 bool fValid = mapCryptedKeys.empty();
421 for (map<vector<unsigned char>, vector<unsigned char> >::iterator mci = mapCryptedKeys.begin();
422 mci != mapCryptedKeys.end(); ++mci)
423 {
424 vector<unsigned char> vchPlaintext;
425 if (!DecryptSecret(vchCandidate, (*mci).second, WalletKeyIV((*mci).first), vchPlaintext))
426 break;
427 CPrivKey vchPrivKey(vchPlaintext.begin(), vchPlaintext.end());
428 CKey key;
429 if (key.SetPrivKey(vchPrivKey) && key.GetPubKey() == (*mci).first)
430 fValid = true;
431 break;
432 }
433 if (fValid)
434 {
435 vWalletMasterKey = vchCandidate;
436 if (!vchCryptedHDMaster.empty())
437 {
438 vector<unsigned char> vchPlainHDMaster;
439 if (!DecryptSecret(vWalletMasterKey, vchCryptedHDMaster,
440 WalletSecretIV("hdmaster"), vchPlainHDMaster) ||
441 vchPlainHDMaster.size() != 32)
442 {
443 strErrorRet = "could not decrypt HD master key";
444 return false;
445 }
446 vchHDMaster = vchPlainHDMaster;
447 }
448 if (!vchCryptedHDChainCode.empty())
449 {
450 vector<unsigned char> vchPlainHDChainCode;
451 if (!DecryptSecret(vWalletMasterKey, vchCryptedHDChainCode,
452 WalletSecretIV("hdchaincode"), vchPlainHDChainCode) ||
453 vchPlainHDChainCode.size() != 32)
454 {
455 strErrorRet = "could not decrypt HD chain code";
456 return false;
457 }
458 vchHDChainCode = vchPlainHDChainCode;
459 }
460 fWalletLocked = false;
461 return true;
462 }
463 }
464 }
465
466 strErrorRet = "passphrase did not unlock the wallet";
467 return false;
468}
469
170static bool WalletAlreadyHasKey(const vector<unsigned char>& vchPubKey) 470static bool WalletAlreadyHasKey(const vector<unsigned char>& vchPubKey)
171{ 471{
172 CRITICAL_BLOCK(cs_mapKeys) 472 CRITICAL_BLOCK(cs_mapKeys)
173 return mapKeys.count(vchPubKey) > 0; 473 return mapKeys.count(vchPubKey) > 0 || mapCryptedKeys.count(vchPubKey) > 0;
174 return false; 474 return false;
175} 475}
176 476
@@ -509,6 +809,12 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet)
509 809
510void TopUpKeyPool() 810void TopUpKeyPool()
511{ 811{
812 if (IsWalletLocked())
813 {
814 printf("TopUpKeyPool() : wallet is encrypted and locked\n");
815 return;
816 }
817
512 CRITICAL_BLOCK(cs_keyPool) 818 CRITICAL_BLOCK(cs_keyPool)
513 { 819 {
514 while ((int)mapKeyPool.size() < KEYPOOL_SIZE) 820 while ((int)mapKeyPool.size() < KEYPOOL_SIZE)
@@ -562,6 +868,12 @@ void TopUpKeyPool()
562 868
563vector<unsigned char> GetKeyFromPool() 869vector<unsigned char> GetKeyFromPool()
564{ 870{
871 if (IsWalletLocked())
872 {
873 printf("GetKeyFromPool() : wallet is encrypted and locked\n");
874 return vector<unsigned char>();
875 }
876
565 CRITICAL_BLOCK(cs_keyPool) 877 CRITICAL_BLOCK(cs_keyPool)
566 { 878 {
567 TopUpKeyPool(); 879 TopUpKeyPool();
@@ -584,7 +896,15 @@ vector<unsigned char> GetKeyFromPool()
584 // locked open elsewhere. Generating on demand is what this code did for 896 // locked open elsewhere. Generating on demand is what this code did for
585 // its whole life, so fall back to it rather than refuse to mine. 897 // its whole life, so fall back to it rather than refuse to mine.
586 printf("GetKeyFromPool() : pool empty, generating a key on demand\n"); 898 printf("GetKeyFromPool() : pool empty, generating a key on demand\n");
587 return GenerateNewKey(); 899 try
900 {
901 return GenerateNewKey();
902 }
903 catch (const std::exception& e)
904 {
905 printf("GetKeyFromPool() : failed to generate a key: %s\n", e.what());
906 return vector<unsigned char>();
907 }
588} 908}
589 909
590 910
@@ -3581,6 +3901,11 @@ bool BitcoinMiner(int nThreadId)
3581 // thread happened to win a block, which meant a backup taken while mining 3901 // thread happened to win a block, which meant a backup taken while mining
3582 // could not spend what the very next block paid. 3902 // could not spend what the very next block paid.
3583 vector<unsigned char> vchPubKey = GetKeyFromPool(); 3903 vector<unsigned char> vchPubKey = GetKeyFromPool();
3904 if (vchPubKey.empty())
3905 {
3906 printf("BitcoinMiner: no wallet key available for coinbase; mining stopped\n");
3907 return false;
3908 }
3584 CBigNum bnExtraNonce = 0; 3909 CBigNum bnExtraNonce = 0;
3585 while (fGenerateBitcoins) 3910 while (fGenerateBitcoins)
3586 { 3911 {
@@ -3759,6 +4084,11 @@ bool BitcoinMiner(int nThreadId)
3759 // them out one at a time under cs_keyPool, which is 4084 // them out one at a time under cs_keyPool, which is
3760 // what keeps concurrent coinbases distinct (#58). 4085 // what keeps concurrent coinbases distinct (#58).
3761 vchPubKey = GetKeyFromPool(); 4086 vchPubKey = GetKeyFromPool();
4087 if (vchPubKey.empty())
4088 {
4089 printf("BitcoinMiner: no replacement wallet key available; mining stopped\n");
4090 fGenerateBitcoins = false;
4091 }
3762 4092
3763 // Process this block the same as if we had received it from another node 4093 // Process this block the same as if we had received it from another node
3764 if (!ProcessBlock(NULL, pblock.release())) 4094 if (!ProcessBlock(NULL, pblock.release()))