306 lines
11 KiB
Python
Executable file
306 lines
11 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
#
|
|
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
|
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
|
#
|
|
# Project source code repository: https://github.com/mmgen/mmgen
|
|
# Licensed according to the terms of GPL Version 3. See LICENSE for details.
|
|
|
|
"""
|
|
ts_input.py: user input tests for the MMGen test.py test suite
|
|
"""
|
|
|
|
from ..include.common import *
|
|
from .ts_base import *
|
|
from .input import *
|
|
from mmgen.wallet import get_wallet_cls
|
|
|
|
class TestSuiteInput(TestSuiteBase):
|
|
'user input'
|
|
networks = ('btc',)
|
|
tmpdir_nums = [1]
|
|
color = True
|
|
cmd_group_in = (
|
|
('subgroup.password', []),
|
|
('subgroup.misc', []),
|
|
('subgroup.wallet', []),
|
|
('subgroup.mnemonic', []),
|
|
('subgroup.dieroll', []),
|
|
)
|
|
cmd_subgroups = {
|
|
'password': (
|
|
'password entry via line_input()',
|
|
('password_entry_noecho', 'utf8 password entry'),
|
|
('password_entry_echo', 'utf8 password entry (echoed)'),
|
|
),
|
|
'misc': (
|
|
'miscellaneous user-level UI functions',
|
|
('get_seed_from_stdin', 'reading seed phrase from STDIN'),
|
|
),
|
|
'wallet': (
|
|
'hash preset, password and label entry',
|
|
('get_passphrase_ui', 'hash preset, password and label (wallet.py)'),
|
|
('get_passphrase_cmdline', 'hash preset, password and label (wallet.py - from cmdline)'),
|
|
('get_passphrase_crypto', 'hash preset, password and label (crypto.py)'),
|
|
),
|
|
'mnemonic': (
|
|
'mnemonic entry',
|
|
('mnemonic_entry_mmgen', 'stealth mnemonic entry (mmgen)'),
|
|
('mnemonic_entry_mmgen_minimal', 'stealth mnemonic entry (mmgen - minimal entry mode)'),
|
|
('mnemonic_entry_bip39', 'stealth mnemonic entry (bip39)'),
|
|
('mnemonic_entry_bip39_short', 'stealth mnemonic entry (bip39 - short entry mode)'),
|
|
('mn2hex_interactive_mmgen', 'mn2hex_interactive (mmgen)'),
|
|
('mn2hex_interactive_mmgen_fixed','mn2hex_interactive (mmgen - fixed (10-letter) entry mode)'),
|
|
('mn2hex_interactive_bip39', 'mn2hex_interactive (bip39)'),
|
|
('mn2hex_interactive_bip39_short','mn2hex_interactive (bip39 - short entry mode (+pad entry))'),
|
|
('mn2hex_interactive_bip39_fixed','mn2hex_interactive (bip39 - fixed (4-letter) entry mode)'),
|
|
('mn2hex_interactive_xmr', 'mn2hex_interactive (xmrseed)'),
|
|
('mn2hex_interactive_xmr_short', 'mn2hex_interactive (xmrseed - short entry mode)'),
|
|
),
|
|
'dieroll': (
|
|
'dieroll entry',
|
|
('dieroll_entry', 'dieroll entry (base6d)'),
|
|
('dieroll_entry_usrrand', 'dieroll entry (base6d) with added user entropy'),
|
|
)
|
|
}
|
|
|
|
def get_seed_from_stdin(self):
|
|
self.spawn('',msg_only=True)
|
|
from subprocess import run,PIPE
|
|
cmd = ['python3','cmds/mmgen-walletconv','--in-fmt=words','--out-fmt=bip39','--outdir=test/trash']
|
|
mn = sample_mn['mmgen']['mn']
|
|
os.environ['MMGEN_TEST_SUITE'] = ''
|
|
cp = run( cmd, input=mn.encode(), stdout=PIPE, stderr=PIPE )
|
|
from mmgen.color import set_vt100
|
|
set_vt100()
|
|
os.environ['MMGEN_TEST_SUITE'] = '1'
|
|
assert b'written to file' in cp.stderr, "test 'get_seed_from_stdin' failed"
|
|
imsg(cp.stderr.decode().strip())
|
|
return 'ok'
|
|
|
|
def get_passphrase_ui(self):
|
|
t = self.spawn('test/misc/get_passphrase.py',['--usr-randchars=0','seed'],cmd_dir='.')
|
|
|
|
# 1 - new wallet, default hp,label;empty pw
|
|
t.expect('accept the default.*: ','\n',regex=True)
|
|
|
|
# bad repeat
|
|
t.expect('new MMGen wallet: ','pass1\n')
|
|
t.expect('peat passphrase: ','pass2\n')
|
|
|
|
# good repeat
|
|
t.expect('new MMGen wallet: ','\n')
|
|
t.expect('peat passphrase: ','\n')
|
|
t.expect('mpty pass')
|
|
|
|
t.expect('no label: ','\n')
|
|
|
|
t.expect('[][3][No Label]')
|
|
|
|
# 2 - new wallet, user-selected hp,pw,label
|
|
t.expect('accept the default.*: ', '1\n', regex=True)
|
|
|
|
t.expect('new MMGen wallet: ','pass1\n')
|
|
t.expect('peat passphrase: ','pass1\n')
|
|
|
|
t.expect('no label: ','lbl1\n')
|
|
|
|
t.expect('[pass1][1][lbl1]')
|
|
|
|
# 3 - passchg, nothing changes
|
|
t.expect('new hash preset')
|
|
t.expect('reuse the old value.*: ','\n',regex=True)
|
|
t.expect('unchanged')
|
|
|
|
t.expect('new passphrase.*: ','pass1\n',regex=True)
|
|
t.expect('peat new passphrase: ','pass1\n')
|
|
t.expect('unchanged')
|
|
|
|
t.expect('reuse the label .*: ','\n',regex=True)
|
|
t.expect('unchanged')
|
|
|
|
t.expect('[pass1][1][lbl1]')
|
|
|
|
# 4 - passchg, everything changes
|
|
t.expect('new hash preset')
|
|
t.expect('reuse the old value.*: ','2\n',regex=True)
|
|
t.expect(' changed to')
|
|
|
|
t.expect('new passphrase.*: ','pass2\n',regex=True)
|
|
t.expect('peat new passphrase: ','pass2\n')
|
|
t.expect(' changed')
|
|
|
|
t.expect('reuse the label .*: ','lbl2\n',regex=True)
|
|
t.expect(' changed to')
|
|
t.expect('[pass2][2][lbl2]')
|
|
|
|
# 5 - wallet from file
|
|
t.expect('from file')
|
|
|
|
# bad passphrase
|
|
t.expect('passphrase for MMGen wallet: ','bad\n')
|
|
t.expect('Trying again')
|
|
|
|
# good passphrase
|
|
t.expect('passphrase for MMGen wallet: ','reference password\n')
|
|
t.expect('[reference password][1][No Label]')
|
|
|
|
return t
|
|
|
|
def get_passphrase_cmdline(self):
|
|
with open('test/trash/pwfile','w') as fp:
|
|
fp.write('reference password\n')
|
|
t = self.spawn('test/misc/get_passphrase.py', [
|
|
'--usr-randchars=0',
|
|
'--label=MyLabel',
|
|
'--passwd-file=test/trash/pwfile',
|
|
'--hash-preset=1',
|
|
'seed' ],
|
|
cmd_dir = '.' )
|
|
for foo in range(4):
|
|
t.expect('[reference password][1][MyLabel]')
|
|
|
|
return t
|
|
|
|
def get_passphrase_crypto(self):
|
|
t = self.spawn('test/misc/get_passphrase.py',['--usr-randchars=0','crypto'],cmd_dir='.')
|
|
|
|
# new passwd
|
|
t.expect('passphrase for .*: ', 'x\n', regex=True)
|
|
t.expect('peat passphrase: ', '\n')
|
|
t.expect('passphrase for .*: ', 'pass1\n', regex=True)
|
|
t.expect('peat passphrase: ', 'pass1\n')
|
|
t.expect('[pass1]')
|
|
|
|
# existing passwd
|
|
t.expect('passphrase for .*: ', 'pass2\n', regex=True)
|
|
t.expect('[pass2]')
|
|
|
|
# hash preset
|
|
t.expect('accept the default .*: ', '0\n', regex=True)
|
|
t.expect('nvalid')
|
|
t.expect('accept the default .*: ', '8\n', regex=True)
|
|
t.expect('nvalid')
|
|
t.expect('accept the default .*: ', '7\n', regex=True)
|
|
t.expect('[7]')
|
|
|
|
# hash preset (default)
|
|
t.expect('accept the default .*: ', '\n', regex=True)
|
|
t.expect(f'[{g.dfl_hash_preset}]')
|
|
|
|
return t
|
|
|
|
def password_entry(self,prompt,cmd_args):
|
|
t = self.spawn('test/misc/password_entry.py',cmd_args,cmd_dir='.')
|
|
pw = 'abc-α'
|
|
t.expect(prompt,pw+'\n')
|
|
ret = t.expect_getend('Entered: ')
|
|
assert ret == pw, f'Password mismatch! {ret} != {pw}'
|
|
return t
|
|
|
|
def password_entry_noecho(self):
|
|
if self.skip_for_win():
|
|
m = "getpass() doesn't work with pexpect.popen_spawn!\n"
|
|
m += 'Perform the following test by hand with non-ASCII password abc-α:\n'
|
|
m += ' test/misc/password_entry.py'
|
|
return ('skip_warn',m)
|
|
return self.password_entry('Enter passphrase: ',[])
|
|
|
|
def password_entry_echo(self):
|
|
if self.skip_for_win():
|
|
m = "getpass() doesn't work with pexpect.popen_spawn!\n"
|
|
m += 'Perform the following test by hand with non-ASCII password abc-α:\n'
|
|
m += ' test/misc/password_entry.py --echo-passphrase'
|
|
return ('skip_warn',m)
|
|
return self.password_entry('Enter passphrase (echoed): ',['--echo-passphrase'])
|
|
|
|
def _mn2hex(self,fmt,entry_mode='full',mn=None,pad_entry=False,enter_for_dfl=False):
|
|
mn = mn or sample_mn[fmt]['mn'].split()
|
|
t = self.spawn('mmgen-tool',['mn2hex_interactive','fmt='+fmt,'mn_len=12','print_mn=1'])
|
|
from mmgen.mn_entry import mn_entry
|
|
mne = mn_entry(fmt,entry_mode)
|
|
t.expect(
|
|
'Type a number.*: ',
|
|
('\n' if enter_for_dfl else str(mne.entry_modes.index(entry_mode)+1)),
|
|
regex = True )
|
|
t.expect('Using (.+) entry mode',regex=True)
|
|
mode = strip_ansi_escapes(t.p.match.group(1)).lower()
|
|
assert mode == mne.em.name.lower(), f'{mode} != {mne.em.name.lower()}'
|
|
stealth_mnemonic_entry(t,mne,mn,entry_mode=entry_mode,pad_entry=pad_entry)
|
|
t.expect(sample_mn[fmt]['hex'])
|
|
return t
|
|
|
|
def _user_seed_entry(self,fmt,usr_rand=False,out_fmt=None,entry_mode='full',mn=None):
|
|
wcls = get_wallet_cls(fmt_code=fmt)
|
|
wf = os.path.join(ref_dir,f'FE3C6545.{wcls.ext}')
|
|
if wcls.base_type == 'mnemonic':
|
|
mn = mn or read_from_file(wf).strip().split()
|
|
elif wcls.type == 'dieroll':
|
|
mn = mn or list(remove_whitespace(read_from_file(wf)))
|
|
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(f'{capfirst(wcls.base_type or wcls.type)} type:.*{wcls.mn_type}',regex=True)
|
|
t.expect(wcls.choose_seedlen_prompt,'1')
|
|
t.expect('(Y/n): ','y')
|
|
if wcls.base_type == 'mnemonic':
|
|
t.expect('Type a number.*: ','6',regex=True)
|
|
t.expect('invalid')
|
|
from mmgen.mn_entry import mn_entry
|
|
mne = mn_entry(fmt,entry_mode)
|
|
t.expect('Type a number.*: ',str(mne.entry_modes.index(entry_mode)+1),regex=True)
|
|
t.expect('Using (.+) entry mode',regex=True)
|
|
mode = strip_ansi_escapes(t.p.match.group(1)).lower()
|
|
assert mode == mne.em.name.lower(), f'{mode} != {mne.em.name.lower()}'
|
|
stealth_mnemonic_entry(t,mne,mn,entry_mode=entry_mode)
|
|
elif wcls.type == '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(f'Valid {wcls.desc} for Seed ID ')
|
|
sid = strip_ansi_escapes(sid.split(',')[0])
|
|
assert sid == sid_chk, f'Seed ID mismatch! {sid} != {sid_chk}'
|
|
t.expect('to confirm: ','YES\n')
|
|
return t
|
|
|
|
def mnemonic_entry_mmgen_minimal(self):
|
|
from mmgen.mn_entry import mn_entry
|
|
# erase_chars: '\b\x7f'
|
|
m = mn_entry('mmgen','minimal')
|
|
np = 2
|
|
mn = (
|
|
'z',
|
|
'aa',
|
|
'1d2ud',
|
|
'fo{}ot{}#'.format('1' * np, '2' * (m.em.pad_max - np)), # substring of 'football'
|
|
'des1p)%erate\n', # substring of 'desperately'
|
|
'#t!(ie',
|
|
'!)sto8o',
|
|
'the123m8!%s',
|
|
'349t(5)rip',
|
|
'di\b\bdesce',
|
|
'cea',
|
|
'bu\x7f\x7fsuic',
|
|
'app\bpl',
|
|
'wd',
|
|
'busy')
|
|
return self._user_seed_entry('words',entry_mode='minimal',mn=mn)
|
|
def mnemonic_entry_mmgen(self): return self._user_seed_entry('words',entry_mode='full')
|
|
def mnemonic_entry_bip39(self): return self._user_seed_entry('bip39',entry_mode='full')
|
|
def mnemonic_entry_bip39_short(self): return self._user_seed_entry('bip39',entry_mode='short')
|
|
|
|
def mn2hex_interactive_mmgen(self): return self._mn2hex('mmgen',entry_mode='full')
|
|
def mn2hex_interactive_mmgen_fixed(self): return self._mn2hex('mmgen',entry_mode='fixed')
|
|
def mn2hex_interactive_bip39(self): return self._mn2hex('bip39',entry_mode='full')
|
|
def mn2hex_interactive_bip39_short(self): return self._mn2hex('bip39',entry_mode='short',pad_entry=True)
|
|
def mn2hex_interactive_bip39_fixed(self): return self._mn2hex('bip39',entry_mode='fixed',enter_for_dfl=True)
|
|
def mn2hex_interactive_xmr(self): return self._mn2hex('xmrseed',entry_mode='full')
|
|
def mn2hex_interactive_xmr_short(self): return self._mn2hex('xmrseed',entry_mode='short')
|
|
|
|
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')
|