diff options
| author | Bitflash <admin@bitflash.network> | 2026-08-14 08:56:57 -0300 |
|---|---|---|
| committer | Bitflash <admin@bitflash.network> | 2026-08-14 08:56:57 -0300 |
| commit | cf597ca37aec53b0dd7016399fee73e1959dc604 (patch) | |
| tree | 8487caa5b4471b1203b46e0f73fce0362bf2c950 | |
| parent | 5306542f6121e0c0b4ed1210e24d55fa4fbb7127 (diff) | |
| parent | 33af98c042340f6431aa93b4b6f893d85f40f237 (diff) | |
Merge pull request 'wallet: fail closed on unsafe wallet storage state' (#39) from wallet-storage-sanity-checks into main
| -rw-r--r-- | src/main_gui.cpp | 9 | ||||
| -rw-r--r-- | src/selftest.cpp | 27 | ||||
| -rw-r--r-- | src/walletcmd.cpp | 135 | ||||
| -rw-r--r-- | src/walletcmd.h | 4 |
4 files changed, 148 insertions, 27 deletions
diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 395cc0f..8763211 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp | |||
| @@ -176,6 +176,8 @@ static void PrintUsage() | |||
| 176 | printf(" printing wallet values, then exit)\n"); | 176 | printf(" printing wallet values, then exit)\n"); |
| 177 | printf(" /walletstorageauditjson=FILE\n"); | 177 | printf(" /walletstorageauditjson=FILE\n"); |
| 178 | printf(" (write the same audit as deterministic JSON)\n"); | 178 | printf(" (write the same audit as deterministic JSON)\n"); |
| 179 | printf(" /walletstoragecheck (fail closed on inconsistent or unsafe\n"); | ||
| 180 | printf(" wallet.dat storage state)\n"); | ||
| 179 | printf(" /rescan (walk the chain for coins this wallet owns\n"); | 181 | printf(" /rescan (walk the chain for coins this wallet owns\n"); |
| 180 | printf(" but never recorded, then exit)\n"); | 182 | printf(" but never recorded, then exit)\n"); |
| 181 | printf("\n"); | 183 | printf("\n"); |
| @@ -719,6 +721,13 @@ int main(int argc, char* argv[]) | |||
| 719 | return nRet; | 721 | return nRet; |
| 720 | } | 722 | } |
| 721 | 723 | ||
| 724 | if (arg(argc,argv,"/walletstoragecheck") || arg(argc,argv,"-walletstoragecheck")) | ||
| 725 | { | ||
| 726 | int nRet = CmdWalletStorageCheck(); | ||
| 727 | DBFlush(true); | ||
| 728 | return nRet; | ||
| 729 | } | ||
| 730 | |||
| 722 | if (arg(argc,argv,"/newphrase") || arg(argc,argv,"-newphrase")) | 731 | if (arg(argc,argv,"/newphrase") || arg(argc,argv,"-newphrase")) |
| 723 | { | 732 | { |
| 724 | int nRet = CmdNewPhrase(); | 733 | int nRet = CmdNewPhrase(); |
diff --git a/src/selftest.cpp b/src/selftest.cpp index 73c2935..5e5bab6 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp | |||
| @@ -1329,6 +1329,19 @@ static int RunWalletPortabilitySelfTest() | |||
| 1329 | FileContainsText(strOriginalStorageAudit, "malformed records: 0"), | 1329 | FileContainsText(strOriginalStorageAudit, "malformed records: 0"), |
| 1330 | "the storage audit recognizes a plaintext phrase wallet") ? 0 : 1; | 1330 | "the storage audit recognizes a plaintext phrase wallet") ? 0 : 1; |
| 1331 | 1331 | ||
| 1332 | string strOriginalStorageCheck = tmp + "/original-storage-check.txt"; | ||
| 1333 | vector<string> vOriginalStorageCheckArgs; | ||
| 1334 | vOriginalStorageCheckArgs.push_back("-datadir=" + strOriginal); | ||
| 1335 | vOriginalStorageCheckArgs.push_back("-nomanagedtor"); | ||
| 1336 | vOriginalStorageCheckArgs.push_back("-nogui"); | ||
| 1337 | vOriginalStorageCheckArgs.push_back("-walletstoragecheck"); | ||
| 1338 | int nOriginalStorageCheckRet = | ||
| 1339 | RunBitflashChild(strExe, vOriginalStorageCheckArgs, NULL, &strOriginalStorageCheck); | ||
| 1340 | nFail += Check(nOriginalStorageCheckRet == 0 && | ||
| 1341 | FileContainsText(strOriginalStorageCheck, "storage sanity: ok") && | ||
| 1342 | FileContainsText(strOriginalStorageCheck, "plain HD seed: complete"), | ||
| 1343 | "the storage sanity check accepts a plaintext phrase wallet") ? 0 : 1; | ||
| 1344 | |||
| 1332 | string strOriginalStorageAuditJson = tmp + "/original-storage-audit.json"; | 1345 | string strOriginalStorageAuditJson = tmp + "/original-storage-audit.json"; |
| 1333 | vector<string> vOriginalStorageAuditJsonArgs; | 1346 | vector<string> vOriginalStorageAuditJsonArgs; |
| 1334 | vOriginalStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); | 1347 | vOriginalStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); |
| @@ -1419,6 +1432,20 @@ static int RunWalletPortabilitySelfTest() | |||
| 1419 | FileContainsText(strEncryptedStorageAudit, "malformed records: 0"), | 1432 | FileContainsText(strEncryptedStorageAudit, "malformed records: 0"), |
| 1420 | "the storage audit recognizes an encrypted wallet") ? 0 : 1; | 1433 | "the storage audit recognizes an encrypted wallet") ? 0 : 1; |
| 1421 | 1434 | ||
| 1435 | string strEncryptedStorageCheck = tmp + "/encrypted-storage-check.txt"; | ||
| 1436 | vector<string> vEncryptedStorageCheckArgs; | ||
| 1437 | vEncryptedStorageCheckArgs.push_back("-datadir=" + strOriginal); | ||
| 1438 | vEncryptedStorageCheckArgs.push_back("-nomanagedtor"); | ||
| 1439 | vEncryptedStorageCheckArgs.push_back("-nogui"); | ||
| 1440 | vEncryptedStorageCheckArgs.push_back("-walletstoragecheck"); | ||
| 1441 | int nEncryptedStorageCheckRet = | ||
| 1442 | RunBitflashChild(strExe, vEncryptedStorageCheckArgs, NULL, &strEncryptedStorageCheck); | ||
| 1443 | nFail += Check(nEncryptedStorageCheckRet == 0 && | ||
| 1444 | FileContainsText(strEncryptedStorageCheck, "storage sanity: ok") && | ||
| 1445 | FileContainsText(strEncryptedStorageCheck, "plain private keys: 0") && | ||
| 1446 | FileContainsText(strEncryptedStorageCheck, "encrypted HD seed: complete"), | ||
| 1447 | "the storage sanity check accepts an encrypted wallet") ? 0 : 1; | ||
| 1448 | |||
| 1422 | string strEncryptedStorageAuditJson = tmp + "/encrypted-storage-audit.json"; | 1449 | string strEncryptedStorageAuditJson = tmp + "/encrypted-storage-audit.json"; |
| 1423 | vector<string> vEncryptedStorageAuditJsonArgs; | 1450 | vector<string> vEncryptedStorageAuditJsonArgs; |
| 1424 | vEncryptedStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); | 1451 | vEncryptedStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); |
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 6f8258d..ce0da02 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp | |||
| @@ -472,33 +472,28 @@ int CmdRecoveryAudit() | |||
| 472 | 472 | ||
| 473 | struct WalletStorageAuditCounts | 473 | struct WalletStorageAuditCounts |
| 474 | { | 474 | { |
| 475 | unsigned int nTotal; | 475 | unsigned int nTotal = 0; |
| 476 | unsigned int nMalformed; | 476 | unsigned int nMalformed = 0; |
| 477 | unsigned int nUnknown; | 477 | unsigned int nUnknown = 0; |
| 478 | unsigned int nVersion; | 478 | unsigned int nVersion = 0; |
| 479 | unsigned int nNames; | 479 | unsigned int nNames = 0; |
| 480 | unsigned int nTransactions; | 480 | unsigned int nTransactions = 0; |
| 481 | unsigned int nPlainKeys; | 481 | unsigned int nPlainKeys = 0; |
| 482 | unsigned int nEncryptedKeys; | 482 | unsigned int nEncryptedKeys = 0; |
| 483 | unsigned int nMasterKeys; | 483 | unsigned int nMasterKeys = 0; |
| 484 | unsigned int nDefaultKey; | 484 | unsigned int nDefaultKey = 0; |
| 485 | unsigned int nPlainHDMaster; | 485 | unsigned int nPlainHDMaster = 0; |
| 486 | unsigned int nPlainHDChainCode; | 486 | unsigned int nPlainHDChainCode = 0; |
| 487 | unsigned int nCryptedHDMaster; | 487 | unsigned int nCryptedHDMaster = 0; |
| 488 | unsigned int nCryptedHDChainCode; | 488 | unsigned int nCryptedHDChainCode = 0; |
| 489 | unsigned int nWalletMinVersion; | 489 | unsigned int nWalletMinVersion = 0; |
| 490 | unsigned int nPool; | 490 | unsigned int nPool = 0; |
| 491 | unsigned int nSettings; | 491 | unsigned int nSettings = 0; |
| 492 | unsigned int nHDNext; | 492 | unsigned int nHDNext = 0; |
| 493 | unsigned int nHDSchema; | 493 | unsigned int nHDSchema = 0; |
| 494 | unsigned int nHDCoinType; | 494 | unsigned int nHDCoinType = 0; |
| 495 | unsigned int nHDReceiveNext; | 495 | unsigned int nHDReceiveNext = 0; |
| 496 | unsigned int nHDChangeNext; | 496 | unsigned int nHDChangeNext = 0; |
| 497 | |||
| 498 | WalletStorageAuditCounts() | ||
| 499 | { | ||
| 500 | memset(this, 0, sizeof(*this)); | ||
| 501 | } | ||
| 502 | }; | 497 | }; |
| 503 | 498 | ||
| 504 | static std::string HDSeedStorageState(unsigned int nMaster, unsigned int nChain) | 499 | static std::string HDSeedStorageState(unsigned int nMaster, unsigned int nChain) |
| @@ -658,6 +653,92 @@ static bool WriteAuditTextFile(const std::string& strPath, | |||
| 658 | return true; | 653 | return true; |
| 659 | } | 654 | } |
| 660 | 655 | ||
| 656 | static void AddWalletStorageFailure(std::vector<std::string>& vFailures, | ||
| 657 | const std::string& strFailure) | ||
| 658 | { | ||
| 659 | vFailures.push_back(strFailure); | ||
| 660 | } | ||
| 661 | |||
| 662 | static void BuildWalletStorageSanityFailures(const WalletStorageAuditCounts& counts, | ||
| 663 | std::vector<std::string>& vFailures) | ||
| 664 | { | ||
| 665 | bool fPlainHD = counts.nPlainHDMaster > 0 || counts.nPlainHDChainCode > 0; | ||
| 666 | bool fCryptedHD = counts.nCryptedHDMaster > 0 || counts.nCryptedHDChainCode > 0; | ||
| 667 | bool fEncryptedRecords = counts.nEncryptedKeys > 0 || | ||
| 668 | counts.nMasterKeys > 0 || | ||
| 669 | fCryptedHD; | ||
| 670 | |||
| 671 | if (counts.nMalformed > 0) | ||
| 672 | AddWalletStorageFailure(vFailures, "wallet.dat contains malformed records"); | ||
| 673 | if (counts.nUnknown > 0) | ||
| 674 | AddWalletStorageFailure(vFailures, "wallet.dat contains unknown records"); | ||
| 675 | if ((counts.nPlainHDMaster > 0) != (counts.nPlainHDChainCode > 0)) | ||
| 676 | AddWalletStorageFailure(vFailures, "plain HD seed is incomplete"); | ||
| 677 | if ((counts.nCryptedHDMaster > 0) != (counts.nCryptedHDChainCode > 0)) | ||
| 678 | AddWalletStorageFailure(vFailures, "encrypted HD seed is incomplete"); | ||
| 679 | if (fPlainHD && fCryptedHD) | ||
| 680 | AddWalletStorageFailure(vFailures, "wallet.dat contains both plain and encrypted HD seed records"); | ||
| 681 | if (counts.nDefaultKey > 1) | ||
| 682 | AddWalletStorageFailure(vFailures, "wallet.dat contains more than one default public key record"); | ||
| 683 | if (counts.nWalletMinVersion > 1) | ||
| 684 | AddWalletStorageFailure(vFailures, "wallet.dat contains more than one minimum-version record"); | ||
| 685 | |||
| 686 | if (fEncryptedRecords) | ||
| 687 | { | ||
| 688 | if (counts.nPlainKeys > 0) | ||
| 689 | AddWalletStorageFailure(vFailures, "encrypted wallet still contains plain private key records"); | ||
| 690 | if (fPlainHD) | ||
| 691 | AddWalletStorageFailure(vFailures, "encrypted wallet still contains plain HD seed records"); | ||
| 692 | if (counts.nMasterKeys == 0) | ||
| 693 | AddWalletStorageFailure(vFailures, "encrypted wallet has no encryption master key record"); | ||
| 694 | if (counts.nWalletMinVersion == 0) | ||
| 695 | AddWalletStorageFailure(vFailures, "encrypted wallet has no minimum-version record"); | ||
| 696 | } | ||
| 697 | } | ||
| 698 | |||
| 699 | int CmdWalletStorageCheck() | ||
| 700 | { | ||
| 701 | AttachTerminal(); | ||
| 702 | |||
| 703 | WalletStorageAuditCounts counts; | ||
| 704 | std::string strError; | ||
| 705 | if (!ReadWalletStorageCounts(counts, strError)) | ||
| 706 | { | ||
| 707 | fprintf(stderr, "Cannot check wallet storage: %s\n", strError.c_str()); | ||
| 708 | return 1; | ||
| 709 | } | ||
| 710 | |||
| 711 | std::vector<std::string> vFailures; | ||
| 712 | BuildWalletStorageSanityFailures(counts, vFailures); | ||
| 713 | |||
| 714 | printf("Wallet storage sanity check\n"); | ||
| 715 | printf(" records total: %u\n", counts.nTotal); | ||
| 716 | printf(" encrypted records: %s\n", | ||
| 717 | (counts.nEncryptedKeys || counts.nMasterKeys || | ||
| 718 | counts.nCryptedHDMaster || counts.nCryptedHDChainCode) ? "yes" : "no"); | ||
| 719 | printf(" plain private keys: %u\n", counts.nPlainKeys); | ||
| 720 | printf(" encrypted private keys: %u\n", counts.nEncryptedKeys); | ||
| 721 | printf(" plain HD seed: %s\n", | ||
| 722 | HDSeedStorageState(counts.nPlainHDMaster, | ||
| 723 | counts.nPlainHDChainCode).c_str()); | ||
| 724 | printf(" encrypted HD seed: %s\n", | ||
| 725 | HDSeedStorageState(counts.nCryptedHDMaster, | ||
| 726 | counts.nCryptedHDChainCode).c_str()); | ||
| 727 | if (vFailures.empty()) | ||
| 728 | { | ||
| 729 | printf(" storage sanity: ok\n"); | ||
| 730 | fflush(stdout); | ||
| 731 | return 0; | ||
| 732 | } | ||
| 733 | |||
| 734 | printf(" storage sanity: failed\n"); | ||
| 735 | for (std::vector<std::string>::const_iterator it = vFailures.begin(); | ||
| 736 | it != vFailures.end(); ++it) | ||
| 737 | printf(" failure: %s\n", it->c_str()); | ||
| 738 | fflush(stdout); | ||
| 739 | return 2; | ||
| 740 | } | ||
| 741 | |||
| 661 | int CmdWalletStorageAudit(const std::string& strJsonOut) | 742 | int CmdWalletStorageAudit(const std::string& strJsonOut) |
| 662 | { | 743 | { |
| 663 | AttachTerminal(); | 744 | AttachTerminal(); |
diff --git a/src/walletcmd.h b/src/walletcmd.h index c6342a6..6077669 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h | |||
| @@ -66,6 +66,10 @@ int CmdRecoveryAudit(); | |||
| 66 | // addresses, transaction ids, labels, or other wallet values. | 66 | // addresses, transaction ids, labels, or other wallet values. |
| 67 | int CmdWalletStorageAudit(const std::string& strJsonOut = ""); | 67 | int CmdWalletStorageAudit(const std::string& strJsonOut = ""); |
| 68 | 68 | ||
| 69 | // Diagnostic: fail closed when wallet.dat contains an internally inconsistent | ||
| 70 | // or migration-unsafe mix of storage records. | ||
| 71 | int CmdWalletStorageCheck(); | ||
| 72 | |||
| 69 | // Rewrite wallet.dat so private keys and the HD seed are encrypted with the | 73 | // Rewrite wallet.dat so private keys and the HD seed are encrypted with the |
| 70 | // given passphrase. Returns 0 on success and exits without starting the node. | 74 | // given passphrase. Returns 0 on success and exits without starting the node. |
| 71 | int CmdEncryptWallet(const std::string& strPassphrase); | 75 | int CmdEncryptWallet(const std::string& strPassphrase); |