Skip to content

Conversation

@achow101
Copy link
Member

An earlier version allowed users to create watchonly wallets (wallets without private keys) that were "encrypted". Such wallets would have a stored encryption keys, but nothing would actually be encrypted with them. This can cause unexpected behavior such as bitcoin-core/gui#772.

We can detect such wallets as they will have the disable private keys flag set, no encrypted keys, and encryption keys. For such wallets, we can remove those encryption keys thereby avoiding any issues that may result from this unexpected situation.

@DrahtBot
Copy link
Contributor

DrahtBot commented Oct 24, 2023

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

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/28724.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK laanwj, sipa, furszy
Stale ACK willcl-ark

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:

  • #28710 (Remove the legacy wallet and BDB dependency by achow101)

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.

Copy link
Member

@laanwj laanwj left a comment

Choose a reason for hiding this comment

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

Code review ACK aaa6b709d164052c8b2496cbedccf8ccf48c4aa2

Copy link
Member

@willcl-ark willcl-ark left a comment

Choose a reason for hiding this comment

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

ACK aaa6b709d164052c8b2496cbedccf8ccf48c4aa2

I left a few comments, but they're mainly thoughts I had and questions for my own understanding.

Also checked the test manually with --legacy-wallet and --descriptors and lightly inspected the dumps to ensure the records were being removed:

log
$ test/functional/wallet_encryption.py
2023-11-01T11:51:31.078000Z TestFramework (INFO): PRNG seed is: 196471712041506435
2023-11-01T11:51:31.079000Z TestFramework (INFO): Initializing test directory /tmp/bitcoin_func_test_hm1fob6j
2023-11-01T11:51:35.739000Z TestFramework (INFO): Check a timeout less than the limit
2023-11-01T11:51:35.850000Z TestFramework (INFO): Check a timeout greater than the limit
2023-11-01T11:51:36.579000Z TestFramework (INFO): Test that wallets without private keys cannot be encrypted
2023-11-01T11:51:36.595000Z TestFramework (INFO): Test that encryption keys in wallets without privkeys are removed
{'walletname': 'noprivs', 'walletversion': 169900, 'format': 'sqlite', 'balance': Decimal('0E-8'), 'unconfirmed_balance': Decimal('0E-8'), 'immature_balance': Decimal('0E-8'), 'txcount': 0, 'keypoolsize': 0, 'keypoolsize_hd_internal': 0, 'paytxfee': Decimal('0E-8'), 'private_keys_enabled': False, 'avoid_reuse': False, 'scanning': False, 'descriptors': True, 'external_signer': False, 'blank': False, 'lastprocessedblock': {'hash': '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206', 'height': 0}}
orig_dump=['BITCOIN_CORE_WALLET_DUMP,1\n', 'format,sqlite\n', '0776657273696f6e,4c1e0400\n', '0a6d696e76657273696f6e,ac970200\n', '0962657374626c6f636b,8011010000\n', '1262657374626c6f636b5f6e6f6d65726b6c65,801101000106226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f\n', '05666c616773,0400000005000000\n', 'checksum,058c1f6af6fcbbf22056c1f690d7b9fa27ec2a37dec2b0bcadca89977666634f\n']
after_dump=['BITCOIN_CORE_WALLET_DUMP,1\n', 'format,sqlite\n', '0776657273696f6e,4c1e0400\n', '0a6d696e76657273696f6e,ac970200\n', '0962657374626c6f636b,8011010000\n', '1262657374626c6f636b5f6e6f6d65726b6c65,801101000106226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f\n', '05666c616773,0400000005000000\n', '046d6b657901000000,300dc926f3b3887aad3d5d5f5a0fc1b1a4a1722f9284bd5c6ff93b64a83902765953939c58fe144013c8b819f42cf698b208e9911e5f0c544fa300000000cc52050000\n', 'checksum,e63f7200466a4d2d6e05b505be87b148a96ae57702590f0fc284fbbeac332036\n']
Result of set(orig_dump) ^ set(after_dump):
{'046d6b657901000000,300dc926f3b3887aad3d5d5f5a0fc1b1a4a1722f9284bd5c6ff93b64a83902765953939c58fe144013c8b819f42cf698b208e9911e5f0c544fa300000000cc52050000\n', 'checksum,058c1f6af6fcbbf22056c1f690d7b9fa27ec2a37dec2b0bcadca89977666634f\n', 'checksum,e63f7200466a4d2d6e05b505be87b148a96ae57702590f0fc284fbbeac332036\n'}
2023-11-01T11:51:37.068000Z TestFramework (INFO): Stopping nodes
2023-11-01T11:51:37.223000Z TestFramework (INFO): Cleaning up /tmp/bitcoin_func_test_hm1fob6j on exit
2023-11-01T11:51:37.223000Z TestFramework (INFO): Tests successful

Copy link
Member

Choose a reason for hiding this comment

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

I notice that nothing in this block requires any locks (even though cs_wallet will be locked at this stage in this function).

For my own understanding, is this because the db operations, i.e. writing to the on-disk file, have their synchronisation handled by the db's transaction system?

I see for example in LoadEncryptionKey that we lock cs_wallet to load the keys into the wallet, which makes intuitive sense. But it feels that conversely there should be something asserting the lock is held here too, as we remove encryption keys from the wallet? Or at least when pwallet->mapMasterKeys.clear(); is called.

Perhaps it doesn't matter as it's done during loading from the db, so nothing else will have started to access the Wallet?

Copy link
Member Author

Choose a reason for hiding this comment

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

The lock is already being held from the top of the function.

Copy link
Member

Choose a reason for hiding this comment

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

I was wondering if if could be a good idea to log the removed keys at this point as a backup (safe-ish seeing as we are deleting), but I'm struggling to think of a scenario where a user with this type of wallet might somehow need these keys again...

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think it would be useful to log them.

Copy link
Member

@furszy furszy Nov 1, 2023

Choose a reason for hiding this comment

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

I'm still not sure about #28142 but, if we ever do something like that, we could have an encrypted wallet without encrypted private keys. So, might want to change the "without private keys" to "without any encrypted record".

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this is fine as is. It can be updated in the future if it becomes outdated.

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.

As the process of reading wallet records from db disregards unknown record types, what if the encryption key is ever used for something else?. Wouldn't that mean that opening such wallet on any previous version (post this PR) would erase the encryption key forever?

The erase process can only verify the private keys disabled flag and whether there is any crypted key or not. But it cannot verify if the encryption key was used for something else.

Guessing that in the case of adding a new type of encrypted record in the future for private keys disabled wallets, we would be forced to add downgrade protection if we merge this PR?

@achow101
Copy link
Member Author

achow101 commented Jan 2, 2024

Guessing that in the case of adding a new type of encrypted record in the future for private keys disabled wallets, we would be forced to add downgrade protection if we merge this PR?

Yes, but I think it's unlikely such usage of encryption keys would be added, and if it were, it could still be added in a way that does not interact with this PR.

For example,although #28142 allows for encryption of records regardless of the wallet flags, it does this at the database level and uses separate records, so this PR doesn't affect it at all.

Due to a bug in earlier versions, some wallets without private keys may
have an encryption key. This encryption key is unused and can lead to
confusing behavior elsewhere. When such wallets are detected, those
encryption keys will now be deleted from the wallet. For safety, we only
do this to wallets which have private keys disabled, have encryption keys,
and definitely do not have encrypted keys.
@bitcoin bitcoin deleted a comment from baycclark Oct 18, 2024
@bitcoin bitcoin deleted a comment from baycclark Oct 18, 2024
Copy link
Member

@laanwj laanwj left a comment

Choose a reason for hiding this comment

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

Code review re-ACK 69e95c2

@sipa
Copy link
Member

sipa commented Dec 23, 2024

utACK 69e95c2.

I can't imagine any way that this could affect operations later. The key isn't used, so deleting it can't hurt backups, and with or without encryption key no ckey records can be added later. Is that correct?

@achow101
Copy link
Member Author

achow101 commented Jan 8, 2025

The key isn't used, so deleting it can't hurt backups, and with or without encryption key no ckey records can be added later. Is that correct?

That's correct. ckey is a private key record, and wallets without private keys definitionally do not have private keys. No private keys can be generated or imported to such wallets.

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.

Code review ACK 69e95c2

@fanquake fanquake merged commit 35bf426 into bitcoin:master Jan 10, 2025
12 checks passed
@hebasto
Copy link
Member

hebasto commented Jan 11, 2025

The new code in test/functional/wallet_encryption.py fails on NetBSD:

230/313 - wallet_encryption.py --legacy-wallet failed, Duration: 8 s

stdout:
2025-01-11T03:37:44.955000Z TestFramework (INFO): PRNG seed is: 6983165365525497692
2025-01-11T03:37:44.956000Z TestFramework (INFO): Initializing test directory /home/runner/work/bitcoin-core-nightly/bitcoin-core-nightly/test_runner_₿_🏃_20250111_031054/wallet_encryption_83
2025-01-11T03:37:51.630000Z TestFramework (INFO): Check a timeout less than the limit
2025-01-11T03:37:51.726000Z TestFramework (INFO): Check a timeout greater than the limit
2025-01-11T03:37:52.424000Z TestFramework (INFO): Test that wallets without private keys cannot be encrypted
2025-01-11T03:37:52.453000Z TestFramework (INFO): Test that encryption keys in wallets without privkeys are removed
2025-01-11T03:37:52.468000Z TestFramework (ERROR): Assertion failed
Traceback (most recent call last):
  File "/home/runner/work/bitcoin-core-nightly/bitcoin-core-nightly/test/functional/test_framework/test_framework.py", line 135, in main
    self.run_test()
    ~~~~~~~~~~~~~^^
  File "/home/runner/work/bitcoin-core-nightly/bitcoin-core-nightly/build/test/functional/wallet_encryption.py", line 133, in run_test
    do_wallet_tool("-wallet=noprivs", f"-dumpfile={dumpfile_path}", "dump")
    ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/runner/work/bitcoin-core-nightly/bitcoin-core-nightly/build/test/functional/wallet_encryption.py", line 122, in do_wallet_tool
    assert_equal(proc.poll(), 0)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^
  File "/home/runner/work/bitcoin-core-nightly/bitcoin-core-nightly/test/functional/test_framework/util.py", line 77, in assert_equal
    raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
AssertionError: not(1 == 0)
2025-01-11T03:37:52.567000Z TestFramework (INFO): Stopping nodes

and

233/313 - wallet_encryption.py --descriptors failed, Duration: 8 s

stdout:
2025-01-11T03:37:47.381000Z TestFramework (INFO): PRNG seed is: 7931229293434170297
2025-01-11T03:37:47.383000Z TestFramework (INFO): Initializing test directory /home/runner/work/bitcoin-core-nightly/bitcoin-core-nightly/test_runner_₿_🏃_20250111_031054/wallet_encryption_82
2025-01-11T03:37:53.996000Z TestFramework (INFO): Check a timeout less than the limit
2025-01-11T03:37:54.142000Z TestFramework (INFO): Check a timeout greater than the limit
2025-01-11T03:37:55.004000Z TestFramework (INFO): Test that wallets without private keys cannot be encrypted
2025-01-11T03:37:55.014000Z TestFramework (INFO): Test that encryption keys in wallets without privkeys are removed
2025-01-11T03:37:55.017000Z TestFramework (ERROR): Assertion failed
Traceback (most recent call last):
  File "/home/runner/work/bitcoin-core-nightly/bitcoin-core-nightly/test/functional/test_framework/test_framework.py", line 135, in main
    self.run_test()
    ~~~~~~~~~~~~~^^
  File "/home/runner/work/bitcoin-core-nightly/bitcoin-core-nightly/build/test/functional/wallet_encryption.py", line 133, in run_test
    do_wallet_tool("-wallet=noprivs", f"-dumpfile={dumpfile_path}", "dump")
    ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/runner/work/bitcoin-core-nightly/bitcoin-core-nightly/build/test/functional/wallet_encryption.py", line 122, in do_wallet_tool
    assert_equal(proc.poll(), 0)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^
  File "/home/runner/work/bitcoin-core-nightly/bitcoin-core-nightly/test/functional/test_framework/util.py", line 77, in assert_equal
    raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
AssertionError: not(1 == 0)
2025-01-11T03:37:55.021000Z TestFramework (INFO): Stopping nodes

@hebasto
Copy link
Member

hebasto commented Jan 12, 2025

The new code in test/functional/wallet_encryption.py fails on NetBSD...

The root cause of the issue is that the -Wl,-z,separate-code linker option is incompatible with NetBSD's dynamic linker. Building with -DENABLE_HARDENING=OFF or -DAPPEND_LDFLAGS="-Wl,-z,noseparate-code" fixes the issue.

@fanquake
Copy link
Member

The root cause of the issue is that the -Wl,-z,separate-code linker option is incompatible with NetBSD's dynamic linker.

That's pretty surprising, because -z,separate-code has been enabled by default, in the NetBSD linker, for the upcoming 11.0 release. https://www.netbsd.org/changes/changes-11.0.html#x86:

Enable the -z separate-code security feature by default in ld(1).

It seems kinda unlikely they'd do that if the option was incompatible with the dynamic linker.

@hebasto
Copy link
Member

hebasto commented Jan 14, 2025

The root cause of the issue is that the -Wl,-z,separate-code linker option is incompatible with NetBSD's dynamic linker.

That's pretty surprising, because -z,separate-code has been enabled by default, in the NetBSD linker, for the upcoming 11.0 release. https://www.netbsd.org/changes/changes-11.0.html#x86:

Enable the -z separate-code security feature by default in ld(1).

It seems kinda unlikely they'd do that if the option was incompatible with the dynamic linker.

The underlying issue with supported NetBSD releases is that ld.elf_so can only handle 2 PT_LOAD segments.

When linking with -z noseparate-code, the bitcoind is produced with 2 PT_LOAD segments:

$ readelf -l ./build/src/bitcoind

Elf file type is DYN (Shared object file)
Entry point 0x2eb40
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000001f8 0x00000000000001f8  R      0x8
  INTERP         0x0000000000000238 0x0000000000000238 0x0000000000000238
                 0x0000000000000017 0x0000000000000017  R      0x1
      [Requesting program interpreter: /usr/libexec/ld.elf_so]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000c1dd55 0x0000000000c1dd55  R E    0x200000
  LOAD           0x0000000000c1e278 0x0000000000e1e278 0x0000000000e1e278
                 0x000000000000ff28 0x0000000000019910  RW     0x200000
  DYNAMIC        0x0000000000c29a78 0x0000000000e29a78 0x0000000000e29a78
                 0x0000000000000290 0x0000000000000290  RW     0x8
  NOTE           0x0000000000000250 0x0000000000000250 0x0000000000000250
                 0x000000000000002c 0x000000000000002c  R      0x4
  TLS            0x0000000000c1e278 0x0000000000e1e278 0x0000000000e1e278
                 0x0000000000000000 0x0000000000000080  R      0x1
  GNU_EH_FRAME   0x0000000000b09a14 0x0000000000b09a14 0x0000000000b09a14
                 0x000000000001becc 0x000000000001becc  R      0x4
  GNU_RELRO      0x0000000000c1e278 0x0000000000e1e278 0x0000000000e1e278
                 0x000000000000cd88 0x000000000000cd88  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.netbsd.ident .note.netbsd.pax .hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame .gcc_except_table 
   03     .init_array .ctors .dtors .data.rel.ro .dynamic .got .data .bss 
   04     .dynamic 
   05     .note.netbsd.ident .note.netbsd.pax 
   06     .tbss 
   07     .eh_frame_hdr 
   08     .init_array .ctors .dtors .data.rel.ro .dynamic .got 

However, linking with -z separate-code results in bitcoind with 4 PT_LOAD segments:


Elf file type is DYN (Shared object file)
Entry point 0x201c00
There are 11 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x0000000000000268 0x0000000000000268  R      0x8
  INTERP         0x00000000000002a8 0x00000000000002a8 0x00000000000002a8
                 0x0000000000000017 0x0000000000000017  R      0x1
      [Requesting program interpreter: /usr/libexec/ld.elf_so]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x000000000002cfc8 0x000000000002cfc8  R      0x200000
  LOAD           0x0000000000200000 0x0000000000200000 0x0000000000200000
                 0x00000000008fe03e 0x00000000008fe03e  R E    0x200000
  LOAD           0x0000000000c00000 0x0000000000c00000 0x0000000000c00000
                 0x00000000002f2dd5 0x00000000002f2dd5  R      0x200000
  LOAD           0x0000000000ef3278 0x00000000010f3278 0x00000000010f3278
                 0x000000000000ff28 0x0000000000019910  RW     0x200000
  DYNAMIC        0x0000000000efea78 0x00000000010fea78 0x00000000010fea78
                 0x0000000000000290 0x0000000000000290  RW     0x8
  NOTE           0x00000000000002c0 0x00000000000002c0 0x00000000000002c0
                 0x000000000000002c 0x000000000000002c  R      0x4
  TLS            0x0000000000ef3278 0x00000000010f3278 0x00000000010f3278
                 0x0000000000000000 0x0000000000000080  R      0x1
  GNU_EH_FRAME   0x0000000000ddea94 0x0000000000ddea94 0x0000000000ddea94
                 0x000000000001becc 0x000000000001becc  R      0x4
  GNU_RELRO      0x0000000000ef3278 0x00000000010f3278 0x00000000010f3278
                 0x000000000000cd88 0x000000000000cd88  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.netbsd.ident .note.netbsd.pax .hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 
   03     .init .plt .plt.got .text .fini 
   04     .rodata .eh_frame_hdr .eh_frame .gcc_except_table 
   05     .init_array .ctors .dtors .data.rel.ro .dynamic .got .data .bss 
   06     .dynamic 
   07     .note.netbsd.ident .note.netbsd.pax 
   08     .tbss 
   09     .eh_frame_hdr 
   10     .init_array .ctors .dtors .data.rel.ro .dynamic .got 

This limitation has been removed in NetBSD/src@acf7fb3 and will be included in the upcoming 11.0 release.