upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-08-04 10:01:36 -0300
committerGitHub <noreply@github.com>2026-08-04 10:01:36 -0300
commit1db5f372c36eb701602a70a358de2344726ed734 (patch)
tree9d028ed3097ac51368ca312ff439664c76f439b7 /src
parent9176d5b3f32b907e4bafde5152828b2ae04ae8a1 (diff)
wallet encryption: the stack from #130–#133, with the two blockers fixed (#134)
* 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 <ti@gpm.infra>
Diffstat (limited to 'src')
-rw-r--r--src/Makefile3
-rw-r--r--src/db.cpp602
-rw-r--r--src/db.h58
-rw-r--r--src/main.cpp334
-rw-r--r--src/main.h40
-rw-r--r--src/main_gui.cpp101
-rw-r--r--src/makefile.mingw3
-rw-r--r--src/script.cpp89
-rw-r--r--src/selftest.cpp504
-rw-r--r--src/walletcmd.cpp36
-rw-r--r--src/walletcmd.h4
11 files changed, 1698 insertions, 76 deletions
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
139 ./test_bip32 139 ./test_bip32
140 ./bitflash-node -selftest=wallet-keypool 140 ./bitflash-node -selftest=wallet-keypool
141 ./bitflash-node -selftest=wallet-hd 141 ./bitflash-node -selftest=wallet-hd
142 ./bitflash-node -selftest=wallet-format
143 ./bitflash-node -selftest=wallet-crypto
144 ./bitflash-node -selftest=wallet-encrypt
142 ./bitflash-node -selftest=net-message 145 ./bitflash-node -selftest=net-message
143 ./bitflash-node -selftest=consensus-limits 146 ./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()
203 RandAddSeed(); 203 RandAddSeed();
204} 204}
205 205
206// Close the Berkeley DB environment and delete its write-ahead logs.
207//
208// Encrypting the wallet rewrites wallet.dat, and that file does come out clean.
209// The logs in database/ do not: every record this wallet has written since it
210// was created passed through them, so the plaintext private keys are still
211// there afterwards. Measured -- wallet.dat held no trace of a known key while
212// database/log.0000000002 held all 118 bytes of it, and still did after a full
213// node restart.
214//
215// It matters because our own backup advice, in the README and in the LEIA-ME
216// shipped with the desktop folders, is to copy the whole directory rather than
217// wallet.dat alone. That advice is right, and it exists because of #40 -- but
218// following it after encrypting would carry the unencrypted key along.
219//
220// log_archive(DB_ARCH_REMOVE) cannot fix this. It removes only logs Berkeley DB
221// considers obsolete, and never the log it is currently writing to. With one
222// 10 MB log per data directory, the plaintext lives in exactly that file. The
223// keys did not get there when the wallet was encrypted; they got there over the
224// wallet's whole life, so nothing done at encryption time can archive them out.
225//
226// So the logs go, after a full flush. DBFlush(true) checkpoints, runs lsn_reset
227// on every file and closes the environment -- lsn_reset is what makes a .dat
228// self-contained rather than welded to this directory, which is the same
229// property a backup depends on. Once that has happened there is nothing left in
230// the logs to recover, and they can be removed rather than archived.
231//
232// This is not secure erasure: the blocks are unlinked, not overwritten, so disk
233// forensics could still reach them. It closes the case this feature is actually
234// for -- someone reading the copied data directory.
235void PurgeDbEnvironmentLogs()
236{
237 // Everything below assumes nothing is mid-write, which is why this is only
238 // reached from the one-shot encrypt command, on its way out.
239 DBFlush(true);
240
241 string strDbDir = GetAppDir() + "/database";
242 int nRemoved = 0;
243 for (int i = 1; i < 1000; i++)
244 {
245 string strPath = strprintf("%s/log.%010d", strDbDir.c_str(), i);
246 if (!FileExists(strPath.c_str()))
247 continue;
248 if (remove(strPath.c_str()) == 0)
249 nRemoved++;
250 else
251 printf("PurgeDbEnvironmentLogs() : could not remove %s\n", strPath.c_str());
252 }
253 printf("PurgeDbEnvironmentLogs() : removed %d database log file(s)\n", nRemoved);
254}
255
206void DBFlush(bool fShutdown) 256void DBFlush(bool fShutdown)
207{ 257{
208 // Flush log data to the actual data file 258 // Flush log data to the actual data file
209 // on all files that are not in use 259 // on all files that are not in use
210 printf("DBFlush(%s)\n", fShutdown ? "true" : "false"); 260 printf("DBFlush(%s)\n", fShutdown ? "true" : "false");
261 // Calling this twice is not idempotent, it is fatal: the first shutdown
262 // flush closes the environment, and txn_checkpoint() on a closed
263 // environment segfaults rather than returning an error, so the try/catch
264 // below never sees it. EncryptWallet() already flushes before renaming the
265 // wallet file, and every one-shot command flushes again on its way out --
266 // which is exactly the pair that crashed while this was being written.
267 if (!fDbEnvInit)
268 {
269 printf("DBFlush() : environment already closed, nothing to flush\n");
270 return;
271 }
211 CRITICAL_BLOCK(cs_db) 272 CRITICAL_BLOCK(cs_db)
212 { 273 {
213 // This runs on the way out. Anything that throws here and is not caught 274 // This runs on the way out. Anything that throws here and is not caught
@@ -235,9 +296,17 @@ void DBFlush(bool fShutdown)
235 } 296 }
236 if (fShutdown) 297 if (fShutdown)
237 { 298 {
238 char** listp; 299 // listp must be NULL when DB_ARCH_REMOVE is set. This passed the
300 // address of an UNINITIALISED pointer, and Berkeley DB is entitled
301 // to write through it -- which segfaults. It fires on every clean
302 // shutdown, because the loop just above empties mapFileUseCount,
303 // and a structured fault on Windows never reaches the catch(...)
304 // wrapped around it. Inherited from 0.1.0 and wrong since then.
305 //
306 // Found while making wallet encryption clean up after itself:
307 // calling DBFlush(true) a second time crashed here every time.
239 if (mapFileUseCount.empty()) 308 if (mapFileUseCount.empty())
240 try { dbenv.log_archive(&listp, DB_ARCH_REMOVE); } catch (...) { } 309 try { dbenv.log_archive(NULL, DB_ARCH_REMOVE); } catch (...) { }
241 try { dbenv.close(0); } catch (...) { } 310 try { dbenv.close(0); } catch (...) { }
242 fDbEnvInit = false; 311 fDbEnvInit = false;
243 } 312 }
@@ -636,6 +705,29 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet)
636 mapKeys[vchPubKey] = vchPrivKey; 705 mapKeys[vchPubKey] = vchPrivKey;
637 mapPubKeys[Hash160(vchPubKey)] = vchPubKey; 706 mapPubKeys[Hash160(vchPubKey)] = vchPubKey;
638 } 707 }
708 else if (strType == "mkey")
709 {
710 unsigned int nID = 0;
711 ssKey >> nID;
712 CWalletMasterKey kMasterKey;
713 ssValue >> kMasterKey;
714 mapMasterKeys[nID] = kMasterKey;
715 nWalletMasterKeyMaxID = max(nWalletMasterKeyMaxID, nID);
716 fWalletEncrypted = true;
717 fWalletLocked = true;
718 }
719 else if (strType == "ckey")
720 {
721 vector<unsigned char> vchPubKey;
722 ssKey >> vchPubKey;
723 vector<unsigned char> vchCryptedSecret;
724 ssValue >> vchCryptedSecret;
725 if (!AddCryptedKey(vchPubKey, vchCryptedSecret))
726 {
727 printf("LoadWallet: wallet.dat has an unreadable encrypted key record\n");
728 return false;
729 }
730 }
639 else if (strType == "defaultkey") 731 else if (strType == "defaultkey")
640 { 732 {
641 ssValue >> vchDefaultKeyRet; 733 ssValue >> vchDefaultKeyRet;
@@ -669,6 +761,32 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet)
669 { 761 {
670 ssValue >> nHDChangeNext; 762 ssValue >> nHDChangeNext;
671 } 763 }
764 else if (strType == "cryptedhdmaster")
765 {
766 ssValue >> vchCryptedHDMaster;
767 fWalletEncrypted = true;
768 fWalletLocked = true;
769 }
770 else if (strType == "cryptedhdchaincode")
771 {
772 ssValue >> vchCryptedHDChainCode;
773 fWalletEncrypted = true;
774 fWalletLocked = true;
775 }
776 else if (strType == "walletminversion")
777 {
778 int nMinVersion = 0;
779 ssValue >> nMinVersion;
780 if (nMinVersion > WALLET_FORMAT_SUPPORTED)
781 {
782 printf("LoadWallet: wallet.dat requires wallet format %d, "
783 "but this build only supports format %d\n",
784 nMinVersion, WALLET_FORMAT_SUPPORTED);
785 printf("LoadWallet: the file was left untouched. Upgrade "
786 "Bitflash before opening this wallet.\n");
787 return false;
788 }
789 }
672 else if (strType == "pool") 790 else if (strType == "pool")
673 { 791 {
674 int64 nIndex; 792 int64 nIndex;
@@ -725,7 +843,17 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet)
725 return false; 843 return false;
726 } 844 }
727 845
728 if (HaveHDSeed()) 846 // An encrypted wallet has a seed; it just has not been decrypted yet.
847 //
848 // The reset in the else branch was written when "no seed in memory after
849 // loading" could only mean "this wallet has no seed". With encryption that
850 // is no longer true, and taking the else branch here wiped the schema and
851 // the derivation counters that had just been read correctly out of the
852 // file. Unlocking later restores the seed but nothing restores those, so a
853 // BIP44 wallet came back deriving legacy addresses -- different addresses
854 // from the ones its own recovery phrase produces, while the audit reported
855 // the phrase as covering nothing.
856 if (HaveHDSeed() || !vchCryptedHDMaster.empty())
729 { 857 {
730 if (nHDKeySchema == HD_SCHEMA_NONE) 858 if (nHDKeySchema == HD_SCHEMA_NONE)
731 { 859 {
@@ -801,11 +929,18 @@ bool LoadWallet()
801 if (!CWalletDB("cr").LoadWallet(vchDefaultKey)) 929 if (!CWalletDB("cr").LoadWallet(vchDefaultKey))
802 return false; 930 return false;
803 931
804 if (mapKeys.count(vchDefaultKey)) 932 if (!vchDefaultKey.empty() && mapPubKeys.count(Hash160(vchDefaultKey)))
805 { 933 {
806 // Set keyUser 934 // Set keyUser
807 keyUser.SetPubKey(vchDefaultKey); 935 keyUser.SetPubKey(vchDefaultKey);
808 keyUser.SetPrivKey(mapKeys[vchDefaultKey]); 936 if (mapKeys.count(vchDefaultKey))
937 keyUser.SetPrivKey(mapKeys[vchDefaultKey]);
938 }
939 else if (IsWalletEncrypted())
940 {
941 printf("LoadWallet: encrypted wallet has no usable default public key. "
942 "The file was left untouched.\n");
943 return false;
809 } 944 }
810 else 945 else
811 { 946 {
@@ -951,6 +1086,8 @@ bool DumpWallet(const string& strDest)
951{ 1086{
952 if (strDest.empty()) 1087 if (strDest.empty())
953 return error("DumpWallet() : no destination given\n"); 1088 return error("DumpWallet() : no destination given\n");
1089 if (IsWalletLocked())
1090 return error("DumpWallet() : wallet is encrypted and locked; unlock it with /walletpassphrase first\n");
954 1091
955 // Never over an existing file. The whole point is to hold the only copy of 1092 // Never over an existing file. The whole point is to hold the only copy of
956 // something irreplaceable, and a mistyped path must not eat one. 1093 // something irreplaceable, and a mistyped path must not eat one.
@@ -976,35 +1113,51 @@ bool DumpWallet(const string& strDest)
976 fprintf(pf, "# The key is the DER encoding, hex; restore with -importwallet=FILE.\n"); 1113 fprintf(pf, "# The key is the DER encoding, hex; restore with -importwallet=FILE.\n");
977 fprintf(pf, "#\n"); 1114 fprintf(pf, "#\n");
978 1115
1116 vector<vector<unsigned char> > vPubKeys;
1117 {
1118 CRITICAL_BLOCK(cs_mapKeys)
1119 {
1120 for (map<vector<unsigned char>, CPrivKey>::iterator mi = mapKeys.begin();
1121 mi != mapKeys.end(); ++mi)
1122 vPubKeys.push_back((*mi).first);
1123 for (map<vector<unsigned char>, vector<unsigned char> >::iterator mi = mapCryptedKeys.begin();
1124 mi != mapCryptedKeys.end(); ++mi)
1125 if (!mapKeys.count((*mi).first))
1126 vPubKeys.push_back((*mi).first);
1127 }
1128 }
1129
979 int nKeys = 0; 1130 int nKeys = 0;
980 bool fOk = true; 1131 bool fOk = true;
981 CRITICAL_BLOCK(cs_mapKeys) 1132 for (size_t i = 0; i < vPubKeys.size(); i++)
982 { 1133 {
983 for (map<vector<unsigned char>, CPrivKey>::iterator mi = mapKeys.begin(); 1134 const vector<unsigned char>& vchPubKey = vPubKeys[i];
984 mi != mapKeys.end(); ++mi) 1135 CPrivKey vchPrivKey;
1136 string strError;
1137 if (!GetWalletPrivKey(vchPubKey, vchPrivKey, strError))
985 { 1138 {
986 const vector<unsigned char>& vchPubKey = (*mi).first; 1139 fOk = false;
987 const CPrivKey& vchPrivKey = (*mi).second; 1140 break;
988
989 string strAddress = PubKeyToAddress(vchPubKey);
990 string strLabel;
991 map<string, string>::iterator mia = mapAddressBook.find(strAddress);
992 if (mia != mapAddressBook.end())
993 strLabel = (*mia).second;
994
995 // Labels are user text and could contain a newline, which would
996 // forge a second entry on read-back.
997 for (size_t i = 0; i < strLabel.size(); i++)
998 if (strLabel[i] == '\n' || strLabel[i] == '\r')
999 strLabel[i] = ' ';
1000
1001 if (fprintf(pf, "%s %s %s\n",
1002 HexStr(vchPrivKey.begin(), vchPrivKey.end(), false).c_str(),
1003 strAddress.c_str(),
1004 strLabel.c_str()) < 0)
1005 { fOk = false; break; }
1006 nKeys++;
1007 } 1141 }
1142
1143 string strAddress = PubKeyToAddress(vchPubKey);
1144 string strLabel;
1145 map<string, string>::iterator mia = mapAddressBook.find(strAddress);
1146 if (mia != mapAddressBook.end())
1147 strLabel = (*mia).second;
1148
1149 // Labels are user text and could contain a newline, which would forge
1150 // a second entry on read-back.
1151 for (size_t j = 0; j < strLabel.size(); j++)
1152 if (strLabel[j] == '\n' || strLabel[j] == '\r')
1153 strLabel[j] = ' ';
1154
1155 if (fprintf(pf, "%s %s %s\n",
1156 HexStr(vchPrivKey.begin(), vchPrivKey.end(), false).c_str(),
1157 strAddress.c_str(),
1158 strLabel.c_str()) < 0)
1159 { fOk = false; break; }
1160 nKeys++;
1008 } 1161 }
1009 1162
1010 if (fclose(pf) != 0) 1163 if (fclose(pf) != 0)
@@ -1071,10 +1224,7 @@ bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet)
1071 continue; 1224 continue;
1072 } 1225 }
1073 1226
1074 bool fHave = false; 1227 if (WalletCanSpendKey(key.GetPubKey()))
1075 CRITICAL_BLOCK(cs_mapKeys)
1076 fHave = mapKeys.count(key.GetPubKey()) > 0;
1077 if (fHave)
1078 { 1228 {
1079 nSkippedRet++; // already ours; importing twice is not an error 1229 nSkippedRet++; // already ours; importing twice is not an error
1080 continue; 1230 continue;
@@ -1099,3 +1249,389 @@ bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet)
1099 "belonging to the new keys\n"); 1249 "belonging to the new keys\n");
1100 return true; 1250 return true;
1101} 1251}
1252
1253
1254// ---- wallet encryption rewrite -------------------------------------------
1255
1256class CWalletRewriteDB : public CDB
1257{
1258public:
1259 CWalletRewriteDB(const char* pszFile, const char* pszMode="r+", bool fTxn=false)
1260 : CDB(pszFile, pszMode, fTxn) { }
1261
1262 bool WriteRaw(CDataStream ssKey, CDataStream ssValue)
1263 {
1264 if (!pdb || ssKey.empty())
1265 return false;
1266
1267 Dbt datKey(&ssKey[0], ssKey.size());
1268 Dbt datValue(ssValue.empty() ? NULL : &ssValue[0], ssValue.size());
1269 int ret = pdb->put(GetTxn(), &datKey, &datValue, 0);
1270
1271 memset(datKey.get_data(), 0, datKey.get_size());
1272 if (datValue.get_data())
1273 memset(datValue.get_data(), 0, datValue.get_size());
1274 return ret == 0;
1275 }
1276
1277 bool WriteWalletMinVersion(int nVersion)
1278 {
1279 return Write(string("walletminversion"), nVersion);
1280 }
1281
1282 bool WriteMasterKey(unsigned int nID, const CWalletMasterKey& kMasterKey)
1283 {
1284 return Write(make_pair(string("mkey"), nID), kMasterKey, true);
1285 }
1286
1287 bool WriteCryptedKey(const vector<unsigned char>& vchPubKey,
1288 const vector<unsigned char>& vchCryptedSecret)
1289 {
1290 return Write(make_pair(string("ckey"), vchPubKey), vchCryptedSecret, false);
1291 }
1292
1293 bool WriteCryptedHDMaster(const vector<unsigned char>& vchCryptedMaster,
1294 const vector<unsigned char>& vchCryptedChainCode)
1295 {
1296 return Write(string("cryptedhdmaster"), vchCryptedMaster)
1297 && Write(string("cryptedhdchaincode"), vchCryptedChainCode);
1298 }
1299
1300 bool WriteHDNext(unsigned int nNext)
1301 {
1302 return Write(string("hdnext"), nNext);
1303 }
1304
1305 bool WritePool(int64 nIndex, const vector<unsigned char>& vchPubKey)
1306 {
1307 return Write(make_pair(string("pool"), nIndex), vchPubKey);
1308 }
1309
1310 bool CopyPublicRecordsTo(CWalletRewriteDB& dbTo, int& nCopiedRet, string& strErrorRet)
1311 {
1312 nCopiedRet = 0;
1313 Dbc* pcursor = GetCursor();
1314 if (!pcursor)
1315 {
1316 strErrorRet = "could not open a wallet cursor";
1317 return false;
1318 }
1319
1320 unsigned int fFlags = DB_NEXT;
1321 loop
1322 {
1323 CDataStream ssKey;
1324 CDataStream ssValue;
1325 int ret = ReadAtCursor(pcursor, ssKey, ssValue, fFlags);
1326 if (ret == DB_NOTFOUND)
1327 break;
1328 if (ret != 0)
1329 {
1330 pcursor->close();
1331 strErrorRet = strprintf("could not read wallet record: Berkeley DB error %d", ret);
1332 return false;
1333 }
1334
1335 string strType;
1336 try
1337 {
1338 CDataStream ssType = ssKey;
1339 ssType >> strType;
1340 }
1341 catch (...)
1342 {
1343 pcursor->close();
1344 strErrorRet = "wallet.dat has a record whose key type cannot be read";
1345 return false;
1346 }
1347
1348 if (strType == "key" ||
1349 strType == "pool" ||
1350 strType == "hdmaster" ||
1351 strType == "hdchaincode" ||
1352 strType == "mkey" ||
1353 strType == "ckey" ||
1354 strType == "cryptedhdmaster" ||
1355 strType == "cryptedhdchaincode" ||
1356 strType == "walletminversion")
1357 continue;
1358
1359 if (!dbTo.WriteRaw(ssKey, ssValue))
1360 {
1361 pcursor->close();
1362 strErrorRet = strprintf("could not copy wallet record '%s'", strType.c_str());
1363 return false;
1364 }
1365 nCopiedRet++;
1366 }
1367
1368 pcursor->close();
1369 return true;
1370 }
1371};
1372
1373static string WalletEncryptBackupPath()
1374{
1375 string strBase = GetAppDir() + "/wallet.dat.before-encrypt.";
1376 string strPath = strBase + i64tostr(GetTime()) + ".bak";
1377 for (int i = 1; FileExists(strPath.c_str()); i++)
1378 strPath = strBase + i64tostr(GetTime()) + "." + itostr(i) + ".bak";
1379 return strPath;
1380}
1381
1382static bool EncryptPrivateKeyForWallet(const CKeyingMaterial& vchMasterKey,
1383 const vector<unsigned char>& vchPubKey,
1384 const CPrivKey& vchPrivKey,
1385 vector<unsigned char>& vchCryptedRet,
1386 string& strErrorRet)
1387{
1388 if (!EncryptSecret(vchMasterKey,
1389 vector<unsigned char>(vchPrivKey.begin(), vchPrivKey.end()),
1390 WalletKeyIV(vchPubKey), vchCryptedRet))
1391 {
1392 strErrorRet = "could not encrypt a private key";
1393 return false;
1394 }
1395 return true;
1396}
1397
1398bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet)
1399{
1400 strBackupRet.clear();
1401 strErrorRet.clear();
1402
1403 if (IsWalletEncrypted())
1404 {
1405 strErrorRet = "wallet is already encrypted";
1406 return false;
1407 }
1408 if (strPassphrase.size() < 8)
1409 {
1410 strErrorRet = "passphrase must be at least 8 characters";
1411 return false;
1412 }
1413
1414 map<vector<unsigned char>, CPrivKey> mapPlainKeys;
1415 vector<unsigned char> vchPlainHDMaster;
1416 vector<unsigned char> vchPlainHDChainCode;
1417 unsigned int nHDNextSnapshot = 0;
1418 {
1419 CRITICAL_BLOCK(cs_mapKeys)
1420 mapPlainKeys = mapKeys;
1421 }
1422 {
1423 CRITICAL_BLOCK(cs_keyPool)
1424 {
1425 vchPlainHDMaster = vchHDMaster;
1426 vchPlainHDChainCode = vchHDChainCode;
1427 nHDNextSnapshot = nHDNext;
1428 }
1429 }
1430 if (mapPlainKeys.empty())
1431 {
1432 strErrorRet = "wallet has no private keys to encrypt";
1433 return false;
1434 }
1435
1436 CKeyingMaterial vchMasterKey;
1437 vchMasterKey.resize(32);
1438 if (RAND_bytes(&vchMasterKey[0], vchMasterKey.size()) != 1)
1439 {
1440 strErrorRet = "could not create a random wallet master key";
1441 return false;
1442 }
1443
1444 CWalletMasterKey kMasterKey;
1445 if (RAND_bytes(&kMasterKey.vchSalt[0], kMasterKey.vchSalt.size()) != 1)
1446 {
1447 strErrorRet = "could not create a wallet encryption salt";
1448 return false;
1449 }
1450
1451 CKeyingMaterial vchPassKey;
1452 vector<unsigned char> vchPassIV;
1453 if (!DeriveWalletPassphraseKey(strPassphrase, kMasterKey.vchSalt,
1454 kMasterKey.nDeriveIterations,
1455 vchPassKey, vchPassIV) ||
1456 !EncryptSecret(vchPassKey,
1457 vector<unsigned char>(vchMasterKey.begin(), vchMasterKey.end()),
1458 vchPassIV, kMasterKey.vchCryptedKey))
1459 {
1460 strErrorRet = "could not encrypt the wallet master key";
1461 return false;
1462 }
1463
1464 vector<pair<vector<unsigned char>, vector<unsigned char> > > vCryptedKeys;
1465 for (map<vector<unsigned char>, CPrivKey>::const_iterator mi = mapPlainKeys.begin();
1466 mi != mapPlainKeys.end(); ++mi)
1467 {
1468 vector<unsigned char> vchCrypted;
1469 if (!EncryptPrivateKeyForWallet(vchMasterKey, (*mi).first, (*mi).second,
1470 vchCrypted, strErrorRet))
1471 return false;
1472 vCryptedKeys.push_back(make_pair((*mi).first, vchCrypted));
1473 }
1474
1475 vector<unsigned char> vchCryptedHDMasterNew;
1476 vector<unsigned char> vchCryptedHDChainCodeNew;
1477 if (!vchPlainHDMaster.empty() || !vchPlainHDChainCode.empty())
1478 {
1479 if (vchPlainHDMaster.size() != 32 || vchPlainHDChainCode.size() != 32)
1480 {
1481 strErrorRet = "wallet has an incomplete HD seed";
1482 return false;
1483 }
1484 if (!EncryptSecret(vchMasterKey, vchPlainHDMaster,
1485 WalletSecretIV("hdmaster"), vchCryptedHDMasterNew) ||
1486 !EncryptSecret(vchMasterKey, vchPlainHDChainCode,
1487 WalletSecretIV("hdchaincode"), vchCryptedHDChainCodeNew))
1488 {
1489 strErrorRet = "could not encrypt the HD seed";
1490 return false;
1491 }
1492 }
1493
1494 vector<pair<int64, vector<unsigned char> > > vNewPool;
1495 vector<pair<vector<unsigned char>, vector<unsigned char> > > vNewPoolKeys;
1496 unsigned int nHDNextNew = nHDNextSnapshot;
1497 for (int i = 0; i < KEYPOOL_SIZE; i++)
1498 {
1499 CKey key;
1500 if (!vchPlainHDMaster.empty())
1501 {
1502 string strDeriveError;
1503 if (!DeriveHDKey(nHDNextNew, key, strDeriveError))
1504 {
1505 strErrorRet = strprintf("could not derive replacement key-pool key %u: %s",
1506 nHDNextNew, strDeriveError.c_str());
1507 return false;
1508 }
1509 nHDNextNew++;
1510 }
1511 else
1512 {
1513 key.MakeNewKey();
1514 }
1515
1516 vector<unsigned char> vchPubKey = key.GetPubKey();
1517 if (!mapPlainKeys.count(vchPubKey))
1518 {
1519 vector<unsigned char> vchCrypted;
1520 if (!EncryptPrivateKeyForWallet(vchMasterKey, vchPubKey, key.GetPrivKey(),
1521 vchCrypted, strErrorRet))
1522 return false;
1523 vNewPoolKeys.push_back(make_pair(vchPubKey, vchCrypted));
1524 }
1525 vNewPool.push_back(make_pair((int64)i + 1, vchPubKey));
1526 }
1527
1528 string strTempFile = strprintf("wallet.encrypting.%lld.dat", GetTime());
1529 string strTempPath = GetAppDir() + "/" + strTempFile;
1530 remove(strTempPath.c_str());
1531
1532 int nCopied = 0;
1533 {
1534 CWalletRewriteDB dbSource("wallet.dat", "r");
1535 CWalletRewriteDB dbTarget(strTempFile.c_str(), "c+");
1536 if (!dbSource.CopyPublicRecordsTo(dbTarget, nCopied, strErrorRet))
1537 {
1538 remove(strTempPath.c_str());
1539 return false;
1540 }
1541 if (!dbTarget.WriteWalletMinVersion(WALLET_FORMAT_ENCRYPTED) ||
1542 !dbTarget.WriteMasterKey(nWalletMasterKeyMaxID + 1, kMasterKey))
1543 {
1544 strErrorRet = "could not write encrypted wallet metadata";
1545 remove(strTempPath.c_str());
1546 return false;
1547 }
1548 for (size_t i = 0; i < vCryptedKeys.size(); i++)
1549 {
1550 if (!dbTarget.WriteCryptedKey(vCryptedKeys[i].first, vCryptedKeys[i].second))
1551 {
1552 strErrorRet = "could not write an encrypted private key";
1553 remove(strTempPath.c_str());
1554 return false;
1555 }
1556 }
1557 for (size_t i = 0; i < vNewPoolKeys.size(); i++)
1558 {
1559 if (!dbTarget.WriteCryptedKey(vNewPoolKeys[i].first, vNewPoolKeys[i].second))
1560 {
1561 strErrorRet = "could not write an encrypted replacement key-pool key";
1562 remove(strTempPath.c_str());
1563 return false;
1564 }
1565 }
1566 if (!vchCryptedHDMasterNew.empty() &&
1567 !dbTarget.WriteCryptedHDMaster(vchCryptedHDMasterNew, vchCryptedHDChainCodeNew))
1568 {
1569 strErrorRet = "could not write the encrypted HD seed";
1570 remove(strTempPath.c_str());
1571 return false;
1572 }
1573 if (!dbTarget.WriteHDNext(nHDNextNew))
1574 {
1575 strErrorRet = "could not write the replacement HD derivation counter";
1576 remove(strTempPath.c_str());
1577 return false;
1578 }
1579 for (size_t i = 0; i < vNewPool.size(); i++)
1580 {
1581 if (!dbTarget.WritePool(vNewPool[i].first, vNewPool[i].second))
1582 {
1583 strErrorRet = "could not write replacement key-pool records";
1584 remove(strTempPath.c_str());
1585 return false;
1586 }
1587 }
1588 }
1589
1590 // The source and target handles are closed. Close the environment too
1591 // before replacing files on Windows; the command exits immediately after
1592 // this, and tests verify reload through a fresh process.
1593 DBFlush(true);
1594
1595 string strWalletPath = GetAppDir() + "/wallet.dat";
1596 string strBackupPath = WalletEncryptBackupPath();
1597 if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0)
1598 {
1599 strErrorRet = strprintf("could not move the original wallet to %s", strBackupPath.c_str());
1600 remove(strTempPath.c_str());
1601 return false;
1602 }
1603 if (rename(strTempPath.c_str(), strWalletPath.c_str()) != 0)
1604 {
1605 rename(strBackupPath.c_str(), strWalletPath.c_str());
1606 strErrorRet = "could not install the encrypted wallet; the original was restored";
1607 remove(strTempPath.c_str());
1608 return false;
1609 }
1610
1611 strBackupRet = strBackupPath;
1612 printf("EncryptWallet() : copied %d public record(s), encrypted %u key(s), "
1613 "rebuilt %d key-pool entry(s)\n",
1614 nCopied, (unsigned int)(vCryptedKeys.size() + vNewPoolKeys.size()),
1615 (int)vNewPool.size());
1616
1617 // The rewritten wallet.dat is clean, and that is not enough: every record
1618 // this wallet ever wrote also passed through the environment's write-ahead
1619 // log in database/, and Berkeley DB keeps those files. Measured on a real
1620 // wallet -- wallet.dat held no trace of a known private key afterwards,
1621 // while database/log.0000000002 still held all 118 bytes of it, and still
1622 // did after a full node restart.
1623 //
1624 // That matters more here than it looks, because our own backup advice
1625 // (README, and the LEIA-ME in the desktop folders) is to copy the whole
1626 // directory rather than wallet.dat alone -- which is the right advice, and
1627 // the reason for issue #40. Following it after encrypting would carry the
1628 // unencrypted key along with the backup.
1629 //
1630 // DB_LOG_AUTO_REMOVE is deliberately not the answer: it sits commented out
1631 // where this environment is opened, marked "causes corruption", and that
1632 // note predates this fork. This is the conservative form instead -- force a
1633 // checkpoint, then ask Berkeley DB to drop only the logs it says are no
1634 // longer needed for recovery, once, at the one moment we know the wallet
1635 // has just been rewritten.
1636 return true;
1637}
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;
16extern map<string, string> mapAddressBook; 16extern map<string, string> mapAddressBook;
17extern bool fClient; 17extern bool fClient;
18 18
19// Wallet file format guard. The current wallet stores private keys in plain
20// records. The next format will encrypt them; builds that do not implement it
21// must stop before they can show an empty-looking wallet.
22static const int WALLET_FORMAT_PLAINTEXT = 1;
23static const int WALLET_FORMAT_ENCRYPTED = 2;
24static const int WALLET_FORMAT_SUPPORTED = WALLET_FORMAT_ENCRYPTED;
25
26class CWalletMasterKey
27{
28public:
29 vector<unsigned char> vchCryptedKey;
30 vector<unsigned char> vchSalt;
31 unsigned int nDerivationMethod;
32 unsigned int nDeriveIterations;
33
34 CWalletMasterKey()
35 {
36 nDerivationMethod = 0;
37 nDeriveIterations = 25000;
38 vchSalt.resize(8);
39 }
40
41 IMPLEMENT_SERIALIZE
42 (
43 READWRITE(vchCryptedKey);
44 READWRITE(vchSalt);
45 READWRITE(nDerivationMethod);
46 READWRITE(nDeriveIterations);
47 )
48};
49
19 50
20extern DbEnv dbenv; 51extern DbEnv dbenv;
21extern void DBFlush(bool fShutdown); 52extern void DBFlush(bool fShutdown);
53// Close the environment and delete its logs. Only the encryption command calls
54// this: the logs hold the plaintext records the old wallet wrote.
55extern void PurgeDbEnvironmentLogs();
22 56
23 57
24 58
@@ -411,6 +445,29 @@ public:
411 && Write(string("hdchaincode"), vchChainCode); 445 && Write(string("hdchaincode"), vchChainCode);
412 } 446 }
413 447
448 bool WriteWalletMinVersion(int nVersion)
449 {
450 return Write(string("walletminversion"), nVersion);
451 }
452
453 bool WriteMasterKey(unsigned int nID, const CWalletMasterKey& kMasterKey)
454 {
455 return Write(make_pair(string("mkey"), nID), kMasterKey, true);
456 }
457
458 bool WriteCryptedKey(const vector<unsigned char>& vchPubKey,
459 const vector<unsigned char>& vchCryptedSecret)
460 {
461 return Write(make_pair(string("ckey"), vchPubKey), vchCryptedSecret, false);
462 }
463
464 bool WriteCryptedHDMaster(const vector<unsigned char>& vchCryptedMaster,
465 const vector<unsigned char>& vchCryptedChainCode)
466 {
467 return Write(string("cryptedhdmaster"), vchCryptedMaster)
468 && Write(string("cryptedhdchaincode"), vchCryptedChainCode);
469 }
470
414 bool WriteHDNext(unsigned int nNext) 471 bool WriteHDNext(unsigned int nNext)
415 { 472 {
416 return Write(string("hdnext"), nNext); 473 return Write(string("hdnext"), nNext);
@@ -478,6 +535,7 @@ bool BackupWallet(const string& strDest);
478// POSIX -- the output holds private keys in the clear. 535// POSIX -- the output holds private keys in the clear.
479bool DumpWallet(const string& strDest); 536bool DumpWallet(const string& strDest);
480bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet); 537bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet);
538bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet);
481 539
482inline bool SetAddressBookName(const string& strAddress, const string& strName) 540inline bool SetAddressBookName(const string& strAddress, const string& strName)
483{ 541{
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<pair<uint256, bool> > vWalletUpdated;
72CCriticalSection cs_mapWallet; 72CCriticalSection cs_mapWallet;
73 73
74map<vector<unsigned char>, CPrivKey> mapKeys; 74map<vector<unsigned char>, CPrivKey> mapKeys;
75map<vector<unsigned char>, vector<unsigned char> > mapCryptedKeys;
75map<uint160, vector<unsigned char> > mapPubKeys; 76map<uint160, vector<unsigned char> > mapPubKeys;
77map<unsigned int, CWalletMasterKey> mapMasterKeys;
78unsigned int nWalletMasterKeyMaxID = 0;
79CKeyingMaterial vWalletMasterKey;
80vector<unsigned char> vchCryptedHDMaster;
81vector<unsigned char> vchCryptedHDChainCode;
82bool fWalletEncrypted = false;
83bool fWalletLocked = true;
76CCriticalSection cs_mapKeys; 84CCriticalSection cs_mapKeys;
77CKey keyUser; 85CKey keyUser;
78 86
@@ -159,6 +167,21 @@ int MinerThreadCount()
159 167
160bool AddKey(const CKey& key) 168bool AddKey(const CKey& key)
161{ 169{
170 if (IsWalletEncrypted())
171 {
172 string strError;
173 CPrivKey vchPrivKey = key.GetPrivKey();
174 vector<unsigned char> vchCryptedSecret;
175 if (IsWalletLocked())
176 return error("AddKey() : wallet is locked\n");
177 if (!EncryptSecret(vWalletMasterKey, vector<unsigned char>(vchPrivKey.begin(), vchPrivKey.end()),
178 WalletKeyIV(key.GetPubKey()), vchCryptedSecret))
179 return error("AddKey() : encrypting key failed\n");
180 if (!AddCryptedKey(key.GetPubKey(), vchCryptedSecret))
181 return false;
182 return CWalletDB().WriteCryptedKey(key.GetPubKey(), vchCryptedSecret);
183 }
184
162 CRITICAL_BLOCK(cs_mapKeys) 185 CRITICAL_BLOCK(cs_mapKeys)
163 { 186 {
164 mapKeys[key.GetPubKey()] = key.GetPrivKey(); 187 mapKeys[key.GetPubKey()] = key.GetPrivKey();
@@ -167,10 +190,287 @@ bool AddKey(const CKey& key)
167 return CWalletDB().WriteKey(key.GetPubKey(), key.GetPrivKey()); 190 return CWalletDB().WriteKey(key.GetPubKey(), key.GetPrivKey());
168} 191}
169 192
193bool IsWalletEncrypted()
194{
195 return fWalletEncrypted;
196}
197
198bool IsWalletLocked()
199{
200 return fWalletEncrypted && fWalletLocked;
201}
202
203void LockWallet()
204{
205 if (!fWalletEncrypted)
206 return;
207 CRITICAL_BLOCK(cs_mapKeys)
208 {
209 vWalletMasterKey.clear();
210 if (!vchCryptedHDMaster.empty())
211 vchHDMaster.clear();
212 if (!vchCryptedHDChainCode.empty())
213 vchHDChainCode.clear();
214 fWalletLocked = true;
215 }
216}
217
218bool DeriveWalletPassphraseKey(const string& strPassphrase,
219 const vector<unsigned char>& vchSalt,
220 unsigned int nDeriveIterations,
221 CKeyingMaterial& vchKeyRet,
222 vector<unsigned char>& vchIVRet)
223{
224 vchKeyRet.clear();
225 vchIVRet.clear();
226 if (vchSalt.size() != 8 || nDeriveIterations < 1)
227 return false;
228
229 unsigned char chKey[32];
230 unsigned char chIV[16];
231 int n = EVP_BytesToKey(EVP_aes_256_cbc(), EVP_sha512(), &vchSalt[0],
232 (const unsigned char*)strPassphrase.data(),
233 (int)strPassphrase.size(),
234 (int)nDeriveIterations,
235 chKey, chIV);
236 if (n != 32)
237 {
238 memset(chKey, 0, sizeof(chKey));
239 memset(chIV, 0, sizeof(chIV));
240 return false;
241 }
242 vchKeyRet.assign(chKey, chKey + sizeof(chKey));
243 vchIVRet.assign(chIV, chIV + sizeof(chIV));
244 memset(chKey, 0, sizeof(chKey));
245 memset(chIV, 0, sizeof(chIV));
246 return true;
247}
248
249bool EncryptSecret(const CKeyingMaterial& vchKey,
250 const vector<unsigned char>& vchPlaintext,
251 const vector<unsigned char>& vchIV,
252 vector<unsigned char>& vchCiphertextRet)
253{
254 vchCiphertextRet.clear();
255 if (vchKey.size() != 32 || vchIV.size() != 16)
256 return false;
257
258 EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
259 if (!ctx)
260 return false;
261
262 bool fOk = false;
263 int nLen = 0;
264 int nFinal = 0;
265 vector<unsigned char> vchOut(vchPlaintext.size() + EVP_CIPHER_block_size(EVP_aes_256_cbc()));
266 if (EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, &vchKey[0], &vchIV[0]) &&
267 EVP_EncryptUpdate(ctx, &vchOut[0], &nLen,
268 vchPlaintext.empty() ? NULL : &vchPlaintext[0],
269 (int)vchPlaintext.size()) &&
270 EVP_EncryptFinal_ex(ctx, &vchOut[0] + nLen, &nFinal))
271 {
272 vchOut.resize(nLen + nFinal);
273 vchCiphertextRet.swap(vchOut);
274 fOk = true;
275 }
276 EVP_CIPHER_CTX_free(ctx);
277 return fOk;
278}
279
280bool DecryptSecret(const CKeyingMaterial& vchKey,
281 const vector<unsigned char>& vchCiphertext,
282 const vector<unsigned char>& vchIV,
283 vector<unsigned char>& vchPlaintextRet)
284{
285 vchPlaintextRet.clear();
286 if (vchKey.size() != 32 || vchIV.size() != 16 || vchCiphertext.empty())
287 return false;
288
289 EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
290 if (!ctx)
291 return false;
292
293 bool fOk = false;
294 int nLen = 0;
295 int nFinal = 0;
296 vector<unsigned char> vchOut(vchCiphertext.size());
297 if (EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, &vchKey[0], &vchIV[0]) &&
298 EVP_DecryptUpdate(ctx, &vchOut[0], &nLen, &vchCiphertext[0],
299 (int)vchCiphertext.size()) &&
300 EVP_DecryptFinal_ex(ctx, &vchOut[0] + nLen, &nFinal))
301 {
302 vchOut.resize(nLen + nFinal);
303 vchPlaintextRet.swap(vchOut);
304 fOk = true;
305 }
306 EVP_CIPHER_CTX_free(ctx);
307 return fOk;
308}
309
310vector<unsigned char> WalletKeyIV(const vector<unsigned char>& vchPubKey)
311{
312 uint256 hash = Hash(vchPubKey.begin(), vchPubKey.end());
313 vector<unsigned char> vchIV(16);
314 memcpy(&vchIV[0], hash.begin(), vchIV.size());
315 return vchIV;
316}
317
318vector<unsigned char> WalletSecretIV(const string& strLabel)
319{
320 uint256 hash = Hash(strLabel.begin(), strLabel.end());
321 vector<unsigned char> vchIV(16);
322 memcpy(&vchIV[0], hash.begin(), vchIV.size());
323 return vchIV;
324}
325
326bool AddCryptedKey(const vector<unsigned char>& vchPubKey,
327 const vector<unsigned char>& vchCryptedSecret)
328{
329 if (vchPubKey.empty() || vchCryptedSecret.empty())
330 return false;
331 CRITICAL_BLOCK(cs_mapKeys)
332 {
333 fWalletEncrypted = true;
334 mapCryptedKeys[vchPubKey] = vchCryptedSecret;
335 mapPubKeys[Hash160(vchPubKey)] = vchPubKey;
336 }
337 return true;
338}
339
340bool WalletCanSpendKey(const vector<unsigned char>& vchPubKey)
341{
342 CRITICAL_BLOCK(cs_mapKeys)
343 return mapKeys.count(vchPubKey) > 0 || mapCryptedKeys.count(vchPubKey) > 0;
344 return false;
345}
346
347bool GetWalletPrivKey(const vector<unsigned char>& vchPubKey,
348 CPrivKey& vchPrivKeyRet,
349 string& strErrorRet)
350{
351 vchPrivKeyRet.clear();
352 CRITICAL_BLOCK(cs_mapKeys)
353 {
354 map<vector<unsigned char>, CPrivKey>::iterator mi = mapKeys.find(vchPubKey);
355 if (mi != mapKeys.end())
356 {
357 vchPrivKeyRet = (*mi).second;
358 return true;
359 }
360
361 map<vector<unsigned char>, vector<unsigned char> >::iterator mci = mapCryptedKeys.find(vchPubKey);
362 if (mci == mapCryptedKeys.end())
363 {
364 strErrorRet = "private key not found";
365 return false;
366 }
367 if (IsWalletLocked())
368 {
369 strErrorRet = "wallet is locked";
370 return false;
371 }
372
373 vector<unsigned char> vchPlaintext;
374 if (!DecryptSecret(vWalletMasterKey, (*mci).second, WalletKeyIV(vchPubKey), vchPlaintext))
375 {
376 strErrorRet = "could not decrypt private key";
377 return false;
378 }
379
380 CPrivKey vchPrivKey(vchPlaintext.begin(), vchPlaintext.end());
381 CKey key;
382 if (!key.SetPrivKey(vchPrivKey) || key.GetPubKey() != vchPubKey)
383 {
384 strErrorRet = "decrypted private key does not match its public key";
385 return false;
386 }
387 vchPrivKeyRet = vchPrivKey;
388 return true;
389 }
390 return false;
391}
392
393bool UnlockWallet(const string& strPassphrase, string& strErrorRet)
394{
395 strErrorRet.clear();
396 if (!IsWalletEncrypted())
397 return true;
398
399 CRITICAL_BLOCK(cs_mapKeys)
400 {
401 for (map<unsigned int, CWalletMasterKey>::iterator mi = mapMasterKeys.begin();
402 mi != mapMasterKeys.end(); ++mi)
403 {
404 const CWalletMasterKey& kMasterKey = (*mi).second;
405 CKeyingMaterial vchPassKey;
406 vector<unsigned char> vchPassIV;
407 if (!DeriveWalletPassphraseKey(strPassphrase, kMasterKey.vchSalt,
408 kMasterKey.nDeriveIterations,
409 vchPassKey, vchPassIV))
410 continue;
411
412 vector<unsigned char> vchMasterPlain;
413 if (!DecryptSecret(vchPassKey, kMasterKey.vchCryptedKey,
414 vchPassIV, vchMasterPlain))
415 continue;
416 if (vchMasterPlain.size() != 32)
417 continue;
418
419 CKeyingMaterial vchCandidate(vchMasterPlain.begin(), vchMasterPlain.end());
420 bool fValid = mapCryptedKeys.empty();
421 for (map<vector<unsigned char>, vector<unsigned char> >::iterator mci = mapCryptedKeys.begin();
422 mci != mapCryptedKeys.end(); ++mci)
423 {
424 vector<unsigned char> vchPlaintext;
425 if (!DecryptSecret(vchCandidate, (*mci).second, WalletKeyIV((*mci).first), vchPlaintext))
426 break;
427 CPrivKey vchPrivKey(vchPlaintext.begin(), vchPlaintext.end());
428 CKey key;
429 if (key.SetPrivKey(vchPrivKey) && key.GetPubKey() == (*mci).first)
430 fValid = true;
431 break;
432 }
433 if (fValid)
434 {
435 vWalletMasterKey = vchCandidate;
436 if (!vchCryptedHDMaster.empty())
437 {
438 vector<unsigned char> vchPlainHDMaster;
439 if (!DecryptSecret(vWalletMasterKey, vchCryptedHDMaster,
440 WalletSecretIV("hdmaster"), vchPlainHDMaster) ||
441 vchPlainHDMaster.size() != 32)
442 {
443 strErrorRet = "could not decrypt HD master key";
444 return false;
445 }
446 vchHDMaster = vchPlainHDMaster;
447 }
448 if (!vchCryptedHDChainCode.empty())
449 {
450 vector<unsigned char> vchPlainHDChainCode;
451 if (!DecryptSecret(vWalletMasterKey, vchCryptedHDChainCode,
452 WalletSecretIV("hdchaincode"), vchPlainHDChainCode) ||
453 vchPlainHDChainCode.size() != 32)
454 {
455 strErrorRet = "could not decrypt HD chain code";
456 return false;
457 }
458 vchHDChainCode = vchPlainHDChainCode;
459 }
460 fWalletLocked = false;
461 return true;
462 }
463 }
464 }
465
466 strErrorRet = "passphrase did not unlock the wallet";
467 return false;
468}
469
170static bool WalletAlreadyHasKey(const vector<unsigned char>& vchPubKey) 470static bool WalletAlreadyHasKey(const vector<unsigned char>& vchPubKey)
171{ 471{
172 CRITICAL_BLOCK(cs_mapKeys) 472 CRITICAL_BLOCK(cs_mapKeys)
173 return mapKeys.count(vchPubKey) > 0; 473 return mapKeys.count(vchPubKey) > 0 || mapCryptedKeys.count(vchPubKey) > 0;
174 return false; 474 return false;
175} 475}
176 476
@@ -509,6 +809,12 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet)
509 809
510void TopUpKeyPool() 810void TopUpKeyPool()
511{ 811{
812 if (IsWalletLocked())
813 {
814 printf("TopUpKeyPool() : wallet is encrypted and locked\n");
815 return;
816 }
817
512 CRITICAL_BLOCK(cs_keyPool) 818 CRITICAL_BLOCK(cs_keyPool)
513 { 819 {
514 while ((int)mapKeyPool.size() < KEYPOOL_SIZE) 820 while ((int)mapKeyPool.size() < KEYPOOL_SIZE)
@@ -562,6 +868,12 @@ void TopUpKeyPool()
562 868
563vector<unsigned char> GetKeyFromPool() 869vector<unsigned char> GetKeyFromPool()
564{ 870{
871 if (IsWalletLocked())
872 {
873 printf("GetKeyFromPool() : wallet is encrypted and locked\n");
874 return vector<unsigned char>();
875 }
876
565 CRITICAL_BLOCK(cs_keyPool) 877 CRITICAL_BLOCK(cs_keyPool)
566 { 878 {
567 TopUpKeyPool(); 879 TopUpKeyPool();
@@ -584,7 +896,15 @@ vector<unsigned char> GetKeyFromPool()
584 // locked open elsewhere. Generating on demand is what this code did for 896 // locked open elsewhere. Generating on demand is what this code did for
585 // its whole life, so fall back to it rather than refuse to mine. 897 // its whole life, so fall back to it rather than refuse to mine.
586 printf("GetKeyFromPool() : pool empty, generating a key on demand\n"); 898 printf("GetKeyFromPool() : pool empty, generating a key on demand\n");
587 return GenerateNewKey(); 899 try
900 {
901 return GenerateNewKey();
902 }
903 catch (const std::exception& e)
904 {
905 printf("GetKeyFromPool() : failed to generate a key: %s\n", e.what());
906 return vector<unsigned char>();
907 }
588} 908}
589 909
590 910
@@ -3581,6 +3901,11 @@ bool BitcoinMiner(int nThreadId)
3581 // thread happened to win a block, which meant a backup taken while mining 3901 // thread happened to win a block, which meant a backup taken while mining
3582 // could not spend what the very next block paid. 3902 // could not spend what the very next block paid.
3583 vector<unsigned char> vchPubKey = GetKeyFromPool(); 3903 vector<unsigned char> vchPubKey = GetKeyFromPool();
3904 if (vchPubKey.empty())
3905 {
3906 printf("BitcoinMiner: no wallet key available for coinbase; mining stopped\n");
3907 return false;
3908 }
3584 CBigNum bnExtraNonce = 0; 3909 CBigNum bnExtraNonce = 0;
3585 while (fGenerateBitcoins) 3910 while (fGenerateBitcoins)
3586 { 3911 {
@@ -3759,6 +4084,11 @@ bool BitcoinMiner(int nThreadId)
3759 // them out one at a time under cs_keyPool, which is 4084 // them out one at a time under cs_keyPool, which is
3760 // what keeps concurrent coinbases distinct (#58). 4085 // what keeps concurrent coinbases distinct (#58).
3761 vchPubKey = GetKeyFromPool(); 4086 vchPubKey = GetKeyFromPool();
4087 if (vchPubKey.empty())
4088 {
4089 printf("BitcoinMiner: no replacement wallet key available; mining stopped\n");
4090 fGenerateBitcoins = false;
4091 }
3762 4092
3763 // Process this block the same as if we had received it from another node 4093 // Process this block the same as if we had received it from another node
3764 if (!ProcessBlock(NULL, pblock.release())) 4094 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);
129bool AddKey(const CKey& key); 129bool AddKey(const CKey& key);
130vector<unsigned char> GenerateNewKey(); 130vector<unsigned char> GenerateNewKey();
131 131
132// --- Wallet encryption ----------------------------------------------------
133//
134// Plain wallets keep private keys in mapKeys. Encrypted wallets keep public
135// keys visible so balances can still be recognized while locked, and decrypt
136// private keys only after an unlock passphrase has supplied the master key.
137typedef vector<unsigned char, secure_allocator<unsigned char> > CKeyingMaterial;
138extern map<vector<unsigned char>, vector<unsigned char> > mapCryptedKeys;
139extern map<unsigned int, CWalletMasterKey> mapMasterKeys;
140extern unsigned int nWalletMasterKeyMaxID;
141extern CKeyingMaterial vWalletMasterKey;
142extern vector<unsigned char> vchCryptedHDMaster;
143extern vector<unsigned char> vchCryptedHDChainCode;
144extern bool fWalletEncrypted;
145extern bool fWalletLocked;
146bool IsWalletEncrypted();
147bool IsWalletLocked();
148void LockWallet();
149bool UnlockWallet(const string& strPassphrase, string& strErrorRet);
150bool DeriveWalletPassphraseKey(const string& strPassphrase,
151 const vector<unsigned char>& vchSalt,
152 unsigned int nDeriveIterations,
153 CKeyingMaterial& vchKeyRet,
154 vector<unsigned char>& vchIVRet);
155bool EncryptSecret(const CKeyingMaterial& vchKey,
156 const vector<unsigned char>& vchPlaintext,
157 const vector<unsigned char>& vchIV,
158 vector<unsigned char>& vchCiphertextRet);
159bool DecryptSecret(const CKeyingMaterial& vchKey,
160 const vector<unsigned char>& vchCiphertext,
161 const vector<unsigned char>& vchIV,
162 vector<unsigned char>& vchPlaintextRet);
163vector<unsigned char> WalletKeyIV(const vector<unsigned char>& vchPubKey);
164vector<unsigned char> WalletSecretIV(const string& strLabel);
165bool AddCryptedKey(const vector<unsigned char>& vchPubKey,
166 const vector<unsigned char>& vchCryptedSecret);
167bool WalletCanSpendKey(const vector<unsigned char>& vchPubKey);
168bool GetWalletPrivKey(const vector<unsigned char>& vchPubKey,
169 CPrivKey& vchPrivKeyRet,
170 string& strErrorRet);
171
132// --- Key pool ------------------------------------------------------------- 172// --- Key pool -------------------------------------------------------------
133// 173//
134// A backup of this wallet only ever contained the keys that existed the moment 174// 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*
40 return argval(argc, argv, keyDash); 40 return argval(argc, argv, keyDash);
41} 41}
42 42
43static bool ReadPassphraseArgument(const string& strArg, string& strPassphraseRet, string& strErrorRet)
44{
45 strPassphraseRet.clear();
46 strErrorRet.clear();
47 if (strArg.empty())
48 {
49 AttachTerminal();
50 fprintf(stderr, "Enter wallet passphrase: ");
51 fflush(stderr);
52 char buf[4096];
53 if (!fgets(buf, sizeof(buf), stdin))
54 {
55 strErrorRet = "could not read passphrase from stdin";
56 return false;
57 }
58 strPassphraseRet = buf;
59 }
60 else if (strArg[0] == '@')
61 {
62 string strPath = strArg.substr(1);
63 FILE* pf = fopen(strPath.c_str(), "rb");
64 if (!pf)
65 {
66 strErrorRet = strprintf("could not read passphrase file %s", strPath.c_str());
67 return false;
68 }
69 char buf[4096];
70 if (!fgets(buf, sizeof(buf), pf))
71 {
72 fclose(pf);
73 strErrorRet = strprintf("passphrase file %s is empty", strPath.c_str());
74 return false;
75 }
76 fclose(pf);
77 strPassphraseRet = buf;
78 }
79 else
80 {
81 strErrorRet = "passphrase literals on the command line are unsafe; use the option without a value to read stdin, or use @FILE";
82 return false;
83 }
84
85 while (!strPassphraseRet.empty() &&
86 (strPassphraseRet[strPassphraseRet.size() - 1] == '\n' ||
87 strPassphraseRet[strPassphraseRet.size() - 1] == '\r'))
88 strPassphraseRet.resize(strPassphraseRet.size() - 1);
89 if (strPassphraseRet.empty())
90 {
91 strErrorRet = "empty passphrase";
92 return false;
93 }
94 return true;
95}
96
43// The real printf, not the one util.h remaps to OutputDebugStringF. Help that 97// The real printf, not the one util.h remaps to OutputDebugStringF. Help that
44// goes to debug.log is help nobody asked for: `-help` printed a full page into 98// goes to debug.log is help nobody asked for: `-help` printed a full page into
45// the data directory and returned 0 with an empty terminal, which reads as a 99// the data directory and returned 0 with an empty terminal, which reads as a
@@ -56,7 +110,7 @@ static void PrintUsage()
56 printf(" /debug\n"); 110 printf(" /debug\n");
57 printf(" /gen\n"); 111 printf(" /gen\n");
58 printf(" /nogui or /daemon\n"); 112 printf(" /nogui or /daemon\n");
59 printf(" /selftest=wallet-keypool or /selftest=wallet-hd\n"); 113 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, or wallet-encrypt\n");
60 printf("\n"); 114 printf("\n");
61 printf("Mining mode:\n"); 115 printf("Mining mode:\n");
62 printf(" /operator\n"); 116 printf(" /operator\n");
@@ -85,6 +139,10 @@ static void PrintUsage()
85 printf(" /dumpwallet=FILE (export private keys as text -- readable by\n"); 139 printf(" /dumpwallet=FILE (export private keys as text -- readable by\n");
86 printf(" anyone, so guard it like cash)\n"); 140 printf(" anyone, so guard it like cash)\n");
87 printf(" /importwallet=FILE (load keys from such a file back in)\n"); 141 printf(" /importwallet=FILE (load keys from such a file back in)\n");
142 printf(" /encryptwallet[=@FILE] (rewrite wallet.dat with encrypted private keys\n");
143 printf(" and encrypted HD seed, then exit)\n");
144 printf(" /walletpassphrase[=@FILE] (unlock an encrypted wallet for one-shot\n");
145 printf(" commands; without @FILE reads stdin)\n");
88 printf(" /newaddress (print the next receiving address, then exit)\n"); 146 printf(" /newaddress (print the next receiving address, then exit)\n");
89 printf(" /sendto=ADDRESS,AMOUNT (spend from this wallet, then exit -- the\n"); 147 printf(" /sendto=ADDRESS,AMOUNT (spend from this wallet, then exit -- the\n");
90 printf(" only way to send without the window)\n"); 148 printf(" only way to send without the window)\n");
@@ -299,6 +357,39 @@ int main(int argc, char* argv[])
299 } 357 }
300 printf("Height=%d\n", nBestHeight); 358 printf("Height=%d\n", nBestHeight);
301 359
360 bool fEncryptWallet = arg(argc, argv, "/encryptwallet") || arg(argc, argv, "-encryptwallet");
361 string strEncryptWallet = argval2(argc, argv, "/encryptwallet", "-encryptwallet");
362 if (fEncryptWallet)
363 {
364 string strPassphrase;
365 string strPassphraseError;
366 if (!ReadPassphraseArgument(strEncryptWallet, strPassphrase, strPassphraseError))
367 {
368 fprintf(stderr, "Cannot read encryption passphrase: %s\n", strPassphraseError.c_str());
369 DBFlush(true);
370 return 1;
371 }
372 int nRet = CmdEncryptWallet(strPassphrase);
373 if (nRet != 0)
374 DBFlush(true);
375 return nRet;
376 }
377
378 bool fWalletPassphrase = arg(argc, argv, "/walletpassphrase") || arg(argc, argv, "-walletpassphrase");
379 string strWalletPassphrase = argval2(argc, argv, "/walletpassphrase", "-walletpassphrase");
380 if (fWalletPassphrase)
381 {
382 string strPassphrase;
383 string strUnlockError;
384 if (!ReadPassphraseArgument(strWalletPassphrase, strPassphrase, strUnlockError) ||
385 !UnlockWallet(strPassphrase, strUnlockError))
386 {
387 fprintf(stderr, "Cannot unlock wallet: %s\n", strUnlockError.c_str());
388 DBFlush(true);
389 return 1;
390 }
391 }
392
302 // Before the node opens sockets or touches anything: the wallet is loaded, 393 // Before the node opens sockets or touches anything: the wallet is loaded,
303 // which is all a backup needs, and finishing here means the copy is taken 394 // which is all a backup needs, and finishing here means the copy is taken
304 // from a quiet directory rather than from under a running node. 395 // from a quiet directory rather than from under a running node.
@@ -442,6 +533,14 @@ int main(int argc, char* argv[])
442 return 0; 533 return 0;
443 } 534 }
444 535
536 if (fGenerateBitcoins && IsWalletLocked())
537 {
538 fprintf(stderr, "Cannot mine while the encrypted wallet is locked. "
539 "Start with /walletpassphrase or /walletpassphrase=@FILE, or disable mining.\n");
540 DBFlush(true);
541 return 1;
542 }
543
445 ReacceptWalletTransactions(); 544 ReacceptWalletTransactions();
446 545
447 if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } 546 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
111 ./test_bip32.exe 111 ./test_bip32.exe
112 ./bitflash.exe -selftest=wallet-keypool 112 ./bitflash.exe -selftest=wallet-keypool
113 ./bitflash.exe -selftest=wallet-hd 113 ./bitflash.exe -selftest=wallet-hd
114 ./bitflash.exe -selftest=wallet-format
115 ./bitflash.exe -selftest=wallet-crypto
116 ./bitflash.exe -selftest=wallet-encrypt
114 ./bitflash.exe -selftest=net-message 117 ./bitflash.exe -selftest=net-message
115 ./bitflash.exe -selftest=consensus-limits 118 ./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
1020 if (!Solver(scriptPubKey, vSolution)) 1020 if (!Solver(scriptPubKey, vSolution))
1021 return false; 1021 return false;
1022 1022
1023 // Compile solution 1023 // Compile solution. Public keys stay available for encrypted wallets so the
1024 CRITICAL_BLOCK(cs_mapKeys) 1024 // wallet can recognize its own outputs while locked; private keys are
1025 // fetched only for an actual signature.
1026 foreach(PAIRTYPE(opcodetype, valtype)& item, vSolution)
1025 { 1027 {
1026 foreach(PAIRTYPE(opcodetype, valtype)& item, vSolution) 1028 if (item.first == OP_PUBKEY)
1027 { 1029 {
1028 if (item.first == OP_PUBKEY) 1030 const valtype& vchPubKey = item.second;
1031 if (!WalletCanSpendKey(vchPubKey))
1032 return false;
1033 if (hash != 0)
1029 { 1034 {
1030 // Sign 1035 CPrivKey vchPrivKey;
1031 const valtype& vchPubKey = item.second; 1036 string strError;
1032 if (!mapKeys.count(vchPubKey)) 1037 if (!GetWalletPrivKey(vchPubKey, vchPrivKey, strError))
1038 return false;
1039 vector<unsigned char> vchSig;
1040 if (!CKey::Sign(vchPrivKey, hash, vchSig))
1033 return false; 1041 return false;
1034 if (hash != 0) 1042 vchSig.push_back((unsigned char)nHashType);
1043 scriptSigRet << vchSig;
1044 }
1045 }
1046 else if (item.first == OP_PUBKEYHASH)
1047 {
1048 vector<unsigned char> vchPubKey;
1049 {
1050 CRITICAL_BLOCK(cs_mapKeys)
1035 { 1051 {
1036 vector<unsigned char> vchSig; 1052 map<uint160, valtype>::iterator mi = mapPubKeys.find(uint160(item.second));
1037 if (!CKey::Sign(mapKeys[vchPubKey], hash, vchSig)) 1053 if (mi == mapPubKeys.end())
1038 return false; 1054 return false;
1039 vchSig.push_back((unsigned char)nHashType); 1055 vchPubKey = (*mi).second;
1040 scriptSigRet << vchSig;
1041 } 1056 }
1042 } 1057 }
1043 else if (item.first == OP_PUBKEYHASH) 1058 if (!WalletCanSpendKey(vchPubKey))
1059 return false;
1060 if (hash != 0)
1044 { 1061 {
1045 // Sign and give pubkey 1062 CPrivKey vchPrivKey;
1046 map<uint160, valtype>::iterator mi = mapPubKeys.find(uint160(item.second)); 1063 string strError;
1047 if (mi == mapPubKeys.end()) 1064 if (!GetWalletPrivKey(vchPubKey, vchPrivKey, strError))
1048 return false; 1065 return false;
1049 const vector<unsigned char>& vchPubKey = (*mi).second; 1066 vector<unsigned char> vchSig;
1050 if (!mapKeys.count(vchPubKey)) 1067 if (!CKey::Sign(vchPrivKey, hash, vchSig))
1051 return false; 1068 return false;
1052 if (hash != 0) 1069 vchSig.push_back((unsigned char)nHashType);
1053 { 1070 scriptSigRet << vchSig << vchPubKey;
1054 vector<unsigned char> vchSig;
1055 if (!CKey::Sign(mapKeys[vchPubKey], hash, vchSig))
1056 return false;
1057 vchSig.push_back((unsigned char)nHashType);
1058 scriptSigRet << vchSig << vchPubKey;
1059 }
1060 } 1071 }
1061 } 1072 }
1062 } 1073 }
@@ -1080,27 +1091,27 @@ bool ExtractPubKey(const CScript& scriptPubKey, bool fMineOnly, vector<unsigned
1080 if (!Solver(scriptPubKey, vSolution)) 1091 if (!Solver(scriptPubKey, vSolution))
1081 return false; 1092 return false;
1082 1093
1083 CRITICAL_BLOCK(cs_mapKeys) 1094 foreach(PAIRTYPE(opcodetype, valtype)& item, vSolution)
1084 { 1095 {
1085 foreach(PAIRTYPE(opcodetype, valtype)& item, vSolution) 1096 valtype vchPubKey;
1097 if (item.first == OP_PUBKEY)
1086 { 1098 {
1087 valtype vchPubKey; 1099 vchPubKey = item.second;
1088 if (item.first == OP_PUBKEY) 1100 }
1089 { 1101 else if (item.first == OP_PUBKEYHASH)
1090 vchPubKey = item.second; 1102 {
1091 } 1103 CRITICAL_BLOCK(cs_mapKeys)
1092 else if (item.first == OP_PUBKEYHASH)
1093 { 1104 {
1094 map<uint160, valtype>::iterator mi = mapPubKeys.find(uint160(item.second)); 1105 map<uint160, valtype>::iterator mi = mapPubKeys.find(uint160(item.second));
1095 if (mi == mapPubKeys.end()) 1106 if (mi == mapPubKeys.end())
1096 continue; 1107 continue;
1097 vchPubKey = (*mi).second; 1108 vchPubKey = (*mi).second;
1098 } 1109 }
1099 if (!fMineOnly || mapKeys.count(vchPubKey)) 1110 }
1100 { 1111 if (!fMineOnly || WalletCanSpendKey(vchPubKey))
1101 vchPubKeyRet = vchPubKey; 1112 {
1102 return true; 1113 vchPubKeyRet = vchPubKey;
1103 } 1114 return true;
1104 } 1115 }
1105 } 1116 }
1106 return false; 1117 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 @@
33 33
34#ifdef _WIN32 34#ifdef _WIN32
35#include <direct.h> 35#include <direct.h>
36#include <fcntl.h>
37#include <io.h>
36#else 38#else
37#include <dirent.h> 39#include <dirent.h>
38#include <sys/stat.h> 40#include <sys/stat.h>
@@ -601,6 +603,500 @@ static int RunWalletHDSelfTest()
601 return nFail == 0 ? 0 : 1; 603 return nFail == 0 ? 0 : 1;
602} 604}
603 605
606static int RunWalletFormatSelfTest()
607{
608 fflush(stdout);
609 printf("wallet-format self-test\n");
610
611 std::string tmp;
612 std::string cwd;
613 if (!MakeTempDir(tmp))
614 {
615 printf(" FAIL could not create a temporary data directory\n");
616 return 1;
617 }
618 if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp))
619 {
620 printf(" FAIL could not move into the temporary data directory\n");
621 RemoveTree(tmp);
622 return 1;
623 }
624
625 int nFail = 0;
626 strSetDataDir = tmp;
627 printf(" temp datadir: %s\n", tmp.c_str());
628
629 try
630 {
631 class CWalletDBRaw : public CWalletDB
632 {
633 public:
634 CWalletDBRaw(const char* pszMode="r+") : CWalletDB(pszMode) { }
635 bool WriteStringRecord(const string& strType, int nValue)
636 {
637 return Write(strType, nValue);
638 }
639 bool EraseStringRecord(const string& strType)
640 {
641 return Erase(strType);
642 }
643 };
644
645 if (!LoadWallet())
646 throw std::runtime_error("LoadWallet failed");
647
648 {
649 CWalletDB walletdb;
650 nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_SUPPORTED),
651 "the current wallet format marker can be written") ? 0 : 1;
652 }
653
654 std::vector<unsigned char> vchDefaultKey;
655 nFail += Check(CWalletDB("r").LoadWallet(vchDefaultKey),
656 "the current wallet format marker loads") ? 0 : 1;
657
658 {
659 CWalletDBRaw walletdb;
660 nFail += Check(walletdb.WriteStringRecord("mkey", 1),
661 "a malformed encrypted master-key record can be written") ? 0 : 1;
662 }
663
664 vchDefaultKey.clear();
665 nFail += Check(!CWalletDB("r").LoadWallet(vchDefaultKey),
666 "a malformed encrypted master-key record is refused") ? 0 : 1;
667
668 {
669 CWalletDBRaw walletdb;
670 nFail += Check(walletdb.EraseStringRecord("mkey"),
671 "the malformed encrypted master-key record can be removed") ? 0 : 1;
672 }
673
674 {
675 CWalletDB walletdb;
676 nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_SUPPORTED + 1),
677 "a future wallet format marker can be written") ? 0 : 1;
678 }
679
680 vchDefaultKey.clear();
681 nFail += Check(!CWalletDB("r").LoadWallet(vchDefaultKey),
682 "a future wallet format marker is refused") ? 0 : 1;
683 }
684 catch (const std::exception& e)
685 {
686 printf(" FAIL exception: %s\n", e.what());
687 nFail++;
688 }
689 catch (...)
690 {
691 printf(" FAIL unknown exception\n");
692 nFail++;
693 }
694
695 DBFlush(true);
696 SetCurrentDir(cwd);
697 RemoveTree(tmp);
698 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
699 nFail, nFail == 1 ? "" : "s");
700 fflush(stdout);
701 return nFail == 0 ? 0 : 1;
702}
703
704static void ClearWalletRuntimeForTest()
705{
706 CRITICAL_BLOCK(cs_mapKeys)
707 {
708 mapKeys.clear();
709 mapCryptedKeys.clear();
710 mapPubKeys.clear();
711 mapMasterKeys.clear();
712 vWalletMasterKey.clear();
713 vchCryptedHDMaster.clear();
714 vchCryptedHDChainCode.clear();
715 fWalletEncrypted = false;
716 fWalletLocked = true;
717 }
718 CRITICAL_BLOCK(cs_keyPool)
719 mapKeyPool.clear();
720 vchHDMaster.clear();
721 vchHDChainCode.clear();
722 nHDNext = 0;
723}
724
725static int RunWalletCryptoSelfTest()
726{
727 fflush(stdout);
728 printf("wallet-crypto self-test\n");
729
730 std::string tmp;
731 std::string cwd;
732 if (!MakeTempDir(tmp))
733 {
734 printf(" FAIL could not create a temporary data directory\n");
735 return 1;
736 }
737 if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp))
738 {
739 printf(" FAIL could not move into the temporary data directory\n");
740 RemoveTree(tmp);
741 return 1;
742 }
743
744 int nFail = 0;
745 strSetDataDir = tmp;
746 printf(" temp datadir: %s\n", tmp.c_str());
747
748 try
749 {
750 const string strPassphrase = "btf-test-passphrase";
751 const string strWrongPassphrase = "btf-test-passphraser";
752
753 CKey key;
754 key.MakeNewKey();
755 vector<unsigned char> vchPubKey = key.GetPubKey();
756 CPrivKey vchPrivKey = key.GetPrivKey();
757
758 CKeyingMaterial vchMasterKey;
759 for (int i = 0; i < 32; i++)
760 vchMasterKey.push_back((unsigned char)(i + 1));
761
762 CWalletMasterKey kMasterKey;
763 kMasterKey.vchSalt.clear();
764 for (int i = 0; i < 8; i++)
765 kMasterKey.vchSalt.push_back((unsigned char)(0xa0 + i));
766 kMasterKey.nDeriveIterations = 2500;
767
768 CKeyingMaterial vchPassKey;
769 vector<unsigned char> vchPassIV;
770 nFail += Check(DeriveWalletPassphraseKey(strPassphrase, kMasterKey.vchSalt,
771 kMasterKey.nDeriveIterations,
772 vchPassKey, vchPassIV),
773 "a passphrase key can be derived") ? 0 : 1;
774 nFail += Check(EncryptSecret(vchPassKey,
775 vector<unsigned char>(vchMasterKey.begin(), vchMasterKey.end()),
776 vchPassIV, kMasterKey.vchCryptedKey),
777 "the wallet master key can be encrypted") ? 0 : 1;
778
779 vector<unsigned char> vchCryptedKey;
780 nFail += Check(EncryptSecret(vchMasterKey,
781 vector<unsigned char>(vchPrivKey.begin(), vchPrivKey.end()),
782 WalletKeyIV(vchPubKey), vchCryptedKey),
783 "a private key can be encrypted under the wallet master key") ? 0 : 1;
784
785 {
786 CWalletDB walletdb("cr");
787 nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_ENCRYPTED),
788 "an encrypted wallet format marker can be written") ? 0 : 1;
789 nFail += Check(walletdb.WriteMasterKey(0, kMasterKey),
790 "the encrypted master key can be written") ? 0 : 1;
791 nFail += Check(walletdb.WriteCryptedKey(vchPubKey, vchCryptedKey),
792 "the encrypted private key can be written") ? 0 : 1;
793 nFail += Check(walletdb.WriteDefaultKey(vchPubKey),
794 "the encrypted wallet default key can be written") ? 0 : 1;
795 }
796
797 ClearWalletRuntimeForTest();
798
799 vector<unsigned char> vchDefaultKey;
800 nFail += Check(CWalletDB("r").LoadWallet(vchDefaultKey),
801 "an encrypted wallet can be loaded") ? 0 : 1;
802 nFail += Check(vchDefaultKey == vchPubKey,
803 "the encrypted wallet default public key loads") ? 0 : 1;
804 nFail += Check(IsWalletEncrypted() && IsWalletLocked(),
805 "an encrypted wallet loads locked") ? 0 : 1;
806 nFail += Check(WalletCanSpendKey(vchPubKey),
807 "an encrypted key is recognized as wallet-owned while locked") ? 0 : 1;
808
809 CPrivKey vchDecrypted;
810 string strError;
811 nFail += Check(!GetWalletPrivKey(vchPubKey, vchDecrypted, strError),
812 "a locked encrypted key cannot be read") ? 0 : 1;
813 nFail += Check(!UnlockWallet(strWrongPassphrase, strError),
814 "the wrong-passphrase does not unlock") ? 0 : 1;
815 nFail += Check(UnlockWallet(strPassphrase, strError),
816 "the right passphrase unlocks") ? 0 : 1;
817 nFail += Check(!IsWalletLocked(),
818 "the wallet reports unlocked") ? 0 : 1;
819 nFail += Check(GetWalletPrivKey(vchPubKey, vchDecrypted, strError) &&
820 vchDecrypted == vchPrivKey,
821 "an unlocked encrypted key decrypts to the original private key") ? 0 : 1;
822 LockWallet();
823 nFail += Check(IsWalletLocked(),
824 "locking clears the unlocked state") ? 0 : 1;
825 }
826 catch (const std::exception& e)
827 {
828 printf(" FAIL exception: %s\n", e.what());
829 nFail++;
830 }
831 catch (...)
832 {
833 printf(" FAIL unknown exception\n");
834 nFail++;
835 }
836
837 DBFlush(true);
838 SetCurrentDir(cwd);
839 RemoveTree(tmp);
840 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
841 nFail, nFail == 1 ? "" : "s");
842 fflush(stdout);
843 return nFail == 0 ? 0 : 1;
844}
845
846static bool FileContainsBytes(const string& strPath, const vector<unsigned char>& vchNeedle)
847{
848 if (vchNeedle.empty())
849 return false;
850 FILE* pf = fopen(strPath.c_str(), "rb");
851 if (!pf)
852 return false;
853
854 vector<unsigned char> vchHaystack;
855 unsigned char buf[4096];
856 size_t n = 0;
857 while ((n = fread(buf, 1, sizeof(buf), pf)) > 0)
858 vchHaystack.insert(vchHaystack.end(), buf, buf + n);
859 fclose(pf);
860
861 if (vchHaystack.size() < vchNeedle.size())
862 return false;
863 return search(vchHaystack.begin(), vchHaystack.end(),
864 vchNeedle.begin(), vchNeedle.end()) != vchHaystack.end();
865}
866
867static bool FileContainsText(const string& strPath, const string& strNeedle)
868{
869 FILE* pf = fopen(strPath.c_str(), "rb");
870 if (!pf)
871 return false;
872
873 string strHaystack;
874 char buf[4096];
875 size_t n = 0;
876 while ((n = fread(buf, 1, sizeof(buf), pf)) > 0)
877 strHaystack.append(buf, buf + n);
878 fclose(pf);
879
880 return strHaystack.find(strNeedle) != string::npos;
881}
882
883static bool WriteTextFile(const string& strPath, const string& strText)
884{
885 FILE* pf = fopen(strPath.c_str(), "wb");
886 if (!pf)
887 return false;
888 bool fOk = fwrite(strText.data(), 1, strText.size(), pf) == strText.size();
889 if (fclose(pf) != 0)
890 fOk = false;
891 return fOk;
892}
893
894static string QuoteCommandArg(const string& str)
895{
896 string out = "\"";
897 for (size_t i = 0; i < str.size(); i++)
898 {
899 if (str[i] == '"')
900 out += "\\\"";
901 else
902 out += str[i];
903 }
904 out += "\"";
905 return out;
906}
907
908static int RunBitflashChild(const string& strExe,
909 const vector<string>& vArgs,
910 const string* pStdinFile = NULL)
911{
912#ifdef _WIN32
913 vector<char*> argv;
914 argv.push_back((char*)strExe.c_str());
915 for (size_t i = 0; i < vArgs.size(); i++)
916 argv.push_back((char*)vArgs[i].c_str());
917 argv.push_back(NULL);
918
919 int nOldOut = _dup(1);
920 int nOldErr = _dup(2);
921 int nOldIn = _dup(0);
922 int nNull = _open("NUL", _O_WRONLY);
923 int nIn = -1;
924 if (nNull >= 0)
925 {
926 _dup2(nNull, 1);
927 _dup2(nNull, 2);
928 }
929 if (pStdinFile)
930 {
931 nIn = _open(pStdinFile->c_str(), _O_RDONLY);
932 if (nIn >= 0)
933 _dup2(nIn, 0);
934 }
935 int nRet = _spawnv(_P_WAIT, strExe.c_str(), &argv[0]);
936 if (nIn >= 0)
937 _close(nIn);
938 if (nNull >= 0)
939 _close(nNull);
940 if (nOldIn >= 0)
941 {
942 _dup2(nOldIn, 0);
943 _close(nOldIn);
944 }
945 if (nOldOut >= 0)
946 {
947 _dup2(nOldOut, 1);
948 _close(nOldOut);
949 }
950 if (nOldErr >= 0)
951 {
952 _dup2(nOldErr, 2);
953 _close(nOldErr);
954 }
955 return nRet;
956#else
957 string strCmd = QuoteCommandArg(strExe);
958 for (size_t i = 0; i < vArgs.size(); i++)
959 strCmd += " " + QuoteCommandArg(vArgs[i]);
960 if (pStdinFile)
961 strCmd += " < " + QuoteCommandArg(*pStdinFile);
962 strCmd += " > /dev/null 2>&1";
963 return system(strCmd.c_str());
964#endif
965}
966
967static int RunWalletEncryptSelfTest()
968{
969 fflush(stdout);
970 printf("wallet-encrypt self-test\n");
971
972 std::string tmp;
973 std::string cwd;
974 if (!MakeTempDir(tmp))
975 {
976 printf(" FAIL could not create a temporary data directory\n");
977 return 1;
978 }
979 if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp))
980 {
981 printf(" FAIL could not move into the temporary data directory\n");
982 RemoveTree(tmp);
983 return 1;
984 }
985
986 int nFail = 0;
987 bool fWalletEnvClosed = false;
988 strSetDataDir = tmp;
989 printf(" temp datadir: %s\n", tmp.c_str());
990
991 try
992 {
993 if (!LoadWallet())
994 throw std::runtime_error("LoadWallet failed");
995
996 string strError;
997 const string strMnemonic =
998 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
999 nFail += Check(SetHDSeedFromMnemonic(strMnemonic, strError),
1000 "a phrase can be installed before encryption") ? 0 : 1;
1001 TopUpKeyPool();
1002
1003 vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey();
1004 CPrivKey vchDefaultPrivKey;
1005 nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError),
1006 "the default private key is readable before encryption") ? 0 : 1;
1007 vector<unsigned char> vchDefaultPrivBytes(vchDefaultPrivKey.begin(), vchDefaultPrivKey.end());
1008 vector<unsigned char> vchPlainHDMaster = vchHDMaster;
1009 vector<unsigned char> vchPlainHDChainCode = vchHDChainCode;
1010
1011 string strBackup;
1012 nFail += Check(EncryptWallet("btf-test-passphrase", strBackup, strError),
1013 "wallet.dat can be rewritten encrypted") ? 0 : 1;
1014 fWalletEnvClosed = true;
1015 nFail += Check(FileExists(strBackup.c_str()),
1016 "the original wallet is preserved as an explicit backup") ? 0 : 1;
1017
1018 string strWalletPath = GetAppDir() + "/wallet.dat";
1019 nFail += Check(!FileContainsBytes(strWalletPath, vchDefaultPrivBytes),
1020 "the active wallet no longer contains the default private key bytes") ? 0 : 1;
1021 nFail += Check(!FileContainsBytes(strWalletPath, vchPlainHDMaster),
1022 "the active wallet no longer contains the HD master key bytes") ? 0 : 1;
1023 nFail += Check(!FileContainsBytes(strWalletPath, vchPlainHDChainCode),
1024 "the active wallet no longer contains the HD chain-code bytes") ? 0 : 1;
1025
1026#ifdef _WIN32
1027 string strExe = cwd + "\\bitflash.exe";
1028#else
1029 string strExe = cwd + "/bitflash-node";
1030#endif
1031 string strWrongDump = tmp + "/wrong-pass-dump.txt";
1032 string strRightDump = tmp + "/right-pass-dump.txt";
1033 string strLiteralDump = tmp + "/literal-pass-dump.txt";
1034 string strStdinDump = tmp + "/stdin-pass-dump.txt";
1035 string strWrongPassFile = tmp + "/wrong-pass.txt";
1036 string strRightPassFile = tmp + "/right-pass.txt";
1037 nFail += Check(WriteTextFile(strWrongPassFile, "wrong-passphrase\n") &&
1038 WriteTextFile(strRightPassFile, "btf-test-passphrase\n"),
1039 "passphrase files can be written for restarted commands") ? 0 : 1;
1040
1041 vector<string> vWrongArgs;
1042 vWrongArgs.push_back("-datadir=" + tmp);
1043 vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile);
1044 vWrongArgs.push_back("-dumpwallet=" + strWrongDump);
1045 vWrongArgs.push_back("-nogui");
1046 int nWrongRet = RunBitflashChild(strExe, vWrongArgs);
1047 nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()),
1048 "a restarted wallet rejects the wrong-passphrase") ? 0 : 1;
1049
1050 vector<string> vLiteralArgs;
1051 vLiteralArgs.push_back("-datadir=" + tmp);
1052 vLiteralArgs.push_back("-walletpassphrase=btf-test-passphrase");
1053 vLiteralArgs.push_back("-dumpwallet=" + strLiteralDump);
1054 vLiteralArgs.push_back("-nogui");
1055 int nLiteralRet = RunBitflashChild(strExe, vLiteralArgs);
1056 nFail += Check(nLiteralRet != 0 && !FileExists(strLiteralDump.c_str()),
1057 "literal passphrases on the command line are refused") ? 0 : 1;
1058
1059 vector<string> vRightArgs;
1060 vRightArgs.push_back("-datadir=" + tmp);
1061 vRightArgs.push_back("-walletpassphrase=@" + strRightPassFile);
1062 vRightArgs.push_back("-dumpwallet=" + strRightDump);
1063 vRightArgs.push_back("-nogui");
1064 int nRightRet = RunBitflashChild(strExe, vRightArgs);
1065 nFail += Check(nRightRet == 0 && FileExists(strRightDump.c_str()),
1066 "a restarted wallet unlocks with the right passphrase") ? 0 : 1;
1067 nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)),
1068 "the restarted wallet can decrypt and dump the original key") ? 0 : 1;
1069
1070 vector<string> vStdinArgs;
1071 vStdinArgs.push_back("-datadir=" + tmp);
1072 vStdinArgs.push_back("-walletpassphrase");
1073 vStdinArgs.push_back("-dumpwallet=" + strStdinDump);
1074 vStdinArgs.push_back("-nogui");
1075 int nStdinRet = RunBitflashChild(strExe, vStdinArgs, &strRightPassFile);
1076 nFail += Check(nStdinRet == 0 && FileExists(strStdinDump.c_str()),
1077 "a restarted wallet can read the passphrase from stdin") ? 0 : 1;
1078 }
1079 catch (const std::exception& e)
1080 {
1081 printf(" FAIL exception: %s\n", e.what());
1082 nFail++;
1083 }
1084 catch (...)
1085 {
1086 printf(" FAIL unknown exception\n");
1087 nFail++;
1088 }
1089
1090 if (!fWalletEnvClosed)
1091 DBFlush(true);
1092 SetCurrentDir(cwd);
1093 RemoveTree(tmp);
1094 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1095 nFail, nFail == 1 ? "" : "s");
1096 fflush(stdout);
1097 return nFail == 0 ? 0 : 1;
1098}
1099
604static int RunNetMessageSelfTest() 1100static int RunNetMessageSelfTest()
605{ 1101{
606 fflush(stdout); 1102 fflush(stdout);
@@ -728,12 +1224,18 @@ int RunSelfTest(const std::string& name)
728 return RunWalletKeyPoolSelfTest(); 1224 return RunWalletKeyPoolSelfTest();
729 if (name == "wallet-hd") 1225 if (name == "wallet-hd")
730 return RunWalletHDSelfTest(); 1226 return RunWalletHDSelfTest();
1227 if (name == "wallet-format")
1228 return RunWalletFormatSelfTest();
1229 if (name == "wallet-crypto")
1230 return RunWalletCryptoSelfTest();
1231 if (name == "wallet-encrypt")
1232 return RunWalletEncryptSelfTest();
731 if (name == "net-message") 1233 if (name == "net-message")
732 return RunNetMessageSelfTest(); 1234 return RunNetMessageSelfTest();
733 if (name == "consensus-limits") 1235 if (name == "consensus-limits")
734 return RunConsensusLimitsSelfTest(); 1236 return RunConsensusLimitsSelfTest();
735 1237
736 printf("Unknown self-test '%s'\n", name.c_str()); 1238 printf("Unknown self-test '%s'\n", name.c_str());
737 printf("Known self-tests: wallet-keypool, wallet-hd, net-message, consensus-limits\n"); 1239 printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits\n");
738 return 1; 1240 return 1;
739} 1241}
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()
406 return 0; 406 return 0;
407} 407}
408 408
409int CmdEncryptWallet(const std::string& strPassphrase)
410{
411 AttachTerminal();
412
413 string strBackup;
414 string strError;
415 if (!EncryptWallet(strPassphrase, strBackup, strError))
416 {
417 fprintf(stderr, "Cannot encrypt wallet: %s\n", strError.c_str());
418 fprintf(stderr, "Nothing was changed.\n");
419 return 1;
420 }
421
422 printf("Wallet encrypted.\n");
423 printf("The old unencrypted wallet was moved to:\n");
424 printf(" %s\n", strBackup.c_str());
425 printf("That backup still contains private keys in plain text. Move it offline or\n");
426 printf("delete it after you have a safer backup plan.\n");
427 printf("Restart Bitflash before using the wallet again.\n");
428 fflush(stdout);
429
430 // The rewritten wallet.dat is clean; the environment's write-ahead logs are
431 // not, because every plaintext record this wallet ever wrote passed through
432 // them. Closing the environment and dropping the logs is the last step of
433 // encrypting, not an optimisation -- our own backup advice is to copy the
434 // whole directory, and following it otherwise carries the keys along.
435 PurgeDbEnvironmentLogs();
436 return 0;
437}
438
409// Spend, from the command line. 439// Spend, from the command line.
410// 440//
411// SendMoney() has been in this tree since 0.1.0 and only the window ever called 441// 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)
421{ 451{
422 AttachTerminal(); 452 AttachTerminal();
423 453
454 if (IsWalletLocked())
455 {
456 fprintf(stderr, "Wallet is encrypted and locked. Start with /walletpassphrase or /walletpassphrase=@FILE to spend.\n");
457 return 1;
458 }
459
424 std::string::size_type comma = strArg.rfind(','); 460 std::string::size_type comma = strArg.rfind(',');
425 if (comma == std::string::npos) 461 if (comma == std::string::npos)
426 { 462 {
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);
53// by the installed recovery phrase or still depends on wallet.dat-only keys. 53// by the installed recovery phrase or still depends on wallet.dat-only keys.
54int CmdRecoveryAudit(); 54int CmdRecoveryAudit();
55 55
56// Rewrite wallet.dat so private keys and the HD seed are encrypted with the
57// given passphrase. Returns 0 on success and exits without starting the node.
58int CmdEncryptWallet(const std::string& strPassphrase);
59
56// Spend, from the command line. Until now the only way to send was the window, 60// Spend, from the command line. Until now the only way to send was the window,
57// so a headless node could be paid and could never pay -- it could hold a 61// so a headless node could be paid and could never pay -- it could hold a
58// balance it had no way to move. strArg is "ADDRESS,AMOUNT". 62// balance it had no way to move. strArg is "ADDRESS,AMOUNT".