From c2dc09cbf3a2458b3ee34a9f344e3c98acf29b60 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sat, 30 May 2020 17:49:22 +0000 Subject: [PATCH] get_passphrase,get_hash_preset,get_label: new test, cleanups, fixes --- mmgen/crypto.py | 18 ++--- mmgen/wallet.py | 128 +++++++++++++++++------------------- test/misc/get_passphrase.py | 55 ++++++++++++++++ test/test_py_d/ts_input.py | 117 ++++++++++++++++++++++++++++++++ 4 files changed, 242 insertions(+), 76 deletions(-) create mode 100755 test/misc/get_passphrase.py diff --git a/mmgen/crypto.py b/mmgen/crypto.py index 52d6bb90..d3832a27 100755 --- a/mmgen/crypto.py +++ b/mmgen/crypto.py @@ -204,7 +204,7 @@ def add_user_random(rand_bytes,desc): return rand_bytes def get_hash_preset_from_user(hp=g.dfl_hash_preset,desc='data'): - prompt = f'Enter hash preset for {desc},\n or hit ENTER to accept the default value ({hp!r}): ' + prompt = f'Enter hash preset for {desc},\nor hit ENTER to accept the default value ({hp!r}): ' while True: ret = my_raw_input(prompt) if ret: @@ -218,14 +218,14 @@ def get_hash_preset_from_user(hp=g.dfl_hash_preset,desc='data'): def get_new_passphrase(desc,passchg=False): - w = '{}passphrase for {}'.format(('','new ')[bool(passchg)], desc) + pw_desc = '{}passphrase for {}'.format(('','new ')[bool(passchg)], desc) if opt.passwd_file: - pw = ' '.join(get_words_from_file(opt.passwd_file,w)) + pw = ' '.join(get_words_from_file(opt.passwd_file,pw_desc)) elif opt.echo_passphrase: - pw = ' '.join(get_words_from_user(f'Enter {w}: ')) + pw = ' '.join(get_words_from_user(f'Enter {pw_desc}: ')) else: for i in range(g.passwd_max_tries): - pw = ' '.join(get_words_from_user(f'Enter {w}: ')) + pw = ' '.join(get_words_from_user(f'Enter {pw_desc}: ')) pw_chk = ' '.join(get_words_from_user('Repeat passphrase: ')) dmsg(f'Passphrases: [{pw}] [{pw_chk}]') if pw == pw_chk: @@ -240,12 +240,12 @@ def get_new_passphrase(desc,passchg=False): return pw def get_passphrase(desc,passchg=False): - prompt ='Enter {}passphrase for {}: '.format(('','old ')[bool(passchg)],desc) + pw_desc ='{}passphrase for {}'.format(('','old ')[bool(passchg)],desc) if opt.passwd_file: pwfile_reuse_warning(opt.passwd_file) - return ' '.join(get_words_from_file(opt.passwd_file,'passphrase')) + return ' '.join(get_words_from_file(opt.passwd_file,pw_desc)) else: - return ' '.join(get_words_from_user(prompt)) + return ' '.join(get_words_from_user(f'Enter {pw_desc}: ')) _salt_len,_sha256_len,_nonce_len = (32,32,32) @@ -257,7 +257,7 @@ def mmgen_encrypt(data,desc='data',hash_preset=''): m = ('user-requested','default')[hp=='3'] vmsg(f'Encrypting {desc}') qmsg(f'Using {m} hash preset of {hp!r}') - passwd = get_new_passphrase(desc,{}) + passwd = get_new_passphrase(desc) key = make_key(passwd,salt,hp) enc_d = encrypt_data(sha256(nonce+data).digest() + nonce + data, key, iv, desc=desc) return salt+iv+enc_d diff --git a/mmgen/wallet.py b/mmgen/wallet.py index 6738d9b4..38168662 100755 --- a/mmgen/wallet.py +++ b/mmgen/wallet.py @@ -274,57 +274,50 @@ class WalletEnc(Wallet): _msg = { 'choose_passphrase': """ -You must choose a passphrase to encrypt your new {} with. -A key will be generated from your passphrase using a hash preset of '{}'. -Please note that no strength checking of passphrases is performed. For -an empty passphrase, just hit ENTER twice. - """.strip() + You must choose a passphrase to encrypt your new {} with. + A key will be generated from your passphrase using a hash preset of '{}'. + Please note that no strength checking of passphrases is performed. + For an empty passphrase, just hit ENTER twice. + """ } - def _get_hash_preset_from_user(self,hp,desc_suf=''): - n = ('','old ')[self.op=='pwchg_old'] - m,n = (('to accept the default',n),('to reuse the old','new '))[self.op=='pwchg_new'] - fs = "Enter {}hash preset for {}{}{},\n or hit ENTER {} value ('{}'): " - p = fs.format( - n, + def _get_hash_preset_from_user(self,hp,add_desc=''): + prompt = 'Enter {}hash preset for {}{}{},\nor hit ENTER to {} value ({!r}): '.format( + ('old ' if self.op=='pwchg_old' else 'new ' if self.op=='pwchg_new' else ''), ('','new ')[self.op=='new'], self.desc, - ('',' '+desc_suf)[bool(desc_suf)], - m, - hp - ) + ('',' '+add_desc)[bool(add_desc)], + ('accept the default','reuse the old')[self.op=='pwchg_new'], + hp ) while True: - ret = my_raw_input(p) + ret = my_raw_input(prompt) if ret: if ret in g.hash_presets: - self.ssdata.hash_preset = ret return ret else: msg('Invalid input. Valid choices are {}'.format(', '.join(g.hash_presets))) else: - self.ssdata.hash_preset = hp return hp - def _get_hash_preset(self,desc_suf=''): + def _get_hash_preset(self,add_desc=''): if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'hash_preset'): old_hp = self.ss_in.ssdata.hash_preset if opt.keep_hash_preset: - qmsg(f'Reusing hash preset {old_hp!r} at user request') - self.ssdata.hash_preset = old_hp + hp = old_hp + qmsg(f'Reusing hash preset {hp!r} at user request') elif opt.hash_preset: - hp = self.ssdata.hash_preset = opt.hash_preset - qmsg(f'Using hash preset {opt.hash_preset!r} requested on command line') + hp = opt.hash_preset + qmsg(f'Using hash preset {hp!r} requested on command line') else: # Prompt, using old value as default - hp = self._get_hash_preset_from_user(old_hp,desc_suf) - + hp = self._get_hash_preset_from_user(old_hp,add_desc) if (not opt.keep_hash_preset) and self.op == 'pwchg_new': - m = (f'changed to {hp!r}','unchanged')[hp==old_hp] - qmsg(f'Hash preset {m}') + qmsg('Hash preset {}'.format('unchanged' if hp==old_hp else f'changed to {hp!r}')) elif opt.hash_preset: - self.ssdata.hash_preset = opt.hash_preset - qmsg(f'Using hash preset {opt.hash_preset!r} requested on command line') + hp = opt.hash_preset + qmsg(f'Using hash preset {hp!r} requested on command line') else: - self._get_hash_preset_from_user(g.dfl_hash_preset,desc_suf) + hp = self._get_hash_preset_from_user(g.dfl_hash_preset,add_desc) + self.ssdata.hash_preset = hp def _get_new_passphrase(self): desc = '{}passphrase for {}{}'.format( @@ -337,29 +330,33 @@ an empty passphrase, just hit ENTER twice. self.passwd_file, desc, quiet = pwfile_reuse_warning(self.passwd_file) )) - elif opt.echo_passphrase: - pw = ' '.join(get_words_from_user(f'Enter {desc}: ')) else: - for i in range(g.passwd_max_tries): + qmsg('\n'+fmt(self.msg['choose_passphrase'].format(self.desc,self.ssdata.hash_preset),indent=' ')) + if opt.echo_passphrase: pw = ' '.join(get_words_from_user(f'Enter {desc}: ')) - pw_chk = ' '.join(get_words_from_user('Repeat passphrase: ')) - dmsg(f'Passphrases: [{pw}] [{pw_chk}]') - if pw == pw_chk: - vmsg('Passphrases match'); break - else: msg('Passphrases do not match. Try again.') else: - die(2,f'User failed to duplicate passphrase in {g.passwd_max_tries} attempts') + for i in range(g.passwd_max_tries): + pw = ' '.join(get_words_from_user(f'Enter {desc}: ')) + pw_chk = ' '.join(get_words_from_user('Repeat passphrase: ')) + dmsg(f'Passphrases: [{pw}] [{pw_chk}]') + if pw == pw_chk: + vmsg('Passphrases match') + break + else: + msg('Passphrases do not match. Try again.') + else: + die(2,f'User failed to duplicate passphrase in {g.passwd_max_tries} attempts') if pw == '': qmsg('WARNING: Empty passphrase') self.ssdata.passwd = pw return pw - def _get_passphrase(self,desc_suf=''): + def _get_passphrase(self,add_desc=''): desc = '{}passphrase for {}{}'.format( ('','old ')[self.op=='pwchg_old'], self.desc, - ('',' '+desc_suf)[bool(desc_suf)] + ('',' '+add_desc)[bool(add_desc)] ) if self.passwd_file: ret = ' '.join(get_words_from_file( @@ -367,7 +364,7 @@ an empty passphrase, just hit ENTER twice. desc, quiet = pwfile_reuse_warning(self.passwd_file) )) else: - ret = ' '.join(get_words_from_user('Enter {}: '.format(desc))) + ret = ' '.join(get_words_from_user(f'Enter {desc}: ')) self.ssdata.passwd = ret def _get_first_pw_and_hp_and_encrypt_seed(self): @@ -382,10 +379,8 @@ an empty passphrase, just hit ENTER twice. else: pw = self._get_new_passphrase() if self.op == 'pwchg_new': - m = ('changed','unchanged')[pw==old_pw] - qmsg('Passphrase {}'.format(m)) + qmsg('Passphrase {}'.format('unchanged' if pw==old_pw else 'changed')) else: - qmsg(self.msg['choose_passphrase'].format(self.desc,d.hash_preset)) self._get_new_passphrase() d.salt = sha256(get_random(128)).digest()[:g.salt_len] @@ -718,44 +713,43 @@ class MMGenWallet(WalletEnc): self.label = None super().__init__(*args,**kwargs) + # logic identical to _get_hash_preset_from_user() def _get_label_from_user(self,old_lbl=''): prompt = 'Enter a wallet label, or hit ENTER {}: '.format( - f'to reuse the label {old_lbl.hl()!r}' if old_lbl else 'for no label' ) + 'to reuse the label {}'.format(old_lbl.hl(encl="''")) if old_lbl else + 'for no label' ) while True: msg_r(prompt) ret = my_raw_input('') if ret: - self.ssdata.label = MMGenWalletLabel(ret,on_fail='return') - if self.ssdata.label: - break + lbl = MMGenWalletLabel(ret,on_fail='return') + if lbl: + return lbl else: msg('Invalid label. Trying again...') else: - self.ssdata.label = old_lbl or MMGenWalletLabel('No Label') - break - return self.ssdata.label + return old_lbl or MMGenWalletLabel('No Label') - # nearly identical to _get_hash_preset() - factor? + # logic identical to _get_hash_preset() def _get_label(self): if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'): old_lbl = self.ss_in.ssdata.label if opt.keep_label: - qmsg(f'Reusing label {old_lbl.hl()!r} at user request') - self.ssdata.label = old_lbl + lbl = old_lbl + qmsg('Reusing label {} at user request'.format(lbl.hl(encl="''"))) elif self.label: - qmsg(f'Using label {self.label.hl()!r} requested on command line') - lbl = self.ssdata.label = self.label + lbl = self.label + qmsg('Using label {} requested on command line'.format(lbl.hl(encl="''"))) else: # Prompt, using old value as default lbl = self._get_label_from_user(old_lbl) - if (not opt.keep_label) and self.op == 'pwchg_new': - m = (f'changed to {lbl!r}','unchanged')[lbl==old_lbl] - qmsg(f'Label {m}') + qmsg('Label {}'.format('unchanged' if lbl==old_lbl else f'changed to {lbl!r}')) elif self.label: - qmsg(f'Using label {self.label.hl()!r} requested on command line') - self.ssdata.label = self.label + lbl = self.label + qmsg('Using label {} requested on command line'.format(lbl.hl(encl="''"))) else: - self._get_label_from_user() + lbl = self._get_label_from_user() + self.ssdata.label = lbl def _encrypt(self): self._get_first_pw_and_hp_and_encrypt_seed() @@ -851,8 +845,8 @@ class MMGenWallet(WalletEnc): def _decrypt(self): d = self.ssdata # Needed for multiple transactions with {}-txsign - suf = ('',os.path.basename(self.infile.name))[bool(opt.quiet)] - self._get_passphrase(desc_suf=suf) + self._get_passphrase( + add_desc = os.path.basename(self.infile.name) if opt.quiet else '' ) key = make_key(d.passwd, d.salt, d.hash_preset) ret = decrypt_seed(d.enc_seed, key, d.seed_id, d.key_id) if ret: @@ -1038,8 +1032,8 @@ to exit and re-run the program with the '--old-incog-fmt' option. def _decrypt(self): d = self.ssdata - self._get_hash_preset(desc_suf=d.incog_id) - self._get_passphrase(desc_suf=d.incog_id) + self._get_hash_preset(add_desc=d.incog_id) + self._get_passphrase(add_desc=d.incog_id) # IV is used BOTH to initialize counter and to salt password! key = make_key(d.passwd, d.iv, d.hash_preset, 'wrapper key') diff --git a/test/misc/get_passphrase.py b/test/misc/get_passphrase.py new file mode 100755 index 00000000..34ce8721 --- /dev/null +++ b/test/misc/get_passphrase.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +import sys,os +pn = os.path.abspath(os.path.dirname(sys.argv[0])) +os.chdir(os.path.dirname(os.path.dirname(pn))) +sys.path[0] = os.curdir + +from mmgen.common import * +g.color = True + +cmd_args = opts.init({ + 'text': { + 'desc': '', + 'usage': '', + 'options': """ +-P, --passwd-file=f a +-p, --hash-preset=p b +-r, --usr-randchars=n c +-L, --label=l d +-m, --keep-label e + """ + }}) + +from mmgen.crypto import get_passphrase,get_new_passphrase,get_hash_preset_from_user +from mmgen.wallet import Wallet + +def crypto(): + pw = get_new_passphrase(desc='test script') + msg(f'==> got new passphrase: [{pw}]\n') + + pw = get_passphrase(desc='test script') + msg(f'==> got passphrase: [{pw}]\n') + + hp = get_hash_preset_from_user(desc='test script') + msg(f'==> got hash preset: [{hp}]') + + hp = get_hash_preset_from_user(desc='test script') + msg(f'==> got hash preset: [{hp}]') + +def seed(): + for n in range(1,3): + msg(f'------- NEW WALLET {n} -------\n') + w1 = Wallet() + msg(f'\n==> got pw,preset,lbl: [{w1.ssdata.passwd}][{w1.ssdata.hash_preset}][{w1.ssdata.label}]\n') + + for n in range(1,3): + msg(f'------- PASSCHG {n} -------\n') + w2 = Wallet(ss=w1,passchg=True) + msg(f'\n==> got pw,preset,lbl: [{w2.ssdata.passwd}][{w2.ssdata.hash_preset}][{w2.ssdata.label}]\n') + + msg(f'------- WALLET FROM FILE -------\n') + w3 = Wallet(fn='test/ref/FE3C6545-D782B529[128,1].mmdat') # passphrase: 'reference password' + msg(f'\n==> got pw,preset,lbl: [{w3.ssdata.passwd}][{w3.ssdata.hash_preset}][{w3.ssdata.label}]\n') + +globals()[cmd_args[0]]() diff --git a/test/test_py_d/ts_input.py b/test/test_py_d/ts_input.py index 3b2bd4d3..24a40e89 100755 --- a/test/test_py_d/ts_input.py +++ b/test/test_py_d/ts_input.py @@ -20,6 +20,9 @@ class TestSuiteInput(TestSuiteBase): networks = ('btc',) tmpdir_nums = [] cmd_group = ( + ('get_passphrase_ui', (1,"hash preset, password and label (wallet.py)", [])), + ('get_passphrase_cmdline', (1,"hash preset, password and label (wallet.py - from cmdline)", [])), + ('get_passphrase_crypto', (1,"hash preset, password and label (crypto.py)", [])), ('password_entry_noecho', (1,"utf8 password entry", [])), ('password_entry_echo', (1,"utf8 password entry (echoed)", [])), ('mnemonic_entry_mmgen', (1,"stealth mnemonic entry (mmgen)", [])), @@ -37,6 +40,120 @@ class TestSuiteInput(TestSuiteBase): ('dieroll_entry_usrrand', (1,"dieroll entry (base6d) with added user entropy", [])), ) + 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 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 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]') + + t.read() + + return t + + def get_passphrase_cmdline(self): + open('test/trash/pwfile','w').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]') + t.read() + 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}]') + + t.read() + return t + def password_entry(self,prompt,cmd_args): t = self.spawn('test/misc/password_entry.py',cmd_args,cmd_dir='.') pw = 'abc-α'