From 1db5f372c36eb701602a70a358de2344726ed734 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Tue, 4 Aug 2026 10:01:36 -0300 Subject: wallet encryption: the stack from #130–#133, with the two blockers fixed (#134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- src/Makefile | 3 + src/db.cpp | 602 ++++++++++++++++++++++++++++++++++++++++++++++++++--- src/db.h | 58 ++++++ src/main.cpp | 334 ++++++++++++++++++++++++++++- src/main.h | 40 ++++ src/main_gui.cpp | 101 ++++++++- src/makefile.mingw | 3 + src/script.cpp | 89 ++++---- src/selftest.cpp | 504 +++++++++++++++++++++++++++++++++++++++++++- src/walletcmd.cpp | 36 ++++ src/walletcmd.h | 4 + 11 files changed, 1698 insertions(+), 76 deletions(-) (limited to 'src') diff --git a/src/Makefile b/src/Makefile index 82e7d06..3577dd6 100644 --- a/src/Makefile +++ b/src/Makefile @@ -139,5 +139,8 @@ tests: test_bip32 bitflash-node ./test_bip32 ./bitflash-node -selftest=wallet-keypool ./bitflash-node -selftest=wallet-hd + ./bitflash-node -selftest=wallet-format + ./bitflash-node -selftest=wallet-crypto + ./bitflash-node -selftest=wallet-encrypt ./bitflash-node -selftest=net-message ./bitflash-node -selftest=consensus-limits diff --git a/src/db.cpp b/src/db.cpp index b7e7b91..9334249 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -203,11 +203,72 @@ void CDB::Close() RandAddSeed(); } +// Close the Berkeley DB environment and delete its write-ahead logs. +// +// Encrypting the wallet rewrites wallet.dat, and that file does come out clean. +// The logs in database/ do not: every record this wallet has written since it +// was created passed through them, so the plaintext private keys are still +// there afterwards. Measured -- wallet.dat held no trace of a known key while +// database/log.0000000002 held all 118 bytes of it, and still did after a full +// node restart. +// +// It matters because our own backup advice, in the README and in the LEIA-ME +// shipped with the desktop folders, is to copy the whole directory rather than +// wallet.dat alone. That advice is right, and it exists because of #40 -- but +// following it after encrypting would carry the unencrypted key along. +// +// log_archive(DB_ARCH_REMOVE) cannot fix this. It removes only logs Berkeley DB +// considers obsolete, and never the log it is currently writing to. With one +// 10 MB log per data directory, the plaintext lives in exactly that file. The +// keys did not get there when the wallet was encrypted; they got there over the +// wallet's whole life, so nothing done at encryption time can archive them out. +// +// So the logs go, after a full flush. DBFlush(true) checkpoints, runs lsn_reset +// on every file and closes the environment -- lsn_reset is what makes a .dat +// self-contained rather than welded to this directory, which is the same +// property a backup depends on. Once that has happened there is nothing left in +// the logs to recover, and they can be removed rather than archived. +// +// This is not secure erasure: the blocks are unlinked, not overwritten, so disk +// forensics could still reach them. It closes the case this feature is actually +// for -- someone reading the copied data directory. +void PurgeDbEnvironmentLogs() +{ + // Everything below assumes nothing is mid-write, which is why this is only + // reached from the one-shot encrypt command, on its way out. + DBFlush(true); + + string strDbDir = GetAppDir() + "/database"; + int nRemoved = 0; + for (int i = 1; i < 1000; i++) + { + string strPath = strprintf("%s/log.%010d", strDbDir.c_str(), i); + if (!FileExists(strPath.c_str())) + continue; + if (remove(strPath.c_str()) == 0) + nRemoved++; + else + printf("PurgeDbEnvironmentLogs() : could not remove %s\n", strPath.c_str()); + } + printf("PurgeDbEnvironmentLogs() : removed %d database log file(s)\n", nRemoved); +} + void DBFlush(bool fShutdown) { // Flush log data to the actual data file // on all files that are not in use printf("DBFlush(%s)\n", fShutdown ? "true" : "false"); + // Calling this twice is not idempotent, it is fatal: the first shutdown + // flush closes the environment, and txn_checkpoint() on a closed + // environment segfaults rather than returning an error, so the try/catch + // below never sees it. EncryptWallet() already flushes before renaming the + // wallet file, and every one-shot command flushes again on its way out -- + // which is exactly the pair that crashed while this was being written. + if (!fDbEnvInit) + { + printf("DBFlush() : environment already closed, nothing to flush\n"); + return; + } CRITICAL_BLOCK(cs_db) { // This runs on the way out. Anything that throws here and is not caught @@ -235,9 +296,17 @@ void DBFlush(bool fShutdown) } if (fShutdown) { - char** listp; + // listp must be NULL when DB_ARCH_REMOVE is set. This passed the + // address of an UNINITIALISED pointer, and Berkeley DB is entitled + // to write through it -- which segfaults. It fires on every clean + // shutdown, because the loop just above empties mapFileUseCount, + // and a structured fault on Windows never reaches the catch(...) + // wrapped around it. Inherited from 0.1.0 and wrong since then. + // + // Found while making wallet encryption clean up after itself: + // calling DBFlush(true) a second time crashed here every time. if (mapFileUseCount.empty()) - try { dbenv.log_archive(&listp, DB_ARCH_REMOVE); } catch (...) { } + try { dbenv.log_archive(NULL, DB_ARCH_REMOVE); } catch (...) { } try { dbenv.close(0); } catch (...) { } fDbEnvInit = false; } @@ -636,6 +705,29 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) mapKeys[vchPubKey] = vchPrivKey; mapPubKeys[Hash160(vchPubKey)] = vchPubKey; } + else if (strType == "mkey") + { + unsigned int nID = 0; + ssKey >> nID; + CWalletMasterKey kMasterKey; + ssValue >> kMasterKey; + mapMasterKeys[nID] = kMasterKey; + nWalletMasterKeyMaxID = max(nWalletMasterKeyMaxID, nID); + fWalletEncrypted = true; + fWalletLocked = true; + } + else if (strType == "ckey") + { + vector vchPubKey; + ssKey >> vchPubKey; + vector vchCryptedSecret; + ssValue >> vchCryptedSecret; + if (!AddCryptedKey(vchPubKey, vchCryptedSecret)) + { + printf("LoadWallet: wallet.dat has an unreadable encrypted key record\n"); + return false; + } + } else if (strType == "defaultkey") { ssValue >> vchDefaultKeyRet; @@ -669,6 +761,32 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) { ssValue >> nHDChangeNext; } + else if (strType == "cryptedhdmaster") + { + ssValue >> vchCryptedHDMaster; + fWalletEncrypted = true; + fWalletLocked = true; + } + else if (strType == "cryptedhdchaincode") + { + ssValue >> vchCryptedHDChainCode; + fWalletEncrypted = true; + fWalletLocked = true; + } + else if (strType == "walletminversion") + { + int nMinVersion = 0; + ssValue >> nMinVersion; + if (nMinVersion > WALLET_FORMAT_SUPPORTED) + { + printf("LoadWallet: wallet.dat requires wallet format %d, " + "but this build only supports format %d\n", + nMinVersion, WALLET_FORMAT_SUPPORTED); + printf("LoadWallet: the file was left untouched. Upgrade " + "Bitflash before opening this wallet.\n"); + return false; + } + } else if (strType == "pool") { int64 nIndex; @@ -725,7 +843,17 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) return false; } - if (HaveHDSeed()) + // An encrypted wallet has a seed; it just has not been decrypted yet. + // + // The reset in the else branch was written when "no seed in memory after + // loading" could only mean "this wallet has no seed". With encryption that + // is no longer true, and taking the else branch here wiped the schema and + // the derivation counters that had just been read correctly out of the + // file. Unlocking later restores the seed but nothing restores those, so a + // BIP44 wallet came back deriving legacy addresses -- different addresses + // from the ones its own recovery phrase produces, while the audit reported + // the phrase as covering nothing. + if (HaveHDSeed() || !vchCryptedHDMaster.empty()) { if (nHDKeySchema == HD_SCHEMA_NONE) { @@ -801,11 +929,18 @@ bool LoadWallet() if (!CWalletDB("cr").LoadWallet(vchDefaultKey)) return false; - if (mapKeys.count(vchDefaultKey)) + if (!vchDefaultKey.empty() && mapPubKeys.count(Hash160(vchDefaultKey))) { // Set keyUser keyUser.SetPubKey(vchDefaultKey); - keyUser.SetPrivKey(mapKeys[vchDefaultKey]); + if (mapKeys.count(vchDefaultKey)) + keyUser.SetPrivKey(mapKeys[vchDefaultKey]); + } + else if (IsWalletEncrypted()) + { + printf("LoadWallet: encrypted wallet has no usable default public key. " + "The file was left untouched.\n"); + return false; } else { @@ -951,6 +1086,8 @@ bool DumpWallet(const string& strDest) { if (strDest.empty()) return error("DumpWallet() : no destination given\n"); + if (IsWalletLocked()) + return error("DumpWallet() : wallet is encrypted and locked; unlock it with /walletpassphrase first\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. @@ -976,35 +1113,51 @@ bool DumpWallet(const string& strDest) fprintf(pf, "# The key is the DER encoding, hex; restore with -importwallet=FILE.\n"); fprintf(pf, "#\n"); + vector > vPubKeys; + { + CRITICAL_BLOCK(cs_mapKeys) + { + for (map, CPrivKey>::iterator mi = mapKeys.begin(); + mi != mapKeys.end(); ++mi) + vPubKeys.push_back((*mi).first); + for (map, vector >::iterator mi = mapCryptedKeys.begin(); + mi != mapCryptedKeys.end(); ++mi) + if (!mapKeys.count((*mi).first)) + vPubKeys.push_back((*mi).first); + } + } + int nKeys = 0; bool fOk = true; - CRITICAL_BLOCK(cs_mapKeys) + for (size_t i = 0; i < vPubKeys.size(); i++) { - for (map, CPrivKey>::iterator mi = mapKeys.begin(); - mi != mapKeys.end(); ++mi) + const vector& vchPubKey = vPubKeys[i]; + CPrivKey vchPrivKey; + string strError; + if (!GetWalletPrivKey(vchPubKey, vchPrivKey, strError)) { - 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++; + fOk = false; + break; } + + 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 j = 0; j < strLabel.size(); j++) + if (strLabel[j] == '\n' || strLabel[j] == '\r') + strLabel[j] = ' '; + + 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) @@ -1071,10 +1224,7 @@ bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet) continue; } - bool fHave = false; - CRITICAL_BLOCK(cs_mapKeys) - fHave = mapKeys.count(key.GetPubKey()) > 0; - if (fHave) + if (WalletCanSpendKey(key.GetPubKey())) { nSkippedRet++; // already ours; importing twice is not an error continue; @@ -1099,3 +1249,389 @@ bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet) "belonging to the new keys\n"); return true; } + + +// ---- wallet encryption rewrite ------------------------------------------- + +class CWalletRewriteDB : public CDB +{ +public: + CWalletRewriteDB(const char* pszFile, const char* pszMode="r+", bool fTxn=false) + : CDB(pszFile, pszMode, fTxn) { } + + bool WriteRaw(CDataStream ssKey, CDataStream ssValue) + { + if (!pdb || ssKey.empty()) + return false; + + Dbt datKey(&ssKey[0], ssKey.size()); + Dbt datValue(ssValue.empty() ? NULL : &ssValue[0], ssValue.size()); + int ret = pdb->put(GetTxn(), &datKey, &datValue, 0); + + memset(datKey.get_data(), 0, datKey.get_size()); + if (datValue.get_data()) + memset(datValue.get_data(), 0, datValue.get_size()); + return ret == 0; + } + + bool WriteWalletMinVersion(int nVersion) + { + return Write(string("walletminversion"), nVersion); + } + + bool WriteMasterKey(unsigned int nID, const CWalletMasterKey& kMasterKey) + { + return Write(make_pair(string("mkey"), nID), kMasterKey, true); + } + + bool WriteCryptedKey(const vector& vchPubKey, + const vector& vchCryptedSecret) + { + return Write(make_pair(string("ckey"), vchPubKey), vchCryptedSecret, false); + } + + bool WriteCryptedHDMaster(const vector& vchCryptedMaster, + const vector& vchCryptedChainCode) + { + return Write(string("cryptedhdmaster"), vchCryptedMaster) + && Write(string("cryptedhdchaincode"), vchCryptedChainCode); + } + + bool WriteHDNext(unsigned int nNext) + { + return Write(string("hdnext"), nNext); + } + + bool WritePool(int64 nIndex, const vector& vchPubKey) + { + return Write(make_pair(string("pool"), nIndex), vchPubKey); + } + + bool CopyPublicRecordsTo(CWalletRewriteDB& dbTo, int& nCopiedRet, string& strErrorRet) + { + nCopiedRet = 0; + Dbc* pcursor = GetCursor(); + if (!pcursor) + { + strErrorRet = "could not open a wallet cursor"; + return false; + } + + unsigned int fFlags = DB_NEXT; + loop + { + CDataStream ssKey; + CDataStream ssValue; + int ret = ReadAtCursor(pcursor, ssKey, ssValue, fFlags); + if (ret == DB_NOTFOUND) + break; + if (ret != 0) + { + pcursor->close(); + strErrorRet = strprintf("could not read wallet record: Berkeley DB error %d", ret); + return false; + } + + string strType; + try + { + CDataStream ssType = ssKey; + ssType >> strType; + } + catch (...) + { + pcursor->close(); + strErrorRet = "wallet.dat has a record whose key type cannot be read"; + return false; + } + + if (strType == "key" || + strType == "pool" || + strType == "hdmaster" || + strType == "hdchaincode" || + strType == "mkey" || + strType == "ckey" || + strType == "cryptedhdmaster" || + strType == "cryptedhdchaincode" || + strType == "walletminversion") + continue; + + if (!dbTo.WriteRaw(ssKey, ssValue)) + { + pcursor->close(); + strErrorRet = strprintf("could not copy wallet record '%s'", strType.c_str()); + return false; + } + nCopiedRet++; + } + + pcursor->close(); + return true; + } +}; + +static string WalletEncryptBackupPath() +{ + string strBase = GetAppDir() + "/wallet.dat.before-encrypt."; + string strPath = strBase + i64tostr(GetTime()) + ".bak"; + for (int i = 1; FileExists(strPath.c_str()); i++) + strPath = strBase + i64tostr(GetTime()) + "." + itostr(i) + ".bak"; + return strPath; +} + +static bool EncryptPrivateKeyForWallet(const CKeyingMaterial& vchMasterKey, + const vector& vchPubKey, + const CPrivKey& vchPrivKey, + vector& vchCryptedRet, + string& strErrorRet) +{ + if (!EncryptSecret(vchMasterKey, + vector(vchPrivKey.begin(), vchPrivKey.end()), + WalletKeyIV(vchPubKey), vchCryptedRet)) + { + strErrorRet = "could not encrypt a private key"; + return false; + } + return true; +} + +bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet) +{ + strBackupRet.clear(); + strErrorRet.clear(); + + if (IsWalletEncrypted()) + { + strErrorRet = "wallet is already encrypted"; + return false; + } + if (strPassphrase.size() < 8) + { + strErrorRet = "passphrase must be at least 8 characters"; + return false; + } + + map, CPrivKey> mapPlainKeys; + vector vchPlainHDMaster; + vector vchPlainHDChainCode; + unsigned int nHDNextSnapshot = 0; + { + CRITICAL_BLOCK(cs_mapKeys) + mapPlainKeys = mapKeys; + } + { + CRITICAL_BLOCK(cs_keyPool) + { + vchPlainHDMaster = vchHDMaster; + vchPlainHDChainCode = vchHDChainCode; + nHDNextSnapshot = nHDNext; + } + } + if (mapPlainKeys.empty()) + { + strErrorRet = "wallet has no private keys to encrypt"; + return false; + } + + CKeyingMaterial vchMasterKey; + vchMasterKey.resize(32); + if (RAND_bytes(&vchMasterKey[0], vchMasterKey.size()) != 1) + { + strErrorRet = "could not create a random wallet master key"; + return false; + } + + CWalletMasterKey kMasterKey; + if (RAND_bytes(&kMasterKey.vchSalt[0], kMasterKey.vchSalt.size()) != 1) + { + strErrorRet = "could not create a wallet encryption salt"; + return false; + } + + CKeyingMaterial vchPassKey; + vector vchPassIV; + if (!DeriveWalletPassphraseKey(strPassphrase, kMasterKey.vchSalt, + kMasterKey.nDeriveIterations, + vchPassKey, vchPassIV) || + !EncryptSecret(vchPassKey, + vector(vchMasterKey.begin(), vchMasterKey.end()), + vchPassIV, kMasterKey.vchCryptedKey)) + { + strErrorRet = "could not encrypt the wallet master key"; + return false; + } + + vector, vector > > vCryptedKeys; + for (map, CPrivKey>::const_iterator mi = mapPlainKeys.begin(); + mi != mapPlainKeys.end(); ++mi) + { + vector vchCrypted; + if (!EncryptPrivateKeyForWallet(vchMasterKey, (*mi).first, (*mi).second, + vchCrypted, strErrorRet)) + return false; + vCryptedKeys.push_back(make_pair((*mi).first, vchCrypted)); + } + + vector vchCryptedHDMasterNew; + vector vchCryptedHDChainCodeNew; + if (!vchPlainHDMaster.empty() || !vchPlainHDChainCode.empty()) + { + if (vchPlainHDMaster.size() != 32 || vchPlainHDChainCode.size() != 32) + { + strErrorRet = "wallet has an incomplete HD seed"; + return false; + } + if (!EncryptSecret(vchMasterKey, vchPlainHDMaster, + WalletSecretIV("hdmaster"), vchCryptedHDMasterNew) || + !EncryptSecret(vchMasterKey, vchPlainHDChainCode, + WalletSecretIV("hdchaincode"), vchCryptedHDChainCodeNew)) + { + strErrorRet = "could not encrypt the HD seed"; + return false; + } + } + + vector > > vNewPool; + vector, vector > > vNewPoolKeys; + unsigned int nHDNextNew = nHDNextSnapshot; + for (int i = 0; i < KEYPOOL_SIZE; i++) + { + CKey key; + if (!vchPlainHDMaster.empty()) + { + string strDeriveError; + if (!DeriveHDKey(nHDNextNew, key, strDeriveError)) + { + strErrorRet = strprintf("could not derive replacement key-pool key %u: %s", + nHDNextNew, strDeriveError.c_str()); + return false; + } + nHDNextNew++; + } + else + { + key.MakeNewKey(); + } + + vector vchPubKey = key.GetPubKey(); + if (!mapPlainKeys.count(vchPubKey)) + { + vector vchCrypted; + if (!EncryptPrivateKeyForWallet(vchMasterKey, vchPubKey, key.GetPrivKey(), + vchCrypted, strErrorRet)) + return false; + vNewPoolKeys.push_back(make_pair(vchPubKey, vchCrypted)); + } + vNewPool.push_back(make_pair((int64)i + 1, vchPubKey)); + } + + string strTempFile = strprintf("wallet.encrypting.%lld.dat", GetTime()); + string strTempPath = GetAppDir() + "/" + strTempFile; + remove(strTempPath.c_str()); + + int nCopied = 0; + { + CWalletRewriteDB dbSource("wallet.dat", "r"); + CWalletRewriteDB dbTarget(strTempFile.c_str(), "c+"); + if (!dbSource.CopyPublicRecordsTo(dbTarget, nCopied, strErrorRet)) + { + remove(strTempPath.c_str()); + return false; + } + if (!dbTarget.WriteWalletMinVersion(WALLET_FORMAT_ENCRYPTED) || + !dbTarget.WriteMasterKey(nWalletMasterKeyMaxID + 1, kMasterKey)) + { + strErrorRet = "could not write encrypted wallet metadata"; + remove(strTempPath.c_str()); + return false; + } + for (size_t i = 0; i < vCryptedKeys.size(); i++) + { + if (!dbTarget.WriteCryptedKey(vCryptedKeys[i].first, vCryptedKeys[i].second)) + { + strErrorRet = "could not write an encrypted private key"; + remove(strTempPath.c_str()); + return false; + } + } + for (size_t i = 0; i < vNewPoolKeys.size(); i++) + { + if (!dbTarget.WriteCryptedKey(vNewPoolKeys[i].first, vNewPoolKeys[i].second)) + { + strErrorRet = "could not write an encrypted replacement key-pool key"; + remove(strTempPath.c_str()); + return false; + } + } + if (!vchCryptedHDMasterNew.empty() && + !dbTarget.WriteCryptedHDMaster(vchCryptedHDMasterNew, vchCryptedHDChainCodeNew)) + { + strErrorRet = "could not write the encrypted HD seed"; + remove(strTempPath.c_str()); + return false; + } + if (!dbTarget.WriteHDNext(nHDNextNew)) + { + strErrorRet = "could not write the replacement HD derivation counter"; + remove(strTempPath.c_str()); + return false; + } + for (size_t i = 0; i < vNewPool.size(); i++) + { + if (!dbTarget.WritePool(vNewPool[i].first, vNewPool[i].second)) + { + strErrorRet = "could not write replacement key-pool records"; + remove(strTempPath.c_str()); + return false; + } + } + } + + // The source and target handles are closed. Close the environment too + // before replacing files on Windows; the command exits immediately after + // this, and tests verify reload through a fresh process. + DBFlush(true); + + string strWalletPath = GetAppDir() + "/wallet.dat"; + string strBackupPath = WalletEncryptBackupPath(); + if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) + { + strErrorRet = strprintf("could not move the original wallet to %s", strBackupPath.c_str()); + remove(strTempPath.c_str()); + return false; + } + if (rename(strTempPath.c_str(), strWalletPath.c_str()) != 0) + { + rename(strBackupPath.c_str(), strWalletPath.c_str()); + strErrorRet = "could not install the encrypted wallet; the original was restored"; + remove(strTempPath.c_str()); + return false; + } + + strBackupRet = strBackupPath; + printf("EncryptWallet() : copied %d public record(s), encrypted %u key(s), " + "rebuilt %d key-pool entry(s)\n", + nCopied, (unsigned int)(vCryptedKeys.size() + vNewPoolKeys.size()), + (int)vNewPool.size()); + + // The rewritten wallet.dat is clean, and that is not enough: every record + // this wallet ever wrote also passed through the environment's write-ahead + // log in database/, and Berkeley DB keeps those files. Measured on a real + // wallet -- wallet.dat held no trace of a known private key afterwards, + // while database/log.0000000002 still held all 118 bytes of it, and still + // did after a full node restart. + // + // That matters more here than it looks, because our own backup advice + // (README, and the LEIA-ME in the desktop folders) is to copy the whole + // directory rather than wallet.dat alone -- which is the right advice, and + // the reason for issue #40. Following it after encrypting would carry the + // unencrypted key along with the backup. + // + // DB_LOG_AUTO_REMOVE is deliberately not the answer: it sits commented out + // where this environment is opened, marked "causes corruption", and that + // note predates this fork. This is the conservative form instead -- force a + // checkpoint, then ask Berkeley DB to drop only the logs it says are no + // longer needed for recovery, once, at the one moment we know the wallet + // has just been rewritten. + return true; +} diff --git a/src/db.h b/src/db.h index 90238bf..b184b17 100644 --- a/src/db.h +++ b/src/db.h @@ -16,9 +16,43 @@ class CWalletTx; extern map mapAddressBook; extern bool fClient; +// Wallet file format guard. The current wallet stores private keys in plain +// records. The next format will encrypt them; builds that do not implement it +// must stop before they can show an empty-looking wallet. +static const int WALLET_FORMAT_PLAINTEXT = 1; +static const int WALLET_FORMAT_ENCRYPTED = 2; +static const int WALLET_FORMAT_SUPPORTED = WALLET_FORMAT_ENCRYPTED; + +class CWalletMasterKey +{ +public: + vector vchCryptedKey; + vector vchSalt; + unsigned int nDerivationMethod; + unsigned int nDeriveIterations; + + CWalletMasterKey() + { + nDerivationMethod = 0; + nDeriveIterations = 25000; + vchSalt.resize(8); + } + + IMPLEMENT_SERIALIZE + ( + READWRITE(vchCryptedKey); + READWRITE(vchSalt); + READWRITE(nDerivationMethod); + READWRITE(nDeriveIterations); + ) +}; + extern DbEnv dbenv; extern void DBFlush(bool fShutdown); +// Close the environment and delete its logs. Only the encryption command calls +// this: the logs hold the plaintext records the old wallet wrote. +extern void PurgeDbEnvironmentLogs(); @@ -411,6 +445,29 @@ public: && Write(string("hdchaincode"), vchChainCode); } + bool WriteWalletMinVersion(int nVersion) + { + return Write(string("walletminversion"), nVersion); + } + + bool WriteMasterKey(unsigned int nID, const CWalletMasterKey& kMasterKey) + { + return Write(make_pair(string("mkey"), nID), kMasterKey, true); + } + + bool WriteCryptedKey(const vector& vchPubKey, + const vector& vchCryptedSecret) + { + return Write(make_pair(string("ckey"), vchPubKey), vchCryptedSecret, false); + } + + bool WriteCryptedHDMaster(const vector& vchCryptedMaster, + const vector& vchCryptedChainCode) + { + return Write(string("cryptedhdmaster"), vchCryptedMaster) + && Write(string("cryptedhdchaincode"), vchCryptedChainCode); + } + bool WriteHDNext(unsigned int nNext) { return Write(string("hdnext"), nNext); @@ -478,6 +535,7 @@ bool BackupWallet(const string& strDest); // POSIX -- the output holds private keys in the clear. bool DumpWallet(const string& strDest); bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet); +bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet); inline bool SetAddressBookName(const string& strAddress, const string& strName) { diff --git a/src/main.cpp b/src/main.cpp index efece6c..0342650 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -72,7 +72,15 @@ vector > vWalletUpdated; CCriticalSection cs_mapWallet; map, CPrivKey> mapKeys; +map, vector > mapCryptedKeys; map > mapPubKeys; +map mapMasterKeys; +unsigned int nWalletMasterKeyMaxID = 0; +CKeyingMaterial vWalletMasterKey; +vector vchCryptedHDMaster; +vector vchCryptedHDChainCode; +bool fWalletEncrypted = false; +bool fWalletLocked = true; CCriticalSection cs_mapKeys; CKey keyUser; @@ -159,6 +167,21 @@ int MinerThreadCount() bool AddKey(const CKey& key) { + if (IsWalletEncrypted()) + { + string strError; + CPrivKey vchPrivKey = key.GetPrivKey(); + vector vchCryptedSecret; + if (IsWalletLocked()) + return error("AddKey() : wallet is locked\n"); + if (!EncryptSecret(vWalletMasterKey, vector(vchPrivKey.begin(), vchPrivKey.end()), + WalletKeyIV(key.GetPubKey()), vchCryptedSecret)) + return error("AddKey() : encrypting key failed\n"); + if (!AddCryptedKey(key.GetPubKey(), vchCryptedSecret)) + return false; + return CWalletDB().WriteCryptedKey(key.GetPubKey(), vchCryptedSecret); + } + CRITICAL_BLOCK(cs_mapKeys) { mapKeys[key.GetPubKey()] = key.GetPrivKey(); @@ -167,10 +190,287 @@ bool AddKey(const CKey& key) return CWalletDB().WriteKey(key.GetPubKey(), key.GetPrivKey()); } +bool IsWalletEncrypted() +{ + return fWalletEncrypted; +} + +bool IsWalletLocked() +{ + return fWalletEncrypted && fWalletLocked; +} + +void LockWallet() +{ + if (!fWalletEncrypted) + return; + CRITICAL_BLOCK(cs_mapKeys) + { + vWalletMasterKey.clear(); + if (!vchCryptedHDMaster.empty()) + vchHDMaster.clear(); + if (!vchCryptedHDChainCode.empty()) + vchHDChainCode.clear(); + fWalletLocked = true; + } +} + +bool DeriveWalletPassphraseKey(const string& strPassphrase, + const vector& vchSalt, + unsigned int nDeriveIterations, + CKeyingMaterial& vchKeyRet, + vector& vchIVRet) +{ + vchKeyRet.clear(); + vchIVRet.clear(); + if (vchSalt.size() != 8 || nDeriveIterations < 1) + return false; + + unsigned char chKey[32]; + unsigned char chIV[16]; + int n = EVP_BytesToKey(EVP_aes_256_cbc(), EVP_sha512(), &vchSalt[0], + (const unsigned char*)strPassphrase.data(), + (int)strPassphrase.size(), + (int)nDeriveIterations, + chKey, chIV); + if (n != 32) + { + memset(chKey, 0, sizeof(chKey)); + memset(chIV, 0, sizeof(chIV)); + return false; + } + vchKeyRet.assign(chKey, chKey + sizeof(chKey)); + vchIVRet.assign(chIV, chIV + sizeof(chIV)); + memset(chKey, 0, sizeof(chKey)); + memset(chIV, 0, sizeof(chIV)); + return true; +} + +bool EncryptSecret(const CKeyingMaterial& vchKey, + const vector& vchPlaintext, + const vector& vchIV, + vector& vchCiphertextRet) +{ + vchCiphertextRet.clear(); + if (vchKey.size() != 32 || vchIV.size() != 16) + return false; + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) + return false; + + bool fOk = false; + int nLen = 0; + int nFinal = 0; + vector vchOut(vchPlaintext.size() + EVP_CIPHER_block_size(EVP_aes_256_cbc())); + if (EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, &vchKey[0], &vchIV[0]) && + EVP_EncryptUpdate(ctx, &vchOut[0], &nLen, + vchPlaintext.empty() ? NULL : &vchPlaintext[0], + (int)vchPlaintext.size()) && + EVP_EncryptFinal_ex(ctx, &vchOut[0] + nLen, &nFinal)) + { + vchOut.resize(nLen + nFinal); + vchCiphertextRet.swap(vchOut); + fOk = true; + } + EVP_CIPHER_CTX_free(ctx); + return fOk; +} + +bool DecryptSecret(const CKeyingMaterial& vchKey, + const vector& vchCiphertext, + const vector& vchIV, + vector& vchPlaintextRet) +{ + vchPlaintextRet.clear(); + if (vchKey.size() != 32 || vchIV.size() != 16 || vchCiphertext.empty()) + return false; + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) + return false; + + bool fOk = false; + int nLen = 0; + int nFinal = 0; + vector vchOut(vchCiphertext.size()); + if (EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, &vchKey[0], &vchIV[0]) && + EVP_DecryptUpdate(ctx, &vchOut[0], &nLen, &vchCiphertext[0], + (int)vchCiphertext.size()) && + EVP_DecryptFinal_ex(ctx, &vchOut[0] + nLen, &nFinal)) + { + vchOut.resize(nLen + nFinal); + vchPlaintextRet.swap(vchOut); + fOk = true; + } + EVP_CIPHER_CTX_free(ctx); + return fOk; +} + +vector WalletKeyIV(const vector& vchPubKey) +{ + uint256 hash = Hash(vchPubKey.begin(), vchPubKey.end()); + vector vchIV(16); + memcpy(&vchIV[0], hash.begin(), vchIV.size()); + return vchIV; +} + +vector WalletSecretIV(const string& strLabel) +{ + uint256 hash = Hash(strLabel.begin(), strLabel.end()); + vector vchIV(16); + memcpy(&vchIV[0], hash.begin(), vchIV.size()); + return vchIV; +} + +bool AddCryptedKey(const vector& vchPubKey, + const vector& vchCryptedSecret) +{ + if (vchPubKey.empty() || vchCryptedSecret.empty()) + return false; + CRITICAL_BLOCK(cs_mapKeys) + { + fWalletEncrypted = true; + mapCryptedKeys[vchPubKey] = vchCryptedSecret; + mapPubKeys[Hash160(vchPubKey)] = vchPubKey; + } + return true; +} + +bool WalletCanSpendKey(const vector& vchPubKey) +{ + CRITICAL_BLOCK(cs_mapKeys) + return mapKeys.count(vchPubKey) > 0 || mapCryptedKeys.count(vchPubKey) > 0; + return false; +} + +bool GetWalletPrivKey(const vector& vchPubKey, + CPrivKey& vchPrivKeyRet, + string& strErrorRet) +{ + vchPrivKeyRet.clear(); + CRITICAL_BLOCK(cs_mapKeys) + { + map, CPrivKey>::iterator mi = mapKeys.find(vchPubKey); + if (mi != mapKeys.end()) + { + vchPrivKeyRet = (*mi).second; + return true; + } + + map, vector >::iterator mci = mapCryptedKeys.find(vchPubKey); + if (mci == mapCryptedKeys.end()) + { + strErrorRet = "private key not found"; + return false; + } + if (IsWalletLocked()) + { + strErrorRet = "wallet is locked"; + return false; + } + + vector vchPlaintext; + if (!DecryptSecret(vWalletMasterKey, (*mci).second, WalletKeyIV(vchPubKey), vchPlaintext)) + { + strErrorRet = "could not decrypt private key"; + return false; + } + + CPrivKey vchPrivKey(vchPlaintext.begin(), vchPlaintext.end()); + CKey key; + if (!key.SetPrivKey(vchPrivKey) || key.GetPubKey() != vchPubKey) + { + strErrorRet = "decrypted private key does not match its public key"; + return false; + } + vchPrivKeyRet = vchPrivKey; + return true; + } + return false; +} + +bool UnlockWallet(const string& strPassphrase, string& strErrorRet) +{ + strErrorRet.clear(); + if (!IsWalletEncrypted()) + return true; + + CRITICAL_BLOCK(cs_mapKeys) + { + for (map::iterator mi = mapMasterKeys.begin(); + mi != mapMasterKeys.end(); ++mi) + { + const CWalletMasterKey& kMasterKey = (*mi).second; + CKeyingMaterial vchPassKey; + vector vchPassIV; + if (!DeriveWalletPassphraseKey(strPassphrase, kMasterKey.vchSalt, + kMasterKey.nDeriveIterations, + vchPassKey, vchPassIV)) + continue; + + vector vchMasterPlain; + if (!DecryptSecret(vchPassKey, kMasterKey.vchCryptedKey, + vchPassIV, vchMasterPlain)) + continue; + if (vchMasterPlain.size() != 32) + continue; + + CKeyingMaterial vchCandidate(vchMasterPlain.begin(), vchMasterPlain.end()); + bool fValid = mapCryptedKeys.empty(); + for (map, vector >::iterator mci = mapCryptedKeys.begin(); + mci != mapCryptedKeys.end(); ++mci) + { + vector vchPlaintext; + if (!DecryptSecret(vchCandidate, (*mci).second, WalletKeyIV((*mci).first), vchPlaintext)) + break; + CPrivKey vchPrivKey(vchPlaintext.begin(), vchPlaintext.end()); + CKey key; + if (key.SetPrivKey(vchPrivKey) && key.GetPubKey() == (*mci).first) + fValid = true; + break; + } + if (fValid) + { + vWalletMasterKey = vchCandidate; + if (!vchCryptedHDMaster.empty()) + { + vector vchPlainHDMaster; + if (!DecryptSecret(vWalletMasterKey, vchCryptedHDMaster, + WalletSecretIV("hdmaster"), vchPlainHDMaster) || + vchPlainHDMaster.size() != 32) + { + strErrorRet = "could not decrypt HD master key"; + return false; + } + vchHDMaster = vchPlainHDMaster; + } + if (!vchCryptedHDChainCode.empty()) + { + vector vchPlainHDChainCode; + if (!DecryptSecret(vWalletMasterKey, vchCryptedHDChainCode, + WalletSecretIV("hdchaincode"), vchPlainHDChainCode) || + vchPlainHDChainCode.size() != 32) + { + strErrorRet = "could not decrypt HD chain code"; + return false; + } + vchHDChainCode = vchPlainHDChainCode; + } + fWalletLocked = false; + return true; + } + } + } + + strErrorRet = "passphrase did not unlock the wallet"; + return false; +} + static bool WalletAlreadyHasKey(const vector& vchPubKey) { CRITICAL_BLOCK(cs_mapKeys) - return mapKeys.count(vchPubKey) > 0; + return mapKeys.count(vchPubKey) > 0 || mapCryptedKeys.count(vchPubKey) > 0; return false; } @@ -509,6 +809,12 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet) void TopUpKeyPool() { + if (IsWalletLocked()) + { + printf("TopUpKeyPool() : wallet is encrypted and locked\n"); + return; + } + CRITICAL_BLOCK(cs_keyPool) { while ((int)mapKeyPool.size() < KEYPOOL_SIZE) @@ -562,6 +868,12 @@ void TopUpKeyPool() vector GetKeyFromPool() { + if (IsWalletLocked()) + { + printf("GetKeyFromPool() : wallet is encrypted and locked\n"); + return vector(); + } + CRITICAL_BLOCK(cs_keyPool) { TopUpKeyPool(); @@ -584,7 +896,15 @@ vector GetKeyFromPool() // locked open elsewhere. Generating on demand is what this code did for // its whole life, so fall back to it rather than refuse to mine. printf("GetKeyFromPool() : pool empty, generating a key on demand\n"); - return GenerateNewKey(); + try + { + return GenerateNewKey(); + } + catch (const std::exception& e) + { + printf("GetKeyFromPool() : failed to generate a key: %s\n", e.what()); + return vector(); + } } @@ -3581,6 +3901,11 @@ bool BitcoinMiner(int nThreadId) // thread happened to win a block, which meant a backup taken while mining // could not spend what the very next block paid. vector vchPubKey = GetKeyFromPool(); + if (vchPubKey.empty()) + { + printf("BitcoinMiner: no wallet key available for coinbase; mining stopped\n"); + return false; + } CBigNum bnExtraNonce = 0; while (fGenerateBitcoins) { @@ -3759,6 +4084,11 @@ bool BitcoinMiner(int nThreadId) // them out one at a time under cs_keyPool, which is // what keeps concurrent coinbases distinct (#58). vchPubKey = GetKeyFromPool(); + if (vchPubKey.empty()) + { + printf("BitcoinMiner: no replacement wallet key available; mining stopped\n"); + fGenerateBitcoins = false; + } // Process this block the same as if we had received it from another node if (!ProcessBlock(NULL, pblock.release())) diff --git a/src/main.h b/src/main.h index cca2cad..c458412 100644 --- a/src/main.h +++ b/src/main.h @@ -129,6 +129,46 @@ FILE* AppendBlockFile(unsigned int& nFileRet); bool AddKey(const CKey& key); vector GenerateNewKey(); +// --- Wallet encryption ---------------------------------------------------- +// +// Plain wallets keep private keys in mapKeys. Encrypted wallets keep public +// keys visible so balances can still be recognized while locked, and decrypt +// private keys only after an unlock passphrase has supplied the master key. +typedef vector > CKeyingMaterial; +extern map, vector > mapCryptedKeys; +extern map mapMasterKeys; +extern unsigned int nWalletMasterKeyMaxID; +extern CKeyingMaterial vWalletMasterKey; +extern vector vchCryptedHDMaster; +extern vector vchCryptedHDChainCode; +extern bool fWalletEncrypted; +extern bool fWalletLocked; +bool IsWalletEncrypted(); +bool IsWalletLocked(); +void LockWallet(); +bool UnlockWallet(const string& strPassphrase, string& strErrorRet); +bool DeriveWalletPassphraseKey(const string& strPassphrase, + const vector& vchSalt, + unsigned int nDeriveIterations, + CKeyingMaterial& vchKeyRet, + vector& vchIVRet); +bool EncryptSecret(const CKeyingMaterial& vchKey, + const vector& vchPlaintext, + const vector& vchIV, + vector& vchCiphertextRet); +bool DecryptSecret(const CKeyingMaterial& vchKey, + const vector& vchCiphertext, + const vector& vchIV, + vector& vchPlaintextRet); +vector WalletKeyIV(const vector& vchPubKey); +vector WalletSecretIV(const string& strLabel); +bool AddCryptedKey(const vector& vchPubKey, + const vector& vchCryptedSecret); +bool WalletCanSpendKey(const vector& vchPubKey); +bool GetWalletPrivKey(const vector& vchPubKey, + CPrivKey& vchPrivKeyRet, + string& strErrorRet); + // --- Key pool ------------------------------------------------------------- // // A backup of this wallet only ever contained the keys that existed the moment 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* return argval(argc, argv, keyDash); } +static bool ReadPassphraseArgument(const string& strArg, string& strPassphraseRet, string& strErrorRet) +{ + strPassphraseRet.clear(); + strErrorRet.clear(); + if (strArg.empty()) + { + AttachTerminal(); + fprintf(stderr, "Enter wallet passphrase: "); + fflush(stderr); + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) + { + strErrorRet = "could not read passphrase from stdin"; + return false; + } + strPassphraseRet = buf; + } + else if (strArg[0] == '@') + { + string strPath = strArg.substr(1); + FILE* pf = fopen(strPath.c_str(), "rb"); + if (!pf) + { + strErrorRet = strprintf("could not read passphrase file %s", strPath.c_str()); + return false; + } + char buf[4096]; + if (!fgets(buf, sizeof(buf), pf)) + { + fclose(pf); + strErrorRet = strprintf("passphrase file %s is empty", strPath.c_str()); + return false; + } + fclose(pf); + strPassphraseRet = buf; + } + else + { + strErrorRet = "passphrase literals on the command line are unsafe; use the option without a value to read stdin, or use @FILE"; + return false; + } + + while (!strPassphraseRet.empty() && + (strPassphraseRet[strPassphraseRet.size() - 1] == '\n' || + strPassphraseRet[strPassphraseRet.size() - 1] == '\r')) + strPassphraseRet.resize(strPassphraseRet.size() - 1); + if (strPassphraseRet.empty()) + { + strErrorRet = "empty passphrase"; + return false; + } + return true; +} + // The real printf, not the one util.h remaps to OutputDebugStringF. Help that // goes to debug.log is help nobody asked for: `-help` printed a full page into // the data directory and returned 0 with an empty terminal, which reads as a @@ -56,7 +110,7 @@ static void PrintUsage() printf(" /debug\n"); printf(" /gen\n"); printf(" /nogui or /daemon\n"); - printf(" /selftest=wallet-keypool or /selftest=wallet-hd\n"); + printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, or wallet-encrypt\n"); printf("\n"); printf("Mining mode:\n"); printf(" /operator\n"); @@ -85,6 +139,10 @@ static void PrintUsage() printf(" /dumpwallet=FILE (export private keys as text -- readable by\n"); printf(" anyone, so guard it like cash)\n"); printf(" /importwallet=FILE (load keys from such a file back in)\n"); + printf(" /encryptwallet[=@FILE] (rewrite wallet.dat with encrypted private keys\n"); + printf(" and encrypted HD seed, then exit)\n"); + printf(" /walletpassphrase[=@FILE] (unlock an encrypted wallet for one-shot\n"); + printf(" commands; without @FILE reads stdin)\n"); printf(" /newaddress (print the next receiving address, then exit)\n"); printf(" /sendto=ADDRESS,AMOUNT (spend from this wallet, then exit -- the\n"); printf(" only way to send without the window)\n"); @@ -299,6 +357,39 @@ int main(int argc, char* argv[]) } printf("Height=%d\n", nBestHeight); + bool fEncryptWallet = arg(argc, argv, "/encryptwallet") || arg(argc, argv, "-encryptwallet"); + string strEncryptWallet = argval2(argc, argv, "/encryptwallet", "-encryptwallet"); + if (fEncryptWallet) + { + string strPassphrase; + string strPassphraseError; + if (!ReadPassphraseArgument(strEncryptWallet, strPassphrase, strPassphraseError)) + { + fprintf(stderr, "Cannot read encryption passphrase: %s\n", strPassphraseError.c_str()); + DBFlush(true); + return 1; + } + int nRet = CmdEncryptWallet(strPassphrase); + if (nRet != 0) + DBFlush(true); + return nRet; + } + + bool fWalletPassphrase = arg(argc, argv, "/walletpassphrase") || arg(argc, argv, "-walletpassphrase"); + string strWalletPassphrase = argval2(argc, argv, "/walletpassphrase", "-walletpassphrase"); + if (fWalletPassphrase) + { + string strPassphrase; + string strUnlockError; + if (!ReadPassphraseArgument(strWalletPassphrase, strPassphrase, strUnlockError) || + !UnlockWallet(strPassphrase, strUnlockError)) + { + fprintf(stderr, "Cannot unlock wallet: %s\n", strUnlockError.c_str()); + DBFlush(true); + return 1; + } + } + // Before the node opens sockets or touches anything: the wallet is loaded, // which is all a backup needs, and finishing here means the copy is taken // from a quiet directory rather than from under a running node. @@ -442,6 +533,14 @@ int main(int argc, char* argv[]) return 0; } + if (fGenerateBitcoins && IsWalletLocked()) + { + fprintf(stderr, "Cannot mine while the encrypted wallet is locked. " + "Start with /walletpassphrase or /walletpassphrase=@FILE, or disable mining.\n"); + DBFlush(true); + return 1; + } + ReacceptWalletTransactions(); if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } diff --git a/src/makefile.mingw b/src/makefile.mingw index 9bbfef1..e076c49 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -111,5 +111,8 @@ tests: test_bip32.exe bitflash.exe ./test_bip32.exe ./bitflash.exe -selftest=wallet-keypool ./bitflash.exe -selftest=wallet-hd + ./bitflash.exe -selftest=wallet-format + ./bitflash.exe -selftest=wallet-crypto + ./bitflash.exe -selftest=wallet-encrypt ./bitflash.exe -selftest=net-message ./bitflash.exe -selftest=consensus-limits diff --git a/src/script.cpp b/src/script.cpp index 6d4fb34..1e6e1ca 100644 --- a/src/script.cpp +++ b/src/script.cpp @@ -1020,43 +1020,54 @@ bool Solver(const CScript& scriptPubKey, uint256 hash, int nHashType, CScript& s if (!Solver(scriptPubKey, vSolution)) return false; - // Compile solution - CRITICAL_BLOCK(cs_mapKeys) + // Compile solution. Public keys stay available for encrypted wallets so the + // wallet can recognize its own outputs while locked; private keys are + // fetched only for an actual signature. + foreach(PAIRTYPE(opcodetype, valtype)& item, vSolution) { - foreach(PAIRTYPE(opcodetype, valtype)& item, vSolution) + if (item.first == OP_PUBKEY) { - if (item.first == OP_PUBKEY) + const valtype& vchPubKey = item.second; + if (!WalletCanSpendKey(vchPubKey)) + return false; + if (hash != 0) { - // Sign - const valtype& vchPubKey = item.second; - if (!mapKeys.count(vchPubKey)) + CPrivKey vchPrivKey; + string strError; + if (!GetWalletPrivKey(vchPubKey, vchPrivKey, strError)) + return false; + vector vchSig; + if (!CKey::Sign(vchPrivKey, hash, vchSig)) return false; - if (hash != 0) + vchSig.push_back((unsigned char)nHashType); + scriptSigRet << vchSig; + } + } + else if (item.first == OP_PUBKEYHASH) + { + vector vchPubKey; + { + CRITICAL_BLOCK(cs_mapKeys) { - vector vchSig; - if (!CKey::Sign(mapKeys[vchPubKey], hash, vchSig)) + map::iterator mi = mapPubKeys.find(uint160(item.second)); + if (mi == mapPubKeys.end()) return false; - vchSig.push_back((unsigned char)nHashType); - scriptSigRet << vchSig; + vchPubKey = (*mi).second; } } - else if (item.first == OP_PUBKEYHASH) + if (!WalletCanSpendKey(vchPubKey)) + return false; + if (hash != 0) { - // Sign and give pubkey - map::iterator mi = mapPubKeys.find(uint160(item.second)); - if (mi == mapPubKeys.end()) + CPrivKey vchPrivKey; + string strError; + if (!GetWalletPrivKey(vchPubKey, vchPrivKey, strError)) return false; - const vector& vchPubKey = (*mi).second; - if (!mapKeys.count(vchPubKey)) + vector vchSig; + if (!CKey::Sign(vchPrivKey, hash, vchSig)) return false; - if (hash != 0) - { - vector vchSig; - if (!CKey::Sign(mapKeys[vchPubKey], hash, vchSig)) - return false; - vchSig.push_back((unsigned char)nHashType); - scriptSigRet << vchSig << vchPubKey; - } + vchSig.push_back((unsigned char)nHashType); + scriptSigRet << vchSig << vchPubKey; } } } @@ -1080,27 +1091,27 @@ bool ExtractPubKey(const CScript& scriptPubKey, bool fMineOnly, vector::iterator mi = mapPubKeys.find(uint160(item.second)); if (mi == mapPubKeys.end()) continue; vchPubKey = (*mi).second; } - if (!fMineOnly || mapKeys.count(vchPubKey)) - { - vchPubKeyRet = vchPubKey; - return true; - } + } + if (!fMineOnly || WalletCanSpendKey(vchPubKey)) + { + vchPubKeyRet = vchPubKey; + return true; } } return false; diff --git a/src/selftest.cpp b/src/selftest.cpp index aee2951..ff0289b 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -33,6 +33,8 @@ #ifdef _WIN32 #include +#include +#include #else #include #include @@ -601,6 +603,500 @@ static int RunWalletHDSelfTest() return nFail == 0 ? 0 : 1; } +static int RunWalletFormatSelfTest() +{ + fflush(stdout); + printf("wallet-format self-test\n"); + + std::string tmp; + std::string cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) + { + printf(" FAIL could not move into the temporary data directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + strSetDataDir = tmp; + printf(" temp datadir: %s\n", tmp.c_str()); + + try + { + class CWalletDBRaw : public CWalletDB + { + public: + CWalletDBRaw(const char* pszMode="r+") : CWalletDB(pszMode) { } + bool WriteStringRecord(const string& strType, int nValue) + { + return Write(strType, nValue); + } + bool EraseStringRecord(const string& strType) + { + return Erase(strType); + } + }; + + if (!LoadWallet()) + throw std::runtime_error("LoadWallet failed"); + + { + CWalletDB walletdb; + nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_SUPPORTED), + "the current wallet format marker can be written") ? 0 : 1; + } + + std::vector vchDefaultKey; + nFail += Check(CWalletDB("r").LoadWallet(vchDefaultKey), + "the current wallet format marker loads") ? 0 : 1; + + { + CWalletDBRaw walletdb; + nFail += Check(walletdb.WriteStringRecord("mkey", 1), + "a malformed encrypted master-key record can be written") ? 0 : 1; + } + + vchDefaultKey.clear(); + nFail += Check(!CWalletDB("r").LoadWallet(vchDefaultKey), + "a malformed encrypted master-key record is refused") ? 0 : 1; + + { + CWalletDBRaw walletdb; + nFail += Check(walletdb.EraseStringRecord("mkey"), + "the malformed encrypted master-key record can be removed") ? 0 : 1; + } + + { + CWalletDB walletdb; + nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_SUPPORTED + 1), + "a future wallet format marker can be written") ? 0 : 1; + } + + vchDefaultKey.clear(); + nFail += Check(!CWalletDB("r").LoadWallet(vchDefaultKey), + "a future wallet format marker is refused") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + DBFlush(true); + SetCurrentDir(cwd); + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + +static void ClearWalletRuntimeForTest() +{ + CRITICAL_BLOCK(cs_mapKeys) + { + mapKeys.clear(); + mapCryptedKeys.clear(); + mapPubKeys.clear(); + mapMasterKeys.clear(); + vWalletMasterKey.clear(); + vchCryptedHDMaster.clear(); + vchCryptedHDChainCode.clear(); + fWalletEncrypted = false; + fWalletLocked = true; + } + CRITICAL_BLOCK(cs_keyPool) + mapKeyPool.clear(); + vchHDMaster.clear(); + vchHDChainCode.clear(); + nHDNext = 0; +} + +static int RunWalletCryptoSelfTest() +{ + fflush(stdout); + printf("wallet-crypto self-test\n"); + + std::string tmp; + std::string cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) + { + printf(" FAIL could not move into the temporary data directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + strSetDataDir = tmp; + printf(" temp datadir: %s\n", tmp.c_str()); + + try + { + const string strPassphrase = "btf-test-passphrase"; + const string strWrongPassphrase = "btf-test-passphraser"; + + CKey key; + key.MakeNewKey(); + vector vchPubKey = key.GetPubKey(); + CPrivKey vchPrivKey = key.GetPrivKey(); + + CKeyingMaterial vchMasterKey; + for (int i = 0; i < 32; i++) + vchMasterKey.push_back((unsigned char)(i + 1)); + + CWalletMasterKey kMasterKey; + kMasterKey.vchSalt.clear(); + for (int i = 0; i < 8; i++) + kMasterKey.vchSalt.push_back((unsigned char)(0xa0 + i)); + kMasterKey.nDeriveIterations = 2500; + + CKeyingMaterial vchPassKey; + vector vchPassIV; + nFail += Check(DeriveWalletPassphraseKey(strPassphrase, kMasterKey.vchSalt, + kMasterKey.nDeriveIterations, + vchPassKey, vchPassIV), + "a passphrase key can be derived") ? 0 : 1; + nFail += Check(EncryptSecret(vchPassKey, + vector(vchMasterKey.begin(), vchMasterKey.end()), + vchPassIV, kMasterKey.vchCryptedKey), + "the wallet master key can be encrypted") ? 0 : 1; + + vector vchCryptedKey; + nFail += Check(EncryptSecret(vchMasterKey, + vector(vchPrivKey.begin(), vchPrivKey.end()), + WalletKeyIV(vchPubKey), vchCryptedKey), + "a private key can be encrypted under the wallet master key") ? 0 : 1; + + { + CWalletDB walletdb("cr"); + nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_ENCRYPTED), + "an encrypted wallet format marker can be written") ? 0 : 1; + nFail += Check(walletdb.WriteMasterKey(0, kMasterKey), + "the encrypted master key can be written") ? 0 : 1; + nFail += Check(walletdb.WriteCryptedKey(vchPubKey, vchCryptedKey), + "the encrypted private key can be written") ? 0 : 1; + nFail += Check(walletdb.WriteDefaultKey(vchPubKey), + "the encrypted wallet default key can be written") ? 0 : 1; + } + + ClearWalletRuntimeForTest(); + + vector vchDefaultKey; + nFail += Check(CWalletDB("r").LoadWallet(vchDefaultKey), + "an encrypted wallet can be loaded") ? 0 : 1; + nFail += Check(vchDefaultKey == vchPubKey, + "the encrypted wallet default public key loads") ? 0 : 1; + nFail += Check(IsWalletEncrypted() && IsWalletLocked(), + "an encrypted wallet loads locked") ? 0 : 1; + nFail += Check(WalletCanSpendKey(vchPubKey), + "an encrypted key is recognized as wallet-owned while locked") ? 0 : 1; + + CPrivKey vchDecrypted; + string strError; + nFail += Check(!GetWalletPrivKey(vchPubKey, vchDecrypted, strError), + "a locked encrypted key cannot be read") ? 0 : 1; + nFail += Check(!UnlockWallet(strWrongPassphrase, strError), + "the wrong-passphrase does not unlock") ? 0 : 1; + nFail += Check(UnlockWallet(strPassphrase, strError), + "the right passphrase unlocks") ? 0 : 1; + nFail += Check(!IsWalletLocked(), + "the wallet reports unlocked") ? 0 : 1; + nFail += Check(GetWalletPrivKey(vchPubKey, vchDecrypted, strError) && + vchDecrypted == vchPrivKey, + "an unlocked encrypted key decrypts to the original private key") ? 0 : 1; + LockWallet(); + nFail += Check(IsWalletLocked(), + "locking clears the unlocked state") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + DBFlush(true); + SetCurrentDir(cwd); + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + +static bool FileContainsBytes(const string& strPath, const vector& vchNeedle) +{ + if (vchNeedle.empty()) + return false; + FILE* pf = fopen(strPath.c_str(), "rb"); + if (!pf) + return false; + + vector vchHaystack; + unsigned char buf[4096]; + size_t n = 0; + while ((n = fread(buf, 1, sizeof(buf), pf)) > 0) + vchHaystack.insert(vchHaystack.end(), buf, buf + n); + fclose(pf); + + if (vchHaystack.size() < vchNeedle.size()) + return false; + return search(vchHaystack.begin(), vchHaystack.end(), + vchNeedle.begin(), vchNeedle.end()) != vchHaystack.end(); +} + +static bool FileContainsText(const string& strPath, const string& strNeedle) +{ + FILE* pf = fopen(strPath.c_str(), "rb"); + if (!pf) + return false; + + string strHaystack; + char buf[4096]; + size_t n = 0; + while ((n = fread(buf, 1, sizeof(buf), pf)) > 0) + strHaystack.append(buf, buf + n); + fclose(pf); + + return strHaystack.find(strNeedle) != string::npos; +} + +static bool WriteTextFile(const string& strPath, const string& strText) +{ + FILE* pf = fopen(strPath.c_str(), "wb"); + if (!pf) + return false; + bool fOk = fwrite(strText.data(), 1, strText.size(), pf) == strText.size(); + if (fclose(pf) != 0) + fOk = false; + return fOk; +} + +static string QuoteCommandArg(const string& str) +{ + string out = "\""; + for (size_t i = 0; i < str.size(); i++) + { + if (str[i] == '"') + out += "\\\""; + else + out += str[i]; + } + out += "\""; + return out; +} + +static int RunBitflashChild(const string& strExe, + const vector& vArgs, + const string* pStdinFile = NULL) +{ +#ifdef _WIN32 + vector argv; + argv.push_back((char*)strExe.c_str()); + for (size_t i = 0; i < vArgs.size(); i++) + argv.push_back((char*)vArgs[i].c_str()); + argv.push_back(NULL); + + int nOldOut = _dup(1); + int nOldErr = _dup(2); + int nOldIn = _dup(0); + int nNull = _open("NUL", _O_WRONLY); + int nIn = -1; + if (nNull >= 0) + { + _dup2(nNull, 1); + _dup2(nNull, 2); + } + if (pStdinFile) + { + nIn = _open(pStdinFile->c_str(), _O_RDONLY); + if (nIn >= 0) + _dup2(nIn, 0); + } + int nRet = _spawnv(_P_WAIT, strExe.c_str(), &argv[0]); + if (nIn >= 0) + _close(nIn); + if (nNull >= 0) + _close(nNull); + if (nOldIn >= 0) + { + _dup2(nOldIn, 0); + _close(nOldIn); + } + if (nOldOut >= 0) + { + _dup2(nOldOut, 1); + _close(nOldOut); + } + if (nOldErr >= 0) + { + _dup2(nOldErr, 2); + _close(nOldErr); + } + return nRet; +#else + string strCmd = QuoteCommandArg(strExe); + for (size_t i = 0; i < vArgs.size(); i++) + strCmd += " " + QuoteCommandArg(vArgs[i]); + if (pStdinFile) + strCmd += " < " + QuoteCommandArg(*pStdinFile); + strCmd += " > /dev/null 2>&1"; + return system(strCmd.c_str()); +#endif +} + +static int RunWalletEncryptSelfTest() +{ + fflush(stdout); + printf("wallet-encrypt self-test\n"); + + std::string tmp; + std::string cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) + { + printf(" FAIL could not move into the temporary data directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + bool fWalletEnvClosed = false; + strSetDataDir = tmp; + printf(" temp datadir: %s\n", tmp.c_str()); + + try + { + if (!LoadWallet()) + throw std::runtime_error("LoadWallet failed"); + + string strError; + const string strMnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + nFail += Check(SetHDSeedFromMnemonic(strMnemonic, strError), + "a phrase can be installed before encryption") ? 0 : 1; + TopUpKeyPool(); + + vector vchDefaultPubKey = keyUser.GetPubKey(); + CPrivKey vchDefaultPrivKey; + nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), + "the default private key is readable before encryption") ? 0 : 1; + vector vchDefaultPrivBytes(vchDefaultPrivKey.begin(), vchDefaultPrivKey.end()); + vector vchPlainHDMaster = vchHDMaster; + vector vchPlainHDChainCode = vchHDChainCode; + + string strBackup; + nFail += Check(EncryptWallet("btf-test-passphrase", strBackup, strError), + "wallet.dat can be rewritten encrypted") ? 0 : 1; + fWalletEnvClosed = true; + nFail += Check(FileExists(strBackup.c_str()), + "the original wallet is preserved as an explicit backup") ? 0 : 1; + + string strWalletPath = GetAppDir() + "/wallet.dat"; + nFail += Check(!FileContainsBytes(strWalletPath, vchDefaultPrivBytes), + "the active wallet no longer contains the default private key bytes") ? 0 : 1; + nFail += Check(!FileContainsBytes(strWalletPath, vchPlainHDMaster), + "the active wallet no longer contains the HD master key bytes") ? 0 : 1; + nFail += Check(!FileContainsBytes(strWalletPath, vchPlainHDChainCode), + "the active wallet no longer contains the HD chain-code bytes") ? 0 : 1; + +#ifdef _WIN32 + string strExe = cwd + "\\bitflash.exe"; +#else + string strExe = cwd + "/bitflash-node"; +#endif + string strWrongDump = tmp + "/wrong-pass-dump.txt"; + string strRightDump = tmp + "/right-pass-dump.txt"; + string strLiteralDump = tmp + "/literal-pass-dump.txt"; + string strStdinDump = tmp + "/stdin-pass-dump.txt"; + string strWrongPassFile = tmp + "/wrong-pass.txt"; + string strRightPassFile = tmp + "/right-pass.txt"; + nFail += Check(WriteTextFile(strWrongPassFile, "wrong-passphrase\n") && + WriteTextFile(strRightPassFile, "btf-test-passphrase\n"), + "passphrase files can be written for restarted commands") ? 0 : 1; + + vector vWrongArgs; + vWrongArgs.push_back("-datadir=" + tmp); + vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); + vWrongArgs.push_back("-dumpwallet=" + strWrongDump); + vWrongArgs.push_back("-nogui"); + int nWrongRet = RunBitflashChild(strExe, vWrongArgs); + nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), + "a restarted wallet rejects the wrong-passphrase") ? 0 : 1; + + vector vLiteralArgs; + vLiteralArgs.push_back("-datadir=" + tmp); + vLiteralArgs.push_back("-walletpassphrase=btf-test-passphrase"); + vLiteralArgs.push_back("-dumpwallet=" + strLiteralDump); + vLiteralArgs.push_back("-nogui"); + int nLiteralRet = RunBitflashChild(strExe, vLiteralArgs); + nFail += Check(nLiteralRet != 0 && !FileExists(strLiteralDump.c_str()), + "literal passphrases on the command line are refused") ? 0 : 1; + + vector vRightArgs; + vRightArgs.push_back("-datadir=" + tmp); + vRightArgs.push_back("-walletpassphrase=@" + strRightPassFile); + vRightArgs.push_back("-dumpwallet=" + strRightDump); + vRightArgs.push_back("-nogui"); + int nRightRet = RunBitflashChild(strExe, vRightArgs); + nFail += Check(nRightRet == 0 && FileExists(strRightDump.c_str()), + "a restarted wallet unlocks with the right passphrase") ? 0 : 1; + nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), + "the restarted wallet can decrypt and dump the original key") ? 0 : 1; + + vector vStdinArgs; + vStdinArgs.push_back("-datadir=" + tmp); + vStdinArgs.push_back("-walletpassphrase"); + vStdinArgs.push_back("-dumpwallet=" + strStdinDump); + vStdinArgs.push_back("-nogui"); + int nStdinRet = RunBitflashChild(strExe, vStdinArgs, &strRightPassFile); + nFail += Check(nStdinRet == 0 && FileExists(strStdinDump.c_str()), + "a restarted wallet can read the passphrase from stdin") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + if (!fWalletEnvClosed) + DBFlush(true); + SetCurrentDir(cwd); + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + static int RunNetMessageSelfTest() { fflush(stdout); @@ -728,12 +1224,18 @@ int RunSelfTest(const std::string& name) return RunWalletKeyPoolSelfTest(); if (name == "wallet-hd") return RunWalletHDSelfTest(); + if (name == "wallet-format") + return RunWalletFormatSelfTest(); + if (name == "wallet-crypto") + return RunWalletCryptoSelfTest(); + if (name == "wallet-encrypt") + return RunWalletEncryptSelfTest(); if (name == "net-message") return RunNetMessageSelfTest(); if (name == "consensus-limits") return RunConsensusLimitsSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool, wallet-hd, net-message, consensus-limits\n"); + printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits\n"); return 1; } diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index b449df9..06f35a5 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -406,6 +406,36 @@ int CmdRecoveryAudit() return 0; } +int CmdEncryptWallet(const std::string& strPassphrase) +{ + AttachTerminal(); + + string strBackup; + string strError; + if (!EncryptWallet(strPassphrase, strBackup, strError)) + { + fprintf(stderr, "Cannot encrypt wallet: %s\n", strError.c_str()); + fprintf(stderr, "Nothing was changed.\n"); + return 1; + } + + printf("Wallet encrypted.\n"); + printf("The old unencrypted wallet was moved to:\n"); + printf(" %s\n", strBackup.c_str()); + printf("That backup still contains private keys in plain text. Move it offline or\n"); + printf("delete it after you have a safer backup plan.\n"); + printf("Restart Bitflash before using the wallet again.\n"); + fflush(stdout); + + // The rewritten wallet.dat is clean; the environment's write-ahead logs are + // not, because every plaintext record this wallet ever wrote passed through + // them. Closing the environment and dropping the logs is the last step of + // encrypting, not an optimisation -- our own backup advice is to copy the + // whole directory, and following it otherwise carries the keys along. + PurgeDbEnvironmentLogs(); + return 0; +} + // Spend, from the command line. // // SendMoney() has been in this tree since 0.1.0 and only the window ever called @@ -421,6 +451,12 @@ int CmdSendTo(const std::string& strArg) { AttachTerminal(); + if (IsWalletLocked()) + { + fprintf(stderr, "Wallet is encrypted and locked. Start with /walletpassphrase or /walletpassphrase=@FILE to spend.\n"); + return 1; + } + std::string::size_type comma = strArg.rfind(','); if (comma == std::string::npos) { diff --git a/src/walletcmd.h b/src/walletcmd.h index 32d3eef..04d3fb7 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -53,6 +53,10 @@ int CmdShowDerived(int nCount); // by the installed recovery phrase or still depends on wallet.dat-only keys. int CmdRecoveryAudit(); +// Rewrite wallet.dat so private keys and the HD seed are encrypted with the +// given passphrase. Returns 0 on success and exits without starting the node. +int CmdEncryptWallet(const std::string& strPassphrase); + // Spend, from the command line. Until now the only way to send was the window, // so a headless node could be paid and could never pay -- it could hold a // balance it had no way to move. strArg is "ADDRESS,AMOUNT". -- cgit v1.2.3