Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mnemonic.make_seed: add "extra_entropy" arg, and expose it to CLI/RPC #8839

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions electrum/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,10 +353,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):
Copy link
Member Author

Choose a reason for hiding this comment

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

ThomasV remarked:

re extra_entropy, I think that option should not be named that way. it is misleading. some users might believe that their seed will have more bits of entropy than what is passed in nbits

Copy link
Member Author

Choose a reason for hiding this comment

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

Alternative names:

  • addentropy, add_entropy, add_to_entropy,
  • mixin_entropy, mix_in_entropy,
  • user_entropy

Out of these, I prefer add_entropy or user_entropy, and re behaviour, we can describe how the value is used here:

'extra_entropy': (None, "Arbitrary string used as additional entropy"),

"""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')
Expand Down Expand Up @@ -1437,6 +1439,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"),
Expand Down
13 changes: 11 additions & 2 deletions electrum/mnemonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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'
Expand All @@ -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:
Expand Down
23 changes: 23 additions & 0 deletions tests/test_mnemonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down