diff options
| author | bitflash <dev@bitflash.network> | 2026-08-18 14:23:38 -0300 |
|---|---|---|
| committer | bitflash <dev@bitflash.network> | 2026-08-18 14:23:38 -0300 |
| commit | e58c4022e0ddd137b93cdb3464753bef5df1d456 (patch) | |
| tree | 8adc9ec855d19e5a7308eb2a1b2be8139db47188 /src | |
| parent | beb5c1435a0d85b7b24d4d371933758ebed90907 (diff) | |
wallet: encrypt atomically under the SQLite backend
EncryptWallet() had only a Berkeley DB path: it rebuilt a wallet.dat temp
file and swapped it in. Under -walletbackend=sqlite that was wrong -- it
would leave the live wallet.sqlite unencrypted and write a divergent
wallet.dat instead. Add a SQLite persistence branch that rebuilds a fresh
wallet.sqlite in one transaction (public records copied from the live
store, encrypted key material serialized byte-for-byte the way the BDB
path writes it), checkpoints it, and swaps it in with a .before-encrypt
backup, mirroring the BDB rename dance. A fresh file rather than an
in-place rewrite keeps the change atomic and leaves no plaintext key in a
freed page.
CmdEncryptWallet skips PurgeDbEnvironmentLogs under SQLite (no Berkeley DB
environment behind the wallet) and reports the backend it used.
New wallet-sqlite-encrypt self-test drives the real command path in child
processes: export a wallet to SQLite, encrypt it through
-walletbackend=sqlite, and prove it comes out encrypted, unlockable with
the right passphrase, refusing the wrong one, with its recovery phrase
intact and the original wallet.dat left untouched.
Diffstat (limited to 'src')
| -rw-r--r-- | src/db.cpp | 210 | ||||
| -rw-r--r-- | src/makefile.mingw | 1 | ||||
| -rw-r--r-- | src/selftest.cpp | 189 | ||||
| -rw-r--r-- | src/walletcmd.cpp | 15 |
4 files changed, 410 insertions, 5 deletions
| @@ -1974,9 +1974,9 @@ static bool CopyPublicWalletRecordsTo(CWalletRewriteDB& dbTo, | |||
| 1974 | return ScanWalletRecords(visitor, strErrorRet); | 1974 | return ScanWalletRecords(visitor, strErrorRet); |
| 1975 | } | 1975 | } |
| 1976 | 1976 | ||
| 1977 | static string WalletEncryptBackupPath() | 1977 | static string WalletEncryptBackupPathFor(const char* pszBaseName) |
| 1978 | { | 1978 | { |
| 1979 | string strBase = GetAppDir() + "/wallet.dat.before-encrypt."; | 1979 | string strBase = GetAppDir() + "/" + pszBaseName + ".before-encrypt."; |
| 1980 | string strPath = strBase + i64tostr(GetTime()) + ".bak"; | 1980 | string strPath = strBase + i64tostr(GetTime()) + ".bak"; |
| 1981 | for (int i = 1; FileExists(strPath.c_str()); i++) | 1981 | for (int i = 1; FileExists(strPath.c_str()); i++) |
| 1982 | strPath = strBase + i64tostr(GetTime()) + "." + itostr(i) + ".bak"; | 1982 | strPath = strBase + i64tostr(GetTime()) + "." + itostr(i) + ".bak"; |
| @@ -2006,6 +2006,192 @@ static void SecureClearBytes(vector<unsigned char>& vch) | |||
| 2006 | vch.clear(); | 2006 | vch.clear(); |
| 2007 | } | 2007 | } |
| 2008 | 2008 | ||
| 2009 | // The SQLite counterpart of CWalletPublicRecordCopyVisitor: same skip list, | ||
| 2010 | // same byte-for-byte copy, but the destination is a CWalletDBSQLite record | ||
| 2011 | // store rather than a Berkeley DB file. | ||
| 2012 | class CWalletPublicRecordCopyToSQLiteVisitor : public CWalletRecordVisitor | ||
| 2013 | { | ||
| 2014 | public: | ||
| 2015 | CWalletDBSQLite& dbTo; | ||
| 2016 | int& nCopied; | ||
| 2017 | |||
| 2018 | CWalletPublicRecordCopyToSQLiteVisitor(CWalletDBSQLite& dbToIn, int& nCopiedIn) | ||
| 2019 | : dbTo(dbToIn), nCopied(nCopiedIn) { } | ||
| 2020 | |||
| 2021 | bool VisitWalletRecord(const CDataStream& ssKey, | ||
| 2022 | const CDataStream& ssValue, | ||
| 2023 | string& strErrorRet) | ||
| 2024 | { | ||
| 2025 | string strType; | ||
| 2026 | try | ||
| 2027 | { | ||
| 2028 | CDataStream ssType = ssKey; | ||
| 2029 | ssType >> strType; | ||
| 2030 | } | ||
| 2031 | catch (...) | ||
| 2032 | { | ||
| 2033 | strErrorRet = "SQLite wallet has a record whose key type cannot be read"; | ||
| 2034 | return false; | ||
| 2035 | } | ||
| 2036 | |||
| 2037 | if (IsWalletEncryptionPrivateRecord(strType)) | ||
| 2038 | return true; | ||
| 2039 | |||
| 2040 | vector<unsigned char> vchKey(ssKey.begin(), ssKey.end()); | ||
| 2041 | vector<unsigned char> vchValue(ssValue.begin(), ssValue.end()); | ||
| 2042 | bool fOk = dbTo.WriteRecord(vchKey, vchValue, strErrorRet, true); | ||
| 2043 | if (!vchKey.empty()) | ||
| 2044 | memset(&vchKey[0], 0, vchKey.size()); | ||
| 2045 | if (!vchValue.empty()) | ||
| 2046 | memset(&vchValue[0], 0, vchValue.size()); | ||
| 2047 | if (!fOk) | ||
| 2048 | { | ||
| 2049 | strErrorRet = strprintf("could not copy wallet record '%s': %s", | ||
| 2050 | strType.c_str(), strErrorRet.c_str()); | ||
| 2051 | return false; | ||
| 2052 | } | ||
| 2053 | nCopied++; | ||
| 2054 | return true; | ||
| 2055 | } | ||
| 2056 | }; | ||
| 2057 | |||
| 2058 | // Persist an encrypted wallet under the SQLite backend. Everything the caller | ||
| 2059 | // computed -- the master key, the re-encrypted private keys, the fresh crypted | ||
| 2060 | // key pool, the encrypted HD seed -- lands in a brand new wallet.sqlite built | ||
| 2061 | // in one transaction, which is then swapped in for the plaintext original. | ||
| 2062 | // | ||
| 2063 | // A fresh file rather than an in-place rewrite for two reasons: it keeps the | ||
| 2064 | // whole change atomic (a crash before the rename leaves the old wallet whole), | ||
| 2065 | // and it guarantees no plaintext key survives in a freed page the way an | ||
| 2066 | // in-place DELETE would leave behind until a VACUUM. | ||
| 2067 | static bool EncryptWalletPersistSQLite( | ||
| 2068 | const vector<pair<vector<unsigned char>, vector<unsigned char> > >& vCryptedKeys, | ||
| 2069 | const vector<pair<vector<unsigned char>, vector<unsigned char> > >& vNewPoolKeys, | ||
| 2070 | const vector<pair<int64, vector<unsigned char> > >& vNewPool, | ||
| 2071 | const CWalletMasterKey& kMasterKey, | ||
| 2072 | const vector<unsigned char>& vchCryptedHDMasterNew, | ||
| 2073 | const vector<unsigned char>& vchCryptedHDChainCodeNew, | ||
| 2074 | unsigned int nHDNextNew, | ||
| 2075 | int& nCopiedRet, | ||
| 2076 | string& strBackupRet, | ||
| 2077 | string& strErrorRet) | ||
| 2078 | { | ||
| 2079 | nCopiedRet = 0; | ||
| 2080 | if (!pWalletSQLiteRuntime) | ||
| 2081 | { | ||
| 2082 | strErrorRet = "SQLite wallet backend is not active"; | ||
| 2083 | return false; | ||
| 2084 | } | ||
| 2085 | |||
| 2086 | string strWalletPath = GetAppDir() + "/wallet.sqlite"; | ||
| 2087 | string strTempPath = GetAppDir() + "/" + | ||
| 2088 | strprintf("wallet.sqlite.encrypting.%lld", GetTime()); | ||
| 2089 | remove(strTempPath.c_str()); | ||
| 2090 | remove((strTempPath + "-wal").c_str()); | ||
| 2091 | remove((strTempPath + "-shm").c_str()); | ||
| 2092 | |||
| 2093 | { | ||
| 2094 | CWalletDBSQLite dbTmp; | ||
| 2095 | if (!dbTmp.Open(strTempPath, strErrorRet)) | ||
| 2096 | { | ||
| 2097 | remove(strTempPath.c_str()); | ||
| 2098 | return false; | ||
| 2099 | } | ||
| 2100 | if (!dbTmp.BeginTransaction(strErrorRet)) | ||
| 2101 | { | ||
| 2102 | dbTmp.Close(); | ||
| 2103 | remove(strTempPath.c_str()); | ||
| 2104 | return false; | ||
| 2105 | } | ||
| 2106 | |||
| 2107 | // The public records come from the live SQLite store -- under this | ||
| 2108 | // backend ScanWalletRecords() has no Berkeley DB behind it to read. | ||
| 2109 | CWalletPublicRecordCopyToSQLiteVisitor visitor(dbTmp, nCopiedRet); | ||
| 2110 | bool fOk = pWalletSQLiteRuntime->ScanRecords(visitor, strErrorRet); | ||
| 2111 | |||
| 2112 | // Encrypted metadata, serialized byte-for-byte the way the Berkeley DB | ||
| 2113 | // path (CWalletRewriteDB) writes it, so a wallet encrypted under either | ||
| 2114 | // backend is the same set of records. ckey uses INSERT-only to match | ||
| 2115 | // WriteCryptedKey; the rest overwrite. | ||
| 2116 | fOk = fOk && | ||
| 2117 | dbTmp.WriteTypedRecord(string("walletminversion"), | ||
| 2118 | (int)WALLET_FORMAT_ENCRYPTED, strErrorRet, true) && | ||
| 2119 | dbTmp.WriteTypedRecord(make_pair(string("mkey"), | ||
| 2120 | (unsigned int)(nWalletMasterKeyMaxID + 1)), | ||
| 2121 | kMasterKey, strErrorRet, true); | ||
| 2122 | for (size_t i = 0; fOk && i < vCryptedKeys.size(); i++) | ||
| 2123 | fOk = dbTmp.WriteTypedRecord(make_pair(string("ckey"), vCryptedKeys[i].first), | ||
| 2124 | vCryptedKeys[i].second, strErrorRet, false); | ||
| 2125 | for (size_t i = 0; fOk && i < vNewPoolKeys.size(); i++) | ||
| 2126 | fOk = dbTmp.WriteTypedRecord(make_pair(string("ckey"), vNewPoolKeys[i].first), | ||
| 2127 | vNewPoolKeys[i].second, strErrorRet, false); | ||
| 2128 | if (fOk && !vchCryptedHDMasterNew.empty()) | ||
| 2129 | fOk = dbTmp.WriteTypedRecord(string("cryptedhdmaster"), | ||
| 2130 | vchCryptedHDMasterNew, strErrorRet, true) && | ||
| 2131 | dbTmp.WriteTypedRecord(string("cryptedhdchaincode"), | ||
| 2132 | vchCryptedHDChainCodeNew, strErrorRet, true); | ||
| 2133 | if (fOk) | ||
| 2134 | fOk = dbTmp.WriteTypedRecord(string("hdnext"), nHDNextNew, strErrorRet, true); | ||
| 2135 | for (size_t i = 0; fOk && i < vNewPool.size(); i++) | ||
| 2136 | fOk = dbTmp.WriteTypedRecord(make_pair(string("pool"), vNewPool[i].first), | ||
| 2137 | vNewPool[i].second, strErrorRet, true); | ||
| 2138 | |||
| 2139 | if (!fOk) | ||
| 2140 | { | ||
| 2141 | string strIgnore; | ||
| 2142 | dbTmp.RollbackTransaction(strIgnore); | ||
| 2143 | dbTmp.Close(); | ||
| 2144 | remove(strTempPath.c_str()); | ||
| 2145 | remove((strTempPath + "-wal").c_str()); | ||
| 2146 | remove((strTempPath + "-shm").c_str()); | ||
| 2147 | return false; | ||
| 2148 | } | ||
| 2149 | |||
| 2150 | if (!dbTmp.CommitTransaction(strErrorRet)) | ||
| 2151 | { | ||
| 2152 | dbTmp.Close(); | ||
| 2153 | remove(strTempPath.c_str()); | ||
| 2154 | remove((strTempPath + "-wal").c_str()); | ||
| 2155 | remove((strTempPath + "-shm").c_str()); | ||
| 2156 | return false; | ||
| 2157 | } | ||
| 2158 | string strCheckpointError; | ||
| 2159 | dbTmp.Checkpoint(strCheckpointError); | ||
| 2160 | dbTmp.Close(); | ||
| 2161 | } | ||
| 2162 | |||
| 2163 | // The rebuilt wallet is committed and self-contained. Close the live handle | ||
| 2164 | // so the file can be swapped: Windows will not rename a file that still has | ||
| 2165 | // an open handle, and the node exits right after this. | ||
| 2166 | WalletSQLiteRuntimeClose(); | ||
| 2167 | |||
| 2168 | string strBackupPath = WalletEncryptBackupPathFor("wallet.sqlite"); | ||
| 2169 | if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) | ||
| 2170 | { | ||
| 2171 | strErrorRet = strprintf("could not move the original SQLite wallet to %s", | ||
| 2172 | strBackupPath.c_str()); | ||
| 2173 | remove(strTempPath.c_str()); | ||
| 2174 | return false; | ||
| 2175 | } | ||
| 2176 | // The moved-aside wallet owns any leftover write-ahead log or shared-memory | ||
| 2177 | // sidecar; drop stale ones so they cannot shadow the new wallet. | ||
| 2178 | remove((strWalletPath + "-wal").c_str()); | ||
| 2179 | remove((strWalletPath + "-shm").c_str()); | ||
| 2180 | |||
| 2181 | if (rename(strTempPath.c_str(), strWalletPath.c_str()) != 0) | ||
| 2182 | { | ||
| 2183 | rename(strBackupPath.c_str(), strWalletPath.c_str()); | ||
| 2184 | strErrorRet = "could not install the encrypted SQLite wallet; the original was restored"; | ||
| 2185 | remove(strTempPath.c_str()); | ||
| 2186 | return false; | ||
| 2187 | } | ||
| 2188 | remove((strTempPath + "-wal").c_str()); | ||
| 2189 | remove((strTempPath + "-shm").c_str()); | ||
| 2190 | |||
| 2191 | strBackupRet = strBackupPath; | ||
| 2192 | return true; | ||
| 2193 | } | ||
| 2194 | |||
| 2009 | bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet) | 2195 | bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet) |
| 2010 | { | 2196 | { |
| 2011 | strBackupRet.clear(); | 2197 | strBackupRet.clear(); |
| @@ -2144,6 +2330,24 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st | |||
| 2144 | vNewPool.push_back(make_pair((int64)i + 1, vchPubKey)); | 2330 | vNewPool.push_back(make_pair((int64)i + 1, vchPubKey)); |
| 2145 | } | 2331 | } |
| 2146 | 2332 | ||
| 2333 | // Every ciphertext is computed; nothing has touched the store yet. Under | ||
| 2334 | // the SQLite backend, persist by rebuilding wallet.sqlite in one atomic | ||
| 2335 | // transaction and swapping it in. The Berkeley DB path below is unchanged. | ||
| 2336 | if (WalletSQLiteRuntimeActive()) | ||
| 2337 | { | ||
| 2338 | int nCopiedSQLite = 0; | ||
| 2339 | if (!EncryptWalletPersistSQLite(vCryptedKeys, vNewPoolKeys, vNewPool, | ||
| 2340 | kMasterKey, vchCryptedHDMasterNew, | ||
| 2341 | vchCryptedHDChainCodeNew, nHDNextNew, | ||
| 2342 | nCopiedSQLite, strBackupRet, strErrorRet)) | ||
| 2343 | return false; | ||
| 2344 | printf("EncryptWallet() : copied %d public record(s), encrypted %u key(s), " | ||
| 2345 | "rebuilt %d key-pool entry(s) [SQLite]\n", | ||
| 2346 | nCopiedSQLite, (unsigned int)(vCryptedKeys.size() + vNewPoolKeys.size()), | ||
| 2347 | (int)vNewPool.size()); | ||
| 2348 | return true; | ||
| 2349 | } | ||
| 2350 | |||
| 2147 | string strTempFile = strprintf("wallet.encrypting.%lld.dat", GetTime()); | 2351 | string strTempFile = strprintf("wallet.encrypting.%lld.dat", GetTime()); |
| 2148 | string strTempPath = GetAppDir() + "/" + strTempFile; | 2352 | string strTempPath = GetAppDir() + "/" + strTempFile; |
| 2149 | remove(strTempPath.c_str()); | 2353 | remove(strTempPath.c_str()); |
| @@ -2211,7 +2415,7 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st | |||
| 2211 | DBFlush(true); | 2415 | DBFlush(true); |
| 2212 | 2416 | ||
| 2213 | string strWalletPath = GetAppDir() + "/wallet.dat"; | 2417 | string strWalletPath = GetAppDir() + "/wallet.dat"; |
| 2214 | string strBackupPath = WalletEncryptBackupPath(); | 2418 | string strBackupPath = WalletEncryptBackupPathFor("wallet.dat"); |
| 2215 | if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) | 2419 | if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) |
| 2216 | { | 2420 | { |
| 2217 | strErrorRet = strprintf("could not move the original wallet to %s", strBackupPath.c_str()); | 2421 | strErrorRet = strprintf("could not move the original wallet to %s", strBackupPath.c_str()); |
diff --git a/src/makefile.mingw b/src/makefile.mingw index 14e6b32..1cb47fb 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw | |||
| @@ -112,6 +112,7 @@ tests: test_bip32.exe bitflash.exe | |||
| 112 | ./bitflash.exe -selftest=wallet-sqlite-migration | 112 | ./bitflash.exe -selftest=wallet-sqlite-migration |
| 113 | ./bitflash.exe -selftest=wallet-crypto | 113 | ./bitflash.exe -selftest=wallet-crypto |
| 114 | ./bitflash.exe -selftest=wallet-encrypt | 114 | ./bitflash.exe -selftest=wallet-encrypt |
| 115 | ./bitflash.exe -selftest=wallet-sqlite-encrypt | ||
| 115 | ./bitflash.exe -selftest=net-message | 116 | ./bitflash.exe -selftest=net-message |
| 116 | ./bitflash.exe -selftest=consensus-limits | 117 | ./bitflash.exe -selftest=consensus-limits |
| 117 | ./bitflash.exe -selftest=pool-stratum | 118 | ./bitflash.exe -selftest=pool-stratum |
diff --git a/src/selftest.cpp b/src/selftest.cpp index 098eed6..fc10947 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp | |||
| @@ -2252,6 +2252,191 @@ static int RunWalletEncryptSelfTest() | |||
| 2252 | return nFail == 0 ? 0 : 1; | 2252 | return nFail == 0 ? 0 : 1; |
| 2253 | } | 2253 | } |
| 2254 | 2254 | ||
| 2255 | // Encrypting a wallet while it runs on the SQLite backend. Unlike the Berkeley | ||
| 2256 | // DB encrypt test above, this drives the real command-line path end to end in | ||
| 2257 | // child processes: build a Berkeley DB wallet, export it to SQLite, then run | ||
| 2258 | // -walletbackend=sqlite -encryptwallet and prove the SQLite wallet came out | ||
| 2259 | // encrypted, unlockable, and with wallet.dat left untouched. | ||
| 2260 | static int RunWalletSQLiteEncryptSelfTest() | ||
| 2261 | { | ||
| 2262 | fflush(stdout); | ||
| 2263 | printf("wallet-sqlite-encrypt self-test\n"); | ||
| 2264 | |||
| 2265 | std::string tmp; | ||
| 2266 | std::string cwd; | ||
| 2267 | if (!MakeTempDir(tmp)) | ||
| 2268 | { | ||
| 2269 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 2270 | return 1; | ||
| 2271 | } | ||
| 2272 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 2273 | { | ||
| 2274 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 2275 | RemoveTree(tmp); | ||
| 2276 | return 1; | ||
| 2277 | } | ||
| 2278 | |||
| 2279 | int nFail = 0; | ||
| 2280 | strSetDataDir = tmp; | ||
| 2281 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 2282 | |||
| 2283 | try | ||
| 2284 | { | ||
| 2285 | // Build a normal Berkeley DB wallet with a recovery phrase, a key pool, | ||
| 2286 | // and a couple of public records, then release it. Everything after this | ||
| 2287 | // runs the real command-line paths in child processes. | ||
| 2288 | if (!LoadWallet()) | ||
| 2289 | throw std::runtime_error("LoadWallet failed"); | ||
| 2290 | |||
| 2291 | string strError; | ||
| 2292 | const string strMnemonic = | ||
| 2293 | "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; | ||
| 2294 | nFail += Check(SetHDSeedFromMnemonic(strMnemonic, strError), | ||
| 2295 | "a phrase can be installed before encryption") ? 0 : 1; | ||
| 2296 | TopUpKeyPool(); | ||
| 2297 | |||
| 2298 | vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey(); | ||
| 2299 | CKey publicLabelKey; | ||
| 2300 | publicLabelKey.MakeNewKey(); | ||
| 2301 | string strPublicLabelAddress = PubKeyToAddress(publicLabelKey.GetPubKey()); | ||
| 2302 | nFail += Check(CWalletDB().WriteName(strPublicLabelAddress, "rewrite-public-name") && | ||
| 2303 | CWalletDB().WriteSetting("rewrite-public-setting", (int64)424242), | ||
| 2304 | "public wallet records can be written before encryption") ? 0 : 1; | ||
| 2305 | |||
| 2306 | CPrivKey vchDefaultPrivKey; | ||
| 2307 | nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), | ||
| 2308 | "the default private key is readable before encryption") ? 0 : 1; | ||
| 2309 | vector<unsigned char> vchDefaultPrivBytes(vchDefaultPrivKey.begin(), vchDefaultPrivKey.end()); | ||
| 2310 | vector<unsigned char> vchPlainHDMaster = vchHDMaster; | ||
| 2311 | vector<unsigned char> vchPlainHDChainCode = vchHDChainCode; | ||
| 2312 | |||
| 2313 | // Release wallet.dat so a child process can read it for the export. | ||
| 2314 | DBFlush(true); | ||
| 2315 | |||
| 2316 | #ifdef _WIN32 | ||
| 2317 | string strExe = cwd + "\\bitflash.exe"; | ||
| 2318 | #else | ||
| 2319 | string strExe = cwd + "/bitflash-node"; | ||
| 2320 | #endif | ||
| 2321 | string strSQLitePath = tmp + "/wallet.sqlite"; | ||
| 2322 | string strWalletDatPath = tmp + "/wallet.dat"; | ||
| 2323 | string strRightPassFile = tmp + "/right-pass.txt"; | ||
| 2324 | string strWrongPassFile = tmp + "/wrong-pass.txt"; | ||
| 2325 | nFail += Check(WriteTextFile(strRightPassFile, "btf-test-passphrase\n") && | ||
| 2326 | WriteTextFile(strWrongPassFile, "wrong-passphrase\n"), | ||
| 2327 | "passphrase files can be written for restarted commands") ? 0 : 1; | ||
| 2328 | |||
| 2329 | // Step 1: export the Berkeley DB wallet to SQLite. | ||
| 2330 | vector<string> vExportArgs; | ||
| 2331 | vExportArgs.push_back("-datadir=" + tmp); | ||
| 2332 | vExportArgs.push_back("-walletsqliteexport=" + strSQLitePath); | ||
| 2333 | vExportArgs.push_back("-nogui"); | ||
| 2334 | int nExportRet = RunBitflashChild(strExe, vExportArgs); | ||
| 2335 | nFail += Check(nExportRet == 0 && FileExists(strSQLitePath.c_str()), | ||
| 2336 | "the wallet exports to a SQLite file") ? 0 : 1; | ||
| 2337 | nFail += Check(FileContainsBytes(strSQLitePath, vchDefaultPrivBytes), | ||
| 2338 | "the exported SQLite wallet holds the plaintext key before encryption") ? 0 : 1; | ||
| 2339 | |||
| 2340 | // Step 2: encrypt through the real SQLite backend command path. | ||
| 2341 | string strEncryptOut = tmp + "/sqlite-encrypt-out.txt"; | ||
| 2342 | vector<string> vEncryptArgs; | ||
| 2343 | vEncryptArgs.push_back("-datadir=" + tmp); | ||
| 2344 | vEncryptArgs.push_back("-walletbackend=sqlite"); | ||
| 2345 | vEncryptArgs.push_back("-encryptwallet=@" + strRightPassFile); | ||
| 2346 | vEncryptArgs.push_back("-nogui"); | ||
| 2347 | int nEncryptRet = RunBitflashChild(strExe, vEncryptArgs, NULL, &strEncryptOut); | ||
| 2348 | nFail += Check(nEncryptRet == 0, | ||
| 2349 | "the SQLite wallet can be encrypted through -walletbackend=sqlite") ? 0 : 1; | ||
| 2350 | nFail += Check(FileContainsText(strEncryptOut, "Wallet backend: SQLite"), | ||
| 2351 | "encryption took the SQLite persistence path") ? 0 : 1; | ||
| 2352 | nFail += Check(FileContainsText(strEncryptOut, "wallet.sqlite.before-encrypt"), | ||
| 2353 | "the plaintext SQLite wallet is preserved as an explicit backup") ? 0 : 1; | ||
| 2354 | |||
| 2355 | // The encrypted SQLite wallet must no longer hold the plaintext secrets. | ||
| 2356 | nFail += Check(!FileContainsBytes(strSQLitePath, vchDefaultPrivBytes), | ||
| 2357 | "the encrypted SQLite wallet no longer contains the default private key bytes") ? 0 : 1; | ||
| 2358 | nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDMaster), | ||
| 2359 | "the encrypted SQLite wallet no longer contains the HD master key bytes") ? 0 : 1; | ||
| 2360 | nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDChainCode), | ||
| 2361 | "the encrypted SQLite wallet no longer contains the HD chain-code bytes") ? 0 : 1; | ||
| 2362 | |||
| 2363 | // The Berkeley DB wallet.dat is a separate file: the SQLite encrypt path | ||
| 2364 | // must never touch it, so its plaintext key is still exactly there. | ||
| 2365 | nFail += Check(FileContainsBytes(strWalletDatPath, vchDefaultPrivBytes), | ||
| 2366 | "the original Berkeley DB wallet.dat is left untouched by the SQLite encrypt") ? 0 : 1; | ||
| 2367 | |||
| 2368 | // Step 3: a locked encrypted SQLite wallet reports that phrase coverage | ||
| 2369 | // needs an unlock, exactly as the Berkeley DB path does (exit code 2). | ||
| 2370 | string strLockedAudit = tmp + "/locked-recovery-audit.txt"; | ||
| 2371 | vector<string> vLockedArgs; | ||
| 2372 | vLockedArgs.push_back("-datadir=" + tmp); | ||
| 2373 | vLockedArgs.push_back("-walletbackend=sqlite"); | ||
| 2374 | vLockedArgs.push_back("-recoveryaudit"); | ||
| 2375 | vLockedArgs.push_back("-nogui"); | ||
| 2376 | int nLockedRet = RunBitflashChild(strExe, vLockedArgs, NULL, &strLockedAudit); | ||
| 2377 | nFail += Check(nLockedRet == 2 && | ||
| 2378 | FileContainsText(strLockedAudit, "recovery phrase: encrypted, unlock wallet to audit"), | ||
| 2379 | "a locked encrypted SQLite wallet reports that phrase coverage needs unlock") ? 0 : 1; | ||
| 2380 | |||
| 2381 | // The wrong passphrase is rejected and reveals nothing. | ||
| 2382 | string strWrongDump = tmp + "/wrong-dump.txt"; | ||
| 2383 | vector<string> vWrongArgs; | ||
| 2384 | vWrongArgs.push_back("-datadir=" + tmp); | ||
| 2385 | vWrongArgs.push_back("-walletbackend=sqlite"); | ||
| 2386 | vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); | ||
| 2387 | vWrongArgs.push_back("-dumpwallet=" + strWrongDump); | ||
| 2388 | vWrongArgs.push_back("-nogui"); | ||
| 2389 | int nWrongRet = RunBitflashChild(strExe, vWrongArgs); | ||
| 2390 | nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), | ||
| 2391 | "a restarted SQLite wallet rejects the wrong passphrase") ? 0 : 1; | ||
| 2392 | |||
| 2393 | // The right passphrase unlocks and can dump the original key. | ||
| 2394 | string strRightDump = tmp + "/right-dump.txt"; | ||
| 2395 | vector<string> vRightArgs; | ||
| 2396 | vRightArgs.push_back("-datadir=" + tmp); | ||
| 2397 | vRightArgs.push_back("-walletbackend=sqlite"); | ||
| 2398 | vRightArgs.push_back("-walletpassphrase=@" + strRightPassFile); | ||
| 2399 | vRightArgs.push_back("-dumpwallet=" + strRightDump); | ||
| 2400 | vRightArgs.push_back("-nogui"); | ||
| 2401 | int nRightRet = RunBitflashChild(strExe, vRightArgs); | ||
| 2402 | nFail += Check(nRightRet == 0 && FileExists(strRightDump.c_str()), | ||
| 2403 | "a restarted SQLite wallet unlocks with the right passphrase") ? 0 : 1; | ||
| 2404 | nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), | ||
| 2405 | "the unlocked SQLite wallet decrypts and dumps the original key") ? 0 : 1; | ||
| 2406 | |||
| 2407 | // The recovery phrase survives the encryption. | ||
| 2408 | string strAudit = tmp + "/recovery-audit.txt"; | ||
| 2409 | vector<string> vAuditArgs; | ||
| 2410 | vAuditArgs.push_back("-datadir=" + tmp); | ||
| 2411 | vAuditArgs.push_back("-walletbackend=sqlite"); | ||
| 2412 | vAuditArgs.push_back("-walletpassphrase=@" + strRightPassFile); | ||
| 2413 | vAuditArgs.push_back("-recoveryaudit"); | ||
| 2414 | vAuditArgs.push_back("-nogui"); | ||
| 2415 | int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit); | ||
| 2416 | nFail += Check(nAuditRet == 0 && | ||
| 2417 | FileContainsText(strAudit, "recovery phrase: present"), | ||
| 2418 | "the encrypted SQLite wallet still has its recovery phrase") ? 0 : 1; | ||
| 2419 | } | ||
| 2420 | catch (const std::exception& e) | ||
| 2421 | { | ||
| 2422 | printf(" FAIL exception: %s\n", e.what()); | ||
| 2423 | nFail++; | ||
| 2424 | } | ||
| 2425 | catch (...) | ||
| 2426 | { | ||
| 2427 | printf(" FAIL unknown exception\n"); | ||
| 2428 | nFail++; | ||
| 2429 | } | ||
| 2430 | |||
| 2431 | DBFlush(true); | ||
| 2432 | SetCurrentDir(cwd); | ||
| 2433 | RemoveTree(tmp); | ||
| 2434 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 2435 | nFail, nFail == 1 ? "" : "s"); | ||
| 2436 | fflush(stdout); | ||
| 2437 | return nFail == 0 ? 0 : 1; | ||
| 2438 | } | ||
| 2439 | |||
| 2255 | static int RunWalletPortabilitySelfTest() | 2440 | static int RunWalletPortabilitySelfTest() |
| 2256 | { | 2441 | { |
| 2257 | fflush(stdout); | 2442 | fflush(stdout); |
| @@ -2981,6 +3166,8 @@ int RunSelfTest(const std::string& name) | |||
| 2981 | return RunWalletCryptoSelfTest(); | 3166 | return RunWalletCryptoSelfTest(); |
| 2982 | if (name == "wallet-encrypt") | 3167 | if (name == "wallet-encrypt") |
| 2983 | return RunWalletEncryptSelfTest(); | 3168 | return RunWalletEncryptSelfTest(); |
| 3169 | if (name == "wallet-sqlite-encrypt") | ||
| 3170 | return RunWalletSQLiteEncryptSelfTest(); | ||
| 2984 | if (name == "wallet-portability") | 3171 | if (name == "wallet-portability") |
| 2985 | return RunWalletPortabilitySelfTest(); | 3172 | return RunWalletPortabilitySelfTest(); |
| 2986 | if (name == "net-message") | 3173 | if (name == "net-message") |
| @@ -2997,6 +3184,6 @@ int RunSelfTest(const std::string& name) | |||
| 2997 | return RunManagedTorSelfTest(); | 3184 | return RunManagedTorSelfTest(); |
| 2998 | 3185 | ||
| 2999 | printf("Unknown self-test '%s'\n", name.c_str()); | 3186 | printf("Unknown self-test '%s'\n", name.c_str()); |
| 3000 | printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); | 3187 | printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-sqlite-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); |
| 3001 | return 1; | 3188 | return 1; |
| 3002 | } | 3189 | } |
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 4dda8dd..374b2cc 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp | |||
| @@ -1365,6 +1365,11 @@ int CmdEncryptWallet(const std::string& strPassphrase) | |||
| 1365 | { | 1365 | { |
| 1366 | AttachTerminal(); | 1366 | AttachTerminal(); |
| 1367 | 1367 | ||
| 1368 | // Capture the backend now: under SQLite, EncryptWallet() closes the runtime | ||
| 1369 | // store to swap the file, so WalletSQLiteRuntimeActive() reads false by the | ||
| 1370 | // time we would decide whether to purge Berkeley DB logs below. | ||
| 1371 | bool fSQLiteBackend = WalletSQLiteRuntimeActive(); | ||
| 1372 | |||
| 1368 | string strBackup; | 1373 | string strBackup; |
| 1369 | string strError; | 1374 | string strError; |
| 1370 | if (!EncryptWallet(strPassphrase, strBackup, strError)) | 1375 | if (!EncryptWallet(strPassphrase, strBackup, strError)) |
| @@ -1375,6 +1380,8 @@ int CmdEncryptWallet(const std::string& strPassphrase) | |||
| 1375 | } | 1380 | } |
| 1376 | 1381 | ||
| 1377 | printf("Wallet encrypted.\n"); | 1382 | printf("Wallet encrypted.\n"); |
| 1383 | if (fSQLiteBackend) | ||
| 1384 | printf("Wallet backend: SQLite (wallet.sqlite).\n"); | ||
| 1378 | printf("The old unencrypted wallet was moved to:\n"); | 1385 | printf("The old unencrypted wallet was moved to:\n"); |
| 1379 | printf(" %s\n", strBackup.c_str()); | 1386 | printf(" %s\n", strBackup.c_str()); |
| 1380 | printf("That backup still contains private keys in plain text. Move it offline or\n"); | 1387 | printf("That backup still contains private keys in plain text. Move it offline or\n"); |
| @@ -1387,7 +1394,13 @@ int CmdEncryptWallet(const std::string& strPassphrase) | |||
| 1387 | // them. Closing the environment and dropping the logs is the last step of | 1394 | // them. Closing the environment and dropping the logs is the last step of |
| 1388 | // encrypting, not an optimisation -- our own backup advice is to copy the | 1395 | // encrypting, not an optimisation -- our own backup advice is to copy the |
| 1389 | // whole directory, and following it otherwise carries the keys along. | 1396 | // whole directory, and following it otherwise carries the keys along. |
| 1390 | PurgeDbEnvironmentLogs(); | 1397 | // |
| 1398 | // The SQLite backend has no Berkeley DB environment behind the wallet, and | ||
| 1399 | // EncryptWallet() already closed and swapped the SQLite store; there is | ||
| 1400 | // nothing to purge, and calling into the environment here would only open | ||
| 1401 | // one needlessly. | ||
| 1402 | if (!fSQLiteBackend) | ||
| 1403 | PurgeDbEnvironmentLogs(); | ||
| 1391 | return 0; | 1404 | return 0; |
| 1392 | } | 1405 | } |
| 1393 | 1406 | ||