Browse Source

get_passphrase,get_hash_preset,get_label: new test, cleanups, fixes

The MMGen Project 4 years ago
parent
commit
c2dc09cbf3
4 changed files with 242 additions and 76 deletions
  1. 9 9
      mmgen/crypto.py
  2. 61 67
      mmgen/wallet.py
  3. 55 0
      test/misc/get_passphrase.py
  4. 117 0
      test/test_py_d/ts_input.py

+ 9 - 9
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

+ 61 - 67
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')

+ 55 - 0
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]]()

+ 117 - 0
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-α'