upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/main_gui.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_gui.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_gui.cpp')
-rw-r--r--src/main_gui.cpp101
1 files changed, 100 insertions, 1 deletions
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index 6164ad7..a2577ca 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -40,6 +40,60 @@ static string argval2(int argc, char* argv[], const char* keySlash, const char*
40 return argval(argc, argv, keyDash); 40 return argval(argc, argv, keyDash);
41} 41}
42 42
43static bool ReadPassphraseArgument(const string& strArg, string& strPassphraseRet, string& strErrorRet)
44{
45 strPassphraseRet.clear();
46 strErrorRet.clear();
47 if (strArg.empty())
48 {
49 AttachTerminal();
50 fprintf(stderr, "Enter wallet passphrase: ");
51 fflush(stderr);
52 char buf[4096];
53 if (!fgets(buf, sizeof(buf), stdin))
54 {
55 strErrorRet = "could not read passphrase from stdin";
56 return false;
57 }
58 strPassphraseRet = buf;
59 }
60 else if (strArg[0] == '@')
61 {
62 string strPath = strArg.substr(1);
63 FILE* pf = fopen(strPath.c_str(), "rb");
64 if (!pf)
65 {
66 strErrorRet = strprintf("could not read passphrase file %s", strPath.c_str());
67 return false;
68 }
69 char buf[4096];
70 if (!fgets(buf, sizeof(buf), pf))
71 {
72 fclose(pf);
73 strErrorRet = strprintf("passphrase file %s is empty", strPath.c_str());
74 return false;
75 }
76 fclose(pf);
77 strPassphraseRet = buf;
78 }
79 else
80 {
81 strErrorRet = "passphrase literals on the command line are unsafe; use the option without a value to read stdin, or use @FILE";
82 return false;
83 }
84
85 while (!strPassphraseRet.empty() &&
86 (strPassphraseRet[strPassphraseRet.size() - 1] == '\n' ||
87 strPassphraseRet[strPassphraseRet.size() - 1] == '\r'))
88 strPassphraseRet.resize(strPassphraseRet.size() - 1);
89 if (strPassphraseRet.empty())
90 {
91 strErrorRet = "empty passphrase";
92 return false;
93 }
94 return true;
95}
96
43// The real printf, not the one util.h remaps to OutputDebugStringF. Help that 97// The real printf, not the one util.h remaps to OutputDebugStringF. Help that
44// goes to debug.log is help nobody asked for: `-help` printed a full page into 98// goes to debug.log is help nobody asked for: `-help` printed a full page into
45// the data directory and returned 0 with an empty terminal, which reads as a 99// the data directory and returned 0 with an empty terminal, which reads as a
@@ -56,7 +110,7 @@ static void PrintUsage()
56 printf(" /debug\n"); 110 printf(" /debug\n");
57 printf(" /gen\n"); 111 printf(" /gen\n");
58 printf(" /nogui or /daemon\n"); 112 printf(" /nogui or /daemon\n");
59 printf(" /selftest=wallet-keypool or /selftest=wallet-hd\n"); 113 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, or wallet-encrypt\n");
60 printf("\n"); 114 printf("\n");
61 printf("Mining mode:\n"); 115 printf("Mining mode:\n");
62 printf(" /operator\n"); 116 printf(" /operator\n");
@@ -85,6 +139,10 @@ static void PrintUsage()
85 printf(" /dumpwallet=FILE (export private keys as text -- readable by\n"); 139 printf(" /dumpwallet=FILE (export private keys as text -- readable by\n");
86 printf(" anyone, so guard it like cash)\n"); 140 printf(" anyone, so guard it like cash)\n");
87 printf(" /importwallet=FILE (load keys from such a file back in)\n"); 141 printf(" /importwallet=FILE (load keys from such a file back in)\n");
142 printf(" /encryptwallet[=@FILE] (rewrite wallet.dat with encrypted private keys\n");
143 printf(" and encrypted HD seed, then exit)\n");
144 printf(" /walletpassphrase[=@FILE] (unlock an encrypted wallet for one-shot\n");
145 printf(" commands; without @FILE reads stdin)\n");
88 printf(" /newaddress (print the next receiving address, then exit)\n"); 146 printf(" /newaddress (print the next receiving address, then exit)\n");
89 printf(" /sendto=ADDRESS,AMOUNT (spend from this wallet, then exit -- the\n"); 147 printf(" /sendto=ADDRESS,AMOUNT (spend from this wallet, then exit -- the\n");
90 printf(" only way to send without the window)\n"); 148 printf(" only way to send without the window)\n");
@@ -299,6 +357,39 @@ int main(int argc, char* argv[])
299 } 357 }
300 printf("Height=%d\n", nBestHeight); 358 printf("Height=%d\n", nBestHeight);
301 359
360 bool fEncryptWallet = arg(argc, argv, "/encryptwallet") || arg(argc, argv, "-encryptwallet");
361 string strEncryptWallet = argval2(argc, argv, "/encryptwallet", "-encryptwallet");
362 if (fEncryptWallet)
363 {
364 string strPassphrase;
365 string strPassphraseError;
366 if (!ReadPassphraseArgument(strEncryptWallet, strPassphrase, strPassphraseError))
367 {
368 fprintf(stderr, "Cannot read encryption passphrase: %s\n", strPassphraseError.c_str());
369 DBFlush(true);
370 return 1;
371 }
372 int nRet = CmdEncryptWallet(strPassphrase);
373 if (nRet != 0)
374 DBFlush(true);
375 return nRet;
376 }
377
378 bool fWalletPassphrase = arg(argc, argv, "/walletpassphrase") || arg(argc, argv, "-walletpassphrase");
379 string strWalletPassphrase = argval2(argc, argv, "/walletpassphrase", "-walletpassphrase");
380 if (fWalletPassphrase)
381 {
382 string strPassphrase;
383 string strUnlockError;
384 if (!ReadPassphraseArgument(strWalletPassphrase, strPassphrase, strUnlockError) ||
385 !UnlockWallet(strPassphrase, strUnlockError))
386 {
387 fprintf(stderr, "Cannot unlock wallet: %s\n", strUnlockError.c_str());
388 DBFlush(true);
389 return 1;
390 }
391 }
392
302 // Before the node opens sockets or touches anything: the wallet is loaded, 393 // Before the node opens sockets or touches anything: the wallet is loaded,
303 // which is all a backup needs, and finishing here means the copy is taken 394 // which is all a backup needs, and finishing here means the copy is taken
304 // from a quiet directory rather than from under a running node. 395 // from a quiet directory rather than from under a running node.
@@ -442,6 +533,14 @@ int main(int argc, char* argv[])
442 return 0; 533 return 0;
443 } 534 }
444 535
536 if (fGenerateBitcoins && IsWalletLocked())
537 {
538 fprintf(stderr, "Cannot mine while the encrypted wallet is locked. "
539 "Start with /walletpassphrase or /walletpassphrase=@FILE, or disable mining.\n");
540 DBFlush(true);
541 return 1;
542 }
543
445 ReacceptWalletTransactions(); 544 ReacceptWalletTransactions();
446 545
447 if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } 546 if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; }