diff options
| author | Bitflash <btf01@proton.me> | 2026-07-30 12:06:08 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-30 12:06:08 -0300 |
| commit | b539d776bd86df9224d97048f26f33d2750d1c92 (patch) | |
| tree | 4a246ca7c8680e0b2d32613e388116a5d39247cf /src | |
| parent | daac46893448e49a21244d9646acf0ec47518537 (diff) | |
wallet: export and import keys as text (#49)
The last door out. Everything else assumes Berkeley DB will
cooperate. When it will not -- a build that cannot read the
file, a truncation, an environment past recovering -- the keys
are usually still perfectly intact and were unreachable
anyway. #40 is one person's account of exactly that, and #46
only rescues the cases where BDB still opens the file at all.
bitflash -dumpwallet=keys.txt
bitflash -importwallet=keys.txt
One key per line with its address and label. No database, no
environment, no version coupling: anything that can read a
line can recover from it.
The format is the DER private key in hex, which round-trips
exactly through SetPrivKey. Not interchangeable with other
wallets -- worth having eventually, but a WIF encoder I got
subtly wrong would be worse than no encoder at all, and this
file exists for the case where nothing else works.
Care taken, because the failure modes here cost money:
- Refuses to overwrite an existing file. A mistyped path
must not eat the only copy of something irreplaceable.
- Written owner-only on POSIX, before any key reaches it.
Windows inherits the folder's ACL; the README says so
rather than pretending otherwise.
- Newlines in labels are flattened, so a label cannot forge
an extra entry on read-back.
- Import skips keys already present, so running it twice
does nothing, and one unreadable line is reported and
stepped over instead of costing the reader every other key
in the file.
Verified on Linux and Windows: a key exported from one wallet
imports into a different one and its address then appears
there; re-importing adds nothing; a corrupted line is skipped
while the rest still load; the dump is 0600 and refuses to
overwrite.
This does not solve #47. Keys still are not derived from a
seed, so a dump is as much a snapshot as any other backup.
What it does is make the keys reachable no matter what state
the database is in.
Diffstat (limited to 'src')
| -rw-r--r-- | src/db.cpp | 191 | ||||
| -rw-r--r-- | src/db.h | 7 | ||||
| -rw-r--r-- | src/main_gui.cpp | 33 |
3 files changed, 231 insertions, 0 deletions
| @@ -708,3 +708,194 @@ bool BackupWallet(const string& strDest) | |||
| 708 | printf("BackupWallet() : wrote %s\n", strDest.c_str()); | 708 | printf("BackupWallet() : wrote %s\n", strDest.c_str()); |
| 709 | return true; | 709 | return true; |
| 710 | } | 710 | } |
| 711 | |||
| 712 | |||
| 713 | // ---- plain-text key export/import ------------------------------------------ | ||
| 714 | // | ||
| 715 | // The last door out. Everything else in this file assumes Berkeley DB will | ||
| 716 | // cooperate; these two do not. A wallet.dat can be unreadable for reasons that | ||
| 717 | // have nothing to do with your keys being gone -- a version this build does not | ||
| 718 | // understand, a file truncated by a bad copy, an environment that cannot be | ||
| 719 | // recovered -- and until now that meant the coins were unreachable even though | ||
| 720 | // the secrets were sitting right there. Issue #40 is one person's account of | ||
| 721 | // exactly that. | ||
| 722 | // | ||
| 723 | // The format is deliberately dull: a text file, one key per line, no database, | ||
| 724 | // no environment, no framing. Anything that can read a line can recover from it. | ||
| 725 | |||
| 726 | static int HexDigitValue(char c) | ||
| 727 | { | ||
| 728 | if (c >= '0' && c <= '9') return c - '0'; | ||
| 729 | if (c >= 'a' && c <= 'f') return c - 'a' + 10; | ||
| 730 | if (c >= 'A' && c <= 'F') return c - 'A' + 10; | ||
| 731 | return -1; | ||
| 732 | } | ||
| 733 | |||
| 734 | static bool ParseHexInto(const string& str, vector<unsigned char>& vchRet) | ||
| 735 | { | ||
| 736 | vchRet.clear(); | ||
| 737 | if (str.empty() || (str.size() % 2) != 0) | ||
| 738 | return false; | ||
| 739 | for (size_t i = 0; i < str.size(); i += 2) | ||
| 740 | { | ||
| 741 | int hi = HexDigitValue(str[i]); | ||
| 742 | int lo = HexDigitValue(str[i + 1]); | ||
| 743 | if (hi < 0 || lo < 0) | ||
| 744 | return false; | ||
| 745 | vchRet.push_back((unsigned char)((hi << 4) | lo)); | ||
| 746 | } | ||
| 747 | return true; | ||
| 748 | } | ||
| 749 | |||
| 750 | bool DumpWallet(const string& strDest) | ||
| 751 | { | ||
| 752 | if (strDest.empty()) | ||
| 753 | return error("DumpWallet() : no destination given\n"); | ||
| 754 | |||
| 755 | // Never over an existing file. The whole point is to hold the only copy of | ||
| 756 | // something irreplaceable, and a mistyped path must not eat one. | ||
| 757 | if (FileExists(strDest.c_str())) | ||
| 758 | return error("DumpWallet() : %s already exists -- refusing to overwrite\n", | ||
| 759 | strDest.c_str()); | ||
| 760 | |||
| 761 | FILE* pf = fopen(strDest.c_str(), "w"); | ||
| 762 | if (!pf) | ||
| 763 | return error("DumpWallet() : cannot write %s\n", strDest.c_str()); | ||
| 764 | #ifndef _WIN32 | ||
| 765 | // Before a single key reaches it. | ||
| 766 | chmod(strDest.c_str(), S_IRUSR | S_IWUSR); | ||
| 767 | #endif | ||
| 768 | |||
| 769 | fprintf(pf, "# Bitflash wallet key dump\n"); | ||
| 770 | fprintf(pf, "# %s\n", DateTimeStr(GetTime()).c_str()); | ||
| 771 | fprintf(pf, "#\n"); | ||
| 772 | fprintf(pf, "# THIS FILE CONTAINS PRIVATE KEYS IN PLAIN TEXT.\n"); | ||
| 773 | fprintf(pf, "# Anyone who reads it can spend these coins. Treat it as cash.\n"); | ||
| 774 | fprintf(pf, "#\n"); | ||
| 775 | fprintf(pf, "# One key per line: <private-key-hex> <address> [label]\n"); | ||
| 776 | fprintf(pf, "# The key is the DER encoding, hex; restore with -importwallet=FILE.\n"); | ||
| 777 | fprintf(pf, "#\n"); | ||
| 778 | |||
| 779 | int nKeys = 0; | ||
| 780 | bool fOk = true; | ||
| 781 | CRITICAL_BLOCK(cs_mapKeys) | ||
| 782 | { | ||
| 783 | for (map<vector<unsigned char>, CPrivKey>::iterator mi = mapKeys.begin(); | ||
| 784 | mi != mapKeys.end(); ++mi) | ||
| 785 | { | ||
| 786 | const vector<unsigned char>& vchPubKey = (*mi).first; | ||
| 787 | const CPrivKey& vchPrivKey = (*mi).second; | ||
| 788 | |||
| 789 | string strAddress = PubKeyToAddress(vchPubKey); | ||
| 790 | string strLabel; | ||
| 791 | map<string, string>::iterator mia = mapAddressBook.find(strAddress); | ||
| 792 | if (mia != mapAddressBook.end()) | ||
| 793 | strLabel = (*mia).second; | ||
| 794 | |||
| 795 | // Labels are user text and could contain a newline, which would | ||
| 796 | // forge a second entry on read-back. | ||
| 797 | for (size_t i = 0; i < strLabel.size(); i++) | ||
| 798 | if (strLabel[i] == '\n' || strLabel[i] == '\r') | ||
| 799 | strLabel[i] = ' '; | ||
| 800 | |||
| 801 | if (fprintf(pf, "%s %s %s\n", | ||
| 802 | HexStr(vchPrivKey.begin(), vchPrivKey.end(), false).c_str(), | ||
| 803 | strAddress.c_str(), | ||
| 804 | strLabel.c_str()) < 0) | ||
| 805 | { fOk = false; break; } | ||
| 806 | nKeys++; | ||
| 807 | } | ||
| 808 | } | ||
| 809 | |||
| 810 | if (fclose(pf) != 0) | ||
| 811 | fOk = false; | ||
| 812 | if (!fOk) | ||
| 813 | { | ||
| 814 | remove(strDest.c_str()); | ||
| 815 | return error("DumpWallet() : writing %s failed\n", strDest.c_str()); | ||
| 816 | } | ||
| 817 | |||
| 818 | printf("DumpWallet() : wrote %d key(s) to %s\n", nKeys, strDest.c_str()); | ||
| 819 | return true; | ||
| 820 | } | ||
| 821 | |||
| 822 | bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet) | ||
| 823 | { | ||
| 824 | nAddedRet = 0; | ||
| 825 | nSkippedRet = 0; | ||
| 826 | |||
| 827 | FILE* pf = fopen(strSrc.c_str(), "r"); | ||
| 828 | if (!pf) | ||
| 829 | return error("ImportWallet() : cannot read %s\n", strSrc.c_str()); | ||
| 830 | |||
| 831 | char buf[8192]; | ||
| 832 | int nLine = 0; | ||
| 833 | int nBad = 0; | ||
| 834 | while (fgets(buf, sizeof(buf), pf)) | ||
| 835 | { | ||
| 836 | nLine++; | ||
| 837 | string strLine = buf; | ||
| 838 | while (!strLine.empty() && (strLine[strLine.size()-1] == '\n' || | ||
| 839 | strLine[strLine.size()-1] == '\r')) | ||
| 840 | strLine.resize(strLine.size() - 1); | ||
| 841 | if (strLine.empty() || strLine[0] == '#') | ||
| 842 | continue; | ||
| 843 | |||
| 844 | string strHex = strLine.substr(0, strLine.find(' ')); | ||
| 845 | vector<unsigned char> vchPrivKey; | ||
| 846 | if (!ParseHexInto(strHex, vchPrivKey)) | ||
| 847 | { | ||
| 848 | printf("ImportWallet() : line %d is not a hex key, skipped\n", nLine); | ||
| 849 | nBad++; | ||
| 850 | continue; | ||
| 851 | } | ||
| 852 | |||
| 853 | CKey key; | ||
| 854 | // A key this build cannot load is reported and stepped over rather than | ||
| 855 | // aborting the run: one damaged line in a salvage file must not cost | ||
| 856 | // the reader every other key in it. | ||
| 857 | try | ||
| 858 | { | ||
| 859 | CPrivKey vchSecure(vchPrivKey.begin(), vchPrivKey.end()); | ||
| 860 | if (!key.SetPrivKey(vchSecure)) | ||
| 861 | { | ||
| 862 | printf("ImportWallet() : line %d is not a usable key, skipped\n", nLine); | ||
| 863 | nBad++; | ||
| 864 | continue; | ||
| 865 | } | ||
| 866 | } | ||
| 867 | catch (const std::exception& e) | ||
| 868 | { | ||
| 869 | printf("ImportWallet() : line %d rejected (%s), skipped\n", nLine, e.what()); | ||
| 870 | nBad++; | ||
| 871 | continue; | ||
| 872 | } | ||
| 873 | |||
| 874 | bool fHave = false; | ||
| 875 | CRITICAL_BLOCK(cs_mapKeys) | ||
| 876 | fHave = mapKeys.count(key.GetPubKey()) > 0; | ||
| 877 | if (fHave) | ||
| 878 | { | ||
| 879 | nSkippedRet++; // already ours; importing twice is not an error | ||
| 880 | continue; | ||
| 881 | } | ||
| 882 | |||
| 883 | if (!AddKey(key)) | ||
| 884 | { | ||
| 885 | fclose(pf); | ||
| 886 | return error("ImportWallet() : failed to store key from line %d\n", nLine); | ||
| 887 | } | ||
| 888 | nAddedRet++; | ||
| 889 | } | ||
| 890 | fclose(pf); | ||
| 891 | |||
| 892 | printf("ImportWallet() : %d added, %d already present, %d unreadable\n", | ||
| 893 | nAddedRet, nSkippedRet, nBad); | ||
| 894 | |||
| 895 | // Imported keys are only useful once their transactions are noticed, and | ||
| 896 | // that scan is a full pass over the chain the node does at startup. | ||
| 897 | if (nAddedRet > 0) | ||
| 898 | printf("ImportWallet() : restart the node so it can find transactions " | ||
| 899 | "belonging to the new keys\n"); | ||
| 900 | return true; | ||
| 901 | } | ||
| @@ -409,6 +409,13 @@ bool LoadWallet(); | |||
| 409 | // address created after this call are not spendable from the file it writes. | 409 | // address created after this call are not spendable from the file it writes. |
| 410 | bool BackupWallet(const string& strDest); | 410 | bool BackupWallet(const string& strDest); |
| 411 | 411 | ||
| 412 | // Plain-text key export/import: no Berkeley DB, no environment, no version | ||
| 413 | // coupling. What you fall back on when wallet.dat itself will not cooperate. | ||
| 414 | // DumpWallet refuses to overwrite an existing file and writes owner-only on | ||
| 415 | // POSIX -- the output holds private keys in the clear. | ||
| 416 | bool DumpWallet(const string& strDest); | ||
| 417 | bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet); | ||
| 418 | |||
| 412 | inline bool SetAddressBookName(const string& strAddress, const string& strName) | 419 | inline bool SetAddressBookName(const string& strAddress, const string& strName) |
| 413 | { | 420 | { |
| 414 | return CWalletDB().WriteName(strAddress, strName); | 421 | return CWalletDB().WriteName(strAddress, strName); |
diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 976cd42..2484592 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp | |||
| @@ -70,6 +70,9 @@ static void PrintUsage() | |||
| 70 | printf("Wallet:\n"); | 70 | printf("Wallet:\n"); |
| 71 | printf(" /backupwallet=FILE (write a wallet.dat that opens on its own,\n"); | 71 | printf(" /backupwallet=FILE (write a wallet.dat that opens on its own,\n"); |
| 72 | printf(" then exit; run it again after new addresses)\n"); | 72 | printf(" then exit; run it again after new addresses)\n"); |
| 73 | printf(" /dumpwallet=FILE (export private keys as text -- readable by\n"); | ||
| 74 | printf(" anyone, so guard it like cash)\n"); | ||
| 75 | printf(" /importwallet=FILE (load keys from such a file back in)\n"); | ||
| 73 | printf("\n"); | 76 | printf("\n"); |
| 74 | printf("Each option also accepts '-' instead of '/'.\n"); | 77 | printf("Each option also accepts '-' instead of '/'.\n"); |
| 75 | } | 78 | } |
| @@ -245,6 +248,36 @@ int main(int argc, char* argv[]) | |||
| 245 | return fOk ? 0 : 1; | 248 | return fOk ? 0 : 1; |
| 246 | } | 249 | } |
| 247 | 250 | ||
| 251 | string strDump = argval2(argc, argv, "/dumpwallet", "-dumpwallet"); | ||
| 252 | if (!strDump.empty()) | ||
| 253 | { | ||
| 254 | bool fOk = DumpWallet(strDump); | ||
| 255 | if (fOk) | ||
| 256 | { | ||
| 257 | printf("Wrote %s\n", strDump.c_str()); | ||
| 258 | printf("It holds your private keys as readable text. Anyone with the file " | ||
| 259 | "can spend these coins -- keep it off shared storage and delete it " | ||
| 260 | "once you have it somewhere safe.\n"); | ||
| 261 | } | ||
| 262 | else | ||
| 263 | fprintf(stderr, "Export failed -- nothing was written.\n"); | ||
| 264 | DBFlush(true); | ||
| 265 | return fOk ? 0 : 1; | ||
| 266 | } | ||
| 267 | |||
| 268 | string strImport = argval2(argc, argv, "/importwallet", "-importwallet"); | ||
| 269 | if (!strImport.empty()) | ||
| 270 | { | ||
| 271 | int nAdded = 0, nSkipped = 0; | ||
| 272 | bool fOk = ImportWallet(strImport, nAdded, nSkipped); | ||
| 273 | if (fOk) | ||
| 274 | printf("Imported %d key(s); %d were already here.\n", nAdded, nSkipped); | ||
| 275 | else | ||
| 276 | fprintf(stderr, "Import failed.\n"); | ||
| 277 | DBFlush(true); | ||
| 278 | return fOk ? 0 : 1; | ||
| 279 | } | ||
| 280 | |||
| 248 | ReacceptWalletTransactions(); | 281 | ReacceptWalletTransactions(); |
| 249 | 282 | ||
| 250 | if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } | 283 | if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } |