ts_input.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2021 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 Wallet
  15. class TestSuiteInput(TestSuiteBase):
  16. 'user input'
  17. networks = ('btc',)
  18. tmpdir_nums = []
  19. color = True
  20. cmd_group = (
  21. ('get_passphrase_ui', (1,"hash preset, password and label (wallet.py)", [])),
  22. ('get_passphrase_cmdline', (1,"hash preset, password and label (wallet.py - from cmdline)", [])),
  23. ('get_passphrase_crypto', (1,"hash preset, password and label (crypto.py)", [])),
  24. ('password_entry_noecho', (1,"utf8 password entry", [])),
  25. ('password_entry_echo', (1,"utf8 password entry (echoed)", [])),
  26. ('mnemonic_entry_mmgen', (1,"stealth mnemonic entry (mmgen)", [])),
  27. ('mnemonic_entry_mmgen_minimal', (1,"stealth mnemonic entry (mmgen - minimal entry mode)", [])),
  28. ('mnemonic_entry_bip39', (1,"stealth mnemonic entry (bip39)", [])),
  29. ('mnemonic_entry_bip39_short', (1,"stealth mnemonic entry (bip39 - short entry mode)", [])),
  30. ('mn2hex_interactive_mmgen', (1,"mn2hex_interactive (mmgen)", [])),
  31. ('mn2hex_interactive_mmgen_fixed',(1,"mn2hex_interactive (mmgen - fixed (10-letter) entry mode)", [])),
  32. ('mn2hex_interactive_bip39', (1,"mn2hex_interactive (bip39)", [])),
  33. ('mn2hex_interactive_bip39_short',(1,"mn2hex_interactive (bip39 - short entry mode (+pad entry))", [])),
  34. ('mn2hex_interactive_bip39_fixed',(1,"mn2hex_interactive (bip39 - fixed (4-letter) entry mode)", [])),
  35. ('mn2hex_interactive_xmr', (1,"mn2hex_interactive (xmrseed)", [])),
  36. ('mn2hex_interactive_xmr_short', (1,"mn2hex_interactive (xmrseed - short entry mode)", [])),
  37. ('dieroll_entry', (1,"dieroll entry (base6d)", [])),
  38. ('dieroll_entry_usrrand', (1,"dieroll entry (base6d) with added user entropy", [])),
  39. )
  40. def get_passphrase_ui(self):
  41. t = self.spawn('test/misc/get_passphrase.py',['--usr-randchars=0','seed'],cmd_dir='.')
  42. # 1 - new wallet, default hp,label;empty pw
  43. t.expect('accept the default.*: ','\n',regex=True)
  44. # bad repeat
  45. t.expect('new MMGen wallet: ','pass1\n')
  46. t.expect('peat passphrase: ','pass2\n')
  47. # good repeat
  48. t.expect('new MMGen wallet: ','\n')
  49. t.expect('peat passphrase: ','\n')
  50. t.expect('mpty pass')
  51. t.expect('no label: ','\n')
  52. t.expect('[][3][No Label]')
  53. # 2 - new wallet, user-selected hp,pw,label
  54. t.expect('accept the default.*: ', '1\n', regex=True)
  55. t.expect('new MMGen wallet: ','pass1\n')
  56. t.expect('peat passphrase: ','pass1\n')
  57. t.expect('no label: ','lbl1\n')
  58. t.expect('[pass1][1][lbl1]')
  59. # 3 - passchg, nothing changes
  60. t.expect('new hash preset')
  61. t.expect('reuse the old value.*: ','\n',regex=True)
  62. t.expect('unchanged')
  63. t.expect('new passphrase.*: ','pass1\n',regex=True)
  64. t.expect('peat passphrase: ','pass1\n')
  65. t.expect('unchanged')
  66. t.expect('reuse the label .*: ','\n',regex=True)
  67. t.expect('unchanged')
  68. t.expect('[pass1][1][lbl1]')
  69. # 4 - passchg, everything changes
  70. t.expect('new hash preset')
  71. t.expect('reuse the old value.*: ','2\n',regex=True)
  72. t.expect(' changed to')
  73. t.expect('new passphrase.*: ','pass2\n',regex=True)
  74. t.expect('peat passphrase: ','pass2\n')
  75. t.expect(' changed')
  76. t.expect('reuse the label .*: ','lbl2\n',regex=True)
  77. t.expect(' changed to')
  78. t.expect('[pass2][2][lbl2]')
  79. # 5 - wallet from file
  80. t.expect('from file')
  81. # bad passphrase
  82. t.expect('passphrase for MMGen wallet: ','bad\n')
  83. t.expect('Trying again')
  84. # good passphrase
  85. t.expect('passphrase for MMGen wallet: ','reference password\n')
  86. t.expect('[reference password][1][No Label]')
  87. t.read()
  88. return t
  89. def get_passphrase_cmdline(self):
  90. open('test/trash/pwfile','w').write('reference password\n')
  91. t = self.spawn('test/misc/get_passphrase.py', [
  92. '--usr-randchars=0',
  93. '--label=MyLabel',
  94. '--passwd-file=test/trash/pwfile',
  95. '--hash-preset=1',
  96. 'seed' ],
  97. cmd_dir = '.' )
  98. for foo in range(4):
  99. t.expect('[reference password][1][MyLabel]')
  100. t.read()
  101. return t
  102. def get_passphrase_crypto(self):
  103. t = self.spawn('test/misc/get_passphrase.py',['--usr-randchars=0','crypto'],cmd_dir='.')
  104. # new passwd
  105. t.expect('passphrase for .*: ', 'x\n', regex=True)
  106. t.expect('peat passphrase: ', '\n')
  107. t.expect('passphrase for .*: ', 'pass1\n', regex=True)
  108. t.expect('peat passphrase: ', 'pass1\n')
  109. t.expect('[pass1]')
  110. # existing passwd
  111. t.expect('passphrase for .*: ', 'pass2\n', regex=True)
  112. t.expect('[pass2]')
  113. # hash preset
  114. t.expect('accept the default .*: ', '0\n', regex=True)
  115. t.expect('nvalid')
  116. t.expect('accept the default .*: ', '8\n', regex=True)
  117. t.expect('nvalid')
  118. t.expect('accept the default .*: ', '7\n', regex=True)
  119. t.expect('[7]')
  120. # hash preset (default)
  121. t.expect('accept the default .*: ', '\n', regex=True)
  122. t.expect(f'[{g.dfl_hash_preset}]')
  123. t.read()
  124. return t
  125. def password_entry(self,prompt,cmd_args):
  126. t = self.spawn('test/misc/password_entry.py',cmd_args,cmd_dir='.')
  127. pw = 'abc-α'
  128. t.expect(prompt,pw)
  129. ret = t.expect_getend('Entered: ')
  130. assert ret == pw,'Password mismatch! {} != {}'.format(ret,pw)
  131. return t
  132. def password_entry_noecho(self):
  133. if self.skip_for_win():
  134. m = "getpass() doesn't work with pexpect.popen_spawn!\n"
  135. m += 'Perform the following test by hand with non-ASCII password abc-α:\n'
  136. m += ' test/misc/password_entry.py'
  137. return ('skip_warn',m)
  138. return self.password_entry('Enter passphrase: ',[])
  139. def password_entry_echo(self):
  140. if self.skip_for_win():
  141. m = "getpass() doesn't work with pexpect.popen_spawn!\n"
  142. m += 'Perform the following test by hand with non-ASCII password abc-α:\n'
  143. m += ' test/misc/password_entry.py --echo-passphrase'
  144. return ('skip_warn',m)
  145. return self.password_entry('Enter passphrase (echoed): ',['--echo-passphrase'])
  146. def _mn2hex(self,fmt,entry_mode='full',mn=None,pad_entry=False,enter_for_dfl=False):
  147. mn = mn or sample_mn[fmt]['mn'].split()
  148. t = self.spawn('mmgen-tool',['mn2hex_interactive','fmt='+fmt,'mn_len=12','print_mn=1'])
  149. from mmgen.mn_entry import mn_entry
  150. mne = mn_entry(fmt,entry_mode)
  151. t.expect(
  152. 'Type a number.*: ',
  153. ('\n' if enter_for_dfl else str(mne.entry_modes.index(entry_mode)+1)),
  154. regex = True )
  155. t.expect('Using (.+) entry mode',regex=True)
  156. mode = strip_ansi_escapes(t.p.match.group(1)).lower()
  157. assert mode == mne.em.name.lower(), '{} != {}'.format(mode,mne.em.name.lower())
  158. stealth_mnemonic_entry(t,mne,mn,entry_mode=entry_mode,pad_entry=pad_entry)
  159. t.expect(sample_mn[fmt]['hex'])
  160. t.read()
  161. return t
  162. def _user_seed_entry(self,fmt,usr_rand=False,out_fmt=None,entry_mode='full',mn=None):
  163. wcls = Wallet.fmt_code_to_type(fmt)
  164. wf = os.path.join(ref_dir,'FE3C6545.{}'.format(wcls.ext))
  165. if wcls.wclass == 'mnemonic':
  166. mn = mn or read_from_file(wf).strip().split()
  167. elif wcls.wclass == 'dieroll':
  168. mn = mn or list(read_from_file(wf).strip().translate(dict((ord(ws),None) for ws in '\t\n ')))
  169. for idx,val in ((5,'x'),(18,'0'),(30,'7'),(44,'9')):
  170. mn.insert(idx,val)
  171. t = self.spawn('mmgen-walletconv',['-r10','-S','-i',fmt,'-o',out_fmt or fmt])
  172. t.expect('{} type:.*{}'.format(capfirst(wcls.wclass),wcls.mn_type),regex=True)
  173. t.expect(wcls.choose_seedlen_prompt,'1')
  174. t.expect('(Y/n): ','y')
  175. if wcls.wclass == 'mnemonic':
  176. t.expect('Type a number.*: ','6',regex=True)
  177. t.expect('invalid')
  178. from mmgen.mn_entry import mn_entry
  179. mne = mn_entry(fmt,entry_mode)
  180. t.expect('Type a number.*: ',str(mne.entry_modes.index(entry_mode)+1),regex=True)
  181. t.expect('Using (.+) entry mode',regex=True)
  182. mode = strip_ansi_escapes(t.p.match.group(1)).lower()
  183. assert mode == mne.em.name.lower(), '{} != {}'.format(mode,mne.em.name.lower())
  184. stealth_mnemonic_entry(t,mne,mn,entry_mode=entry_mode)
  185. elif wcls.wclass == 'dieroll':
  186. user_dieroll_entry(t,mn)
  187. if usr_rand:
  188. t.expect(wcls.user_entropy_prompt,'y')
  189. t.usr_rand(10)
  190. else:
  191. t.expect(wcls.user_entropy_prompt,'n')
  192. if not usr_rand:
  193. sid_chk = 'FE3C6545'
  194. sid = t.expect_getend('Valid {} for Seed ID '.format(wcls.desc))
  195. sid = strip_ansi_escapes(sid.split(',')[0])
  196. assert sid == sid_chk,'Seed ID mismatch! {} != {}'.format(sid,sid_chk)
  197. t.expect('to confirm: ','YES\n')
  198. t.read()
  199. return t
  200. def mnemonic_entry_mmgen_minimal(self):
  201. from mmgen.mn_entry import mn_entry
  202. # erase_chars: '\b\x7f'
  203. m = mn_entry('mmgen','minimal')
  204. np = 2
  205. mn = (
  206. 'z',
  207. 'aa',
  208. '1d2ud',
  209. 'fo{}ot{}#'.format('1' * np, '2' * (m.em.pad_max - np)), # substring of 'football'
  210. 'des1p)%erate\n', # substring of 'desperately'
  211. '#t!(ie',
  212. '!)sto8o',
  213. 'the123m8!%s',
  214. '349t(5)rip',
  215. 'di\b\bdesce',
  216. 'cea',
  217. 'bu\x7f\x7fsuic',
  218. 'app\bpl',
  219. 'wd',
  220. 'busy')
  221. return self._user_seed_entry('words',entry_mode='minimal',mn=mn)
  222. def mnemonic_entry_mmgen(self): return self._user_seed_entry('words',entry_mode='full')
  223. def mnemonic_entry_bip39(self): return self._user_seed_entry('bip39',entry_mode='full')
  224. def mnemonic_entry_bip39_short(self): return self._user_seed_entry('bip39',entry_mode='short')
  225. def mn2hex_interactive_mmgen(self): return self._mn2hex('mmgen',entry_mode='full')
  226. def mn2hex_interactive_mmgen_fixed(self): return self._mn2hex('mmgen',entry_mode='fixed')
  227. def mn2hex_interactive_bip39(self): return self._mn2hex('bip39',entry_mode='full')
  228. def mn2hex_interactive_bip39_short(self): return self._mn2hex('bip39',entry_mode='short',pad_entry=True)
  229. def mn2hex_interactive_bip39_fixed(self): return self._mn2hex('bip39',entry_mode='fixed',enter_for_dfl=True)
  230. def mn2hex_interactive_xmr(self): return self._mn2hex('xmrseed',entry_mode='full')
  231. def mn2hex_interactive_xmr_short(self): return self._mn2hex('xmrseed',entry_mode='short')
  232. def dieroll_entry(self): return self._user_seed_entry('dieroll')
  233. def dieroll_entry_usrrand(self): return self._user_seed_entry('dieroll',usr_rand=True,out_fmt='bip39')