diff --git a/electrum/commands.py b/electrum/commands.py index d16d04cad071..f689d58c623a 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -352,10 +352,12 @@ async def setconfig(self, key, value): cv.set(value) @command('') - async def make_seed(self, nbits=None, language=None, seed_type=None): + async def make_seed(self, nbits=None, language=None, seed_type=None, extra_entropy: str = None): """Create a seed""" + if extra_entropy is not None: + extra_entropy = extra_entropy.encode("utf-8") from .mnemonic import Mnemonic - s = Mnemonic(language).make_seed(seed_type=seed_type, num_bits=nbits) + s = Mnemonic(language).make_seed(seed_type=seed_type, num_bits=nbits, extra_entropy=extra_entropy) return s @command('n') @@ -1433,6 +1435,7 @@ def eval_bool(x: str) -> bool: 'from_coins': (None, "Source coins (must be in wallet; use sweep to spend from non-wallet address)."), 'change_addr': ("-c", "Change address. Default is a spare address, or the source address if it's not in the wallet"), 'nbits': (None, "Number of bits of entropy"), + 'extra_entropy': (None, "Arbitrary string used as additional entropy"), 'seed_type': (None, "The type of seed to create, e.g. 'standard' or 'segwit'"), 'language': ("-L", "Default language for wordlist"), 'passphrase': (None, "Seed extension"), diff --git a/electrum/mnemonic.py b/electrum/mnemonic.py index d458c68c7213..11be6fead569 100644 --- a/electrum/mnemonic.py +++ b/electrum/mnemonic.py @@ -31,7 +31,7 @@ from types import MappingProxyType from .util import resource_path, bfh, randrange -from .crypto import hmac_oneshot +from .crypto import hmac_oneshot, sha256 from . import version from .logging import Logger @@ -198,7 +198,7 @@ def mnemonic_decode(self, seed: str) -> int: i = i*n + k return i - def make_seed(self, *, seed_type: str = None, num_bits: int = None) -> str: + def make_seed(self, *, seed_type: str = None, num_bits: int = None, extra_entropy: bytes = None) -> str: from .keystore import bip39_is_checksum_valid if seed_type is None: seed_type = 'segwit' @@ -209,10 +209,19 @@ def make_seed(self, *, seed_type: str = None, num_bits: int = None) -> str: bpw = math.log(len(self.wordlist), 2) num_bits = int(math.ceil(num_bits/bpw) * bpw) self.logger.info(f"make_seed. prefix: '{prefix}', entropy: {num_bits} bits") + # prepare user-provided additional entropy + if extra_entropy: + assert isinstance(extra_entropy, bytes), type(extra_entropy) + extra_entropy = sha256(extra_entropy) + extra_entropy_int = int.from_bytes(extra_entropy, byteorder="big", signed=False) + extra_entropy_int &= ((1 << num_bits) - 1) # limit length to "num_bits" + else: + extra_entropy_int = 0 # generate random entropy = 1 while entropy < pow(2, num_bits - bpw): # try again if seed would not contain enough words entropy = randrange(pow(2, num_bits)) + entropy ^= extra_entropy_int # mix-in provided additional entropy, if any # brute-force seed that has correct "version number" nonce = 0 while True: diff --git a/electrum/tests/test_mnemonic.py b/electrum/tests/test_mnemonic.py index b330909ad9ff..6ba4772ab1b3 100644 --- a/electrum/tests/test_mnemonic.py +++ b/electrum/tests/test_mnemonic.py @@ -133,6 +133,29 @@ def test_random_seeds(self): self.assertTrue(12 <= len(seed.split()) <= 13) self.assertEqual(iters, len(pool)) + def test_extra_entropy(self): + pool = set() + num_pool = 0 + extra_entropies = ( + b"asd", + UNICODE_HORROR.encode("utf-8"), + (2**4096-1).to_bytes(length=512, byteorder="big"), + ) + m = mnemonic.Mnemonic(lang='en') + for ee in extra_entropies: + seed = m.make_seed(seed_type="standard", extra_entropy=ee, num_bits=128) + pool.add(seed) + num_pool += 1 + with self.subTest(seed=seed, msg="num-words"): + self.assertTrue(12 <= len(seed.split()) <= 13) + for ee in extra_entropies: + seed = m.make_seed(seed_type="standard", extra_entropy=ee, num_bits=256) + pool.add(seed) + num_pool += 1 + with self.subTest(seed=seed, msg="num-words"): + self.assertTrue(24 <= len(seed.split()) <= 25) + self.assertEqual(num_pool, len(pool)) + class Test_OldMnemonic(ElectrumTestCase):