From a783e5cc2e5c464b7fb0e7e4b6c6864f1a391537 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 3 Jul 2023 16:20:39 -0400 Subject: [PATCH 01/15] crypter: Separate setting IV from setting key --- src/wallet/crypter.cpp | 19 +++++++++++++++++-- src/wallet/crypter.h | 3 +++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/wallet/crypter.cpp b/src/wallet/crypter.cpp index e2799c2d057..089c28b44e0 100644 --- a/src/wallet/crypter.cpp +++ b/src/wallet/crypter.cpp @@ -59,16 +59,31 @@ bool CCrypter::SetKeyFromPassphrase(const SecureString& strKeyData, const std::v bool CCrypter::SetKey(const CKeyingMaterial& chNewKey, const std::vector& chNewIV) { - if (chNewKey.size() != WALLET_CRYPTO_KEY_SIZE || chNewIV.size() != WALLET_CRYPTO_IV_SIZE) + return SetKey(chNewKey) && SetIV(chNewIV); +} + +bool CCrypter::SetKey(const CKeyingMaterial& chNewKey) +{ + if (chNewKey.size() != WALLET_CRYPTO_KEY_SIZE) return false; memcpy(vchKey.data(), chNewKey.data(), chNewKey.size()); - memcpy(vchIV.data(), chNewIV.data(), chNewIV.size()); fKeySet = true; return true; } +bool CCrypter::SetIV(const std::vector& chNewIV) +{ + if (chNewIV.size() != WALLET_CRYPTO_IV_SIZE) + return false; + + memcpy(vchIV.data(), chNewIV.data(), chNewIV.size()); + + m_iv_set = true; + return true; +} + bool CCrypter::Encrypt(const CKeyingMaterial& vchPlaintext, std::vector &vchCiphertext) const { if (!fKeySet) diff --git a/src/wallet/crypter.h b/src/wallet/crypter.h index b776a9c4979..53355552058 100644 --- a/src/wallet/crypter.h +++ b/src/wallet/crypter.h @@ -74,6 +74,7 @@ friend class wallet_crypto_tests::TestCrypter; // for test access to chKey/chIV std::vector> vchKey; std::vector> vchIV; bool fKeySet; + bool m_iv_set; int BytesToKeySHA512AES(const std::vector& chSalt, const SecureString& strKeyData, int count, unsigned char *key,unsigned char *iv) const; @@ -82,6 +83,8 @@ friend class wallet_crypto_tests::TestCrypter; // for test access to chKey/chIV bool Encrypt(const CKeyingMaterial& vchPlaintext, std::vector &vchCiphertext) const; bool Decrypt(const std::vector& vchCiphertext, CKeyingMaterial& vchPlaintext) const; bool SetKey(const CKeyingMaterial& chNewKey, const std::vector& chNewIV); + bool SetKey(const CKeyingMaterial& key); + bool SetIV(const std::vector& chNewIV); void CleanKey() { From 2c3d81fa6c81f6e500a8b06ec2aca14d9bea4674 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 3 Jul 2023 16:21:41 -0400 Subject: [PATCH 02/15] crypter: Make sure IV is set too --- src/wallet/crypter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wallet/crypter.cpp b/src/wallet/crypter.cpp index 089c28b44e0..faf5383cf8e 100644 --- a/src/wallet/crypter.cpp +++ b/src/wallet/crypter.cpp @@ -86,7 +86,7 @@ bool CCrypter::SetIV(const std::vector& chNewIV) bool CCrypter::Encrypt(const CKeyingMaterial& vchPlaintext, std::vector &vchCiphertext) const { - if (!fKeySet) + if (!fKeySet && !m_iv_set) return false; // max ciphertext len for a n bytes of plaintext is @@ -104,7 +104,7 @@ bool CCrypter::Encrypt(const CKeyingMaterial& vchPlaintext, std::vector& vchCiphertext, CKeyingMaterial& vchPlaintext) const { - if (!fKeySet) + if (!fKeySet && !m_iv_set) return false; // plaintext will always be equal to or lesser than length of ciphertext From a203061dc076c1b627064db209209caa32a36ad9 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 3 Jul 2023 17:33:38 -0400 Subject: [PATCH 03/15] walletdb: Add EncryptedDatabase and its batch and cursor classes EncryptedDatabase is a WalletDatabase that encrypts the records before writing them to an underlying WalletDatabase. This encryption occurs transparently to the higher level application logic so the wallet does not need to be concerned about whether the data it is writing is encrypted. In order to work with prefix matching and cursor iteration in an order that we are expecting, EncryptedDatabase maintains a map of the unencrypted record keys to the encrypted record keys. When given the plaintext record key to pull up, it can retrieve the encrypted record key and then retrieve the encrypted record from the underlying database. --- src/Makefile.am | 3 +- src/wallet/db.cpp | 3 + src/wallet/db.h | 8 +- src/wallet/encrypted_db.cpp | 366 +++++++++++++++++++++++++++++++++++ src/wallet/encrypted_db.h | 127 ++++++++++++ src/wallet/test/db_tests.cpp | 4 + src/wallet/test/util.cpp | 5 - 7 files changed, 509 insertions(+), 7 deletions(-) create mode 100644 src/wallet/encrypted_db.cpp create mode 100644 src/wallet/encrypted_db.h diff --git a/src/Makefile.am b/src/Makefile.am index e1ae049b15a..8e6dd71d419 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -336,6 +336,7 @@ BITCOIN_CORE_H = \ wallet/crypter.h \ wallet/db.h \ wallet/dump.h \ + wallet/encrypted_db.h \ wallet/external_signer_scriptpubkeyman.h \ wallet/feebumper.h \ wallet/fees.h \ @@ -516,7 +517,7 @@ libbitcoin_wallet_a_SOURCES = \ $(BITCOIN_CORE_H) if USE_SQLITE -libbitcoin_wallet_a_SOURCES += wallet/sqlite.cpp +libbitcoin_wallet_a_SOURCES += wallet/sqlite.cpp wallet/encrypted_db.cpp endif if USE_BDB libbitcoin_wallet_a_SOURCES += wallet/bdb.cpp wallet/salvage.cpp diff --git a/src/wallet/db.cpp b/src/wallet/db.cpp index 0c249205162..2e9bf66656c 100644 --- a/src/wallet/db.cpp +++ b/src/wallet/db.cpp @@ -16,6 +16,9 @@ #include namespace wallet { +bool operator<(BytePrefix a, Span b) { return a.prefix < b.subspan(0, std::min(a.prefix.size(), b.size())); } +bool operator<(Span a, BytePrefix b) { return a.subspan(0, std::min(a.size(), b.prefix.size())) < b.prefix; } + std::vector ListDatabases(const fs::path& wallet_dir) { std::vector paths; diff --git a/src/wallet/db.h b/src/wallet/db.h index 9d684225c34..dac02ef166b 100644 --- a/src/wallet/db.h +++ b/src/wallet/db.h @@ -20,6 +20,11 @@ class ArgsManager; struct bilingual_str; namespace wallet { +// BytePrefix compares equality with other byte spans that begin with the same prefix. +struct BytePrefix { Span prefix; }; +bool operator<(BytePrefix a, Span b); +bool operator<(Span a, BytePrefix b); + void SplitWalletPath(const fs::path& wallet_path, fs::path& env_directory, std::string& database_filename); class DatabaseCursor @@ -185,7 +190,8 @@ struct DatabaseOptions { bool require_create = false; std::optional require_format; uint64_t create_flags = 0; - SecureString create_passphrase; + SecureString create_passphrase; //!< The passphrase for wallet-level encryption + SecureString db_passphrase; //!< The passphrase for database-level encryption // Specialized options. Not every option is supported by every backend. bool verify = true; //!< Check data integrity on load. diff --git a/src/wallet/encrypted_db.cpp b/src/wallet/encrypted_db.cpp new file mode 100644 index 00000000000..df08e364803 --- /dev/null +++ b/src/wallet/encrypted_db.cpp @@ -0,0 +1,366 @@ +// Copyright (c) 2023 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wallet { +EncryptedDatabase::EncryptedDatabase(std::unique_ptr database, const SecureString& passphrase, bool create) + : m_database(std::move(database)) +{ + std::unique_ptr batch = m_database->MakeBatch(); + + if (create) { + // Doesn't exist, set it up + // Generate the encryption secret + CKeyingMaterial enc_secret; + enc_secret.resize(WALLET_CRYPTO_KEY_SIZE); + GetStrongRandBytes(enc_secret); + + // Encrypt the secret with the passphrase + CMasterKey enc_key; + enc_key.vchSalt.resize(WALLET_CRYPTO_SALT_SIZE); + GetStrongRandBytes(enc_key.vchSalt); + + CCrypter crypter; + constexpr MillisecondsDouble target{100}; + auto start{SteadyClock::now()}; + crypter.SetKeyFromPassphrase(passphrase, enc_key.vchSalt, 25000, enc_key.nDerivationMethod); + enc_key.nDeriveIterations = static_cast(25000 * target / (SteadyClock::now() - start)); + + start = SteadyClock::now(); + crypter.SetKeyFromPassphrase(passphrase, enc_key.vchSalt, enc_key.nDeriveIterations, enc_key.nDerivationMethod); + enc_key.nDeriveIterations = (enc_key.nDeriveIterations + static_cast(enc_key.nDeriveIterations * target / (SteadyClock::now() - start))) / 2; + + if (enc_key.nDeriveIterations < 25000) + enc_key.nDeriveIterations = 25000; + + if (!crypter.SetKeyFromPassphrase(passphrase, enc_key.vchSalt, enc_key.nDeriveIterations, enc_key.nDerivationMethod)) { + throw std::runtime_error("Unable to set encryption key from passphrase"); + } + if (!crypter.Encrypt(enc_secret, enc_key.vchCryptedKey)) { + throw std::runtime_error("Unable to encrypt key with passphrase"); + } + + // Write that to disk + if (!batch->Write(ENCRYPTION_RECORD, enc_key)) { + throw std::runtime_error("Unable to write the encryption key data"); + } + } + + // Read the encrypted encryption key + CMasterKey enc_key; + if (!batch->Read(ENCRYPTION_RECORD, enc_key)) { + throw std::runtime_error("Unable to read the encryption key data"); + } + batch.reset(); + + // Decrypt the key with passphrase + CCrypter crypter; + if (!crypter.SetKeyFromPassphrase(passphrase, enc_key.vchSalt, enc_key.nDeriveIterations, enc_key.nDerivationMethod)) { + throw std::runtime_error("Unable to get decryption key from passphrase"); + } + CKeyingMaterial enc_secret; + if (!crypter.Decrypt(enc_key.vchCryptedKey, enc_secret)) { + throw std::runtime_error("Unable to decrypt database, are you sure the passphrase is correct?"); + } + m_enc_secret = std::move(enc_secret); + + // Make sure our crypter has the correct secret + m_crypter.SetKey(m_enc_secret); + + Open(); +} + +util::Result EncryptedDatabase::DecryptRecordData(Span data) +{ + DataStream s_data(data); + std::vector iv; + std::vector ciphertext; + s_data >> iv; + s_data >> ciphertext; + + CKeyingMaterial plaintext; + if (!m_crypter.SetIV(iv)) return util::Error{Untranslated("IV is not valid")}; + if (!m_crypter.Decrypt(ciphertext, plaintext)) return util::Error{Untranslated("Could not decrypt data")}; + + SerializeData out{reinterpret_cast(plaintext.data()), reinterpret_cast(plaintext.data() + plaintext.size())}; + return out; +} + +util::Result EncryptedDatabase::EncryptRecordData(Span data) +{ + DataStream s_out; + + HashWriter hasher; + hasher << data; + uint256 hash = hasher.GetHash(); + std::vector iv(hash.begin(), hash.begin() + WALLET_CRYPTO_IV_SIZE); + if (!m_crypter.SetIV(iv)) return util::Error{Untranslated("Unable to set IV")}; + s_out << iv; + + CKeyingMaterial plaintext(UCharCast(data.begin()), UCharCast(data.end())); + std::vector enc; + if (!m_crypter.Encrypt(plaintext, enc)) return util::Error{Untranslated("Cound not encrypt data")}; + s_out << enc; + SerializeData out{s_out.begin(), s_out.end()}; + return out; +} + +void EncryptedDatabase::Open() +{ + // Read all records into memory and decrypt them + std::unique_ptr batch = m_database->MakeBatch(); + if (!batch) { + throw std::runtime_error("Error getting database batch"); + } + std::unique_ptr cursor = batch->GetNewCursor(); + if (!cursor) { + throw std::runtime_error("Error getting database cursor"); + } + DataStream key, value; + while (true) { + DatabaseCursor::Status status = cursor->Next(key, value); + if (status == DatabaseCursor::Status::DONE) { + break; + } else if (status == DatabaseCursor::Status::FAIL) { + throw std::runtime_error("Error reading next record in database"); + } + + // If this record is the encrypted key record, ignore it + if (key == ENCRYPTION_RECORD) { + continue; + } + + // Every key-value record is serialized in the same way: both keys and values are an + // IV followed by the ciphertext as a vector of bytes + // The decrypted ciphertext is the data that the application stored. + util::Result key_data = DecryptRecordData(key); + if (!key_data) { + throw std::runtime_error(util::ErrorString(key_data).original); + } + SerializeData enc_key_data{key.begin(), key.end()}; + m_record_keys.emplace(*key_data, enc_key_data); + } +} + +bool EncryptedDBBatch::ReadKey(DataStream&& key, DataStream& value) +{ + // Lookup the encrypted key data from our map + SerializeData key_data{key.begin(), key.end()}; + const auto& it = m_database.m_record_keys.find(key_data); + if (it == m_database.m_record_keys.end()) { + return false; + } + return ReadEncryptedKey(it->second, value); +} + +bool EncryptedDBBatch::ReadEncryptedKey(SerializeData enc_key, DataStream& value) +{ + DataStream crypt_value; + if (!m_batch->Read(MakeByteSpan(enc_key), crypt_value)) { + return false; + } + util::Result value_data = m_database.DecryptRecordData(crypt_value); + if (!value_data) { + return false; + } + value.write(*value_data); + return true; +} + +bool EncryptedDBBatch::WriteKey(DataStream&& key, DataStream&& value, bool overwrite) +{ + util::Result enc_value = m_database.EncryptRecordData(value); + if (!enc_value) { + return false; + } + + // Lookup the encrypted key data from our map + SerializeData key_data{key.begin(), key.end()}; + SerializeData enc_key; + const auto& it = m_database.m_record_keys.find(key_data); + if (it != m_database.m_record_keys.end()) { + enc_key = it->second; + } else { + util::Result enc_key_res = m_database.EncryptRecordData(key); + if (!enc_key_res) { + return false; + } + enc_key = *enc_key_res; + } + + if (!m_batch->Write(MakeByteSpan(enc_key), MakeByteSpan(*enc_value), overwrite)) { + return false; + } + if (m_txn_started) { + m_txn_writes.emplace_back(key_data, enc_key); + } else { + m_database.m_record_keys.emplace(key_data, enc_key); + } + return true; +} + +bool EncryptedDBBatch::EraseKey(DataStream&& key) +{ + // Lookup the encrypted key data from our map + SerializeData key_data{key.begin(), key.end()}; + const auto& it = m_database.m_record_keys.find(key_data); + if (it == m_database.m_record_keys.end()) { + return false; + } + if (!m_batch->Erase(MakeByteSpan(it->second))) { + return false; + } + if (m_txn_started) { + m_txn_erases.emplace_back(key_data); + } else { + m_database.m_record_keys.erase(it); + } + return true; +} +bool EncryptedDBBatch::HasKey(DataStream&& key) +{ + // Lookup the encrypted key data from our map + SerializeData key_data{key.begin(), key.end()}; + const auto& it = m_database.m_record_keys.find(key_data); + if (it == m_database.m_record_keys.end()) { + return false; + } + Assume(m_batch->Exists(MakeByteSpan(it->second))); + return true; +} + +void EncryptedDBBatch::Close() +{ + if (m_txn_started) { + TxnAbort(); + } + m_batch->Close(); +} + +bool EncryptedDBBatch::ErasePrefix(Span prefix) +{ + auto it = m_database.m_record_keys.begin(); + while (it != m_database.m_record_keys.end()) { + auto& key = it->first; + if (key.size() < prefix.size() || std::search(key.begin(), key.end(), prefix.begin(), prefix.end()) != key.begin()) { + it++; + continue; + } + m_batch->Erase(MakeByteSpan(it->second)); + if (m_txn_started) { + m_txn_erases.emplace_back(key); + it++; + } else { + it = m_database.m_record_keys.erase(it); + } + } + return true; +} + +bool EncryptedDBBatch::TxnBegin() +{ + if (m_txn_started) { + return false; + } + if (!m_batch->TxnBegin()) { + return false; + } + m_txn_writes.clear(); + m_txn_erases.clear(); + m_txn_started = true; + return true; +} + +bool EncryptedDBBatch::TxnCommit() +{ + if (!m_txn_started) { + return false; + } + if (!m_batch->TxnCommit()) { + return false; + } + + for (const auto& [key_data, enc_key] : m_txn_writes) { + m_database.m_record_keys.emplace(key_data, enc_key); + } + for (const auto& key_data : m_txn_erases) { + m_database.m_record_keys.erase(key_data); + } + + m_txn_started = false; + m_txn_writes.clear(); + m_txn_erases.clear(); + return true; +} + +bool EncryptedDBBatch::TxnAbort() +{ + if (!m_txn_started) { + return false; + } + if (!m_batch->TxnAbort()) { + return false; + } + m_txn_started = false; + m_txn_writes.clear(); + m_txn_erases.clear(); + return true; +} + +std::unique_ptr EncryptedDBBatch::GetNewCursor() +{ + return std::make_unique(m_database.m_record_keys, *this); +} + +std::unique_ptr EncryptedDBBatch::GetNewPrefixCursor(Span prefix) +{ + return std::make_unique(m_database.m_record_keys, *this, prefix); +} + +EncryptedDBCursor::EncryptedDBCursor(const DecryptedRecordKeys& records, EncryptedDBBatch& batch, Span prefix) : m_batch(batch) +{ + std::tie(m_cursor, m_cursor_end) = records.equal_range(BytePrefix{prefix}); +} + +DatabaseCursor::Status EncryptedDBCursor::Next(DataStream& key, DataStream& value) +{ + if (m_cursor == m_cursor_end) { + return DatabaseCursor::Status::DONE; + } + key.clear(); + value.clear(); + + const auto& [key_data, enc_key] = *m_cursor; + key.write(key_data); + + if (!m_batch.ReadEncryptedKey(enc_key, value)) { + return DatabaseCursor::Status::FAIL; + } + m_cursor++; + return DatabaseCursor::Status::MORE; +} + +std::unique_ptr MakeEncryptedSQLiteDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error) +{ + std::unique_ptr backing_db = MakeSQLiteDatabase(path, options, status, error); + try { + auto db = std::make_unique(std::move(backing_db), options.db_passphrase, options.require_create); + status = DatabaseStatus::SUCCESS; + return db; + } catch (const std::runtime_error& e) { + status = DatabaseStatus::FAILED_LOAD; + error = Untranslated(e.what()); + return nullptr; + } +} + +} // namespace wallet diff --git a/src/wallet/encrypted_db.h b/src/wallet/encrypted_db.h new file mode 100644 index 00000000000..05f48ec2524 --- /dev/null +++ b/src/wallet/encrypted_db.h @@ -0,0 +1,127 @@ +// Copyright (c) 2023 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_WALLET_ENCRYPTED_DB_H +#define BITCOIN_WALLET_ENCRYPTED_DB_H + +#include +#include +#include +#include + +namespace wallet { +// Map of decrypted record key data to the encrypted record key data +// This allows us to get the actual db key data in order to lookups against the underlying db. +using DecryptedRecordKeys = std::map>; + +class EncryptedDatabase; +class EncryptedDBBatch; + +class EncryptedDBCursor : public DatabaseCursor +{ +public: + DecryptedRecordKeys::const_iterator m_cursor; + DecryptedRecordKeys::const_iterator m_cursor_end; + EncryptedDBBatch& m_batch; + + explicit EncryptedDBCursor(const DecryptedRecordKeys& records, EncryptedDBBatch& batch) : m_cursor(records.begin()), m_cursor_end(records.end()), m_batch(batch) {} + EncryptedDBCursor(const DecryptedRecordKeys& records, EncryptedDBBatch& batch, Span prefix); + ~EncryptedDBCursor() {} + + Status Next(DataStream& key, DataStream& value) override; +}; + +/** RAII class that provides access to a WalletDatabase */ +class EncryptedDBBatch : public DatabaseBatch +{ +private: + //! A DatabaseBatch for the db underlying the EncryptedDatabase + std::unique_ptr m_batch; + EncryptedDatabase& m_database; + + bool m_txn_started{false}; + std::vector> m_txn_writes; + std::vector m_txn_erases; + + bool ReadKey(DataStream&& key, DataStream& value) override; + bool WriteKey(DataStream&& key, DataStream&& value, bool overwrite = true) override; + bool EraseKey(DataStream&& key) override; + bool HasKey(DataStream&& key) override; + +public: + explicit EncryptedDBBatch(std::unique_ptr batch, EncryptedDatabase& database) : m_batch(std::move(batch)), m_database(database) {} + ~EncryptedDBBatch() {} + + void Flush() override { m_batch->Flush(); } + void Close() override; + + bool ErasePrefix(Span prefix) override; + + std::unique_ptr GetNewCursor() override; + std::unique_ptr GetNewPrefixCursor(Span prefix) override; + bool TxnBegin() override; + bool TxnCommit() override; + bool TxnAbort() override; + + bool ReadEncryptedKey(SerializeData enc_key, DataStream& value); +}; + +/** + * EncryptedDatabase encrypts and decrypts records as they are read and written from an underlying + * database. Most functions are simply passed through. + * An unencrypted copy of every record key is held in memory. This allows to lookup records by + * unencrypted record key. The value will be read from the underlying db and decrypted. + **/ +class EncryptedDatabase : public WalletDatabase +{ +private: + /** The underlying database */ + std::unique_ptr m_database; + + /** CCrypter which encrypts and decrypts the data */ + CCrypter m_crypter; + /** The key used to encrypt the records */ + CKeyingMaterial m_enc_secret; + +public: + /** The unencrypted record keys, using a data type with secure allocation */ + DecryptedRecordKeys m_record_keys; + + EncryptedDatabase() = delete; + + EncryptedDatabase(std::unique_ptr database, const SecureString& passphrase, bool create); + + ~EncryptedDatabase() {}; + + inline static const Span ENCRYPTION_RECORD = MakeByteSpan("encrypted_db_key"); + + util::Result DecryptRecordData(Span data); + util::Result EncryptRecordData(Span data); + + /** Open the database if it is not already opened. */ + void Open() override; + + std::string Format() override { return "encrypted_" + m_database->Format(); } + + /** Passthrough */ + void Close() override { m_database->Close(); } + void AddRef() override { m_database->AddRef() ;} + void RemoveRef() override { m_database->RemoveRef(); } + bool Rewrite(const char* pszSkip=nullptr) override { return m_database->Rewrite(pszSkip); } + bool Backup(const std::string& strDest) const override { return m_database->Backup(strDest); } + void Flush() override { m_database->Flush(); } + bool PeriodicFlush() override { return m_database->PeriodicFlush(); } + void IncrementUpdateCounter() override { m_database->IncrementUpdateCounter(); } + void ReloadDbEnv() override { m_database->ReloadDbEnv(); } + std::string Filename() override { return m_database->Filename(); } + + /** Make a DatabaseBatch connected to this database */ + std::unique_ptr MakeBatch(bool flush_on_close = true) override { return std::make_unique(std::move(m_database->MakeBatch(flush_on_close)), *this); } +}; + +std::unique_ptr MakeEncryptedSQLiteDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error); + +} // namespace wallet + +#endif // BITCOIN_WALLET_ENCRYPTED_DB_H diff --git a/src/wallet/test/db_tests.cpp b/src/wallet/test/db_tests.cpp index d341e84d9b5..934bd0b9c75 100644 --- a/src/wallet/test/db_tests.cpp +++ b/src/wallet/test/db_tests.cpp @@ -13,6 +13,7 @@ #endif #ifdef USE_SQLITE #include +#include #endif #include #include // for WALLET_FLAG_DESCRIPTORS @@ -126,6 +127,7 @@ static std::vector> TestDatabases(const fs::path { std::vector> dbs; DatabaseOptions options; + options.require_create = true; DatabaseStatus status; bilingual_str error; #ifdef USE_BDB @@ -133,6 +135,8 @@ static std::vector> TestDatabases(const fs::path #endif #ifdef USE_SQLITE dbs.emplace_back(MakeSQLiteDatabase(path_root / "sqlite", options, status, error)); + options.db_passphrase = "pass"; + dbs.emplace_back(MakeEncryptedSQLiteDatabase(path_root / "enc_sqlite", options, status, error)); #endif dbs.emplace_back(CreateMockableWalletDatabase()); return dbs; diff --git a/src/wallet/test/util.cpp b/src/wallet/test/util.cpp index 069ab25f260..a6ea87e1ec0 100644 --- a/src/wallet/test/util.cpp +++ b/src/wallet/test/util.cpp @@ -92,11 +92,6 @@ CTxDestination getNewDestination(CWallet& w, OutputType output_type) return *Assert(w.GetNewDestination(output_type, "")); } -// BytePrefix compares equality with other byte spans that begin with the same prefix. -struct BytePrefix { Span prefix; }; -bool operator<(BytePrefix a, Span b) { return a.prefix < b.subspan(0, std::min(a.prefix.size(), b.size())); } -bool operator<(Span a, BytePrefix b) { return a.subspan(0, std::min(a.size(), b.prefix.size())) < b.prefix; } - MockableCursor::MockableCursor(const MockableData& records, bool pass, Span prefix) { m_pass = pass; From 037e6f08ad16f7e49438939a70fccbaea3f4e98c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 17 Jul 2023 15:33:09 -0400 Subject: [PATCH 04/15] walletdb: Add WalletBatch::Read overload for DataStream It's useful to be able to just read a record without the batch doing any sort of deserialization. The new overload of Read will just place the record's value into the provided DataStream. --- src/wallet/db.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/wallet/db.h b/src/wallet/db.h index dac02ef166b..686c0bf3f7b 100644 --- a/src/wallet/db.h +++ b/src/wallet/db.h @@ -82,6 +82,16 @@ class DatabaseBatch } } + template + bool Read(const K& key, DataStream& value) + { + DataStream s_key{}; + s_key.reserve(1000); + s_key << key; + + return ReadKey(std::move(s_key), value); + } + template bool Write(const K& key, const T& value, bool fOverwrite = true) { From 29cbbbcd4ce1f6107f64179384676614e6752b68 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 17 Jul 2023 16:06:45 -0400 Subject: [PATCH 05/15] walletdb: Use a different SQLite application id for encrypted db EncrytpedDB wallets will use sqlite but with a different application id. This provides downgrade protection in addition to easy identification of encrypted dbs. The application id will be the network magic XOR'd with 0x36932d47 (randomly generated value). --- src/wallet/db.cpp | 37 +++++++++++++++++++++++++++++++++---- src/wallet/db.h | 3 +++ src/wallet/encrypted_db.cpp | 2 +- src/wallet/sqlite.cpp | 28 +++++++++++++++++++--------- src/wallet/sqlite.h | 6 ++++-- 5 files changed, 60 insertions(+), 16 deletions(-) diff --git a/src/wallet/db.cpp b/src/wallet/db.cpp index 2e9bf66656c..cb26f164869 100644 --- a/src/wallet/db.cpp +++ b/src/wallet/db.cpp @@ -39,7 +39,12 @@ std::vector ListDatabases(const fs::path& wallet_dir) const fs::path path{it->path().lexically_relative(wallet_dir)}; if (it->status().type() == fs::file_type::directory && - (IsBDBFile(BDBDataFile(it->path())) || IsSQLiteFile(SQLiteDataFile(it->path())))) { + ( + IsBDBFile(BDBDataFile(it->path())) + || IsSQLiteFile(SQLiteDataFile(it->path())) + || IsEncryptedSQLiteFile(SQLiteDataFile(it->path())) + ) + ) { // Found a directory which contains wallet.dat btree file, add it as a wallet. paths.emplace_back(path); } else if (it.depth() == 0 && it->symlink_status().type() == fs::file_type::regular && IsBDBFile(it->path())) { @@ -108,7 +113,7 @@ bool IsBDBFile(const fs::path& path) return data == 0x00053162 || data == 0x62310500; } -bool IsSQLiteFile(const fs::path& path) +static bool IsSQLiteFile(const fs::path& path, std::array id) { if (!fs::exists(path)) return false; @@ -138,8 +143,32 @@ bool IsSQLiteFile(const fs::path& path) return false; } - // Check the application id matches our network magic - return memcmp(Params().MessageStart(), app_id, 4) == 0; + // Check the application id matches our intended id + return memcmp(id.data(), app_id, 4) == 0; +} + +bool IsSQLiteFile(const fs::path& path) +{ + // For unencrypted files, application id is the network magic + std::array app_id = { + std::byte{Params().MessageStart()[0]}, + std::byte{Params().MessageStart()[1]}, + std::byte{Params().MessageStart()[2]}, + std::byte{Params().MessageStart()[3]}, + }; + return IsSQLiteFile(path, app_id); +} + +bool IsEncryptedSQLiteFile(const fs::path& path) +{ + // For encrypted files, application id is the network magic XOR'd with 36932d47 + std::array app_id = { + std::byte{Params().MessageStart()[0]} ^ ENCRYPTED_DB_XOR[0], + std::byte{Params().MessageStart()[1]} ^ ENCRYPTED_DB_XOR[1], + std::byte{Params().MessageStart()[2]} ^ ENCRYPTED_DB_XOR[2], + std::byte{Params().MessageStart()[3]} ^ ENCRYPTED_DB_XOR[3], + }; + return IsSQLiteFile(path, app_id); } void ReadDatabaseArgs(const ArgsManager& args, DatabaseOptions& options) diff --git a/src/wallet/db.h b/src/wallet/db.h index 686c0bf3f7b..3603cd00023 100644 --- a/src/wallet/db.h +++ b/src/wallet/db.h @@ -27,6 +27,8 @@ bool operator<(Span a, BytePrefix b); void SplitWalletPath(const fs::path& wallet_path, fs::path& env_directory, std::string& database_filename); +constexpr std::array ENCRYPTED_DB_XOR{std::byte{0x36}, std::byte{0x93}, std::byte{0x2d}, std::byte{0x47}}; + class DatabaseCursor { public: @@ -234,6 +236,7 @@ fs::path BDBDataFile(const fs::path& path); fs::path SQLiteDataFile(const fs::path& path); bool IsBDBFile(const fs::path& path); bool IsSQLiteFile(const fs::path& path); +bool IsEncryptedSQLiteFile(const fs::path& path); } // namespace wallet #endif // BITCOIN_WALLET_DB_H diff --git a/src/wallet/encrypted_db.cpp b/src/wallet/encrypted_db.cpp index df08e364803..cf6c22827bb 100644 --- a/src/wallet/encrypted_db.cpp +++ b/src/wallet/encrypted_db.cpp @@ -351,7 +351,7 @@ DatabaseCursor::Status EncryptedDBCursor::Next(DataStream& key, DataStream& valu std::unique_ptr MakeEncryptedSQLiteDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error) { - std::unique_ptr backing_db = MakeSQLiteDatabase(path, options, status, error); + std::unique_ptr backing_db = MakeSQLiteDatabase(path, options, status, error, ENCRYPTED_DB_XOR); try { auto db = std::make_unique(std::move(backing_db), options.db_passphrase, options.require_create); status = DatabaseStatus::SUCCESS; diff --git a/src/wallet/sqlite.cpp b/src/wallet/sqlite.cpp index ecd34bb96a6..2ce2d0c83fb 100644 --- a/src/wallet/sqlite.cpp +++ b/src/wallet/sqlite.cpp @@ -109,8 +109,18 @@ static void SetPragma(sqlite3* db, const std::string& key, const std::string& va Mutex SQLiteDatabase::g_sqlite_mutex; int SQLiteDatabase::g_sqlite_count = 0; -SQLiteDatabase::SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, const DatabaseOptions& options, bool mock) - : WalletDatabase(), m_mock(mock), m_dir_path(fs::PathToString(dir_path)), m_file_path(fs::PathToString(file_path)), m_use_unsafe_sync(options.use_unsafe_sync) +SQLiteDatabase::SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, const DatabaseOptions& options, bool mock, std::array app_id_xor) + : WalletDatabase(), + m_mock(mock), + m_dir_path(fs::PathToString(dir_path)), + m_file_path(fs::PathToString(file_path)), + m_app_id({ + std::byte{Params().MessageStart()[0]} ^ app_id_xor[0], + std::byte{Params().MessageStart()[1]} ^ app_id_xor[1], + std::byte{Params().MessageStart()[2]} ^ app_id_xor[2], + std::byte{Params().MessageStart()[3]} ^ app_id_xor[3], + }), + m_use_unsafe_sync(options.use_unsafe_sync) { { LOCK(g_sqlite_mutex); @@ -189,13 +199,13 @@ bool SQLiteDatabase::Verify(bilingual_str& error) { assert(m_db); - // Check the application ID matches our network magic + // Check the application ID matches the db's stored app_id auto read_result = ReadPragmaInteger(m_db, "application_id", "the application id", error); if (!read_result.has_value()) return false; uint32_t app_id = static_cast(read_result.value()); - uint32_t net_magic = ReadBE32(Params().MessageStart()); - if (app_id != net_magic) { - error = strprintf(_("SQLiteDatabase: Unexpected application id. Expected %u, got %u"), net_magic, app_id); + uint32_t magic = ReadBE32(reinterpret_cast(m_app_id.data())); + if (app_id != magic) { + error = strprintf(_("SQLiteDatabase: Unexpected application id. Expected %u, got %u"), magic, app_id); return false; } @@ -324,7 +334,7 @@ void SQLiteDatabase::Open() } // Set the application id - uint32_t app_id = ReadBE32(Params().MessageStart()); + uint32_t app_id = ReadBE32(reinterpret_cast(m_app_id.data())); SetPragma(m_db, "application_id", strprintf("%d", static_cast(app_id)), "Failed to set the application id"); @@ -634,11 +644,11 @@ bool SQLiteBatch::TxnAbort() return res == SQLITE_OK; } -std::unique_ptr MakeSQLiteDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error) +std::unique_ptr MakeSQLiteDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::array app_id_xor) { try { fs::path data_file = SQLiteDataFile(path); - auto db = std::make_unique(data_file.parent_path(), data_file, options); + auto db = std::make_unique(data_file.parent_path(), data_file, options, /*mock=*/false, app_id_xor); if (options.verify && !db->Verify(error)) { status = DatabaseStatus::FAILED_VERIFY; return nullptr; diff --git a/src/wallet/sqlite.h b/src/wallet/sqlite.h index f1ce0567e18..69f8775d3cb 100644 --- a/src/wallet/sqlite.h +++ b/src/wallet/sqlite.h @@ -84,6 +84,8 @@ class SQLiteDatabase : public WalletDatabase const std::string m_file_path; + std::array m_app_id; + /** * This mutex protects SQLite initialization and shutdown. * sqlite3_config() and sqlite3_shutdown() are not thread-safe (sqlite3_initialize() is). @@ -99,7 +101,7 @@ class SQLiteDatabase : public WalletDatabase SQLiteDatabase() = delete; /** Create DB handle to real database */ - SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, const DatabaseOptions& options, bool mock = false); + SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, const DatabaseOptions& options, bool mock = false, std::array app_id_xor = {std::byte{0}, std::byte{0}, std::byte{0}, std::byte{0}}); ~SQLiteDatabase(); @@ -146,7 +148,7 @@ class SQLiteDatabase : public WalletDatabase bool m_use_unsafe_sync; }; -std::unique_ptr MakeSQLiteDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error); +std::unique_ptr MakeSQLiteDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::array app_id_xor = {std::byte{0}, std::byte{0}, std::byte{0}, std::byte{0}}); std::string SQLiteDatabaseVersion(); } // namespace wallet From 25682776581ed5fa76f1aa9d985963920d9f6363 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 17 Jul 2023 17:05:33 -0400 Subject: [PATCH 06/15] walletdb: Have MakeDatabase also create encrypted dbs --- src/wallet/db.h | 1 + src/wallet/walletdb.cpp | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/wallet/db.h b/src/wallet/db.h index 3603cd00023..a15d7c34337 100644 --- a/src/wallet/db.h +++ b/src/wallet/db.h @@ -195,6 +195,7 @@ class WalletDatabase enum class DatabaseFormat { BERKELEY, SQLITE, + ENCRYPTED_SQLITE, }; struct DatabaseOptions { diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 8212c044649..2ed696b9fad 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -19,6 +19,7 @@ #include #endif #ifdef USE_SQLITE +#include #include #endif #include @@ -1457,6 +1458,19 @@ std::unique_ptr MakeDatabase(const fs::path& path, const Databas } format = DatabaseFormat::SQLITE; } + if (IsEncryptedSQLiteFile(SQLiteDataFile(path))) { + if (format) { + error = Untranslated(strprintf("Failed to load database path '%s'. Data is in ambiguous format.", fs::PathToString(path))); + status = DatabaseStatus::FAILED_BAD_FORMAT; + return nullptr; + } + format = DatabaseFormat::ENCRYPTED_SQLITE; + if (options.db_passphrase.empty()) { + error = Untranslated(strprintf("Unable to load database '%s'. Database is encrypted but passphrase was not provided.", fs::PathToString(path))); + status = DatabaseStatus::FAILED_ENCRYPT; + return nullptr; + } + } } else if (options.require_existing) { error = Untranslated(strprintf("Failed to load database path '%s'. Path does not exist.", fs::PathToString(path))); status = DatabaseStatus::FAILED_NOT_FOUND; @@ -1489,15 +1503,22 @@ std::unique_ptr MakeDatabase(const fs::path& path, const Databas if (!format) { #ifdef USE_SQLITE format = DatabaseFormat::SQLITE; + if (!options.db_passphrase.empty()) { + format = DatabaseFormat::ENCRYPTED_SQLITE; + } #endif #ifdef USE_BDB format = DatabaseFormat::BERKELEY; #endif } - if (format == DatabaseFormat::SQLITE) { + if (format == DatabaseFormat::SQLITE || format == DatabaseFormat::ENCRYPTED_SQLITE) { #ifdef USE_SQLITE - return MakeSQLiteDatabase(path, options, status, error); + if (format == DatabaseFormat::SQLITE) { + return MakeSQLiteDatabase(path, options, status, error); + } else if (format == DatabaseFormat::ENCRYPTED_SQLITE) { + return MakeEncryptedSQLiteDatabase(path, options, status, error); + } #endif error = Untranslated(strprintf("Failed to open database path '%s'. Build does not support SQLite database format.", fs::PathToString(path))); status = DatabaseStatus::FAILED_BAD_FORMAT; From 4c7df1f96aa91f355726bdd3edae680894af2f97 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 17 Jul 2023 17:05:57 -0400 Subject: [PATCH 07/15] wallet, rpc: Be able to create and load wallets with encrypted dbs --- src/wallet/rpc/wallet.cpp | 20 +++++++++++++++++++- src/wallet/wallet.cpp | 12 +++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index fb4b642bbab..efd4c3a1972 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -216,6 +216,7 @@ static RPCHelpMan loadwallet() { {"filename", RPCArg::Type::STR, RPCArg::Optional::NO, "The wallet directory or .dat file."}, {"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged."}, + {"db_passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Passphrase for the wallet database if the database is encrypted"}, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -244,6 +245,11 @@ static RPCHelpMan loadwallet() std::vector warnings; std::optional load_on_start = request.params[1].isNull() ? std::nullopt : std::optional(request.params[1].get_bool()); + options.db_passphrase.reserve(100); + if (!request.params[2].isNull()) { + options.db_passphrase = std::string_view{request.params[2].get_str()}; + } + { LOCK(context.wallets_mutex); if (std::any_of(context.wallets.begin(), context.wallets.end(), [&name](const auto& wallet) { return wallet->GetName() == name; })) { @@ -340,13 +346,14 @@ static RPCHelpMan createwallet() {"wallet_name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name for the new wallet. If this is a path, the wallet will be created at the path location."}, {"disable_private_keys", RPCArg::Type::BOOL, RPCArg::Default{false}, "Disable the possibility of private keys (only watchonlys are possible in this mode)."}, {"blank", RPCArg::Type::BOOL, RPCArg::Default{false}, "Create a blank wallet. A blank wallet has no keys or HD seed. One can be set using sethdseed."}, - {"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Encrypt the wallet with this passphrase."}, + {"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Encrypt the keys stored in this wallet with this passphrase."}, {"avoid_reuse", RPCArg::Type::BOOL, RPCArg::Default{false}, "Keep track of coin reuse, and treat dirty and clean coins differently with privacy considerations in mind."}, {"descriptors", RPCArg::Type::BOOL, RPCArg::Default{true}, "Create a native descriptor wallet. The wallet will use descriptors internally to handle address creation." " Setting to \"false\" will create a legacy wallet; however, the legacy wallet type is being deprecated and" " support for creating and opening legacy wallets will be removed in the future."}, {"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged."}, {"external_signer", RPCArg::Type::BOOL, RPCArg::Default{false}, "Use an external signer such as a hardware wallet. Requires -signer to be configured. Wallet creation will fail if keys cannot be fetched. Requires disable_private_keys and descriptors set to true."}, + {"db_passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Encrypt the entire wallet database with this passphrase."}, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -409,12 +416,23 @@ static RPCHelpMan createwallet() } #endif + SecureString db_passphrase; + db_passphrase.reserve(100); + if (!request.params[8].isNull()) { + db_passphrase = std::string_view{request.params[8].get_str()}; + if (db_passphrase.empty()) { + // Empty string means unencrypted + warnings.emplace_back(Untranslated("Empty string given as database passphrase, wallet database will not be encrypted.")); + } + } + DatabaseOptions options; DatabaseStatus status; ReadDatabaseArgs(*context.args, options); options.require_create = true; options.create_flags = flags; options.create_passphrase = passphrase; + options.db_passphrase = db_passphrase; bilingual_str error; std::optional load_on_start = request.params[6].isNull() ? std::nullopt : std::optional(request.params[6].get_bool()); const std::shared_ptr wallet = CreateWallet(context, request.params[0].get_str(), load_on_start, options, status, error, warnings); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 8fa93b97d65..b59021927d7 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -342,7 +342,17 @@ std::shared_ptr CreateWallet(WalletContext& context, const std::string& uint64_t wallet_creation_flags = options.create_flags; const SecureString& passphrase = options.create_passphrase; - if (wallet_creation_flags & WALLET_FLAG_DESCRIPTORS) options.require_format = DatabaseFormat::SQLITE; + if (wallet_creation_flags & WALLET_FLAG_DESCRIPTORS) { + if (!options.db_passphrase.empty()) { + options.require_format = DatabaseFormat::ENCRYPTED_SQLITE; + } else { + options.require_format = DatabaseFormat::SQLITE; + } + } else if (!options.db_passphrase.empty()) { + error = Untranslated("Database encryption is only supported for descriptor wallets"); + status = DatabaseStatus::FAILED_CREATE; + return nullptr; + } // Indicate that the wallet is actually supposed to be blank and not just blank to make it encrypted bool create_blank = (wallet_creation_flags & WALLET_FLAG_BLANK_WALLET); From 0a38dc84ab452952b821c4a1b990801fc9d1aabb Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 24 Jul 2023 14:19:24 -0400 Subject: [PATCH 08/15] wallet: Skip loading on start of wallets with encrypted databases Wallets with encrypted databases need the user to provide their database passphrase which cannot be done on start, so skip any such wallets on startup. --- src/wallet/load.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wallet/load.cpp b/src/wallet/load.cpp index 4cdfadbee27..0dd286c95bb 100644 --- a/src/wallet/load.cpp +++ b/src/wallet/load.cpp @@ -92,6 +92,8 @@ bool VerifyWallets(WalletContext& context) if (!MakeWalletDatabase(wallet_file, options, status, error_string)) { if (status == DatabaseStatus::FAILED_NOT_FOUND) { chain.initWarning(Untranslated(strprintf("Skipping -wallet path that doesn't exist. %s", error_string.original))); + } else if (status == DatabaseStatus::FAILED_ENCRYPT) { + chain.initWarning(Untranslated(strprintf("Skipping -wallet path to encrypted wallet, use loadwallet to load it. %s", error_string.original))); } else { chain.initError(error_string); return false; @@ -120,7 +122,7 @@ bool LoadWallets(WalletContext& context) bilingual_str error; std::vector warnings; std::unique_ptr database = MakeWalletDatabase(name, options, status, error); - if (!database && status == DatabaseStatus::FAILED_NOT_FOUND) { + if (!database && (status == DatabaseStatus::FAILED_NOT_FOUND || status == DatabaseStatus::FAILED_ENCRYPT)) { continue; } chain.initMessage(_("Loading wallet…").translated); From d97c315f6dd0b6eabb2fd25f76ecb1ebe909b646 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 24 Jul 2023 15:19:27 -0400 Subject: [PATCH 09/15] test: Add functional test for wallets with encrypted db --- test/functional/test_framework/test_node.py | 4 +- test/functional/test_runner.py | 1 + test/functional/wallet_createwallet.py | 8 +- test/functional/wallet_db_encryption.py | 159 ++++++++++++++++++++ 4 files changed, 167 insertions(+), 5 deletions(-) create mode 100755 test/functional/wallet_db_encryption.py diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index 1fcef6ce1c8..448a1d1bef4 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -789,10 +789,10 @@ def __getattr__(self, name): def createwallet_passthrough(self, *args, **kwargs): return self.__getattr__("createwallet")(*args, **kwargs) - def createwallet(self, wallet_name, disable_private_keys=None, blank=None, passphrase='', avoid_reuse=None, descriptors=None, load_on_startup=None, external_signer=None): + def createwallet(self, wallet_name, disable_private_keys=None, blank=None, passphrase='', avoid_reuse=None, descriptors=None, load_on_startup=None, external_signer=None, db_passphrase=''): if descriptors is None: descriptors = self.descriptors - return self.__getattr__('createwallet')(wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup, external_signer) + return self.__getattr__('createwallet')(wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup, external_signer, db_passphrase) def importprivkey(self, privkey, label=None, rescan=None): wallet_info = self.getwalletinfo() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 9762476a5d6..689e422603a 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -292,6 +292,7 @@ 'p2p_leak.py', 'wallet_encryption.py --legacy-wallet', 'wallet_encryption.py --descriptors', + 'wallet_db_encryption.py --descriptors', 'feature_dersig.py', 'feature_cltv.py', 'rpc_uptime.py', diff --git a/test/functional/wallet_createwallet.py b/test/functional/wallet_createwallet.py index 75b507c3875..417dc375e77 100755 --- a/test/functional/wallet_createwallet.py +++ b/test/functional/wallet_createwallet.py @@ -16,6 +16,8 @@ EMPTY_PASSPHRASE_MSG = "Empty string given as passphrase, wallet will not be encrypted." +EMPTY_DB_PASSPHRASE_MSG = "Empty string given as database passphrase, wallet database will not be encrypted." +EMPTY_PASSPHRASE_MSGS = [EMPTY_PASSPHRASE_MSG, EMPTY_DB_PASSPHRASE_MSG] LEGACY_WALLET_MSG = "Wallet created successfully. The legacy wallet type is being deprecated and support for creating and opening legacy wallets will be removed in the future." @@ -161,7 +163,7 @@ def run_test(self): assert_equal(walletinfo['keypoolsize_hd_internal'], keys) # Allow empty passphrase, but there should be a warning resp = self.nodes[0].createwallet(wallet_name='w7', disable_private_keys=False, blank=False, passphrase='') - assert_equal(resp["warnings"], [EMPTY_PASSPHRASE_MSG] if self.options.descriptors else [EMPTY_PASSPHRASE_MSG, LEGACY_WALLET_MSG]) + assert_equal(resp["warnings"], EMPTY_PASSPHRASE_MSGS if self.options.descriptors else EMPTY_PASSPHRASE_MSGS + [LEGACY_WALLET_MSG]) w7 = node.get_wallet_rpc('w7') assert_raises_rpc_error(-15, 'Error: running with an unencrypted wallet, but walletpassphrase was called.', w7.walletpassphrase, '', 60) @@ -180,12 +182,12 @@ def run_test(self): result = self.nodes[0].createwallet(wallet_name="legacy_w0", descriptors=False, passphrase=None) assert_equal(result, { "name": "legacy_w0", - "warnings": [LEGACY_WALLET_MSG], + "warnings": [EMPTY_DB_PASSPHRASE_MSG, LEGACY_WALLET_MSG], }) result = self.nodes[0].createwallet(wallet_name="legacy_w1", descriptors=False, passphrase="") assert_equal(result, { "name": "legacy_w1", - "warnings": [EMPTY_PASSPHRASE_MSG, LEGACY_WALLET_MSG], + "warnings": EMPTY_PASSPHRASE_MSGS + [LEGACY_WALLET_MSG], }) diff --git a/test/functional/wallet_db_encryption.py b/test/functional/wallet_db_encryption.py new file mode 100755 index 00000000000..07fa8c8e26e --- /dev/null +++ b/test/functional/wallet_db_encryption.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php. +"""Test Wallets with encrypted database""" + +import subprocess + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_raises_rpc_error, + assert_equal, + assert_greater_than, +) + + +class WalletDBEncryptionTest(BitcoinTestFramework): + PASSPHRASE = "WalletPassphrase" + PASSPHRASE2 = "SecondWalletPassphrase" + WRONG_PASSPHRASE = "NotTheRightPassphrase" + PASSPHRASE_WITH_NULLS = "Passphrase\0With\0Nulls" + + def add_options(self, parser): + self.add_wallet_options(parser, descriptors=True) + + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def test_no_legacy(self): + if not self.is_bdb_compiled(): + return + self.log.info("Test that legacy wallets do not support encrypted databases") + assert_raises_rpc_error(-4, "Database encryption is only supported for descriptor wallets", self.nodes[0].createwallet, wallet_name="legacy_encdb", db_passphrase=self.PASSPHRASE, descriptors=False) + + def test_create_and_load(self): + self.log.info("Testing that a wallet with encrypted database can be created") + self.nodes[0].createwallet(wallet_name="basic_encrypted_db", db_passphrase=self.PASSPHRASE) + wallet = self.nodes[0].get_wallet_rpc("basic_encrypted_db") + info = wallet.getwalletinfo() + assert_equal("encrypted_sqlite", info["format"]) + + # Add some data to the wallet that we should see persisted + addr = wallet.getnewaddress() + txid_in = self.default_wallet.sendtoaddress(addr, 10) + self.generate(self.nodes[0], 1) + txid_out = wallet.sendtoaddress(self.default_wallet.getnewaddress(), 5) + self.generate(self.nodes[0], 1) + + wallet.unloadwallet() + + self.log.info("Testing loading of a wallet with encrypted database") + assert_raises_rpc_error(-4, "Database is encrypted but passphrase was not provided", self.nodes[0].loadwallet, filename="basic_encrypted_db") + assert_raises_rpc_error(-4, "Unable to decrypt database, are you sure the passphrase is correct?", self.nodes[0].loadwallet, filename="basic_encrypted_db", db_passphrase=self.WRONG_PASSPHRASE) + self.nodes[0].loadwallet(filename="basic_encrypted_db", db_passphrase=self.PASSPHRASE) + info = wallet.getwalletinfo() + assert_equal("encrypted_sqlite", info["format"]) + + # Make sure that our presisted data is still here + addr_info = wallet.getaddressinfo(addr) + assert_equal(addr_info["ismine"], True) + tx_in = wallet.gettransaction(txid_in) + assert_equal(tx_in["amount"], 10) + tx_out = wallet.gettransaction(txid_out) + assert_equal(tx_out["amount"], -5) + wallet.sendtoaddress(self.default_wallet.getnewaddress(), 2) + + wallet.unloadwallet() + + self.log.info("Test that listwalletdir lists wallets with encrypted dbs") + wallets = [w["name"] for w in self.nodes[0].listwalletdir()["wallets"]] + assert "basic_encrypted_db" in wallets + + def test_passphrases_with_nulls(self): + self.log.info("Testing passphrases with nulls for wallets with encrypted databases") + self.nodes[0].createwallet(wallet_name="encdb_with_nulls", db_passphrase=self.PASSPHRASE_WITH_NULLS) + wallet = self.nodes[0].get_wallet_rpc("encdb_with_nulls") + info = wallet.getwalletinfo() + assert_equal("encrypted_sqlite", info["format"]) + wallet.unloadwallet() + + assert_raises_rpc_error(-4, "Unable to decrypt database, are you sure the passphrase is correct?", self.nodes[0].loadwallet, filename="encdb_with_nulls", db_passphrase=self.PASSPHRASE_WITH_NULLS.partition("\0")[0]) + self.nodes[0].loadwallet(filename="encdb_with_nulls", db_passphrase=self.PASSPHRASE_WITH_NULLS) + + def test_on_start(self): + self.log.info("Test that wallets with encrypted db are ignored on startup") + self.nodes[0].createwallet(wallet_name="startup_encdb", db_passphrase=self.PASSPHRASE) + with self.nodes[0].assert_debug_log(expected_msgs=["Warning: Skipping -wallet path to encrypted wallet, use loadwallet to load it."]): + self.stop_node(0) + self.start_node(0, extra_args=["-wallet=startup_encdb"]) + # Need to clear stderr file so that test shutdown works + self.nodes[0].stderr.seek(0) + self.nodes[0].stderr.truncate(0) + assert_equal(self.nodes[0].listwallets(), [self.default_wallet_name]) + self.nodes[0].loadwallet(filename="startup_encdb", db_passphrase=self.PASSPHRASE) + + def test_double_encrypted(self): + self.log.info("Test that wallet encryption is not db encryption") + self.nodes[0].createwallet(wallet_name="enc_wallet_not_db", passphrase=self.PASSPHRASE) + wallet = self.nodes[0].get_wallet_rpc("enc_wallet_not_db") + info = wallet.getwalletinfo() + assert_equal(info["format"], "sqlite") + assert_equal(info["unlocked_until"], 0) + + self.nodes[0].createwallet(wallet_name="enc_wallet_not_db2") + wallet = self.nodes[0].get_wallet_rpc("enc_wallet_not_db2") + wallet.encryptwallet(self.PASSPHRASE) + info = wallet.getwalletinfo() + assert_equal(info["format"], "sqlite") + assert_equal(info["unlocked_until"], 0) + + self.log.info("Test that wallets with encrypted db can also be encrypted normally") + self.nodes[0].createwallet(wallet_name="double_enc", db_passphrase=self.PASSPHRASE, passphrase=self.PASSPHRASE2) + wallet = self.nodes[0].get_wallet_rpc("double_enc") + info = wallet.getwalletinfo() + assert_equal(info["format"], "encrypted_sqlite") + assert_equal(info["unlocked_until"], 0) + wallet.walletpassphrase(self.PASSPHRASE2, 10) + assert_greater_than(wallet.getwalletinfo()["unlocked_until"], 0) + wallet.walletlock() + + self.nodes[0].createwallet(wallet_name="double_enc2", db_passphrase=self.PASSPHRASE) + wallet = self.nodes[0].get_wallet_rpc("double_enc2") + wallet.encryptwallet(self.PASSPHRASE2) + info = wallet.getwalletinfo() + assert_equal(info["format"], "encrypted_sqlite") + assert_equal(info["unlocked_until"], 0) + wallet.walletpassphrase(self.PASSPHRASE2, 10) + assert_greater_than(wallet.getwalletinfo()["unlocked_until"], 0) + wallet.walletlock() + + self.log.info("Test that changing wallet passphrase does not affect database passphrase") + wallet.walletpassphrase(self.PASSPHRASE2, 10) + wallet.walletpassphrasechange(self.PASSPHRASE2, self.PASSPHRASE_WITH_NULLS) + wallet.walletlock() + assert_raises_rpc_error(-14, "Error: The wallet passphrase entered was incorrect.", wallet.walletpassphrase, self.PASSPHRASE, 10) + assert_raises_rpc_error(-14, "Error: The wallet passphrase entered was incorrect.", wallet.walletpassphrase, self.PASSPHRASE2, 10) + wallet.walletpassphrase(self.PASSPHRASE_WITH_NULLS, 10) + wallet.unloadwallet() + + assert_raises_rpc_error(-4, "Wallet file verification failed. Unable to decrypt database, are you sure the passphrase is correct?", self.nodes[0].loadwallet, filename="double_enc2", db_passphrase=self.PASSPHRASE_WITH_NULLS) + assert_raises_rpc_error(-4, "Wallet file verification failed. Unable to decrypt database, are you sure the passphrase is correct?", self.nodes[0].loadwallet, filename="double_enc2", db_passphrase=self.PASSPHRASE2) + self.nodes[0].loadwallet(filename="double_enc2", db_passphrase=self.PASSPHRASE) + + def run_test(self): + self.default_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + self.generate(self.nodes[0], 101) + + self.test_no_legacy() + self.test_create_and_load() + self.test_passphrases_with_nulls() + self.test_on_start() + self.test_double_encrypted() + +if __name__ == '__main__': + WalletDBEncryptionTest().main() From 854ab2e8b2df15d04d36006d7d7be6458ead4918 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 24 Jul 2023 16:06:50 -0400 Subject: [PATCH 10/15] gui: Allow AskPassphraseDialog without WalletModel Sometimes we just need the dialog without an attached wallet. --- src/qt/askpassphrasedialog.cpp | 43 +++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/src/qt/askpassphrasedialog.cpp b/src/qt/askpassphrasedialog.cpp index 0a96be038b2..a8707171742 100644 --- a/src/qt/askpassphrasedialog.cpp +++ b/src/qt/askpassphrasedialog.cpp @@ -84,8 +84,6 @@ void AskPassphraseDialog::setModel(WalletModel *_model) void AskPassphraseDialog::accept() { SecureString oldpass, newpass1, newpass2; - if (!model && mode != Encrypt) - return; oldpass.reserve(MAX_PASSPHRASE_SIZE); newpass1.reserve(MAX_PASSPHRASE_SIZE); newpass2.reserve(MAX_PASSPHRASE_SIZE); @@ -151,29 +149,36 @@ void AskPassphraseDialog::accept() } } break; case Unlock: - try { - if (!model->setWalletLocked(false, oldpass)) { - // Check if the passphrase has a null character (see #27067 for details) - if (oldpass.find('\0') == std::string::npos) { - QMessageBox::critical(this, tr("Wallet unlock failed"), - tr("The passphrase entered for the wallet decryption was incorrect.")); + if (m_passphrase_out) { + m_passphrase_out->assign(oldpass); + QDialog::accept(); // Success + } else { + try { + assert(model); + if (!model->setWalletLocked(false, oldpass)) { + // Check if the passphrase has a null character (see #27067 for details) + if (oldpass.find('\0') == std::string::npos) { + QMessageBox::critical(this, tr("Wallet unlock failed"), + tr("The passphrase entered for the wallet decryption was incorrect.")); + } else { + QMessageBox::critical(this, tr("Wallet unlock failed"), + tr("The passphrase entered for the wallet decryption is incorrect. " + "It contains a null character (ie - a zero byte). " + "If the passphrase was set with a version of this software prior to 25.0, " + "please try again with only the characters up to — but not including — " + "the first null character. If this is successful, please set a new " + "passphrase to avoid this issue in the future.")); + } } else { - QMessageBox::critical(this, tr("Wallet unlock failed"), - tr("The passphrase entered for the wallet decryption is incorrect. " - "It contains a null character (ie - a zero byte). " - "If the passphrase was set with a version of this software prior to 25.0, " - "please try again with only the characters up to — but not including — " - "the first null character. If this is successful, please set a new " - "passphrase to avoid this issue in the future.")); + QDialog::accept(); // Success } - } else { - QDialog::accept(); // Success + } catch (const std::runtime_error& e) { + QMessageBox::critical(this, tr("Wallet unlock failed"), e.what()); } - } catch (const std::runtime_error& e) { - QMessageBox::critical(this, tr("Wallet unlock failed"), e.what()); } break; case ChangePass: + assert(model); if(newpass1 == newpass2) { if(model->changePassphrase(oldpass, newpass1)) From a85e428c629b31b791fe3b748aa5e34bbdb1ffac Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 24 Jul 2023 16:07:35 -0400 Subject: [PATCH 11/15] interfaces: Allow opening and detecting wallets with encrypted db --- src/interfaces/wallet.h | 5 ++++- src/qt/walletcontroller.cpp | 2 +- src/wallet/interfaces.cpp | 7 ++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 8c31112fc94..d61c3f6714e 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -324,7 +324,7 @@ class WalletLoader : public ChainClient virtual util::Result> createWallet(const std::string& name, const SecureString& passphrase, uint64_t wallet_creation_flags, std::vector& warnings) = 0; //! Load existing wallet. - virtual util::Result> loadWallet(const std::string& name, std::vector& warnings) = 0; + virtual util::Result> loadWallet(const std::string& name, std::vector& warnings, const SecureString& db_passphrase) = 0; //! Return default wallet directory. virtual std::string getWalletDir() = 0; @@ -344,6 +344,9 @@ class WalletLoader : public ChainClient using LoadWalletFn = std::function wallet)>; virtual std::unique_ptr handleLoadWallet(LoadWalletFn fn) = 0; + //! Return whether the named wallet has an encrypted database + virtual bool isWalletDBEncrypted(const std::string& name) = 0; + //! Return pointer to internal context, useful for testing. virtual wallet::WalletContext* context() { return nullptr; } }; diff --git a/src/qt/walletcontroller.cpp b/src/qt/walletcontroller.cpp index d782838d6ff..9f900d92110 100644 --- a/src/qt/walletcontroller.cpp +++ b/src/qt/walletcontroller.cpp @@ -351,7 +351,7 @@ void OpenWalletActivity::open(const std::string& path) tr("Opening Wallet %1…").arg(name.toHtmlEscaped())); QTimer::singleShot(0, worker(), [this, path] { - auto wallet{node().walletLoader().loadWallet(path, m_warning_message)}; + auto wallet{node().walletLoader().loadWallet(path, m_warning_message, m_db_passphrase)}; if (wallet) { m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(*wallet)); diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index cd438cfe2f3..3b4ff3556c8 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -606,12 +606,13 @@ class WalletLoaderImpl : public WalletLoader return util::Error{error}; } } - util::Result> loadWallet(const std::string& name, std::vector& warnings) override + util::Result> loadWallet(const std::string& name, std::vector& warnings, const SecureString& db_passphrase) override { DatabaseOptions options; DatabaseStatus status; ReadDatabaseArgs(*m_context.args, options); options.require_existing = true; + options.db_passphrase = db_passphrase; bilingual_str error; std::unique_ptr wallet{MakeWallet(m_context, LoadWallet(m_context, name, /*load_on_start=*/true, options, status, error, warnings))}; if (wallet) { @@ -656,6 +657,10 @@ class WalletLoaderImpl : public WalletLoader return HandleLoadWallet(m_context, std::move(fn)); } WalletContext* context() override { return &m_context; } + bool isWalletDBEncrypted(const std::string& name) override + { + return IsEncryptedSQLiteFile(SQLiteDataFile(GetWalletDir() / fs::PathFromString(name))); + } WalletContext m_context; const std::vector m_wallet_filenames; From a6212f95a923b972688b3cde36cc9f8ac4e9cc33 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 24 Jul 2023 16:07:50 -0400 Subject: [PATCH 12/15] gui: Be able to open wallets with encrypted dbs --- src/qt/walletcontroller.cpp | 28 +++++++++++++++++++++++++++- src/qt/walletcontroller.h | 5 +++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/qt/walletcontroller.cpp b/src/qt/walletcontroller.cpp index 9f900d92110..1b14bf3469e 100644 --- a/src/qt/walletcontroller.cpp +++ b/src/qt/walletcontroller.cpp @@ -339,7 +339,33 @@ void OpenWalletActivity::finish() Q_EMIT finished(); } -void OpenWalletActivity::open(const std::string& path) +void OpenWalletActivity::askPassphrase(const std::string& name) +{ + m_passphrase_dialog = new AskPassphraseDialog(AskPassphraseDialog::Unlock, m_parent_widget, &m_db_passphrase); + m_passphrase_dialog->setWindowModality(Qt::ApplicationModal); + m_passphrase_dialog->show(); + + connect(m_passphrase_dialog, &QObject::destroyed, [this] { + m_passphrase_dialog = nullptr; + }); + connect(m_passphrase_dialog, &QDialog::accepted, [this, &name] { + openWallet(name); + }); + connect(m_passphrase_dialog, &QDialog::rejected, [this] { + Q_EMIT finished(); + }); +} + +void OpenWalletActivity::open(const std::string& name) +{ + if (node().walletLoader().isWalletDBEncrypted(name)) { + askPassphrase(name); + } else { + openWallet(name); + } +} + +void OpenWalletActivity::openWallet(const std::string& path) { QString name = path.empty() ? QString("["+tr("default wallet")+"]") : QString::fromStdString(path); diff --git a/src/qt/walletcontroller.h b/src/qt/walletcontroller.h index fcd65756c67..48a580a99ef 100644 --- a/src/qt/walletcontroller.h +++ b/src/qt/walletcontroller.h @@ -147,6 +147,11 @@ class OpenWalletActivity : public WalletControllerActivity private: void finish(); + void openWallet(const std::string& path); + void askPassphrase(const std::string& name); + + SecureString m_db_passphrase; + AskPassphraseDialog* m_passphrase_dialog{nullptr}; }; class LoadWalletsActivity : public WalletControllerActivity From 5506eeaf32426a09133353962b87596eb4b134e0 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 24 Jul 2023 16:56:28 -0400 Subject: [PATCH 13/15] gui: Allow AskPassphraseDialog to have customized warning text The warning text that appears above the passphrase inputs may need to be customized depending on the context in which the passphrase is being entered, so let the caller optionally set it. --- src/qt/askpassphrasedialog.cpp | 19 +++++++++++++++---- src/qt/askpassphrasedialog.h | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/qt/askpassphrasedialog.cpp b/src/qt/askpassphrasedialog.cpp index a8707171742..606cec59e3a 100644 --- a/src/qt/askpassphrasedialog.cpp +++ b/src/qt/askpassphrasedialog.cpp @@ -19,7 +19,7 @@ #include #include -AskPassphraseDialog::AskPassphraseDialog(Mode _mode, QWidget *parent, SecureString* passphrase_out) : +AskPassphraseDialog::AskPassphraseDialog(Mode _mode, QWidget *parent, SecureString* passphrase_out, QString warning_text) : QDialog(parent, GUIUtil::dialog_flags), ui(new Ui::AskPassphraseDialog), mode(_mode), @@ -43,13 +43,20 @@ AskPassphraseDialog::AskPassphraseDialog(Mode _mode, QWidget *parent, SecureStri switch(mode) { case Encrypt: // Ask passphrase x2 - ui->warningLabel->setText(tr("Enter the new passphrase for the wallet.
Please use a passphrase of ten or more random characters, or eight or more words.")); + if (warning_text.isEmpty()) { + warning_text = tr("Enter the new passphrase for encrypting the private keys in the wallet."); + } + ui->warningLabel->setText(warning_text + tr("
Please use a passphrase of ten or more random characters, or eight or more words.")); ui->passLabel1->hide(); ui->passEdit1->hide(); setWindowTitle(tr("Encrypt wallet")); break; case Unlock: // Ask passphrase - ui->warningLabel->setText(tr("This operation needs your wallet passphrase to unlock the wallet.")); + if (warning_text.isEmpty()) { + ui->warningLabel->setText(tr("This operation needs your wallet passphrase to unlock the wallet.")); + } else { + ui->warningLabel->setText(warning_text); + } ui->passLabel2->hide(); ui->passEdit2->hide(); ui->passLabel3->hide(); @@ -58,7 +65,11 @@ AskPassphraseDialog::AskPassphraseDialog(Mode _mode, QWidget *parent, SecureStri break; case ChangePass: // Ask old passphrase + new passphrase x2 setWindowTitle(tr("Change passphrase")); - ui->warningLabel->setText(tr("Enter the old passphrase and new passphrase for the wallet.")); + if (warning_text.isEmpty()) { + ui->warningLabel->setText(tr("Enter the old passphrase and new passphrase for the wallet.")); + } else { + ui->warningLabel->setText(warning_text); + } break; } textChanged(); diff --git a/src/qt/askpassphrasedialog.h b/src/qt/askpassphrasedialog.h index 370ea1de7ec..b9d56609189 100644 --- a/src/qt/askpassphrasedialog.h +++ b/src/qt/askpassphrasedialog.h @@ -28,7 +28,7 @@ class AskPassphraseDialog : public QDialog ChangePass, /**< Ask old passphrase + new passphrase twice */ }; - explicit AskPassphraseDialog(Mode mode, QWidget *parent, SecureString* passphrase_out = nullptr); + explicit AskPassphraseDialog(Mode mode, QWidget *parent, SecureString* passphrase_out = nullptr, QString warning_text = ""); ~AskPassphraseDialog(); void accept() override; From 33cceb91068dd34565acf54b836e548d5f2ad572 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 24 Jul 2023 16:44:52 -0400 Subject: [PATCH 14/15] interfaces: Allow creating a wallet with encrypted database --- src/interfaces/wallet.h | 2 +- src/wallet/interfaces.cpp | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index d61c3f6714e..fcd62a9219a 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -321,7 +321,7 @@ class WalletLoader : public ChainClient { public: //! Create new wallet. - virtual util::Result> createWallet(const std::string& name, const SecureString& passphrase, uint64_t wallet_creation_flags, std::vector& warnings) = 0; + virtual util::Result> createWallet(const std::string& name, const SecureString& passphrase, const SecureString& db_passphrase, uint64_t wallet_creation_flags, std::vector& warnings) = 0; //! Load existing wallet. virtual util::Result> loadWallet(const std::string& name, std::vector& warnings, const SecureString& db_passphrase) = 0; diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 3b4ff3556c8..3d0a8fae4ec 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -590,7 +590,7 @@ class WalletLoaderImpl : public WalletLoader void setMockTime(int64_t time) override { return SetMockTime(time); } //! WalletLoader methods - util::Result> createWallet(const std::string& name, const SecureString& passphrase, uint64_t wallet_creation_flags, std::vector& warnings) override + util::Result> createWallet(const std::string& name, const SecureString& passphrase, const SecureString& db_passphrase, uint64_t wallet_creation_flags, std::vector& warnings) override { DatabaseOptions options; DatabaseStatus status; @@ -598,6 +598,7 @@ class WalletLoaderImpl : public WalletLoader options.require_create = true; options.create_flags = wallet_creation_flags; options.create_passphrase = passphrase; + options.db_passphrase = db_passphrase; bilingual_str error; std::unique_ptr wallet{MakeWallet(m_context, CreateWallet(m_context, name, /*load_on_start=*/true, options, status, error, warnings))}; if (wallet) { From 93fe316ff098b396fa0d98428ba1a2b25e4abce1 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 24 Jul 2023 16:45:12 -0400 Subject: [PATCH 15/15] gui: Add option to create a wallet with encrypted database --- src/qt/createwalletdialog.cpp | 7 +++++++ src/qt/createwalletdialog.h | 1 + src/qt/forms/createwalletdialog.ui | 12 +++++++++++- src/qt/walletcontroller.cpp | 29 +++++++++++++++++++++++------ src/qt/walletcontroller.h | 3 ++- 5 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/qt/createwalletdialog.cpp b/src/qt/createwalletdialog.cpp index 5b3c8bcf481..1f8cb8bbea0 100644 --- a/src/qt/createwalletdialog.cpp +++ b/src/qt/createwalletdialog.cpp @@ -92,6 +92,8 @@ CreateWalletDialog::CreateWalletDialog(QWidget* parent) : ui->descriptor_checkbox->setChecked(false); ui->external_signer_checkbox->setEnabled(false); ui->external_signer_checkbox->setChecked(false); + ui->encrypt_db_checkbox->setEnabled(false); + ui->encrypt_db_checkbox->setChecked(false); #endif #ifndef USE_BDB @@ -144,6 +146,11 @@ bool CreateWalletDialog::isEncryptWalletChecked() const return ui->encrypt_wallet_checkbox->isChecked(); } +bool CreateWalletDialog::isEncryptDBChecked() const +{ + return ui->encrypt_db_checkbox->isChecked(); +} + bool CreateWalletDialog::isDisablePrivateKeysChecked() const { return ui->disable_privkeys_checkbox->isChecked(); diff --git a/src/qt/createwalletdialog.h b/src/qt/createwalletdialog.h index 939b82ff78c..1b777b1d8f1 100644 --- a/src/qt/createwalletdialog.h +++ b/src/qt/createwalletdialog.h @@ -33,6 +33,7 @@ class CreateWalletDialog : public QDialog QString walletName() const; bool isEncryptWalletChecked() const; + bool isEncryptDBChecked() const; bool isDisablePrivateKeysChecked() const; bool isMakeBlankWalletChecked() const; bool isDescriptorWalletChecked() const; diff --git a/src/qt/forms/createwalletdialog.ui b/src/qt/forms/createwalletdialog.ui index 56adbe17a5c..07b0e5486bc 100644 --- a/src/qt/forms/createwalletdialog.ui +++ b/src/qt/forms/createwalletdialog.ui @@ -7,7 +7,7 @@ 0 0 364 - 249 + 316 @@ -122,6 +122,16 @@ + + + + Encrypt the wallet's database. The database will be encrypted with a passphrase of your choice. Wallets with an encrypted database cannot be loaded automatically on startup. + + + Encrypt Database + + + diff --git a/src/qt/walletcontroller.cpp b/src/qt/walletcontroller.cpp index 1b14bf3469e..577da5812d3 100644 --- a/src/qt/walletcontroller.cpp +++ b/src/qt/walletcontroller.cpp @@ -220,17 +220,17 @@ CreateWalletActivity::~CreateWalletActivity() delete m_passphrase_dialog; } -void CreateWalletActivity::askPassphrase() +void CreateWalletActivity::askPassphrase(SecureString* passphrase_out, std::function next_func, QString warning_text) { - m_passphrase_dialog = new AskPassphraseDialog(AskPassphraseDialog::Encrypt, m_parent_widget, &m_passphrase); + m_passphrase_dialog = new AskPassphraseDialog(AskPassphraseDialog::Encrypt, m_parent_widget, passphrase_out, warning_text); m_passphrase_dialog->setWindowModality(Qt::ApplicationModal); m_passphrase_dialog->show(); connect(m_passphrase_dialog, &QObject::destroyed, [this] { m_passphrase_dialog = nullptr; }); - connect(m_passphrase_dialog, &QDialog::accepted, [this] { - createWallet(); + connect(m_passphrase_dialog, &QDialog::accepted, [next_func] { + next_func(); }); connect(m_passphrase_dialog, &QDialog::rejected, [this] { Q_EMIT finished(); @@ -262,7 +262,7 @@ void CreateWalletActivity::createWallet() } QTimer::singleShot(500ms, worker(), [this, name, flags] { - auto wallet{node().walletLoader().createWallet(name, m_passphrase, flags, m_warning_message)}; + auto wallet{node().walletLoader().createWallet(name, m_passphrase, m_db_passphrase, flags, m_warning_message)}; if (wallet) { m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(*wallet)); @@ -314,7 +314,24 @@ void CreateWalletActivity::create() }); connect(m_create_wallet_dialog, &QDialog::accepted, [this] { if (m_create_wallet_dialog->isEncryptWalletChecked()) { - askPassphrase(); + if (m_create_wallet_dialog->isEncryptDBChecked()) { + // When both are checked, we need to first get the passphrase for wallet encryption + // then the passphrase for db encryption, then make the wallet, hence this chain of binds + askPassphrase( + &m_passphrase, + std::bind( + &CreateWalletActivity::askPassphrase, + this, + &m_db_passphrase, + [this]() { createWallet(); }, + tr("Enter the new passphrase for encrypting all records in the wallet database.") + ) + ); + } else { + askPassphrase(&m_passphrase, std::bind(&CreateWalletActivity::createWallet, this)); + } + } else if (m_create_wallet_dialog->isEncryptDBChecked()) { + askPassphrase(&m_db_passphrase, std::bind(&CreateWalletActivity::createWallet, this), tr("Enter the new passphrase for encrypting all records in the wallet database.")); } else { createWallet(); } diff --git a/src/qt/walletcontroller.h b/src/qt/walletcontroller.h index 48a580a99ef..ba90c00028f 100644 --- a/src/qt/walletcontroller.h +++ b/src/qt/walletcontroller.h @@ -124,11 +124,12 @@ class CreateWalletActivity : public WalletControllerActivity void created(WalletModel* wallet_model); private: - void askPassphrase(); + void askPassphrase(SecureString* passphrase_out, std::function next_func, QString warning_text = ""); void createWallet(); void finish(); SecureString m_passphrase; + SecureString m_db_passphrase; CreateWalletDialog* m_create_wallet_dialog{nullptr}; AskPassphraseDialog* m_passphrase_dialog{nullptr}; };