From 4714ef84d5bbc270bdee31612a57bd3493421523 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Wed, 30 Oct 2019 21:49:17 +0000 Subject: [PATCH] die roll wallet: interactive input support - Create a wallet of any MMGen-supported format by inputting rolls of a die interactively at the keyboard. Testing: $ test/test.py -e input Examples: Create a default MMGen wallet from interactive die rolls: $ mmgen-walletconv -i dieroll Create a BIP39 mnemonic seed phrase from interactive die rolls, outputting to screen without prompting: $ mmgen-walletconv -Sq -i dieroll -o bip39 --- mmgen/seed.py | 54 +++++++++++++++++++++++++++++++++++++++ mmgen/util.py | 3 +++ test/test_py_d/common.py | 5 ++++ test/test_py_d/ts_misc.py | 15 +++++++++++ 4 files changed, 77 insertions(+) diff --git a/mmgen/seed.py b/mmgen/seed.py index 35fd50ba..07c97e05 100755 --- a/mmgen/seed.py +++ b/mmgen/seed.py @@ -1031,24 +1031,78 @@ class DieRollSeedFile(SeedSourceUnenc): desc = 'base6d die roll seed data' ext = 'b6d' conv_cls = baseconv + wclass = 'dieroll' + wl_id = 'b6d' + mn_type = 'base6d' + choose_seedlen_prompt = 'Choose a seed length: 1) 128 bits, 2) 192 bits, 3) 256 bits: ' + choose_seedlen_confirm = 'Seed length of {} bits chosen. OK?' + user_entropy_prompt = 'Would you like to provide some additional entropy from the keyboard?' + interactive_input = False def _format(self): d = baseconv.frombytes(self.seed.data,'b6d',pad='seed',tostr=True) + '\n' self.fmt_data = block_format(d,gw=5,cols=5) def _deformat(self): + d = self.fmt_data.translate(dict((ord(ws),None) for ws in '\t\n ')) # truncate seed to correct length, discarding high bits seed_len = self.conv_cls.seedlen_map_rev['b6d'][len(d)] seed_bytes = baseconv.tobytes(d,'b6d',pad='seed')[-seed_len:] + if self.interactive_input and opt.usr_randchars: + if keypress_confirm(self.user_entropy_prompt): + seed_bytes = add_user_random(seed_bytes,'die roll data') + self.desc += ' plus user-supplied entropy' + self.seed = Seed(seed_bytes) self.ssdata.hexseed = seed_bytes.hex() check_usr_seed_len(self.seed.bitlen) return True + def _get_data_from_user(self,desc): + + if not g.stdin_tty: + return get_data_from_user(desc) + + seed_bitlens = [n*8 for n in sorted(self.conv_cls.seedlen_map['b6d'])] + seed_bitlen = self._choose_seedlen(self.wclass,seed_bitlens,self.mn_type) + nDierolls = self.conv_cls.seedlen_map['b6d'][seed_bitlen // 8] + + m = 'For a {sb}-bit seed you must roll the die {nd} times. After each die roll,\n' + m += 'enter the result on the keyboard as a digit. If you make an invalid entry,\n' + m += "you'll be prompted to re-enter it." + + msg('\n'+m.format(sb=seed_bitlen,nd=nDierolls)+'\n') + + b6d_digits = self.conv_cls.digits['b6d'] + + from mmgen.term import get_char,get_char + def get_digit(n): + p = '\b\b\b \rEnter die roll #{}: '+ CUR_SHOW + sleep = 0.3 + while True: + ch = get_char(p.format(n),num_chars=1,sleep=sleep).decode() + if ch in b6d_digits: + msg_r(CUR_HIDE + ' OK') + return ch + else: + msg_r(CUR_HIDE + '\rInvalid entry ') + sleep = 0.7 + p = '\r' + ' '*25 + CUR_SHOW + p + + dierolls,n = [],1 + while len(dierolls) < nDierolls: + dierolls.append(get_digit(n)) + n += 1 + + msg('Die rolls successfully entered' + CUR_SHOW) + self.interactive_input = True + + return ''.join(dierolls) + class PlainHexSeedFile(SeedSourceUnenc): stdin_ok = True diff --git a/mmgen/util.py b/mmgen/util.py index bd121319..1213b4ae 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -96,6 +96,9 @@ def pp_fmt(d): def pp_msg(d): msg(pp_fmt(d)) +CUR_HIDE = '\033[?25l' +CUR_SHOW = '\033[?25h' + def set_for_type(val,refval,desc,invert_bool=False,src=None): src_str = (''," in '{}'".format(src))[bool(src)] if type(refval) == bool: diff --git a/test/test_py_d/common.py b/test/test_py_d/common.py index 9036cc38..93a0c499 100755 --- a/test/test_py_d/common.py +++ b/test/test_py_d/common.py @@ -173,3 +173,8 @@ def stealth_mnemonic_entry(t,mn,fmt): for j in range(len(w)): t.send(w[j]) time.sleep(0.005) + +def user_dieroll_entry(t,data): + for s in data: + t.expect(r'Enter die roll #.+: ',s,regex=True) + time.sleep(0.005) diff --git a/test/test_py_d/ts_misc.py b/test/test_py_d/ts_misc.py index dc4d852e..4d581383 100755 --- a/test/test_py_d/ts_misc.py +++ b/test/test_py_d/ts_misc.py @@ -120,6 +120,8 @@ class TestSuiteInput(TestSuiteBase): ('password_entry_echo', (1,"utf8 password entry (echoed)", [])), ('mnemonic_entry_mmgen', (1,"stealth mnemonic entry (MMGen native)", [])), ('mnemonic_entry_bip39', (1,"stealth mnemonic entry (BIP39)", [])), + ('dieroll_entry', (1,"dieroll entry (base6d)", [])), + ('dieroll_entry_usrrand', (1,"dieroll entry (base6d) with added user entropy", [])), ) def password_entry(self,prompt,cmd_args): @@ -150,12 +152,23 @@ class TestSuiteInput(TestSuiteBase): if wcls.wclass == 'mnemonic': mn = read_from_file(wf).strip().split() mn = ['foo'] + mn[:5] + ['grac','graceful'] + mn[5:] + elif wcls.wclass == 'dieroll': + mn = list(read_from_file(wf).strip().translate(dict((ord(ws),None) for ws in '\t\n '))) + for idx,val in ((5,'x'),(18,'0'),(30,'7'),(44,'9')): + mn.insert(idx,val) t = self.spawn('mmgen-walletconv',['-r10','-S','-i',fmt,'-o',out_fmt or fmt]) t.expect('{} type: {}'.format(capfirst(wcls.wclass),wcls.mn_type)) t.expect(wcls.choose_seedlen_prompt,'1') t.expect('(Y/n): ','y') if wcls.wclass == 'mnemonic': stealth_mnemonic_entry(t,mn,fmt=fmt) + elif wcls.wclass == 'dieroll': + user_dieroll_entry(t,mn) + if usr_rand: + t.expect(wcls.user_entropy_prompt,'y') + t.usr_rand(10) + else: + t.expect(wcls.user_entropy_prompt,'n') if not usr_rand: sid_chk = 'FE3C6545' sid = t.expect_getend('Valid {} for Seed ID '.format(wcls.desc))[:8] @@ -166,6 +179,8 @@ class TestSuiteInput(TestSuiteBase): def mnemonic_entry_mmgen(self): return self._user_seed_entry('words') def mnemonic_entry_bip39(self): return self._user_seed_entry('bip39') + def dieroll_entry(self): return self._user_seed_entry('dieroll') + def dieroll_entry_usrrand(self):return self._user_seed_entry('dieroll',usr_rand=True,out_fmt='bip39') class TestSuiteTool(TestSuiteMain,TestSuiteBase): "tests for interactive 'mmgen-tool' commands"