Skip to content

Commit e7ee1e6

Browse files
authored
Merge pull request #10933 from SomberNight/202609_crandom_prep
prep crandom.py: trivial refactor to centralise our RNG code
2 parents 68f0c71 + ff49607 commit e7ee1e6

18 files changed

Lines changed: 71 additions & 46 deletions

electrum/commands.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
from . import crypto
8080
from . import constants
8181
from . import descriptor
82+
from . import crandom
8283

8384
if TYPE_CHECKING:
8485
from .network import Network
@@ -2329,7 +2330,7 @@ async def get_blinded_path_via(self, node_id: str, dummy_hops: int = 0, wallet:
23292330
assert peer, 'node_id not a peer'
23302331

23312332
path = [pubkey, wallet.lnworker.node_keypair.pubkey]
2332-
session_key = os.urandom(32)
2333+
session_key = crandom.get_rand_bytes(32)
23332334
blinded_path = create_blinded_path(session_key, path=path, final_recipient_data={}, dummy_hops=dummy_hops)
23342335

23352336
with io.BytesIO() as blinded_path_fd:

electrum/crandom.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright (C) 2026 The Electrum developers
2+
# Distributed under the MIT software license, see the accompanying
3+
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
4+
#
5+
# Cryptographically secure RNG.
6+
7+
import os
8+
import secrets
9+
10+
11+
def get_rand_bytes(nbytes: int) -> bytes:
12+
"""Returns uniformly distributed bytes, of length nbytes."""
13+
assert nbytes >= 0, nbytes
14+
return os.urandom(nbytes)
15+
16+
17+
def get_rand_below(upper_bound: int) -> int:
18+
"""Return a uniformly distributed int in the range [0, upper_bound)."""
19+
assert upper_bound > 0, upper_bound
20+
return secrets.randbelow(upper_bound)
21+

electrum/crypto.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from .util import assert_bytes, InvalidPassword, to_bytes, to_string, WalletFileException, versiontuple
3737
from .i18n import _
3838
from .logging import get_logger
39+
from . import crandom
3940

4041
_logger = get_logger(__name__)
4142

@@ -179,7 +180,7 @@ def aes_decrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
179180

180181
def EncodeAES_bytes(secret: bytes, msg: bytes) -> bytes:
181182
assert_bytes(msg)
182-
iv = bytes(os.urandom(16))
183+
iv = crandom.get_rand_bytes(16)
183184
ct = aes_encrypt_with_iv(secret, iv, msg)
184185
return iv + ct
185186

electrum/daemon.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@
4040
from aiohttp import web, client_exceptions
4141
from aiorpcx import ignore_after
4242

43+
from . import crandom
4344
from . import util
4445
from .network import Network
4546
from .util import (
4647
json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare, InvalidPassword,
47-
log_exceptions, randrange, OldTaskGroup, UserFacingException, JsonRPCError, os_chmod
48+
log_exceptions, OldTaskGroup, UserFacingException, JsonRPCError, os_chmod
4849
)
4950
from .wallet import Wallet, Abstract_Wallet
5051
from .storage import WalletStorage
@@ -186,7 +187,7 @@ def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]:
186187
rpc_user = 'user'
187188
bits = 128
188189
nbytes = bits // 8 + (bits % 8 > 0)
189-
pw_int = randrange(pow(2, bits))
190+
pw_int = crandom.get_rand_below(pow(2, bits))
190191
pw_b64 = b64encode(
191192
pw_int.to_bytes(nbytes, 'big'), b'-_')
192193
rpc_password = to_string(pw_b64, 'ascii')

electrum/gui/qml/qebiometrics.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import os
2-
import secrets
32
from enum import Enum
43
from typing import Optional, TYPE_CHECKING
54

65
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QMetaObject, Qt
76

7+
from electrum import crandom
88
from electrum.i18n import _
99
from electrum.logging import get_logger
1010
from electrum.base_crash_reporter import send_exception_to_crash_reporter
@@ -81,7 +81,7 @@ def enable(self, unified_wallet_password: str):
8181
The encryption key for the wrap_key is stored in the AndroidKeyStore.
8282
This way the wallet password doesn't have to leave the process.
8383
"""
84-
wrap_key, iv = secrets.token_bytes(32), secrets.token_bytes(16)
84+
wrap_key, iv = crandom.get_rand_bytes(32), crandom.get_rand_bytes(16)
8585
wrapped_wallet_password = aes_encrypt_with_iv(
8686
key=wrap_key,
8787
iv=iv,

electrum/lnutil.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
Transaction, PartialTransaction, PartialTxInput, TxOutpoint, PartialTxOutput, opcodes, OPPushDataPubkey
2626
)
2727
from . import bitcoin, crypto, transaction, descriptor, segwit_addr
28+
from . import crandom
2829
from .bitcoin import redeem_script_to_address, address_to_script, construct_witness, \
2930
construct_script, NLOCKTIME_BLOCKHEIGHT_MAX
3031
from .i18n import _
@@ -2007,8 +2008,7 @@ def generate_keypair(node: BIP32Node, key_family: LnKeyFamily) -> Keypair:
20072008

20082009

20092010
def generate_random_keypair() -> Keypair:
2010-
import secrets
2011-
k = secrets.token_bytes(32)
2011+
k = crandom.get_rand_bytes(32)
20122012
cK = ecc.ECPrivkey(k).get_public_key_bytes()
20132013
return Keypair(cK, k)
20142014

electrum/lnworker.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
from . import constants, util, lnutil
3737
from . import bitcoin
38+
from . import crandom
3839
from .util import (
3940
profiler, OldTaskGroup, ESocksProxy, NetworkRetryManager, JsonRPCClient, NotEnoughFunds, EventListener,
4041
event_listener, bfh, InvoiceError, resolve_dns_srv, is_ip_address, log_exceptions, ignore_exceptions,
@@ -664,7 +665,7 @@ class LNGossip(Logger):
664665

665666
def __init__(self, config: 'SimpleConfig'):
666667
self.config = config
667-
seed = os.urandom(32)
668+
seed = crandom.get_rand_bytes(32)
668669
node = BIP32Node.from_rootseed(seed, xtype='standard')
669670
xprv = node.to_xprv()
670671
node_keypair = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.NODE_KEY)
@@ -1695,7 +1696,7 @@ async def _open_channel_coroutine(
16951696
public=public,
16961697
zeroconf=zeroconf,
16971698
opening_fee=opening_fee,
1698-
temp_channel_id=os.urandom(32))
1699+
temp_channel_id=crandom.get_rand_bytes(32))
16991700
chan, funding_tx = await util.wait_for2(coro, LN_P2P_NETWORK_TIMEOUT)
17001701
util.trigger_callback('channels_updated', self.wallet)
17011702
self.wallet.adb.add_transaction(funding_tx) # save tx as local into the wallet
@@ -1752,7 +1753,7 @@ def make_local_config_for_new_channel(
17521753
channel_seed: bytes | None = None,
17531754
) -> LocalConfig:
17541755
if channel_seed is None:
1755-
channel_seed = os.urandom(32)
1756+
channel_seed = crandom.get_rand_bytes(32)
17561757
initial_msat = funding_sat * 1000 - push_msat if initiator == LOCAL else push_msat
17571758

17581759
# sending empty bytes as the upfront_shutdown_script will give us the
@@ -2508,7 +2509,7 @@ async def create_routes_for_payment(
25082509
budget=budget._replace(fee_msat=budget.fee_msat // len(per_trampoline_channel_amounts)),
25092510
)
25102511
# node_features is only used to determine is_tlv
2511-
per_trampoline_secret = os.urandom(32)
2512+
per_trampoline_secret = crandom.get_rand_bytes(32)
25122513
per_trampoline_fees = per_trampoline_amount_with_fees - per_trampoline_amount
25132514
self.logger.info(f'created route with trampoline fee level={paysession.trampoline_fee_level}')
25142515
self.logger.info(f'trampoline hops: {[hop.end_node.hex() for hop in trampoline_route]}')
@@ -2776,7 +2777,7 @@ def create_payment_info(
27762777
) -> bytes:
27772778
if amount_msat == 0:
27782779
raise ValueError("amount_msat must not be 0. Use None instead.")
2779-
payment_preimage = os.urandom(32)
2780+
payment_preimage = crandom.get_rand_bytes(32)
27802781
payment_hash = sha256(payment_preimage)
27812782
min_final_cltv_delta = min_final_cltv_delta or MIN_FINAL_CLTV_DELTA_ACCEPTED
27822783
invoice_features = self._prepare_invoice_features(self.features.for_bolt11_invoice(), amount_msat=amount_msat)
@@ -4175,7 +4176,7 @@ async def _maybe_forward_trampoline(
41754176
payload = any_trampoline_onion.hop_data.payload
41764177
payment_data = payload.get('payment_data')
41774178
try:
4178-
payment_secret = payment_data['payment_secret'] if payment_data else os.urandom(32)
4179+
payment_secret = payment_data['payment_secret'] if payment_data else crandom.get_rand_bytes(32)
41794180
outgoing_node_id = payload["outgoing_node_id"]["outgoing_node_id"]
41804181
amt_to_forward = payload["amt_to_forward"]["amt_to_forward"]
41814182
out_cltv_abs = payload["outgoing_cltv_value"]["outgoing_cltv_value"]
@@ -4332,7 +4333,7 @@ def create_onion_for_route(
43324333
for i in range(len(route)):
43334334
self.logger.info(f" {i}: edge={route[i].short_channel_id} hop_data={hops_data[i]!r}")
43344335
assert final_cltv_abs <= cltv_abs, (final_cltv_abs, cltv_abs)
4335-
session_key = os.urandom(32) # session_key
4336+
session_key = crandom.get_rand_bytes(32) # session_key
43364337
# if we are forwarding a trampoline payment, add trampoline onion
43374338
if trampoline_onion:
43384339
self.logger.info(f'adding trampoline onion to final payload')

electrum/mnemonic.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
from typing import Sequence, Dict, Iterator, Optional
3131
from types import MappingProxyType
3232

33-
from .util import resource_path, bfh, randrange
33+
from . import crandom
34+
from .util import resource_path, bfh
3435
from .crypto import hmac_oneshot
3536
from . import version
3637
from .logging import Logger
@@ -212,7 +213,7 @@ def make_seed(self, *, seed_type: str | None = None, num_bits: int | None = None
212213
# generate random
213214
entropy = 1
214215
while entropy < pow(2, num_bits - bpw): # try again if seed would not contain enough words
215-
entropy = randrange(pow(2, num_bits))
216+
entropy = crandom.get_rand_below(pow(2, num_bits))
216217
# brute-force seed that has correct "version number"
217218
nonce = 0
218219
while True:

electrum/onion_message.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from electrum.lnutil import (LnFeatures, MIN_FINAL_CLTV_DELTA_ACCEPTED, MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED,
4646
MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE)
4747
from electrum.util import OldTaskGroup, log_exceptions, random_shuffled_copy
48+
from electrum import crandom
4849

4950

5051
def now() -> float:
@@ -268,7 +269,7 @@ def send_onion_message_to(
268269
session_key: bytes | None = None
269270
) -> None:
270271
if session_key is None:
271-
session_key = os.urandom(32)
272+
session_key = crandom.get_rand_bytes(32)
272273

273274
if len(node_id_or_blinded_path) > 33: # assume blinded path
274275
with io.BytesIO(node_id_or_blinded_path) as blinded_path_fd:
@@ -447,7 +448,7 @@ def get_blinded_paths_to_me(
447448
continue
448449
payinfos.append(payinfo)
449450
blinded_path = create_blinded_path(
450-
session_key=os.urandom(32),
451+
session_key=crandom.get_rand_bytes(32),
451452
path=[chan.node_id, mynodeid],
452453
final_recipient_data=final_recipient_data,
453454
hop_extras=hop_extras,
@@ -466,7 +467,7 @@ def get_blinded_paths_to_me(
466467
raise NoOnionMessagePeers('no ONION_MESSAGE capable peers')
467468
rpeers = random_shuffled_copy(my_onionmsg_peers)
468469
for peer in rpeers[:max_paths]:
469-
blinded_path = create_blinded_path(os.urandom(32), [peer.pubkey, mynodeid], final_recipient_data)
470+
blinded_path = create_blinded_path(crandom.get_rand_bytes(32), [peer.pubkey, mynodeid], final_recipient_data)
470471
result.append(blinded_path)
471472

472473
assert result
@@ -683,7 +684,7 @@ def submit_send(
683684
684685
:return: returns awaitable task"""
685686
if not key:
686-
key = os.urandom(8)
687+
key = crandom.get_rand_bytes(8)
687688
assert type(key) is bytes and len(key) >= 8
688689

689690
self.logger.debug(f'submit_send {key=} {payload=} {node_id_or_blinded_paths=}')

electrum/plugin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
make_dir, make_aiohttp_session)
5151
from . import bip32
5252
from . import plugins
53+
from . import crandom
5354
from .simple_config import SimpleConfig
5455
from .logging import get_logger, Logger
5556
from .crypto import sha256
@@ -440,7 +441,7 @@ def _delete_plugin_key_from_windows_registry(self) -> None:
440441
pass
441442

442443
def create_new_key(self, password:str) -> str:
443-
salt = os.urandom(32)
444+
salt = crandom.get_rand_bytes(32)
444445
privkey = self.derive_privkey(password, salt)
445446
pubkey = privkey.get_public_key_bytes()
446447
key = bytes([PLUGIN_PASSWORD_VERSION]) + salt + pubkey

0 commit comments

Comments
 (0)