-
Notifications
You must be signed in to change notification settings - Fork 37.7k
wallet: Allow users to create a wallet that encrypts all database records #28142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code CoverageFor detailed information about the code coverage, see the test coverage report. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
eb35b83
to
d97c315
Compare
cc @stevenroose |
Concept NACK, I think. Wallet encryption is not going to help you if someone steals your wallet, only if someone gains very temporary access to your keyboard/mouse only. I don't think this full-encryption you're proposing has a real use case. (What might make sense is to support fully encrypting backups.) |
@@ -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<const std::byte, 4> app_id_xor = {std::byte{0}, std::byte{0}, std::byte{0}, std::byte{0}}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, const DatabaseOptions& options, bool mock = false, std::array<const std::byte, 4> app_id_xor = {std::byte{0}, std::byte{0}, std::byte{0}, std::byte{0}}); | |
SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, const DatabaseOptions& options, bool mock = false, std::array<const std::byte, 4> app_id_xor = {}); |
Nit: I think you can just let the compiler fill in the zeros for you by using {}
.
@@ -146,7 +148,7 @@ class SQLiteDatabase : public WalletDatabase | |||
bool m_use_unsafe_sync; | |||
}; | |||
|
|||
std::unique_ptr<SQLiteDatabase> MakeSQLiteDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error); | |||
std::unique_ptr<SQLiteDatabase> MakeSQLiteDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::array<const std::byte, 4> app_id_xor = {std::byte{0}, std::byte{0}, std::byte{0}, std::byte{0}}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same
d97c315
to
d581930
Compare
I requested this because I have a real use case.. I run a full node that is on an unencrypted external drive. Bitcoin data is public data, it shouldn't be encrypted. I want to use the wallet for a watch-only wallet though. Unfortunately the wallet files live alongside the public blockchain data so I would have to store the wallet on an unencrypted drive (that I occasionally want to borrow out to friends for them to copy the blockchain in order not to have to download it). That would reveal my entire wallet to anyone that gets hold of this drive. Same would hold for a laptop. The blockchain is public data, encrypting it entirely is just a pointless performance loss. So it should always be stored on an unencrypted disk for performance reasons. But then you can't use the wallet. Even an encrypted wallet (currently only encrypting the private keys) leaks your entire wallet's history. |
Concept ACK :) (Concept Request, in fact) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I requested this because I have a real use case.. I run a full node that is on an unencrypted external drive. Bitcoin data is public data, it shouldn't be encrypted. I want to use the wallet for a watch-only wallet though. Unfortunately the wallet files live alongside the public blockchain data so I would have to store the wallet on an unencrypted drive (that I occasionally want to borrow out to friends for them to copy the blockchain in order not to have to download it). That would reveal my entire wallet to anyone that gets hold of this drive.
Why not just load the wallet from a different directory?
It is as simple as typing loadwallet <absolute_wallet_dir_path> true
once.
Much safer to not share the wallet data at all than sharing it encrypted.
@furszy That would work. My experience though is that touching anything inside Core's directories can get me in trouble. I once renamed a wallet file and Qt broke. It would only work again if I went into some JSON file to rename the wallet there too.. I'll do that for now. But anyway, someone getting hold of your hard drive shouldn't learn your wallet metadata, I think that's a good principle. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@furszy That would work. My experience though is that touching anything inside Core's directories can get me in trouble. I once renamed a wallet file and Qt broke. It would only work again if I went into some JSON file to rename the wallet there too..
Well, that is not the case for loading the wallet from an external directory. No need to touch anything on your system. Just run the RPC command.
That being said, if you can trigger any crash by manually changing something on your system, feel more than welcome to create as many issues as you need until all crashes are resolved. We should be able to fix them all. Core shouldn't crash (unless you modify something when is running..).
Maybe mark as draft while CI is red? |
7e963f4
to
7870146
Compare
7870146
to
9137722
Compare
9137722
to
f7fc592
Compare
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.
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.
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).
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.
f7fc592
to
9b845cc
Compare
Marking as up for grabs. |
A couple of users have requested that we support wallets that encrypt everything, even if the wallet is watch-only, in order to have better privacy if the wallet is stolen. This PR introduces an
EncryptedDatabase
backend for the wallet which encrypts/decrypts each key-value record individually before reading from or writing to the database.EncryptedDatabase
is only supported for SQLite databases and descriptor wallets. This was done in order to have an easier way to get downgrade protection that also does not involve writing an existing record in plaintext (e.g.minversion
orflags
). SQLite has a fixed field "application id" that we already use for cross-network protection. This is reused to detect if a sqlite database is an encrypted wallet, and thus it prevents older software from attempting to open such wallets.In order to read records from the database,
EncryptedDatabase
will cache the decrypted key in memory so that it can lookup the encrypted key in the database. Values will always be decrypted when read.The encryption scheme is the same one that we use for encrypting private keys. It's not that great, but I didn't feel like re-implementing it, and it seems good enough. The encryption key itself is encrypted with the passphrase and stored in a new record.
Wallets with encrypted databases cannot be loaded on start. They can only be loaded by explicit user action through
loadwallet
which now has adb_passphrase
parameter to allow the decryption of these wallets.createwallet
also has adb_passphrase
to create these wallets. For now, there is no way to encrypt the database after it has been created, nor is there a way to change the passphrase.