From b539d776bd86df9224d97048f26f33d2750d1c92 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Thu, 30 Jul 2026 12:06:08 -0300 Subject: 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. --- src/db.cpp | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) (limited to 'src/db.cpp') 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) printf("BackupWallet() : wrote %s\n", strDest.c_str()); return true; } + + +// ---- plain-text key export/import ------------------------------------------ +// +// The last door out. Everything else in this file assumes Berkeley DB will +// cooperate; these two do not. A wallet.dat can be unreadable for reasons that +// have nothing to do with your keys being gone -- a version this build does not +// understand, a file truncated by a bad copy, an environment that cannot be +// recovered -- and until now that meant the coins were unreachable even though +// the secrets were sitting right there. Issue #40 is one person's account of +// exactly that. +// +// The format is deliberately dull: a text file, one key per line, no database, +// no environment, no framing. Anything that can read a line can recover from it. + +static int HexDigitValue(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static bool ParseHexInto(const string& str, vector& vchRet) +{ + vchRet.clear(); + if (str.empty() || (str.size() % 2) != 0) + return false; + for (size_t i = 0; i < str.size(); i += 2) + { + int hi = HexDigitValue(str[i]); + int lo = HexDigitValue(str[i + 1]); + if (hi < 0 || lo < 0) + return false; + vchRet.push_back((unsigned char)((hi << 4) | lo)); + } + return true; +} + +bool DumpWallet(const string& strDest) +{ + if (strDest.empty()) + return error("DumpWallet() : no destination given\n"); + + // Never over an existing file. The whole point is to hold the only copy of + // something irreplaceable, and a mistyped path must not eat one. + if (FileExists(strDest.c_str())) + return error("DumpWallet() : %s already exists -- refusing to overwrite\n", + strDest.c_str()); + + FILE* pf = fopen(strDest.c_str(), "w"); + if (!pf) + return error("DumpWallet() : cannot write %s\n", strDest.c_str()); +#ifndef _WIN32 + // Before a single key reaches it. + chmod(strDest.c_str(), S_IRUSR | S_IWUSR); +#endif + + fprintf(pf, "# Bitflash wallet key dump\n"); + fprintf(pf, "# %s\n", DateTimeStr(GetTime()).c_str()); + fprintf(pf, "#\n"); + fprintf(pf, "# THIS FILE CONTAINS PRIVATE KEYS IN PLAIN TEXT.\n"); + fprintf(pf, "# Anyone who reads it can spend these coins. Treat it as cash.\n"); + fprintf(pf, "#\n"); + fprintf(pf, "# One key per line:
[label]\n"); + fprintf(pf, "# The key is the DER encoding, hex; restore with -importwallet=FILE.\n"); + fprintf(pf, "#\n"); + + int nKeys = 0; + bool fOk = true; + CRITICAL_BLOCK(cs_mapKeys) + { + for (map, CPrivKey>::iterator mi = mapKeys.begin(); + mi != mapKeys.end(); ++mi) + { + const vector& vchPubKey = (*mi).first; + const CPrivKey& vchPrivKey = (*mi).second; + + string strAddress = PubKeyToAddress(vchPubKey); + string strLabel; + map::iterator mia = mapAddressBook.find(strAddress); + if (mia != mapAddressBook.end()) + strLabel = (*mia).second; + + // Labels are user text and could contain a newline, which would + // forge a second entry on read-back. + for (size_t i = 0; i < strLabel.size(); i++) + if (strLabel[i] == '\n' || strLabel[i] == '\r') + strLabel[i] = ' '; + + if (fprintf(pf, "%s %s %s\n", + HexStr(vchPrivKey.begin(), vchPrivKey.end(), false).c_str(), + strAddress.c_str(), + strLabel.c_str()) < 0) + { fOk = false; break; } + nKeys++; + } + } + + if (fclose(pf) != 0) + fOk = false; + if (!fOk) + { + remove(strDest.c_str()); + return error("DumpWallet() : writing %s failed\n", strDest.c_str()); + } + + printf("DumpWallet() : wrote %d key(s) to %s\n", nKeys, strDest.c_str()); + return true; +} + +bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet) +{ + nAddedRet = 0; + nSkippedRet = 0; + + FILE* pf = fopen(strSrc.c_str(), "r"); + if (!pf) + return error("ImportWallet() : cannot read %s\n", strSrc.c_str()); + + char buf[8192]; + int nLine = 0; + int nBad = 0; + while (fgets(buf, sizeof(buf), pf)) + { + nLine++; + string strLine = buf; + while (!strLine.empty() && (strLine[strLine.size()-1] == '\n' || + strLine[strLine.size()-1] == '\r')) + strLine.resize(strLine.size() - 1); + if (strLine.empty() || strLine[0] == '#') + continue; + + string strHex = strLine.substr(0, strLine.find(' ')); + vector vchPrivKey; + if (!ParseHexInto(strHex, vchPrivKey)) + { + printf("ImportWallet() : line %d is not a hex key, skipped\n", nLine); + nBad++; + continue; + } + + CKey key; + // A key this build cannot load is reported and stepped over rather than + // aborting the run: one damaged line in a salvage file must not cost + // the reader every other key in it. + try + { + CPrivKey vchSecure(vchPrivKey.begin(), vchPrivKey.end()); + if (!key.SetPrivKey(vchSecure)) + { + printf("ImportWallet() : line %d is not a usable key, skipped\n", nLine); + nBad++; + continue; + } + } + catch (const std::exception& e) + { + printf("ImportWallet() : line %d rejected (%s), skipped\n", nLine, e.what()); + nBad++; + continue; + } + + bool fHave = false; + CRITICAL_BLOCK(cs_mapKeys) + fHave = mapKeys.count(key.GetPubKey()) > 0; + if (fHave) + { + nSkippedRet++; // already ours; importing twice is not an error + continue; + } + + if (!AddKey(key)) + { + fclose(pf); + return error("ImportWallet() : failed to store key from line %d\n", nLine); + } + nAddedRet++; + } + fclose(pf); + + printf("ImportWallet() : %d added, %d already present, %d unreadable\n", + nAddedRet, nSkippedRet, nBad); + + // Imported keys are only useful once their transactions are noticed, and + // that scan is a full pass over the chain the node does at startup. + if (nAddedRet > 0) + printf("ImportWallet() : restart the node so it can find transactions " + "belonging to the new keys\n"); + return true; +} -- cgit v1.2.3