Skip to content

Conversation

achow101
Copy link
Member

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 or flags). 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 a db_passphrase parameter to allow the decryption of these wallets. createwallet also has a db_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.

@DrahtBot
Copy link
Contributor

DrahtBot commented Jul 24, 2023

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage

For detailed information about the code coverage, see the test coverage report.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept NACK luke-jr
Concept ACK stevenroose

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #29129 (wallet, rpc: add BIP44 account in createwallet by brunoerg)
  • #29119 (refactor: Use std::span over Span by maflcko)
  • #29112 (sqlite: Disallow writing from multiple SQLiteBatchs by achow101)
  • #29071 (refactor: Remove Span operator==, Use std::ranges::equal by maflcko)
  • #29054 (wallet: reenable sethdseed for descriptor wallets by achow101)
  • #26728 (wallet: Have the wallet store the key for automatically generated descriptors by achow101)
  • #25907 (wallet: rpc to add automatically generated descriptors by achow101)
  • #22341 (rpc: add path to gethdkey by Sjors)

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.

@achow101
Copy link
Member Author

cc @stevenroose

@luke-jr
Copy link
Member

luke-jr commented Jul 25, 2023

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}});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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}});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

@stevenroose
Copy link
Contributor

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.)

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.

@stevenroose
Copy link
Contributor

Concept ACK :) (Concept Request, in fact)

Copy link
Member

@furszy furszy left a 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.

@stevenroose
Copy link
Contributor

@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.

Copy link
Member

@furszy furszy left a 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..).

@maflcko
Copy link
Member

maflcko commented Aug 23, 2023

Maybe mark as draft while CI is red?

@achow101 achow101 force-pushed the encrypt-watch-only branch 2 times, most recently from 7e963f4 to 7870146 Compare August 23, 2023 16:27
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.
@achow101
Copy link
Member Author

achow101 commented Jan 6, 2024

Marking as up for grabs.

@achow101 achow101 closed this Jan 6, 2024
@bitcoin bitcoin locked and limited conversation to collaborators Jan 5, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants