Browse Source

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
The MMGen Project 5 years ago
parent
commit
4714ef84
4 changed files with 77 additions and 0 deletions
  1. 54 0
      mmgen/seed.py
  2. 3 0
      mmgen/util.py
  3. 5 0
      test/test_py_d/common.py
  4. 15 0
      test/test_py_d/ts_misc.py

+ 54 - 0
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

+ 3 - 0
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:

+ 5 - 0
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)

+ 15 - 0
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"