Commit Graph

188 Commits

Author SHA1 Message Date
ghost43 09a09057f6 Merge pull request #10548 from SomberNight/202603_lockdown_rpcserver
in GUI mode, only start a limited minimal RPC server
2026-03-31 15:25:52 +00:00
SomberNight 9d204abfae daemon: set restrictive permission on RPC-server unix domain socket
0600 instead of 0775.
2026-03-27 18:20:52 +00:00
SomberNight d951a3d2f4 in GUI mode, only start a limited minimal RPC server
To limit attack surface.

Context:
- both in daemon mode and in GUI mode, we start an RPC server
- the RPC server uses HTTP basic auth, with a random password that is saved in the config file
- read access to the config file implies access to the RPC server
- the traffic is unencrypted
- by default the server listens
  - on Windows, on localhost TCP
  - all other platform, via unix domain sockets
- if an attacker can listen to localhost TCP traffic, and there was traffic
  - they could see the plaintext RPC password and issue their own commands
  - e.g. if wireshark was already installed on the system, this might not require root access
- the "ping" and "gui" commands are used by everyday operations that affect most users:
  - "ping" is used when trying to launch a second instance of electrum, to contact the first instance and enforce "singleton" behaviour
  - "gui" is used for URI handling (`$ xdg-open bitcoin:asdasd`)
- many other sensitive commands, that operate on wallets, require *also* the wallet password
  - but note that wallet.unlock can be used by the user to bypass this and store the wallet password in memory (exposed in GUI)

I propose locking down the RPC server when running in GUI mode:
- we still start it, as it is used for "ping" and "gui" RPCs, however we disable all other RPCs
- we could opt-in enable it, using a config var, except that ofc would not help against an attacker that has filesystem write access to the config file
- so I think it's even safer to just "hardcode" disable it: however the functionality is useful for development
  - I propose we branch based on `constants.net.TESTNET`
  - an alternative we could branch on that is hard to fake is `is_git_clone` in run_electrum
2026-03-25 18:44:56 +00:00
SomberNight e08390a01d daemon: (trivial) CommandsServer.run: move tcp-specific line
this line would raise if site was a web.NamedPipeSite
2026-03-25 16:19:14 +00:00
SomberNight 0dcef9780b daemon: forbid "setconfig" command to change rpcserver settings in-flight
It is much easier to reason about the rpcserver if we don't allow changing its basic settings while it is already running. What does it mean to change the TCP port it is listening on ("rpcport") if it's already running? It is even problematic to change the rpcpassword: care needs to be taken to already update it for the current server.
(ref https://github.com/spesmilo/electrum/issues/6762)

This commit disallows changing all of the "rpc*" config variables if the daemon is already running.

---

Simultaneously, it also ensures rpc_password is always set and auth cannot be disabled.

Previously if there was a daemon running, and the user ran
`$ electrum setconfig rpcpassword ""` that would leave the RPC unauthenticated
for the current session. However next time the daemon restarted, get_rpc_credentials would see
the unset password and generate one.

I think this was the worst of both worlds:
- we did not really allow removing the rpc password, except for the current session, and
- perhaps unexpectedly, we would generate a new password on daemon restart

Instead now we explicitly make sure the RPC server can never get into a state where it does not have a password set.

Based on a report by `Zuzana Kotásková <36777@mail.vsfs.cz>`
2026-03-24 17:07:26 +00:00
ThomasV baf877252d Merge pull request #10345 from f321x/enforce_unified_password_qml_button
qml: limit creation of new wallets to existing password
2025-12-18 19:25:04 +01:00
f321x 91b3a4a5dc daemon: reset CURRENT_WALLET if the wallet gets deleted
Set SimpleConfig.CURRENT_WALLET = None if the wallet gets deleted,
otherwise we try to open it on the next startup which will show an error
message in QML.
2025-12-18 17:54:56 +01:00
Sander van Grieken 7d5d51975f qml: store current wallet when switching to already open wallet 2025-12-18 09:48:09 +01:00
f321x 3b028b06a0 qml: enforce use of existing password for wallet creation
When creating a new wallet in a Electrum instance with existing wallets
this change forces the user to reuse a password of any existing wallet
if `SimpleConfig.WALLET_USE_SINGLE_PASSWORD` is True.
This prevents the amount of different passwords from increasing and
guides the user towards a single wallet password (the intended default).
2025-12-17 14:01:41 +01:00
ThomasV 9c4c7f01ac daemon: pass cmdname to register_method
This allows plugins to use already existing names without Electrum complaining about collisions
2025-11-04 12:42:54 +01:00
SomberNight c68deb25ff daemon: get_wallet: handle OSError for weiiird paths
I think _wallet_key_from_path should not raise.
This is probably the sane way to deal with this.
Though all this is assuming that os.path.realpath can be treated as consistent/stateless.

closes https://github.com/spesmilo/electrum/issues/10182
2025-09-03 14:14:22 +00:00
SomberNight 92bdc4d4ca daemon: load_wallet: add force_check_password arg, and use it in qml
- fix: qml gui errors when trying to open a wallets with only keystore-encryption
  - fixes https://github.com/spesmilo/electrum/issues/10171
- qml gui to prompt for password on wallet open even if wallet is not storage-encrypted
2025-08-23 16:54:48 +00:00
SomberNight 85fc95c71b wallet_db: add configvar for partial_writes, disable by default
Adds a new configvar `WALLET_PARTIAL_WRITES` to enable/disable partial writes for the walletDB.
This is a further restriction on top of the existing restrictions,
e.g. wallet files still need to have file encryption disabled for partial writes.
It defaults to off, so even for unencrypted wallets we disable partial writes for now.

This is used as a stopgap measure until we fix the issues found with the partial writes impl
(see https://github.com/spesmilo/electrum/issues/10000).
2025-07-15 14:03:48 +00:00
SomberNight 61623d6d56 daemon lockfile: if create_time is in the future, don't wait for it :D
- if Electrum cannot do a graceful shutdown, the daemon lockfile won't get cleaned up
- the daemon lockfile contains the creation time of the daemon
- then, especially for an always offline phone/laptop, if the battery dies, the clock
  might go back to the past
- the bug: next time Electrum starts, the program will wait until create_time + 1.0
  before giving up on the daemon and creating a new one

closes https://github.com/spesmilo/electrum/issues/9802
  - certainly fixes at least https://github.com/spesmilo/electrum/issues/9802#issuecomment-3020220593
  - the OP there might or might not be the same issue
closes https://github.com/spesmilo/electrum/issues/9529
  - same here, might or might not be the same issue

the logic bug is quite old, from https://github.com/spesmilo/electrum/commit/e6020975a51f8f0d9e739d860454b2ade5b80e1f
2025-06-30 22:36:49 +00:00
SomberNight ae70d204b0 daemon: should only log "Ignoring -w" for "daemon" command, not for gui 2025-06-28 03:17:27 +00:00
ThomasV eaa402dab6 daemon: do not set CURRENT_WALLET if the -w option was passed
This was the previous behaviour with gui_last_wallet.
2025-06-05 16:57:15 +02:00
ThomasV 37914d5af0 cmdline: use 'wallet_path' argument to pass wallet_path 2025-06-05 09:06:29 +02:00
SomberNight 3e4601c61d base64.b64decode: always set validate=True
Notably verifymessage and decrypt(message) were silently ignoring trailing garbage
or inserted non-base64 characters present in signatures/ciphertext.
(both the CLI commands and in the GUI)
I think it is much cleaner and preferable to treat such signatures/ciphertext as invalid.

In fact I find it surprising that base64.b64decode(validate=False) is the default.
Perhaps we should create a helper function for it that set validate=True and use that.
2025-06-03 18:58:05 +00:00
SomberNight 902ec09791 daemon: re config.CURRENT_WALLET: wallet_key MUST NOT be used as path 2025-06-03 15:08:03 +00:00
ThomasV 972ebb0420 Daemon: do not set config.CURRENT_WALLET to None
Instead, raise a UserFacingException if the daemon has several wallets in memory.
2025-06-03 10:41:03 +02:00
ThomasV 9e225d1269 Replace config GUI_LAST_WALLET with CURRENT_WALLET
- CURRENT_WALLET is set when a single wallet is loaded in memory, and it
   remains set after Electrum stops running.
 - If several wallets are loaded at the same time, CURRENT_WALLET is unset,
   and RPCs must specify the wallet explicitly (using --wallet for the CLI)
 - The fallback to 'default_wallet' essentially only applies when
   creating a new wallet file
2025-06-02 14:05:53 +02:00
ThomasV b91c5d18cb call lnwatcher.trigger_callbacks in offline mode 2025-05-26 11:48:00 +02:00
ThomasV 07ba0e6329 Qt: do not require password in memory
- do not require full encryption
 - do not store password on startup
 - add lock/unlock functions to qt GUI
2025-05-06 13:04:18 +02:00
ThomasV 70ab4f2190 Fix password passed to daemon.load_wallet
The password should not be an empty string.

Traceback (most recent call last):
  File "/opt/electrum/electrum/gui/qt/__init__.py", line 377, in start_new_window
    wallet = self._start_wizard_to_select_or_create_wallet(path)
  File "/opt/electrum/electrum/gui/qt/__init__.py", line 446, in _start_wizard_to_select_or_create_wallet
    wallet = self.daemon.load_wallet(wallet_file, d['password'], upgrade=True)
  File "/opt/electrum/electrum/daemon.py", line 461, in func_wrapper
    return func(self, *args, **kwargs)
  File "/opt/electrum/electrum/daemon.py", line 474, in load_wallet
    wallet.unlock(password)
  File "/opt/electrum/electrum/wallet.py", line 3418, in unlock
    self.check_password(password)
  File "/opt/electrum/electrum/wallet.py", line 3069, in check_password
    raise InvalidPassword("password given but wallet has no password")
electrum.util.InvalidPassword: password given but wallet has no password
2025-04-15 18:01:25 +02:00
SomberNight 536fce3f94 cli: don't add wallet to running online GUI, if called with -o 2025-03-18 19:33:24 +00:00
f321x ae64583ebc add handling of plugin commands 2025-03-15 13:22:28 +01:00
ThomasV 22995b4a34 follow-up 912e1a3a5b: password may be None if wallet is not file encrypted 2025-02-13 11:52:46 +01:00
Sander van Grieken 19cd408f98 organize import, whitespace 2025-01-23 12:58:28 +01:00
SomberNight be2cd02e54 some clean-ups now that we require python 3.10 2025-01-10 18:52:53 +00:00
ThomasV 29a8c41025 move watchtower to a plugin.
remove watchtower dialog in qt
2024-12-20 15:34:26 +01:00
ThomasV ee42e09387 anchor channels: unlock wallet on startup if the wallet has channels 2024-12-20 10:10:07 +01:00
Sander van Grieken 2113cd4f2e qt: keep list of recently used wallets in sync across windows
this also removes alpha-sort of the wallet list in favor of
placing most recently opened on top
2024-10-28 10:59:57 +01:00
SomberNight 0866581b2c daemon error-handling: fix traceback.format_exception() on old python
The new API for traceback.format_exception was only added in python 3.10 (https://github.com/python/cpython/commit/91e93794d5dd1aa91fbe142099c2955e0c4c1660).
2024-06-05 14:45:28 +00:00
SomberNight bd492fbd14 cli/rpc: nicer error messages and error-passing
Previously, generally, in case of any error, commands would raise a generic "Exception()" and the CLI/RPC would convert that and return it as `str(e)`.

With this change, we now distinguish "user-facing exceptions" (e.g. "Password required" or "wallet not loaded") and "internal errors" (e.g. bugs).
- for "user-facing exceptions", the behaviour is unchanged
- for "internal errors", we now pass around the traceback (e.g. from daemon server to rpc client) and show it to the user (previously, assuming there was a daemon running, the user could only retrieve the exception from the log of that daemon). These errors use a new jsonrpc error code int (code 2).

As the logic only changes for "internal errors", I deem this change not to be compatibility-breaking.

----------
Examples follow.
Consider the following two commands:
```
@command('')
async def errorgood(self):
	from electrum.util import UserFacingException
	raise UserFacingException("heyheyhey")

@command('')
async def errorbad(self):
	raise Exception("heyheyhey")
```

----------
(before change)

CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9221)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
heyheyhey
$ ./run_electrum --testnet stop
Daemon stopped
```

CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
heyheyhey
```

RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
```

----------
(after change)

CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9254)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
(inside daemon): Traceback (most recent call last):
  File "/home/user/wspace/electrum/electrum/daemon.py", line 254, in handle
    response['result'] = await f(*params)
  File "/home/user/wspace/electrum/electrum/daemon.py", line 361, in run_cmdline
    result = await func(*args, **kwargs)
  File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
    return await func(*args, **kwargs)
  File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
    raise Exception("heyheyhey")
Exception: heyheyhey

internal error while executing RPC
$ ./run_electrum --testnet stop
Daemon stopped
```

CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
  0.78 | E | __main__ | error running command (without daemon)
Traceback (most recent call last):
  File "/home/user/wspace/electrum/./run_electrum", line 534, in handle_cmd
    result = fut.result()
  File "/usr/lib/python3.10/concurrent/futures/_base.py", line 458, in result
    return self.__get_result()
  File "/usr/lib/python3.10/concurrent/futures/_base.py", line 403, in __get_result
    raise self._exception
  File "/home/user/wspace/electrum/./run_electrum", line 255, in run_offline_command
    result = await func(*args, **kwargs)
  File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
    return await func(*args, **kwargs)
  File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
    raise Exception("heyheyhey")
Exception: heyheyhey
```

RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 2, "message": "internal error while executing RPC", "data": {"exception": "Exception('heyheyhey')", "traceback": "Traceback (most recent call last):\n  File \"/home/user/wspace/electrum/electrum/daemon.py\", line 254, in handle\n    response['result'] = await f(*params)\n  File \"/home/user/wspace/electrum/electrum/commands.py\", line 163, in func_wrapper\n    return await func(*args, **kwargs)\n  File \"/home/user/wspace/electrum/electrum/commands.py\", line 217, in errorbad\n    raise Exception(\"heyheyhey\")\nException: heyheyhey\n"}}}
```
2024-02-12 19:02:02 +00:00
Sander van Grieken 7ca9b735d5 daemon: refactor load_wallet to not just return None, but raise specific exceptions.
The following exceptions should be expected:
FileNotFoundError: given wallet path does not exist
StorageReadWriteError: given file is not readable/writable or containing folder is not writable
InvalidPassword: wallet requires a password but no password or an invalid password was given
WalletFileException: any internal wallet data issue. specific subclasses can be caught separately:
-  WalletRequiresSplit: wallet needs splitting (split_data passed in Exception)
-  WalletRequiresUpgrade: wallet needs upgrade, and no upgrade=True was passed to load_wallet
-  WalletUnfinished: wallet file contains an action and needs additional information to finalize. (WalletDB passed in exception)

Removed qml/qewalletdb.py

This patch also fixes load_wallet calls in electrum/scripts and adds a qml workaround for dialogs opening and closing so
fast that the dialog opened==true property change is missed (which we need to manage the dialog/page stack)
2023-10-10 17:42:07 +02:00
ThomasV 68159b3ef6 walletDB: replace 'manual_upgrades' parameter with 'upgrade', with opposite semantics 2023-09-22 15:02:07 +02:00
ThomasV b5bc5ff9ed Separate WalletDB from storage upgrades.
Make sure that WalletDB.data is always a StoredDict.
Perform db upgrades in a separate class, since they
operate on a dict object.
2023-09-22 15:02:06 +02:00
SomberNight 1dfc1d567b follow-up prev 2023-08-24 17:30:06 +00:00
SomberNight 90f39bce88 run_electrum: have daemon manage Plugins object, and call Plugins.stop
Plugins.stop was never called, so the Plugins thread only stopped
because of the is_running() check in run(), which triggers too late:
the Plugins thread was stopping after the main thread stopped.

E.g. playing around in the qt wizard with wallet creation for a Trezor,
and closing the wizard (only window):
``` 24.85 | E | p/plugin.Plugins |
Traceback (most recent call last):
  File "/home/user/wspace/electrum/electrum/util.py", line 386, in run_jobs
    job.run()
  File "/home/user/wspace/electrum/electrum/plugin.py", line 430, in run
    client.timeout(cutoff)
  File "/home/user/wspace/electrum/electrum/plugin.py", line 363, in wrapper
    return run_in_hwd_thread(partial(func, *args, **kwargs))
  File "/home/user/wspace/electrum/electrum/plugin.py", line 355, in run_in_hwd_thread
    fut = _hwd_comms_executor.submit(func)
  File "/usr/lib/python3.10/concurrent/futures/thread.py", line 167, in submit
    raise RuntimeError('cannot schedule new futures after shutdown')
RuntimeError: cannot schedule new futures after shutdown
```
2023-08-24 17:15:14 +00:00
ThomasV b96cc82333 Make storage a field of db
This comes from the jsonpatch_new branch.
I rather have in master now, because it touches a lot of filese.
2023-08-18 08:08:31 +02:00
ThomasV 9f5f802cd1 config: save ports instead of net addresses (follow-up 012ce1c1bb) 2023-08-11 08:12:54 +02:00
ThomasV 012ce1c1bb Remove SSL options from config.
This is out of scope for Electrum; HTTP services that require SSL
should be exposed to the world through a reverse proxy.
2023-08-10 17:24:29 +02:00
SomberNight 8b195ee77a cli: "./run_electrum daemon -d" to block until daemon becomes ready
Without this,
`$ python3 -m unittest electrum.tests.regtest.TestUnixSockets.test_unixsockets`
was failing on my machine but succeeding on CI, due to timing differences.
2023-08-03 17:21:05 +00:00
SomberNight 58a9904a34 daemon: rm "daemon_jobs". maybe makes _run API less error-prone
(follow-up prev)
2023-08-03 17:21:01 +00:00
SomberNight d65aa3369f daemon: split standardize_path from daemon._wallets keying
- standardize_path is made more lenient: it no longer calls os.path.realpath,
  as that was causing issues on Windows with some mounted drives
- daemon._wallets is still keyed on the old strict standardize_path, but
  filesystem operations in WalletStorage use the new lenient standardize_path.
  - daemon._wallets is strict to forbid opening the same logical file twice concurrently
  - fs operations however work better on the non-resolved paths, so they use those

closes https://github.com/spesmilo/electrum/issues/8495
2023-06-30 11:15:16 +00:00
SomberNight 24980feab7 config: introduce ConfigVars
A new config API is introduced, and ~all of the codebase is adapted to it.
The old API is kept but mainly only for dynamic usage where its extra flexibility is needed.

Using examples, the old config API looked this:
```
>>> config.get("request_expiry", 86400)
604800
>>> config.set_key("request_expiry", 86400)
>>>
```

The new config API instead:
```
>>> config.WALLET_PAYREQ_EXPIRY_SECONDS
604800
>>> config.WALLET_PAYREQ_EXPIRY_SECONDS = 86400
>>>
```

The old API operated on arbitrary string keys, the new one uses
a static ~enum-like list of variables.

With the new API:
- there is a single centralised list of config variables, as opposed to
  these being scattered all over
- no more duplication of default values (in the getters)
- there is now some (minimal for now) type-validation/conversion for
  the config values

closes https://github.com/spesmilo/electrum/pull/5640
closes https://github.com/spesmilo/electrum/pull/5649

Note: there is yet a third API added here, for certain niche/abstract use-cases,
where we need a reference to the config variable itself.
It should only be used when needed:
```
>>> var = config.cv.WALLET_PAYREQ_EXPIRY_SECONDS
>>> var
<ConfigVarWithConfig key='request_expiry'>
>>> var.get()
604800
>>> var.set(3600)
>>> var.get_default_value()
86400
>>> var.is_set()
True
>>> var.is_modifiable()
True
```
2023-05-25 17:39:48 +00:00
SomberNight 4219022c2e fix flake8-bugbear B023
B023 Function definition does not bind loop variable 'already_selected_buckets_value_sum'

in keepkey/qt.py, looks like this was an actual bug
(fixed in trezor plugin already: https://github.com/spesmilo/electrum/commit/52a4810752c37312aeb7cb4739b5462dfdde2c58 )
2023-04-24 13:00:07 +00:00
SomberNight 1530668960 qt/qml: delay starting network until after first-start-network-setup
The qt, qml, and kivy GUIs have a first-start network-setup screen
that allows the user customising the network settings before creating a wallet.
Previously the daemon used to create the network and start it, before this screen,
before the GUI even starts. If the user changed network settings, those would
be set on the already running network, potentially including restarting the network.

Now it becomes the responsibility of the GUI to start the network, allowing this
first-start customisation to take place before starting the network at all.
The qt and the qml GUIs are adapted to make use of this. Kivy, and the other
prototype GUIs are not adapted and just start the network right away, as before.
2023-03-30 00:59:02 +00:00
SomberNight 512b63c424 exchange_rate: FxThread does not need network 2023-03-29 16:41:09 +00:00
SomberNight 9df5f55a1f password unification: bugfix, now passes test cases
fixes https://github.com/spesmilo/electrum/issues/8259

note that technically this is an API change for
- wallet.check_password
- wallet.update_password
- storage.check_password
2023-03-20 20:07:53 +00:00