Skip to content

New kdf - #11040

Open
ecdsa wants to merge 7 commits into
masterfrom
new_kdf
Open

New kdf#11040
ecdsa wants to merge 7 commits into
masterfrom
new_kdf

Conversation

@ecdsa

@ecdsa ecdsa commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

see #4909.
Note that this is not a DB upgrade (it cannot be, because it requires password).
Also, be careful when visiting this branch: the gui will upgrade your wallet without user confirmation (unlike a DB upgrade)

@ecdsa
ecdsa force-pushed the new_kdf branch 3 times, most recently from 506ef9a to 6e00672 Compare October 16, 2026 08:26
 - With the new KDF, we will need to store pw_key in WalletDB.
   Better store the password at the same location.
 - get_unlocked_password does not need to re-check the password.
   (re-check will get expensive once the KDF is stretched)
 - Daemon.load_wallet: replace 'force_check_password' with 'unlock'
   The QML gui is the only caller and it unlocks right after loading.
ecdsa added 6 commits October 16, 2026 16:21
 - 100k iterations of PBKDF2-HMAC512 with a random salt
 - there is a single salt per wallet file, stored in the db and shared
   by all the secrets of the wallet, so that the KDF only needs to be
   run once, however many secrets we touch
 - while the wallet is unlocked, the derived key is kept next to the
   password in WalletDB, so that the KDF does not run again

Note that there is no security benefit for the moment, as we also
need to apply the stronger KDF to the file storage. The only case
where this increases security is if the file is NOT encrypted :-)
 - if the file is encrypted, we already have the password when the file
   is opened, so the keystores can move to the latest pw hash version
   right then, instead of waiting for the next password change
 - wallets that only encrypt their keystore keep their old pw hash
   version until the next password change, as we have no password at
   opening time

Note that this update is one-way: once a wallet has been migrated,
older versions of Electrum will refuse to open it, and report
"Unexpected password hash version: 3".
No change in behaviour, as has_lightning() returns bool(self.lnworker).

This prepares for encrypting the lightning secrets (see #4909): lnworker will
exist while the wallet is locked, and has_lightning() will keep meaning that
the wallet has lightning, whether or not its secrets are loaded.
LNWallet is created from public data only: channels, onchain channel
backups, swaps and payments. load_secrets() then derives the keys needed to
run lightning (node, payment, backup, funding root and nostr keys), loads the
channel keys and the imported channel backups, and registers the hold
invoices of swaps. For now, it is called right after creation, so nothing
changes.

This prepares for encrypting the lightning secrets with the wallet password
(see #4909): lnworker will exist while the wallet is locked, and load its
secrets once we have the password.

 - lnwatcher registers its callbacks when it starts, as they may need the
   secrets. Channels are subscribed to it once the secrets are loaded.
 - lightning_xprv, channel_seed, multisig_privkey, imported channel backups,
   preimages and SwapData privkeys are encrypted with a key derived from
   the wallet password
 - That key is kept in memory. The derivation is specific to lightning,
   so that it cannot decrypt the keystore
 - they are only encrypted if the keystore is: hw and watching-only wallets
   keep them in plaintext, even if the file is encrypted
 - do not propose to disable keystore for wallets that have lightning (this
   feature is only useful for multisig wallets)

Lightning can only run once we have the password. If the wallet is not
encrypted, the Qt and text GUIs ask for it at startup if the wallet has lightning.
With the daemon, the wallet should be unlocked, because sweeping a force-closed
anchor channel needs the password: use 'load_wallet --unlock' (or 'unlock');
load_wallet returns a warning otherwise.

Until then, lnworker only gives access to public data: channels, swaps,
balances, history, requests and invoices. Its keys, pw_key, the channel keys
and the swap secrets raise LightningLocked. has_lightning() still means
that the wallet has lightning; is_lightning_loaded() tells whether it can run.

Lightning keys are encrypted at the same time as the new KDF migration.
update_password re-encrypts the lightning secrets and replaces
lnworker.pw_key, while lnworker may be encrypting or decrypting
secrets on another thread. A secret written with the old key after
the re-encryption could never be decrypted again.

 - the re-encryption and the key swap are done under db.lock, and so
   is every read or write of a lightning secret
 - add_new_channel re-encrypts the secrets of a new channel when it is
   stored, as pw_key may have changed while the channel was being opened
@ecdsa
ecdsa marked this pull request as ready for review October 16, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant