ts_input.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # Project source code repository: https://github.com/mmgen/mmgen
  7. # Licensed according to the terms of GPL Version 3. See LICENSE for details.
  8. """
  9. ts_input.py: user input tests for the MMGen test.py test suite
  10. """
  11. from ..include.common import *
  12. from .ts_base import *
  13. from .input import *
  14. from mmgen.wallet import get_wallet_cls
  15. class TestSuiteInput(TestSuiteBase):
  16. 'user input'
  17. networks = ('btc',)
  18. tmpdir_nums = [1]
  19. color = True
  20. cmd_group_in = (
  21. ('subgroup.password', []),
  22. ('subgroup.misc', []),
  23. ('subgroup.wallet', []),
  24. ('subgroup.mnemonic', []),
  25. ('subgroup.dieroll', []),
  26. )
  27. cmd_subgroups = {
  28. 'password': (
  29. 'password entry via line_input()',
  30. ('password_entry_noecho', 'utf8 password entry'),
  31. ('password_entry_echo', 'utf8 password entry (echoed)'),
  32. ),
  33. 'misc': (
  34. 'miscellaneous user-level UI functions',
  35. ('get_seed_from_stdin', 'reading seed phrase from STDIN'),
  36. ),
  37. 'wallet': (
  38. 'hash preset, password and label entry',
  39. ('get_passphrase_ui', 'hash preset, password and label (wallet.py)'),
  40. ('get_passphrase_cmdline', 'hash preset, password and label (wallet.py - from cmdline)'),
  41. ('get_passphrase_crypto', 'hash preset, password and label (crypto.py)'),
  42. ),
  43. 'mnemonic': (
  44. 'mnemonic entry',
  45. ('mnemonic_entry_mmgen', 'stealth mnemonic entry (mmgen)'),
  46. ('mnemonic_entry_mmgen_minimal', 'stealth mnemonic entry (mmgen - minimal entry mode)'),
  47. ('mnemonic_entry_bip39', 'stealth mnemonic entry (bip39)'),
  48. ('mnemonic_entry_bip39_short', 'stealth mnemonic entry (bip39 - short entry mode)'),
  49. ('mn2hex_interactive_mmgen', 'mn2hex_interactive (mmgen)'),
  50. ('mn2hex_interactive_mmgen_fixed','mn2hex_interactive (mmgen - fixed (10-letter) entry mode)'),
  51. ('mn2hex_interactive_bip39', 'mn2hex_interactive (bip39)'),
  52. ('mn2hex_interactive_bip39_short','mn2hex_interactive (bip39 - short entry mode (+pad entry))'),
  53. ('mn2hex_interactive_bip39_fixed','mn2hex_interactive (bip39 - fixed (4-letter) entry mode)'),
  54. ('mn2hex_interactive_xmr', 'mn2hex_interactive (xmrseed)'),
  55. ('mn2hex_interactive_xmr_short', 'mn2hex_interactive (xmrseed - short entry mode)'),
  56. ),
  57. 'dieroll': (
  58. 'dieroll entry',
  59. ('dieroll_entry', 'dieroll entry (base6d)'),
  60. ('dieroll_entry_usrrand', 'dieroll entry (base6d) with added user entropy'),
  61. )
  62. }
  63. def get_seed_from_stdin(self):
  64. self.spawn('',msg_only=True)
  65. from subprocess import run,PIPE
  66. cmd = ['python3','cmds/mmgen-walletconv','--in-fmt=words','--out-fmt=bip39','--outdir=test/trash']
  67. mn = sample_mn['mmgen']['mn']
  68. os.environ['MMGEN_TEST_SUITE'] = ''
  69. cp = run( cmd, input=mn.encode(), stdout=PIPE, stderr=PIPE )
  70. from mmgen.color import set_vt100
  71. set_vt100()
  72. os.environ['MMGEN_TEST_SUITE'] = '1'
  73. assert b'written to file' in cp.stderr, "test 'get_seed_from_stdin' failed"
  74. imsg(cp.stderr.decode().strip())
  75. return 'ok'
  76. def get_passphrase_ui(self):
  77. t = self.spawn('test/misc/get_passphrase.py',['--usr-randchars=0','seed'],cmd_dir='.')
  78. # 1 - new wallet, default hp,label;empty pw
  79. t.expect('accept the default.*: ','\n',regex=True)
  80. # bad repeat
  81. t.expect('new MMGen wallet: ','pass1\n')
  82. t.expect('peat passphrase: ','pass2\n')
  83. # good repeat
  84. t.expect('new MMGen wallet: ','\n')
  85. t.expect('peat passphrase: ','\n')
  86. t.expect('mpty pass')
  87. t.expect('no label: ','\n')
  88. t.expect('[][3][No Label]')
  89. # 2 - new wallet, user-selected hp,pw,label
  90. t.expect('accept the default.*: ', '1\n', regex=True)
  91. t.expect('new MMGen wallet: ','pass1\n')
  92. t.expect('peat passphrase: ','pass1\n')
  93. t.expect('no label: ','lbl1\n')
  94. t.expect('[pass1][1][lbl1]')
  95. # 3 - passchg, nothing changes
  96. t.expect('new hash preset')
  97. t.expect('reuse the old value.*: ','\n',regex=True)
  98. t.expect('unchanged')
  99. t.expect('new passphrase.*: ','pass1\n',regex=True)
  100. t.expect('peat new passphrase: ','pass1\n')
  101. t.expect('unchanged')
  102. t.expect('reuse the label .*: ','\n',regex=True)
  103. t.expect('unchanged')
  104. t.expect('[pass1][1][lbl1]')
  105. # 4 - passchg, everything changes
  106. t.expect('new hash preset')
  107. t.expect('reuse the old value.*: ','2\n',regex=True)
  108. t.expect(' changed to')
  109. t.expect('new passphrase.*: ','pass2\n',regex=True)
  110. t.expect('peat new passphrase: ','pass2\n')
  111. t.expect(' changed')
  112. t.expect('reuse the label .*: ','lbl2\n',regex=True)
  113. t.expect(' changed to')
  114. t.expect('[pass2][2][lbl2]')
  115. # 5 - wallet from file
  116. t.expect('from file')
  117. # bad passphrase
  118. t.expect('passphrase for MMGen wallet: ','bad\n')
  119. t.expect('Trying again')
  120. # good passphrase
  121. t.expect('passphrase for MMGen wallet: ','reference password\n')
  122. t.expect('[reference password][1][No Label]')
  123. return t
  124. def get_passphrase_cmdline(self):
  125. with open('test/trash/pwfile','w') as fp:
  126. fp.write('reference password\n')
  127. t = self.spawn('test/misc/get_passphrase.py', [
  128. '--usr-randchars=0',
  129. '--label=MyLabel',
  130. '--passwd-file=test/trash/pwfile',
  131. '--hash-preset=1',
  132. 'seed' ],
  133. cmd_dir = '.' )
  134. for foo in range(4):
  135. t.expect('[reference password][1][MyLabel]')
  136. return t
  137. def get_passphrase_crypto(self):
  138. t = self.spawn('test/misc/get_passphrase.py',['--usr-randchars=0','crypto'],cmd_dir='.')
  139. # new passwd
  140. t.expect('passphrase for .*: ', 'x\n', regex=True)
  141. t.expect('peat passphrase: ', '\n')
  142. t.expect('passphrase for .*: ', 'pass1\n', regex=True)
  143. t.expect('peat passphrase: ', 'pass1\n')
  144. t.expect('[pass1]')
  145. # existing passwd
  146. t.expect('passphrase for .*: ', 'pass2\n', regex=True)
  147. t.expect('[pass2]')
  148. # hash preset
  149. t.expect('accept the default .*: ', '0\n', regex=True)
  150. t.expect('nvalid')
  151. t.expect('accept the default .*: ', '8\n', regex=True)
  152. t.expect('nvalid')
  153. t.expect('accept the default .*: ', '7\n', regex=True)
  154. t.expect('[7]')
  155. # hash preset (default)
  156. t.expect('accept the default .*: ', '\n', regex=True)
  157. t.expect(f'[{g.dfl_hash_preset}]')
  158. return t
  159. def _password_entry(self,prompt,opts=[],term=False):
  160. t = self.spawn( 'test/misc/password_entry.py', opts, cmd_dir='.', pexpect_spawn=term )
  161. imsg('Terminal: {}'.format(term))
  162. pw = 'abc-α'
  163. t.expect(prompt,pw+'\n')
  164. ret = t.expect_getend('Entered: ')
  165. assert ret == pw, f'Password mismatch! {ret} != {pw}'
  166. return t
  167. # TODO: has this been fixed?
  168. winskip_msg = """
  169. getpass() doesn't work with pexpect.popen_spawn on MSYS2!
  170. Perform the following test by hand with non-ASCII password abc-α
  171. or another password in your native alphabet:
  172. test/misc/input_func.py{} passphrase
  173. """
  174. def password_entry_noecho(self,term=False):
  175. return self._password_entry('Enter passphrase: ',term=term)
  176. def password_entry_echo(self,term=False):
  177. return self._password_entry('Enter passphrase (echoed): ',['--echo-passphrase'],term=term)
  178. def _mn2hex(self,fmt,entry_mode='full',mn=None,pad_entry=False,enter_for_dfl=False):
  179. mn = mn or sample_mn[fmt]['mn'].split()
  180. t = self.spawn('mmgen-tool',['mn2hex_interactive','fmt='+fmt,'mn_len=12','print_mn=1'])
  181. from mmgen.mn_entry import mn_entry
  182. mne = mn_entry(fmt,entry_mode)
  183. t.expect(
  184. 'Type a number.*: ',
  185. ('\n' if enter_for_dfl else str(mne.entry_modes.index(entry_mode)+1)),
  186. regex = True )
  187. t.expect('Using (.+) entry mode',regex=True)
  188. mode = strip_ansi_escapes(t.p.match.group(1)).lower()
  189. assert mode == mne.em.name.lower(), f'{mode} != {mne.em.name.lower()}'
  190. stealth_mnemonic_entry(t,mne,mn,entry_mode=entry_mode,pad_entry=pad_entry)
  191. t.expect(sample_mn[fmt]['hex'])
  192. return t
  193. def _user_seed_entry(self,fmt,usr_rand=False,out_fmt=None,entry_mode='full',mn=None):
  194. wcls = get_wallet_cls(fmt_code=fmt)
  195. wf = os.path.join(ref_dir,f'FE3C6545.{wcls.ext}')
  196. if wcls.base_type == 'mnemonic':
  197. mn = mn or read_from_file(wf).strip().split()
  198. elif wcls.type == 'dieroll':
  199. mn = mn or list(remove_whitespace(read_from_file(wf)))
  200. for idx,val in ((5,'x'),(18,'0'),(30,'7'),(44,'9')):
  201. mn.insert(idx,val)
  202. t = self.spawn('mmgen-walletconv',['-r10','-S','-i',fmt,'-o',out_fmt or fmt])
  203. t.expect(f'{capfirst(wcls.base_type or wcls.type)} type:.*{wcls.mn_type}',regex=True)
  204. t.expect(wcls.choose_seedlen_prompt,'1')
  205. t.expect('(Y/n): ','y')
  206. if wcls.base_type == 'mnemonic':
  207. t.expect('Type a number.*: ','6',regex=True)
  208. t.expect('invalid')
  209. from mmgen.mn_entry import mn_entry
  210. mne = mn_entry(fmt,entry_mode)
  211. t.expect('Type a number.*: ',str(mne.entry_modes.index(entry_mode)+1),regex=True)
  212. t.expect('Using (.+) entry mode',regex=True)
  213. mode = strip_ansi_escapes(t.p.match.group(1)).lower()
  214. assert mode == mne.em.name.lower(), f'{mode} != {mne.em.name.lower()}'
  215. stealth_mnemonic_entry(t,mne,mn,entry_mode=entry_mode)
  216. elif wcls.type == 'dieroll':
  217. user_dieroll_entry(t,mn)
  218. if usr_rand:
  219. t.expect(wcls.user_entropy_prompt,'y')
  220. t.usr_rand(10)
  221. else:
  222. t.expect(wcls.user_entropy_prompt,'n')
  223. if not usr_rand:
  224. sid_chk = 'FE3C6545'
  225. sid = t.expect_getend(f'Valid {wcls.desc} for Seed ID ')
  226. sid = strip_ansi_escapes(sid.split(',')[0])
  227. assert sid == sid_chk, f'Seed ID mismatch! {sid} != {sid_chk}'
  228. t.expect('to confirm: ','YES\n')
  229. return t
  230. def mnemonic_entry_mmgen_minimal(self):
  231. from mmgen.mn_entry import mn_entry
  232. # erase_chars: '\b\x7f'
  233. m = mn_entry('mmgen','minimal')
  234. np = 2
  235. mn = (
  236. 'z',
  237. 'aa',
  238. '1d2ud',
  239. 'fo{}ot{}#'.format('1' * np, '2' * (m.em.pad_max - np)), # substring of 'football'
  240. 'des1p)%erate\n', # substring of 'desperately'
  241. '#t!(ie',
  242. '!)sto8o',
  243. 'the123m8!%s',
  244. '349t(5)rip',
  245. 'di\b\bdesce',
  246. 'cea',
  247. 'bu\x7f\x7fsuic',
  248. 'app\bpl',
  249. 'wd',
  250. 'busy')
  251. return self._user_seed_entry('words',entry_mode='minimal',mn=mn)
  252. def mnemonic_entry_mmgen(self): return self._user_seed_entry('words',entry_mode='full')
  253. def mnemonic_entry_bip39(self): return self._user_seed_entry('bip39',entry_mode='full')
  254. def mnemonic_entry_bip39_short(self): return self._user_seed_entry('bip39',entry_mode='short')
  255. def mn2hex_interactive_mmgen(self): return self._mn2hex('mmgen',entry_mode='full')
  256. def mn2hex_interactive_mmgen_fixed(self): return self._mn2hex('mmgen',entry_mode='fixed')
  257. def mn2hex_interactive_bip39(self): return self._mn2hex('bip39',entry_mode='full')
  258. def mn2hex_interactive_bip39_short(self): return self._mn2hex('bip39',entry_mode='short',pad_entry=True)
  259. def mn2hex_interactive_bip39_fixed(self): return self._mn2hex('bip39',entry_mode='fixed',enter_for_dfl=True)
  260. def mn2hex_interactive_xmr(self): return self._mn2hex('xmrseed',entry_mode='full')
  261. def mn2hex_interactive_xmr_short(self): return self._mn2hex('xmrseed',entry_mode='short')
  262. def dieroll_entry(self): return self._user_seed_entry('dieroll')
  263. def dieroll_entry_usrrand(self): return self._user_seed_entry('dieroll',usr_rand=True,out_fmt='bip39')