upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-07-30 12:06:08 -0300
committerGitHub <noreply@github.com>2026-07-30 12:06:08 -0300
commitb539d776bd86df9224d97048f26f33d2750d1c92 (patch)
tree4a246ca7c8680e0b2d32613e388116a5d39247cf
parentdaac46893448e49a21244d9646acf0ec47518537 (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.
-rw-r--r--README.md24
-rw-r--r--src/db.cpp191
-rw-r--r--src/db.h7
-rw-r--r--src/main_gui.cpp33
4 files changed, 255 insertions, 0 deletions
diff --git a/README.md b/README.md
index da8183a..16e979f 100644
--- a/README.md
+++ b/README.md
@@ -98,6 +98,30 @@ The second point is the pre-BIP32 problem that deterministic wallets were
98invented to solve; see [#47](https://github.com/Bitflash-sh/bitflash/issues/47). 98invented to solve; see [#47](https://github.com/Bitflash-sh/bitflash/issues/47).
99Until that lands, repetition is the only defence. 99Until that lands, repetition is the only defence.
100 100
101### When wallet.dat itself will not open
102
103Everything above still runs through Berkeley DB. If the database is the thing
104that is broken — a build that will not read it, a file truncated by a bad copy,
105an environment beyond recovery — the keys are usually still fine, and you can
106take them out as text:
107
108```
109bitflash -dumpwallet=/path/to/keys.txt
110bitflash -importwallet=/path/to/keys.txt # into any wallet, on any machine
111```
112
113One key per line with its address and label, no database and no environment.
114Importing skips keys the wallet already holds, so running it twice is safe, and
115an unreadable line is reported and stepped over rather than abandoning the rest.
116Restart the node afterwards so it scans the chain for transactions belonging to
117the new keys.
118
119**The dump is your private keys in the clear.** Anyone who reads that file can
120spend those coins. It is written owner-only on Linux and macOS; on Windows it
121inherits whatever the containing folder allows, so choose the folder carefully.
122Move it somewhere safe and delete the copy. `-dumpwallet` will not overwrite an
123existing file.
124
101--- 125---
102 126
103## Mining 127## Mining
diff --git a/src/db.cpp b/src/db.cpp
index fc5f180..4cc54ac 100644
--- a/src/db.cpp
+++ b/src/db.cpp
@@ -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
726static 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
734static 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
750bool 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
822bool 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}
diff --git a/src/db.h b/src/db.h
index 987786c..22efc77 100644
--- a/src/db.h
+++ b/src/db.h
@@ -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.
410bool BackupWallet(const string& strDest); 410bool 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.
416bool DumpWallet(const string& strDest);
417bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet);
418
412inline bool SetAddressBookName(const string& strAddress, const string& strName) 419inline 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; }