diff options
| author | Bitflash <btf01@proton.me> | 2026-08-04 10:01:36 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-04 10:01:36 -0300 |
| commit | 1db5f372c36eb701602a70a358de2344726ed734 (patch) | |
| tree | 9d028ed3097ac51368ca312ff439664c76f439b7 /src | |
| parent | 9176d5b3f32b907e4bafde5152828b2ae04ae8a1 (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/Makefile | 3 | ||||
| -rw-r--r-- | src/db.cpp | 602 | ||||
| -rw-r--r-- | src/db.h | 58 | ||||
| -rw-r--r-- | src/main.cpp | 334 | ||||
| -rw-r--r-- | src/main.h | 40 | ||||
| -rw-r--r-- | src/main_gui.cpp | 101 | ||||
| -rw-r--r-- | src/makefile.mingw | 3 | ||||
| -rw-r--r-- | src/script.cpp | 89 | ||||
| -rw-r--r-- | src/selftest.cpp | 504 | ||||
| -rw-r--r-- | src/walletcmd.cpp | 36 | ||||
| -rw-r--r-- | src/walletcmd.h | 4 |
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 |
| @@ -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. | ||
| 235 | void 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 | |||
| 206 | void DBFlush(bool fShutdown) | 256 | void 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 | |||
| 1256 | class CWalletRewriteDB : public CDB | ||
| 1257 | { | ||
| 1258 | public: | ||
| 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 | |||
| 1373 | static 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 | |||
| 1382 | static 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 | |||
| 1398 | bool 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 | } | ||
| @@ -16,9 +16,43 @@ class CWalletTx; | |||
| 16 | extern map<string, string> mapAddressBook; | 16 | extern map<string, string> mapAddressBook; |
| 17 | extern bool fClient; | 17 | extern 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. | ||
| 22 | static const int WALLET_FORMAT_PLAINTEXT = 1; | ||
| 23 | static const int WALLET_FORMAT_ENCRYPTED = 2; | ||
| 24 | static const int WALLET_FORMAT_SUPPORTED = WALLET_FORMAT_ENCRYPTED; | ||
| 25 | |||
| 26 | class CWalletMasterKey | ||
| 27 | { | ||
| 28 | public: | ||
| 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 | ||
| 20 | extern DbEnv dbenv; | 51 | extern DbEnv dbenv; |
| 21 | extern void DBFlush(bool fShutdown); | 52 | extern 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. | ||
| 55 | extern 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. |
| 479 | bool DumpWallet(const string& strDest); | 536 | bool DumpWallet(const string& strDest); |
| 480 | bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet); | 537 | bool ImportWallet(const string& strSrc, int& nAddedRet, int& nSkippedRet); |
| 538 | bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet); | ||
| 481 | 539 | ||
| 482 | inline bool SetAddressBookName(const string& strAddress, const string& strName) | 540 | inline 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; | |||
| 72 | CCriticalSection cs_mapWallet; | 72 | CCriticalSection cs_mapWallet; |
| 73 | 73 | ||
| 74 | map<vector<unsigned char>, CPrivKey> mapKeys; | 74 | map<vector<unsigned char>, CPrivKey> mapKeys; |
| 75 | map<vector<unsigned char>, vector<unsigned char> > mapCryptedKeys; | ||
| 75 | map<uint160, vector<unsigned char> > mapPubKeys; | 76 | map<uint160, vector<unsigned char> > mapPubKeys; |
| 77 | map<unsigned int, CWalletMasterKey> mapMasterKeys; | ||
| 78 | unsigned int nWalletMasterKeyMaxID = 0; | ||
| 79 | CKeyingMaterial vWalletMasterKey; | ||
| 80 | vector<unsigned char> vchCryptedHDMaster; | ||
| 81 | vector<unsigned char> vchCryptedHDChainCode; | ||
| 82 | bool fWalletEncrypted = false; | ||
| 83 | bool fWalletLocked = true; | ||
| 76 | CCriticalSection cs_mapKeys; | 84 | CCriticalSection cs_mapKeys; |
| 77 | CKey keyUser; | 85 | CKey keyUser; |
| 78 | 86 | ||
| @@ -159,6 +167,21 @@ int MinerThreadCount() | |||
| 159 | 167 | ||
| 160 | bool AddKey(const CKey& key) | 168 | bool 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 | ||
| 193 | bool IsWalletEncrypted() | ||
| 194 | { | ||
| 195 | return fWalletEncrypted; | ||
| 196 | } | ||
| 197 | |||
| 198 | bool IsWalletLocked() | ||
| 199 | { | ||
| 200 | return fWalletEncrypted && fWalletLocked; | ||
| 201 | } | ||
| 202 | |||
| 203 | void 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 | |||
| 218 | bool 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 | |||
| 249 | bool 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 | |||
| 280 | bool 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 | |||
| 310 | vector<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 | |||
| 318 | vector<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 | |||
| 326 | bool 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 | |||
| 340 | bool 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 | |||
| 347 | bool 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 | |||
| 393 | bool 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 | |||
| 170 | static bool WalletAlreadyHasKey(const vector<unsigned char>& vchPubKey) | 470 | static 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 | ||
| 510 | void TopUpKeyPool() | 810 | void 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 | ||
| 563 | vector<unsigned char> GetKeyFromPool() | 869 | vector<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())) |
| @@ -129,6 +129,46 @@ FILE* AppendBlockFile(unsigned int& nFileRet); | |||
| 129 | bool AddKey(const CKey& key); | 129 | bool AddKey(const CKey& key); |
| 130 | vector<unsigned char> GenerateNewKey(); | 130 | vector<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. | ||
| 137 | typedef vector<unsigned char, secure_allocator<unsigned char> > CKeyingMaterial; | ||
| 138 | extern map<vector<unsigned char>, vector<unsigned char> > mapCryptedKeys; | ||
| 139 | extern map<unsigned int, CWalletMasterKey> mapMasterKeys; | ||
| 140 | extern unsigned int nWalletMasterKeyMaxID; | ||
| 141 | extern CKeyingMaterial vWalletMasterKey; | ||
| 142 | extern vector<unsigned char> vchCryptedHDMaster; | ||
| 143 | extern vector<unsigned char> vchCryptedHDChainCode; | ||
| 144 | extern bool fWalletEncrypted; | ||
| 145 | extern bool fWalletLocked; | ||
| 146 | bool IsWalletEncrypted(); | ||
| 147 | bool IsWalletLocked(); | ||
| 148 | void LockWallet(); | ||
| 149 | bool UnlockWallet(const string& strPassphrase, string& strErrorRet); | ||
| 150 | bool DeriveWalletPassphraseKey(const string& strPassphrase, | ||
| 151 | const vector<unsigned char>& vchSalt, | ||
| 152 | unsigned int nDeriveIterations, | ||
| 153 | CKeyingMaterial& vchKeyRet, | ||
| 154 | vector<unsigned char>& vchIVRet); | ||
| 155 | bool EncryptSecret(const CKeyingMaterial& vchKey, | ||
| 156 | const vector<unsigned char>& vchPlaintext, | ||
| 157 | const vector<unsigned char>& vchIV, | ||
| 158 | vector<unsigned char>& vchCiphertextRet); | ||
| 159 | bool DecryptSecret(const CKeyingMaterial& vchKey, | ||
| 160 | const vector<unsigned char>& vchCiphertext, | ||
| 161 | const vector<unsigned char>& vchIV, | ||
| 162 | vector<unsigned char>& vchPlaintextRet); | ||
| 163 | vector<unsigned char> WalletKeyIV(const vector<unsigned char>& vchPubKey); | ||
| 164 | vector<unsigned char> WalletSecretIV(const string& strLabel); | ||
| 165 | bool AddCryptedKey(const vector<unsigned char>& vchPubKey, | ||
| 166 | const vector<unsigned char>& vchCryptedSecret); | ||
| 167 | bool WalletCanSpendKey(const vector<unsigned char>& vchPubKey); | ||
| 168 | bool 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 | ||
| 43 | static 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 | ||
| 606 | static 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 | |||
| 704 | static 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 | |||
| 725 | static 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 | |||
| 846 | static 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 | |||
| 867 | static 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 | |||
| 883 | static 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 | |||
| 894 | static 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 | |||
| 908 | static 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 | |||
| 967 | static 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 | |||
| 604 | static int RunNetMessageSelfTest() | 1100 | static 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 | ||
| 409 | int 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. |
| 54 | int CmdRecoveryAudit(); | 54 | int 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. | ||
| 58 | int 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". |