Browse Source

crypto.py: reimplement as class

The MMGen Project 2 years ago
parent
commit
19f0730913

+ 3 - 4
mmgen/addrfile.py

@@ -49,12 +49,11 @@ class AddrFile(MMGenObject):
 		self.infile = None
 		self.infile = None
 
 
 	def encrypt(self):
 	def encrypt(self):
-		from .crypto import mmgen_encrypt,mmenc_ext
-		from .globalvars import g
-		self.fmt_data = mmgen_encrypt(
+		from .crypto import Crypto
+		self.fmt_data = Crypto().mmgen_encrypt(
 			data = self.fmt_data.encode(),
 			data = self.fmt_data.encode(),
 			desc = f'new {self.parent.desc} list' )
 			desc = f'new {self.parent.desc} list' )
-		self.ext += f'.{mmenc_ext}'
+		self.ext += f'.{Crypto.mmenc_ext}'
 
 
 	@property
 	@property
 	def filename(self):
 	def filename(self):

+ 2 - 2
mmgen/addrlist.py

@@ -312,11 +312,11 @@ class AddrList(MMGenObject): # Address info for a single seed ID
 			scramble_key = self.proto.coin.lower()
 			scramble_key = self.proto.coin.lower()
 		else:
 		else:
 			scramble_key = (self.proto.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
 			scramble_key = (self.proto.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
-		from .crypto import scramble_seed
+		from .crypto import Crypto
 		if self.proto.testnet:
 		if self.proto.testnet:
 			scramble_key += ':' + self.proto.network
 			scramble_key += ':' + self.proto.network
 		self.dmsg_sc('str',scramble_key)
 		self.dmsg_sc('str',scramble_key)
-		return scramble_seed(seed,scramble_key.encode())
+		return Crypto().scramble_seed(seed,scramble_key.encode())
 
 
 	def idxs(self):
 	def idxs(self):
 		return [e.idx for e in self.data]
 		return [e.idx for e in self.data]

+ 342 - 334
mmgen/crypto.py

@@ -39,368 +39,376 @@ from .util import (
 	oneshot_warning,
 	oneshot_warning,
 )
 )
 
 
-mmenc_ext = 'mmenc'
-scramble_hash_rounds = 10
-
-salt_len       = 16
-aesctr_iv_len  = 16
-aesctr_dfl_iv  = int.to_bytes(1,aesctr_iv_len,'big')
-hincog_chk_len = 8
-
-# Scrypt params: 'id_num': [N, r, p] (N is an exponent of two)
-# NB: hashlib.scrypt in Python (>=v3.6) supports max N value of 14.  This means that
-# for hash presets > 3 the standalone scrypt library must be used!
-_hp = namedtuple('scrypt_preset',['N','r','p'])
-hash_presets = {
-	'1': _hp(12, 8, 1),
-	'2': _hp(13, 8, 4),
-	'3': _hp(14, 8, 8),
-	'4': _hp(15, 8, 12),
-	'5': _hp(16, 8, 16),
-	'6': _hp(17, 8, 20),
-	'7': _hp(18, 8, 24),
-}
-
-class pwfile_reuse_warning(oneshot_warning):
-	message = 'Reusing passphrase from file {!r} at user request'
-	def __init__(self,fn):
-		oneshot_warning.__init__(self,div=fn,fmt_args=[fn],reverse=True)
-
-def get_hash_params(hash_preset):
-	if hash_preset in hash_presets:
-		return hash_presets[hash_preset] # N,r,p
-	else: # Shouldn't be here
-		die(3,f"{hash_preset}: invalid 'hash_preset' value")
-
-def sha256_rounds(s,n):
-	from hashlib import sha256
-	for i in range(n):
-		s = sha256(s).digest()
-	return s
-
-def scramble_seed(seed,scramble_key):
-	import hmac
-	step1 = hmac.digest(seed,scramble_key,'sha256')
-	if g.debug:
-		msg(f'Seed:  {seed.hex()!r}\nScramble key: {scramble_key}\nScrambled seed: {step1.hex()}\n')
-	return sha256_rounds( step1, scramble_hash_rounds )
-
-def encrypt_seed(data,key,desc='seed'):
-	return encrypt_data(data,key,desc=desc)
-
-def decrypt_seed(enc_seed,key,seed_id,key_id):
-	vmsg_r('Checking key...')
-	chk1 = make_chksum_8(key)
-	if key_id:
-		if not compare_chksums(key_id,'key ID',chk1,'computed'):
-			msg('Incorrect passphrase or hash preset')
-			return False
-
-	dec_seed = decrypt_data(enc_seed,key,desc='seed')
-	chk2     = make_chksum_8(dec_seed)
-	if seed_id:
-		if compare_chksums(seed_id,'Seed ID',chk2,'decrypted seed'):
-			qmsg('Passphrase is OK')
-		else:
-			if not opt.debug:
-				msg_r('Checking key ID...')
-				if compare_chksums(key_id,'key ID',chk1,'computed'):
-					msg('Key ID is correct but decryption of seed failed')
-				else:
-					msg('Incorrect passphrase or hash preset')
-			vmsg('')
-			return False
-#	else:
-#		qmsg(f'Generated IDs (Seed/Key): {chk2}/{chk1}')
-
-	dmsg(f'Decrypted seed: {dec_seed.hex()}')
-	return dec_seed
-
-def encrypt_data(data,key,iv=aesctr_dfl_iv,desc='data',verify=True,silent=False):
-	from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
-	from cryptography.hazmat.backends import default_backend
-	if not silent:
-		vmsg(f'Encrypting {desc}')
-	c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
-	encryptor = c.encryptor()
-	enc_data = encryptor.update(data) + encryptor.finalize()
 
 
-	if verify:
-		vmsg_r(f'Performing a test decryption of the {desc}...')
+class Crypto:
+
+	mmenc_ext = 'mmenc'
+	scramble_hash_rounds = 10
+
+	salt_len       = 16
+	aesctr_iv_len  = 16
+	aesctr_dfl_iv  = int.to_bytes(1,aesctr_iv_len,'big')
+	hincog_chk_len = 8
+
+	mmenc_salt_len = 32
+	mmenc_nonce_len = 32
+
+	# Scrypt params: 'id_num': [N, r, p] (N is an exponent of two)
+	# NB: hashlib.scrypt in Python (>=v3.6) supports max N value of 14.  This means that
+	# for hash presets > 3 the standalone scrypt library must be used!
+	_hp = namedtuple('scrypt_preset',['N','r','p'])
+	hash_presets = {
+		'1': _hp(12, 8, 1),
+		'2': _hp(13, 8, 4),
+		'3': _hp(14, 8, 8),
+		'4': _hp(15, 8, 12),
+		'5': _hp(16, 8, 16),
+		'6': _hp(17, 8, 20),
+		'7': _hp(18, 8, 24),
+	}
+
+	class pwfile_reuse_warning(oneshot_warning):
+		message = 'Reusing passphrase from file {!r} at user request'
+		def __init__(self,fn):
+			oneshot_warning.__init__(self,div=fn,fmt_args=[fn],reverse=True)
+
+	def get_hash_params(self,hash_preset):
+		if hash_preset in self.hash_presets:
+			return self.hash_presets[hash_preset] # N,r,p
+		else: # Shouldn't be here
+			die(3,f"{hash_preset}: invalid 'hash_preset' value")
+
+	def sha256_rounds(self,s):
+		from hashlib import sha256
+		for i in range(self.scramble_hash_rounds):
+			s = sha256(s).digest()
+		return s
+
+	def scramble_seed(self,seed,scramble_key):
+		import hmac
+		step1 = hmac.digest(seed,scramble_key,'sha256')
+		if g.debug:
+			msg(f'Seed:  {seed.hex()!r}\nScramble key: {scramble_key}\nScrambled seed: {step1.hex()}\n')
+		return self.sha256_rounds(step1)
+
+	def encrypt_seed(self,data,key,desc='seed'):
+		return self.encrypt_data(data,key,desc=desc)
+
+	def decrypt_seed(self,enc_seed,key,seed_id,key_id):
+		vmsg_r('Checking key...')
+		chk1 = make_chksum_8(key)
+		if key_id:
+			if not compare_chksums(key_id,'key ID',chk1,'computed'):
+				msg('Incorrect passphrase or hash preset')
+				return False
+
+		dec_seed = self.decrypt_data(enc_seed,key,desc='seed')
+		chk2     = make_chksum_8(dec_seed)
+		if seed_id:
+			if compare_chksums(seed_id,'Seed ID',chk2,'decrypted seed'):
+				qmsg('Passphrase is OK')
+			else:
+				if not opt.debug:
+					msg_r('Checking key ID...')
+					if compare_chksums(key_id,'key ID',chk1,'computed'):
+						msg('Key ID is correct but decryption of seed failed')
+					else:
+						msg('Incorrect passphrase or hash preset')
+				vmsg('')
+				return False
+
+		dmsg(f'Decrypted seed: {dec_seed.hex()}')
+		return dec_seed
+
+	def encrypt_data(self,data,key,iv=aesctr_dfl_iv,desc='data',verify=True,silent=False):
+		from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
+		from cryptography.hazmat.backends import default_backend
+		if not silent:
+			vmsg(f'Encrypting {desc}')
 		c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
 		c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
 		encryptor = c.encryptor()
 		encryptor = c.encryptor()
-		dec_data = encryptor.update(enc_data) + encryptor.finalize()
-		if dec_data != data:
-			die(2,f'ERROR.\nDecrypted {desc} doesn’t match original {desc}')
-		if not silent:
-			vmsg('done')
-
-	return enc_data
-
-def decrypt_data(enc_data,key,iv=aesctr_dfl_iv,desc='data'):
-	from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
-	from cryptography.hazmat.backends import default_backend
-	vmsg_r(f'Decrypting {desc} with key...')
-	c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
-	encryptor = c.encryptor()
-	return encryptor.update(enc_data) + encryptor.finalize()
-
-def scrypt_hash_passphrase(passwd,salt,hash_preset,buflen=32):
-
-	# Buflen arg is for brainwallets only, which use this function to generate
-	# the seed directly.
-	ps = get_hash_params(hash_preset)
-
-	if isinstance(passwd,str):
-		passwd = passwd.encode()
-
-	def do_hashlib_scrypt():
-		from hashlib import scrypt
-		return scrypt(
-			password = passwd,
-			salt     = salt,
-			n        = 2**ps.N,
-			r        = ps.r,
-			p        = ps.p,
-			maxmem   = 0,
-			dklen    = buflen )
-
-	def do_standalone_scrypt():
-		import scrypt
-		return scrypt.hash(
-			password = passwd,
-			salt     = salt,
-			N        = 2**ps.N,
-			r        = ps.r,
-			p        = ps.p,
-			buflen   = buflen )
-
-	if int(hash_preset) > 3:
-		msg_r('Hashing passphrase, please wait...')
-
-	# hashlib.scrypt doesn't support N > 14 (hash preset > 3)
-	ret = (
-		do_standalone_scrypt() if ps.N > 14 or g.force_standalone_scrypt_module else
-		do_hashlib_scrypt() )
-
-	if int(hash_preset) > 3:
-		msg_r('\b'*34 + ' '*34 + '\b'*34)
+		enc_data = encryptor.update(data) + encryptor.finalize()
+
+		if verify:
+			vmsg_r(f'Performing a test decryption of the {desc}...')
+			c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
+			encryptor = c.encryptor()
+			dec_data = encryptor.update(enc_data) + encryptor.finalize()
+			if dec_data != data:
+				die(2,f'ERROR.\nDecrypted {desc} doesn’t match original {desc}')
+			if not silent:
+				vmsg('done')
+
+		return enc_data
+
+	def decrypt_data(self,enc_data,key,iv=aesctr_dfl_iv,desc='data'):
+		from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
+		from cryptography.hazmat.backends import default_backend
+		vmsg_r(f'Decrypting {desc} with key...')
+		c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
+		encryptor = c.encryptor()
+		return encryptor.update(enc_data) + encryptor.finalize()
+
+	def scrypt_hash_passphrase(self,passwd,salt,hash_preset,buflen=32):
+
+		# Buflen arg is for brainwallets only, which use this function to generate
+		# the seed directly.
+		ps = self.get_hash_params(hash_preset)
+
+		if isinstance(passwd,str):
+			passwd = passwd.encode()
+
+		def do_hashlib_scrypt():
+			from hashlib import scrypt
+			return scrypt(
+				password = passwd,
+				salt     = salt,
+				n        = 2**ps.N,
+				r        = ps.r,
+				p        = ps.p,
+				maxmem   = 0,
+				dklen    = buflen )
+
+		def do_standalone_scrypt():
+			import scrypt
+			return scrypt.hash(
+				password = passwd,
+				salt     = salt,
+				N        = 2**ps.N,
+				r        = ps.r,
+				p        = ps.p,
+				buflen   = buflen )
+
+		if int(hash_preset) > 3:
+			msg_r('Hashing passphrase, please wait...')
+
+		# hashlib.scrypt doesn't support N > 14 (hash preset > 3)
+		ret = (
+			do_standalone_scrypt() if ps.N > 14 or g.force_standalone_scrypt_module else
+			do_hashlib_scrypt() )
+
+		if int(hash_preset) > 3:
+			msg_r('\b'*34 + ' '*34 + '\b'*34)
+
+		return ret
+
+	def make_key(self,passwd,salt,hash_preset,desc='encryption key',from_what='passphrase',verbose=False):
+		if opt.verbose or verbose:
+			msg_r(f"Generating {desc}{' from ' + from_what if from_what else ''}...")
+		key = self.scrypt_hash_passphrase(passwd,salt,hash_preset)
+		if opt.verbose or verbose: msg('done')
+		dmsg(f'Key: {key.hex()}')
+		return key
+
+	def _get_random_data_from_user(self,uchars=None,desc='data'):
+
+		if uchars is None:
+			uchars = opt.usr_randchars
+
+		info1 = f"""
+			Now we're going to gather some additional input from the keyboard to further
+			randomize the random data {desc}.
+
+			An encryption key will be created from this input, and the random data will
+			be encrypted using the key.  The resulting data is guaranteed to be at least
+			as random as the original random data, so even if you type very predictably
+			no harm will be done.
+
+			However, to gain the maximum benefit, try making your input as random as
+			possible.  Type slowly and choose your symbols carefully.  Try to use both
+			upper and lowercase letters as well as punctuation and numerals. The timings
+			between your keystrokes will also be used as a source of entropy, so be as
+			random as possible in your timing as well.
+		"""
+		info2 = f"""
+			Please type {uchars} symbols on your keyboard.  What you type will not be displayed
+			on the screen.
+		"""
 
 
-	return ret
+		msg(f'Enter {uchars} random symbols' if opt.quiet else
+			'\n' + fmt(info1,indent='  ') +
+			'\n' + fmt(info2) )
 
 
-def make_key(passwd,salt,hash_preset,desc='encryption key',from_what='passphrase',verbose=False):
-	if opt.verbose or verbose:
-		msg_r(f"Generating {desc}{' from ' + from_what if from_what else ''}...")
-	key = scrypt_hash_passphrase(passwd,salt,hash_preset)
-	if opt.verbose or verbose: msg('done')
-	dmsg(f'Key: {key.hex()}')
-	return key
+		import time
+		from .term import get_char_raw
+		key_data = ''
+		time_data = []
 
 
-def _get_random_data_from_user(uchars,desc):
-	info1 = f"""
-		Now we're going to gather some additional input from the keyboard to further
-		randomize the random data {desc}.
+		for i in range(uchars):
+			key_data += get_char_raw(f'\rYou may begin typing.  {uchars-i} symbols left: ')
+			time_data.append(time.time())
 
 
-		An encryption key will be created from this input, and the random data will
-		be encrypted using the key.  The resulting data is guaranteed to be at least
-		as random as the original random data, so even if you type very predictably
-		no harm will be done.
+		msg_r( '\r' if opt.quiet else f'\rThank you.  That’s enough.{" "*18}\n\n' )
 
 
-		However, to gain the maximum benefit, try making your input as random as
-		possible.  Type slowly and choose your symbols carefully.  Try to use both
-		upper and lowercase letters as well as punctuation and numerals. The timings
-		between your keystrokes will also be used as a source of entropy, so be as
-		random as possible in your timing as well.
-	"""
-	info2 = f"""
-		Please type {uchars} symbols on your keyboard.  What you type will not be displayed
-		on the screen.
-	"""
+		time_data = [f'{t:.22f}'.rstrip('0') for t in time_data]
 
 
-	msg(f'Enter {uchars} random symbols' if opt.quiet else
-		'\n' + fmt(info1,indent='  ') +
-		'\n' + fmt(info2) )
+		avg_prec = sum(len(t.split('.')[1]) for t in time_data) // len(time_data)
 
 
-	import time
-	from .term import get_char_raw
-	key_data = ''
-	time_data = []
+		if avg_prec < g.min_time_precision:
+			ymsg(f'WARNING: Avg. time precision of only {avg_prec} decimal points. User entropy quality is degraded!')
 
 
-	for i in range(uchars):
-		key_data += get_char_raw(f'\rYou may begin typing.  {uchars-i} symbols left: ')
-		time_data.append(time.time())
+		ret = key_data + '\n' + '\n'.join(time_data)
 
 
-	msg_r( '\r' if opt.quiet else f'\rThank you.  That’s enough.{" "*18}\n\n' )
+		if g.debug:
+			msg(f'USER ENTROPY (user input + keystroke timings):\n{ret}')
 
 
-	time_data = [f'{t:.22f}'.rstrip('0') for t in time_data]
+		from .ui import line_input
+		line_input( 'User random data successfully acquired.  Press ENTER to continue: ' )
 
 
-	avg_prec = sum(len(t.split('.')[1]) for t in time_data) // len(time_data)
+		return ret.encode()
 
 
-	if avg_prec < g.min_time_precision:
-		ymsg(f'WARNING: Avg. time precision of only {avg_prec} decimal points. User entropy quality is degraded!')
+	def get_random(self,length):
 
 
-	ret = key_data + '\n' + '\n'.join(time_data)
+		os_rand = os.urandom(length)
+		assert len(os_rand) == length, f'OS random number generator returned {len(os_rand)} (!= {length}) bytes!'
 
 
-	if g.debug:
-		msg(f'USER ENTROPY (user input + keystroke timings):\n{ret}')
+		return self.add_user_random(
+			rand_bytes = os_rand,
+			desc       = 'from your operating system' )
 
 
-	from .ui import line_input
-	line_input('User random data successfully acquired.  Press ENTER to continue: ')
+	def add_user_random(
+			self,
+			rand_bytes,
+			desc,
+			urand = {'data':b'', 'counter':0} ):
 
 
-	return ret.encode()
+		assert type(rand_bytes) == bytes, 'add_user_random_chk1'
 
 
-def get_random(length):
+		if opt.usr_randchars:
 
 
-	os_rand = os.urandom(length)
-	assert len(os_rand) == length, f'OS random number generator returned {len(os_rand)} (!= {length}) bytes!'
+			if not urand['data']:
+				from hashlib import sha256
+				urand['data'] = sha256(self._get_random_data_from_user(desc=desc)).digest()
 
 
-	return add_user_random(
-		rand_bytes = os_rand,
-		desc       = 'from your operating system' )
+			# counter protects against very evil rng that might repeatedly output the same data
+			urand['counter'] += 1
 
 
-def add_user_random(
-		rand_bytes,
-		desc,
-		urand = {'data':b'', 'counter':0} ):
+			os_rand = os.urandom(8)
+			assert len(os_rand) == 8, f'OS random number generator returned {len(os_rand)} (!= 8) bytes!'
 
 
-	assert type(rand_bytes) == bytes, 'add_user_random_chk1'
+			import hmac
+			key = hmac.digest(
+				urand['data'],
+				os_rand + int.to_bytes(urand['counter'],8,'big'),
+				'sha256' )
 
 
-	if opt.usr_randchars:
+			msg('Encrypting random data {} with ephemeral key #{}'.format( desc, urand['counter'] ))
 
 
-		if not urand['data']:
-			from hashlib import sha256
-			urand['data'] = sha256(_get_random_data_from_user(opt.usr_randchars,desc)).digest()
+			return self.encrypt_data( data=rand_bytes, key=key, desc=desc, verify=False, silent=True )
+		else:
+			return rand_bytes
+
+	def get_hash_preset_from_user(
+			self,
+			old_preset = g.dfl_hash_preset,
+			data_desc  = 'data',
+			prompt     = None ):
+
+		prompt = prompt or (
+			f'Enter hash preset for {data_desc},\n' +
+			f'or hit ENTER to accept the default value ({old_preset!r}): ' )
+
+		from .ui import line_input
+		while True:
+			ret = line_input( prompt )
+			if ret:
+				if ret in self.hash_presets:
+					return ret
+				else:
+					msg('Invalid input.  Valid choices are {}'.format(', '.join(self.hash_presets)))
+			else:
+				return old_preset
+
+	def get_new_passphrase(self,data_desc,hash_preset,passwd_file,pw_desc='passphrase'):
+		message = f"""
+				You must choose a passphrase to encrypt your {data_desc} with.
+				A key will be generated from your passphrase using a hash preset of '{hash_preset}'.
+				Please note that no strength checking of passphrases is performed.
+				For an empty passphrase, just hit ENTER twice.
+			"""
+		if passwd_file:
+			from .fileutil import get_words_from_file
+			pw = ' '.join(get_words_from_file(
+				infile = passwd_file,
+				desc   = f'{pw_desc} for {data_desc}',
+				quiet  = self.pwfile_reuse_warning(passwd_file).warning_shown ))
+		else:
+			qmsg('\n'+fmt(message,indent='  '))
+			from .ui import get_words_from_user
+			if opt.echo_passphrase:
+				pw = ' '.join(get_words_from_user( f'Enter {pw_desc} for {data_desc}: ' ))
+			else:
+				for i in range(g.passwd_max_tries):
+					pw = ' '.join(get_words_from_user( f'Enter {pw_desc} for {data_desc}: ' ))
+					pw_chk = ' '.join(get_words_from_user( f'Repeat {pw_desc}: ' ))
+					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')
 
 
-		# counter protects against very evil rng that might repeatedly output the same data
-		urand['counter'] += 1
+		if pw == '':
+			qmsg('WARNING: Empty passphrase')
 
 
-		os_rand = os.urandom(8)
-		assert len(os_rand) == 8, f'OS random number generator returned {len(os_rand)} (!= 8) bytes!'
+		return pw
 
 
-		import hmac
-		key = hmac.digest(
-			urand['data'],
-			os_rand + int.to_bytes(urand['counter'],8,'big'),
-			'sha256' )
-
-		msg('Encrypting random data {} with ephemeral key #{}'.format( desc, urand['counter'] ))
-
-		return encrypt_data( data=rand_bytes, key=key, desc=desc, verify=False, silent=True )
-	else:
-		return rand_bytes
-
-def get_hash_preset_from_user(
-		old_preset = g.dfl_hash_preset,
-		data_desc = 'data',
-		prompt = None ):
-
-	prompt = prompt or (
-		f'Enter hash preset for {data_desc},\n' +
-		f'or hit ENTER to accept the default value ({old_preset!r}): ' )
-
-	from .ui import line_input
-	while True:
-		ret = line_input(prompt)
-		if ret:
-			if ret in hash_presets:
-				return ret
-			else:
-				msg('Invalid input.  Valid choices are {}'.format(', '.join(hash_presets)))
+	def get_passphrase(self,data_desc,passwd_file,pw_desc='passphrase'):
+		if passwd_file:
+			from .fileutil import get_words_from_file
+			return ' '.join(get_words_from_file(
+				infile = passwd_file,
+				desc   = f'{pw_desc} for {data_desc}',
+				quiet  = self.pwfile_reuse_warning(passwd_file).warning_shown ))
 		else:
 		else:
-			return old_preset
-
-def get_new_passphrase(data_desc,hash_preset,passwd_file,pw_desc='passphrase'):
-	message = f"""
-			You must choose a passphrase to encrypt your {data_desc} with.
-			A key will be generated from your passphrase using a hash preset of '{hash_preset}'.
-			Please note that no strength checking of passphrases is performed.
-			For an empty passphrase, just hit ENTER twice.
-		"""
-	if passwd_file:
-		from .fileutil import get_words_from_file
-		pw = ' '.join(get_words_from_file(
-			infile = passwd_file,
-			desc = f'{pw_desc} for {data_desc}',
-			quiet = pwfile_reuse_warning(passwd_file).warning_shown ))
-	else:
-		qmsg('\n'+fmt(message,indent='  '))
-		from .ui import get_words_from_user
-		if opt.echo_passphrase:
-			pw = ' '.join(get_words_from_user(f'Enter {pw_desc} for {data_desc}: '))
+			from .ui import get_words_from_user
+			return ' '.join(get_words_from_user( f'Enter {pw_desc} for {data_desc}: ' ))
+
+	def mmgen_encrypt(self,data,desc='data',hash_preset=None):
+		salt  = self.get_random(self.mmenc_salt_len)
+		iv    = self.get_random(self.aesctr_iv_len)
+		nonce = self.get_random(self.mmenc_nonce_len)
+		hp    = hash_preset or opt.hash_preset or self.get_hash_preset_from_user(data_desc=desc)
+		m     = ('user-requested','default')[hp=='3']
+		vmsg(f'Encrypting {desc}')
+		qmsg(f'Using {m} hash preset of {hp!r}')
+		passwd = self.get_new_passphrase(
+			data_desc = desc,
+			hash_preset = hp,
+			passwd_file = opt.passwd_file )
+		key    = self.make_key(passwd,salt,hp)
+		from hashlib import sha256
+		enc_d  = self.encrypt_data( sha256(nonce+data).digest() + nonce + data, key, iv, desc=desc )
+		return salt+iv+enc_d
+
+	def mmgen_decrypt(self,data,desc='data',hash_preset=None):
+		vmsg(f'Preparing to decrypt {desc}')
+		dstart = self.mmenc_salt_len + self.aesctr_iv_len
+		salt   = data[:self.mmenc_salt_len]
+		iv     = data[self.mmenc_salt_len:dstart]
+		enc_d  = data[dstart:]
+		hp     = hash_preset or opt.hash_preset or self.get_hash_preset_from_user(data_desc=desc)
+		m  = ('user-requested','default')[hp=='3']
+		qmsg(f'Using {m} hash preset of {hp!r}')
+		passwd = self.get_passphrase(
+			data_desc = desc,
+			passwd_file = opt.passwd_file )
+		key    = self.make_key(passwd,salt,hp)
+		dec_d  = self.decrypt_data( enc_d, key, iv, desc )
+		sha256_len = 32
+		from hashlib import sha256
+		if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest():
+			vmsg('OK')
+			return dec_d[sha256_len+self.mmenc_nonce_len:]
 		else:
 		else:
-			for i in range(g.passwd_max_tries):
-				pw = ' '.join(get_words_from_user(f'Enter {pw_desc} for {data_desc}: '))
-				pw_chk = ' '.join(get_words_from_user(f'Repeat {pw_desc}: '))
-				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')
-
-	return pw
-
-def get_passphrase(data_desc,passwd_file,pw_desc='passphrase'):
-	if passwd_file:
-		from .fileutil import get_words_from_file
-		return ' '.join(get_words_from_file(
-			infile = passwd_file,
-			desc = f'{pw_desc} for {data_desc}',
-			quiet = pwfile_reuse_warning(passwd_file).warning_shown ))
-	else:
-		from .ui import get_words_from_user
-		return ' '.join(get_words_from_user(f'Enter {pw_desc} for {data_desc}: '))
-
-mmenc_salt_len = 32
-mmenc_nonce_len = 32
-
-def mmgen_encrypt(data,desc='data',hash_preset=None):
-	salt  = get_random(mmenc_salt_len)
-	iv    = get_random(aesctr_iv_len)
-	nonce = get_random(mmenc_nonce_len)
-	hp    = hash_preset or opt.hash_preset or get_hash_preset_from_user(data_desc=desc)
-	m     = ('user-requested','default')[hp=='3']
-	vmsg(f'Encrypting {desc}')
-	qmsg(f'Using {m} hash preset of {hp!r}')
-	passwd = get_new_passphrase(
-		data_desc = desc,
-		hash_preset = hp,
-		passwd_file = opt.passwd_file )
-	key    = make_key(passwd,salt,hp)
-	from hashlib import sha256
-	enc_d  = encrypt_data( sha256(nonce+data).digest() + nonce + data, key, iv, desc=desc )
-	return salt+iv+enc_d
-
-def mmgen_decrypt(data,desc='data',hash_preset=None):
-	vmsg(f'Preparing to decrypt {desc}')
-	dstart = mmenc_salt_len + aesctr_iv_len
-	salt   = data[:mmenc_salt_len]
-	iv     = data[mmenc_salt_len:dstart]
-	enc_d  = data[dstart:]
-	hp     = hash_preset or opt.hash_preset or get_hash_preset_from_user(data_desc=desc)
-	m  = ('user-requested','default')[hp=='3']
-	qmsg(f'Using {m} hash preset of {hp!r}')
-	passwd = get_passphrase(
-		data_desc = desc,
-		passwd_file = opt.passwd_file )
-	key    = make_key(passwd,salt,hp)
-	dec_d  = decrypt_data( enc_d, key, iv, desc )
-	sha256_len = 32
-	from hashlib import sha256
-	if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest():
-		vmsg('OK')
-		return dec_d[sha256_len+mmenc_nonce_len:]
-	else:
-		msg('Incorrect passphrase or hash preset')
-		return False
-
-def mmgen_decrypt_retry(d,desc='data'):
-	while True:
-		d_dec = mmgen_decrypt(d,desc)
-		if d_dec: return d_dec
-		msg('Trying again...')
+			msg('Incorrect passphrase or hash preset')
+			return False
+
+	def mmgen_decrypt_retry(self,d,desc='data'):
+		while True:
+			d_dec = self.mmgen_decrypt(d,desc)
+			if d_dec:
+				return d_dec
+			msg('Trying again...')

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-13.3.dev40
+13.3.dev41

+ 3 - 4
mmgen/fileutil.py

@@ -322,13 +322,12 @@ def get_lines_from_file(
 
 
 	def decrypt_file_maybe():
 	def decrypt_file_maybe():
 		data = get_data_from_file( fn, desc=desc, binary=True, quiet=quiet, silent=silent )
 		data = get_data_from_file( fn, desc=desc, binary=True, quiet=quiet, silent=silent )
-		from .crypto import mmenc_ext
-		have_enc_ext = get_extension(fn) == mmenc_ext
+		from .crypto import Crypto
+		have_enc_ext = get_extension(fn) == Crypto.mmenc_ext
 		if have_enc_ext or not is_utf8(data):
 		if have_enc_ext or not is_utf8(data):
 			m = ('Attempting to decrypt','Decrypting')[have_enc_ext]
 			m = ('Attempting to decrypt','Decrypting')[have_enc_ext]
 			qmsg(f'{m} {desc} {fn!r}')
 			qmsg(f'{m} {desc} {fn!r}')
-			from .crypto import mmgen_decrypt_retry
-			data = mmgen_decrypt_retry(data,desc)
+			data = Crypto().mmgen_decrypt_retry(data,desc)
 		return data
 		return data
 
 
 	lines = decrypt_file_maybe().decode().splitlines()
 	lines = decrypt_file_maybe().decode().splitlines()

+ 7 - 7
mmgen/opts.py

@@ -96,11 +96,11 @@ def die_on_incompatible_opts(incompat_list):
 def _show_hash_presets():
 def _show_hash_presets():
 	fs = '  {:<7} {:<6} {:<3}  {}'
 	fs = '  {:<7} {:<6} {:<3}  {}'
 	from .util import msg
 	from .util import msg
-	from .crypto import hash_presets
+	from .crypto import Crypto
 	msg('Available parameters for scrypt.hash():')
 	msg('Available parameters for scrypt.hash():')
 	msg(fs.format('Preset','N','r','p'))
 	msg(fs.format('Preset','N','r','p'))
-	for i in sorted(hash_presets.keys()):
-		msg(fs.format(i,*hash_presets[i]))
+	for i in sorted(Crypto.hash_presets.keys()):
+		msg(fs.format(i,*Crypto.hash_presets[i]))
 	msg('N = memory usage (power of two), p = iterations (rounds)')
 	msg('N = memory usage (power of two), p = iterations (rounds)')
 	sys.exit(0)
 	sys.exit(0)
 
 
@@ -559,11 +559,10 @@ def check_usr_opts(usr_opts): # Raises an exception if any check fails
 		opt_is_in_list(int(val),Seed.lens,desc)
 		opt_is_in_list(int(val),Seed.lens,desc)
 
 
 	def chk_hash_preset(key,val,desc):
 	def chk_hash_preset(key,val,desc):
-		from .crypto import hash_presets
-		opt_is_in_list(val,list(hash_presets.keys()),desc)
+		from .crypto import Crypto
+		opt_is_in_list(val,list(Crypto.hash_presets.keys()),desc)
 
 
 	def chk_brain_params(key,val,desc):
 	def chk_brain_params(key,val,desc):
-		from .crypto import hash_presets
 		a = val.split(',')
 		a = val.split(',')
 		if len(a) != 2:
 		if len(a) != 2:
 			opt_display(key,val)
 			opt_display(key,val)
@@ -571,7 +570,8 @@ def check_usr_opts(usr_opts): # Raises an exception if any check fails
 		opt_is_int(a[0],'seed length '+desc)
 		opt_is_int(a[0],'seed length '+desc)
 		from .seed import Seed
 		from .seed import Seed
 		opt_is_in_list(int(a[0]),Seed.lens,'seed length '+desc)
 		opt_is_in_list(int(a[0]),Seed.lens,'seed length '+desc)
-		opt_is_in_list(a[1],list(hash_presets.keys()),'hash preset '+desc)
+		from .crypto import Crypto
+		opt_is_in_list(a[1],list(Crypto.hash_presets.keys()),'hash preset '+desc)
 
 
 	def chk_usr_randchars(key,val,desc):
 	def chk_usr_randchars(key,val,desc):
 		if val == 0:
 		if val == 0:

+ 2 - 2
mmgen/passwdlist.py

@@ -239,5 +239,5 @@ class PasswordList(AddrList):
 			scramble_key = f'hex:{pwlen}:{self.pw_id_str}'
 			scramble_key = f'hex:{pwlen}:{self.pw_id_str}'
 
 
 		self.dmsg_sc('str',scramble_key)
 		self.dmsg_sc('str',scramble_key)
-		from .crypto import scramble_seed
-		return scramble_seed(seed,scramble_key.encode())
+		from .crypto import Crypto
+		return Crypto().scramble_seed(seed,scramble_key.encode())

+ 2 - 2
mmgen/seed.py

@@ -57,10 +57,10 @@ class SeedBase(MMGenObject):
 	def __init__(self,seed_bin=None,nSubseeds=None):
 	def __init__(self,seed_bin=None,nSubseeds=None):
 		if not seed_bin:
 		if not seed_bin:
 			from .opts import opt
 			from .opts import opt
-			from .crypto import get_random
+			from .crypto import Crypto
 			from hashlib import sha256
 			from hashlib import sha256
 			# Truncate random data for smaller seed lengths
 			# Truncate random data for smaller seed lengths
-			seed_bin = sha256(get_random(1033)).digest()[:(opt.seed_len or self.dfl_len)//8]
+			seed_bin = sha256(Crypto().get_random(1033)).digest()[:(opt.seed_len or self.dfl_len)//8]
 		elif len(seed_bin)*8 not in self.lens:
 		elif len(seed_bin)*8 not in self.lens:
 			die(3,f'{len(seed_bin)*8}: invalid seed bit length')
 			die(3,f'{len(seed_bin)*8}: invalid seed bit length')
 
 

+ 4 - 3
mmgen/seedsplit.py

@@ -24,6 +24,7 @@ from .globalvars import g
 from .color import yellow
 from .color import yellow
 from .obj import MMGenPWIDString,MMGenIdx
 from .obj import MMGenPWIDString,MMGenIdx
 from .subseed import *
 from .subseed import *
+from .crypto import Crypto
 
 
 class SeedShareIdx(MMGenIdx):
 class SeedShareIdx(MMGenIdx):
 	max_val = 1024
 	max_val = 1024
@@ -209,7 +210,7 @@ class SeedShare(SeedShareBase,SubSeed):
 				b':master:' +
 				b':master:' +
 				parent_list.master_share.idx.to_bytes(2,'big')
 				parent_list.master_share.idx.to_bytes(2,'big')
 			)
 			)
-		return scramble_seed(seed.data,scramble_key)[:seed.byte_len]
+		return Crypto().scramble_seed(seed.data,scramble_key)[:seed.byte_len]
 
 
 class SeedShareLast(SeedShareBase,SeedBase):
 class SeedShareLast(SeedShareBase,SeedBase):
 
 
@@ -261,14 +262,14 @@ class SeedShareMaster(SeedBase,SeedShareBase):
 		seed = self.parent_list.parent_seed
 		seed = self.parent_list.parent_seed
 		# field maximums: idx: 65535 (1024)
 		# field maximums: idx: 65535 (1024)
 		scramble_key = b'master_share:' + self.idx.to_bytes(2,'big') + self.nonce.to_bytes(2,'big')
 		scramble_key = b'master_share:' + self.idx.to_bytes(2,'big') + self.nonce.to_bytes(2,'big')
-		return scramble_seed(seed.data,scramble_key)[:seed.byte_len]
+		return Crypto().scramble_seed(seed.data,scramble_key)[:seed.byte_len]
 
 
 	# Don't bother with avoiding seed ID collision here, as sid of derived seed is not used
 	# Don't bother with avoiding seed ID collision here, as sid of derived seed is not used
 	# by user as an identifier
 	# by user as an identifier
 	def make_derived_seed_bin(self,id_str,count):
 	def make_derived_seed_bin(self,id_str,count):
 		# field maximums: id_str: none (256 chars), count: 65535 (1024)
 		# field maximums: id_str: none (256 chars), count: 65535 (1024)
 		scramble_key = id_str.encode() + b':' + count.to_bytes(2,'big')
 		scramble_key = id_str.encode() + b':' + count.to_bytes(2,'big')
-		return scramble_seed(self.data,scramble_key)[:self.byte_len]
+		return Crypto().scramble_seed(self.data,scramble_key)[:self.byte_len]
 
 
 	def get_desc(self,ui=False):
 	def get_desc(self,ui=False):
 		psid = self.parent_list.parent_seed.sid
 		psid = self.parent_list.parent_seed.sid

+ 2 - 2
mmgen/subseed.py

@@ -24,7 +24,6 @@ from .color import green
 from .util import msg_r,msg,qmsg,die
 from .util import msg_r,msg,qmsg,die
 from .obj import MMGenRange,IndexedDict
 from .obj import MMGenRange,IndexedDict
 from .seed import *
 from .seed import *
-from .crypto import scramble_seed
 
 
 class SubSeedIdxRange(MMGenRange):
 class SubSeedIdxRange(MMGenRange):
 	min_idx = 1
 	min_idx = 1
@@ -75,7 +74,8 @@ class SubSeed(SeedBase):
 		short = { 'short': True, 'long': False }[length]
 		short = { 'short': True, 'long': False }[length]
 		# field maximums: idx: 4294967295 (1000000), nonce: 65535 (1000), short: 255 (1)
 		# field maximums: idx: 4294967295 (1000000), nonce: 65535 (1000), short: 255 (1)
 		scramble_key  = idx.to_bytes(4,'big') + nonce.to_bytes(2,'big') + short.to_bytes(1,'big')
 		scramble_key  = idx.to_bytes(4,'big') + nonce.to_bytes(2,'big') + short.to_bytes(1,'big')
-		return scramble_seed(seed.data,scramble_key)[:16 if short else seed.byte_len]
+		from .crypto import Crypto
+		return Crypto().scramble_seed(seed.data,scramble_key)[:16 if short else seed.byte_len]
 
 
 class SubSeedList(MMGenObject):
 class SubSeedList(MMGenObject):
 	have_short = True
 	have_short = True

+ 4 - 4
mmgen/tool/coin.py

@@ -51,20 +51,20 @@ class tool_cmd(tool_cmd_base):
 
 
 	def randwif(self):
 	def randwif(self):
 		"generate a random private key in WIF format"
 		"generate a random private key in WIF format"
-		from ..crypto import get_random
+		from ..crypto import Crypto
 		return PrivKey(
 		return PrivKey(
 			self.proto,
 			self.proto,
-			get_random(32),
+			Crypto().get_random(32),
 			pubkey_type = self.mmtype.pubkey_type,
 			pubkey_type = self.mmtype.pubkey_type,
 			compressed  = self.mmtype.compressed ).wif
 			compressed  = self.mmtype.compressed ).wif
 
 
 	def randpair(self):
 	def randpair(self):
 		"generate a random private key/address pair"
 		"generate a random private key/address pair"
 		gd = self._init_generators()
 		gd = self._init_generators()
-		from ..crypto import get_random
+		from ..crypto import Crypto
 		privkey = PrivKey(
 		privkey = PrivKey(
 			self.proto,
 			self.proto,
-			get_random(32),
+			Crypto().get_random(32),
 			pubkey_type = self.mmtype.pubkey_type,
 			pubkey_type = self.mmtype.pubkey_type,
 			compressed  = self.mmtype.compressed )
 			compressed  = self.mmtype.compressed )
 		return (
 		return (

+ 5 - 5
mmgen/tool/filecrypt.py

@@ -23,7 +23,7 @@ tool.filecrypt: File encryption/decryption routines for the 'mmgen-tool' utility
 import os
 import os
 
 
 from .common import tool_cmd_base
 from .common import tool_cmd_base
-from ..crypto import mmgen_encrypt,mmgen_decrypt,mmenc_ext
+from ..crypto import Crypto
 from ..fileutil import get_data_from_file,write_data_to_file
 from ..fileutil import get_data_from_file,write_data_to_file
 
 
 class tool_cmd(tool_cmd_base):
 class tool_cmd(tool_cmd_base):
@@ -38,9 +38,9 @@ class tool_cmd(tool_cmd_base):
 	def encrypt(self,infile:str,outfile='',hash_preset=''):
 	def encrypt(self,infile:str,outfile='',hash_preset=''):
 		"encrypt a file"
 		"encrypt a file"
 		data = get_data_from_file( infile, 'data for encryption', binary=True )
 		data = get_data_from_file( infile, 'data for encryption', binary=True )
-		enc_d = mmgen_encrypt( data, 'data', hash_preset )
+		enc_d = Crypto().mmgen_encrypt( data, 'data', hash_preset )
 		if not outfile:
 		if not outfile:
-			outfile = f'{os.path.basename(infile)}.{mmenc_ext}'
+			outfile = f'{os.path.basename(infile)}.{Crypto.mmenc_ext}'
 		write_data_to_file( outfile, enc_d, 'encrypted data', binary=True )
 		write_data_to_file( outfile, enc_d, 'encrypted data', binary=True )
 		return True
 		return True
 
 
@@ -48,14 +48,14 @@ class tool_cmd(tool_cmd_base):
 		"decrypt a file"
 		"decrypt a file"
 		enc_d = get_data_from_file( infile, 'encrypted data', binary=True )
 		enc_d = get_data_from_file( infile, 'encrypted data', binary=True )
 		while True:
 		while True:
-			dec_d = mmgen_decrypt( enc_d, 'data', hash_preset )
+			dec_d = Crypto().mmgen_decrypt( enc_d, 'data', hash_preset )
 			if dec_d:
 			if dec_d:
 				break
 				break
 			msg('Trying again...')
 			msg('Trying again...')
 		if not outfile:
 		if not outfile:
 			from ..util import remove_extension
 			from ..util import remove_extension
 			o = os.path.basename(infile)
 			o = os.path.basename(infile)
-			outfile = remove_extension(o,mmenc_ext)
+			outfile = remove_extension(o,Crypto.mmenc_ext)
 			if outfile == o:
 			if outfile == o:
 				outfile += '.dec'
 				outfile += '.dec'
 		write_data_to_file( outfile, dec_d, 'decrypted data', binary=True )
 		write_data_to_file( outfile, dec_d, 'decrypted data', binary=True )

+ 4 - 4
mmgen/tool/fileutil.py

@@ -24,7 +24,7 @@ import os
 
 
 from .common import tool_cmd_base
 from .common import tool_cmd_base
 from ..util import msg,msg_r,qmsg,die,suf,make_full_path
 from ..util import msg,msg_r,qmsg,die,suf,make_full_path
-from ..crypto import get_random,aesctr_iv_len
+from ..crypto import Crypto
 
 
 class tool_cmd(tool_cmd_base):
 class tool_cmd(tool_cmd_base):
 	"file utilities"
 	"file utilities"
@@ -38,7 +38,7 @@ class tool_cmd(tool_cmd_base):
 		from hashlib import sha256
 		from hashlib import sha256
 		from ..globalvars import g
 		from ..globalvars import g
 
 
-		ivsize,bsize,mod = ( aesctr_iv_len, 4096, 4096*8 )
+		ivsize,bsize,mod = ( Crypto.aesctr_iv_len, 4096, 4096*8 )
 		n,carry = 0,b' '*ivsize
 		n,carry = 0,b' '*ivsize
 		flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
 		flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
 		f = os.open(filename,flgs)
 		f = os.open(filename,flgs)
@@ -98,7 +98,7 @@ class tool_cmd(tool_cmd_base):
 		from ..util2 import parse_bytespec
 		from ..util2 import parse_bytespec
 
 
 		def encrypt_worker(wid):
 		def encrypt_worker(wid):
-			ctr_init_val = os.urandom( aesctr_iv_len )
+			ctr_init_val = os.urandom( Crypto.aesctr_iv_len )
 			c = Cipher( algorithms.AES(key), modes.CTR(ctr_init_val), backend=default_backend() )
 			c = Cipher( algorithms.AES(key), modes.CTR(ctr_init_val), backend=default_backend() )
 			encryptor = c.encryptor()
 			encryptor = c.encryptor()
 			while True:
 			while True:
@@ -115,7 +115,7 @@ class tool_cmd(tool_cmd_base):
 			outfile = make_full_path( opt.outdir, outfile )
 			outfile = make_full_path( opt.outdir, outfile )
 		f = open(outfile,'wb')
 		f = open(outfile,'wb')
 
 
-		key = get_random(32)
+		key = Crypto().get_random(32)
 		q1,q2 = ( Queue(), Queue() )
 		q1,q2 = ( Queue(), Queue() )
 
 
 		for i in range(max(1,threads-2)):
 		for i in range(max(1,threads-2)):

+ 2 - 2
mmgen/tool/mnemonic.py

@@ -71,8 +71,8 @@ class tool_cmd(tool_cmd_base):
 
 
 	def _do_random_mn(self,nbytes:int,fmt:str):
 	def _do_random_mn(self,nbytes:int,fmt:str):
 		assert nbytes in (16,24,32), 'nbytes must be 16, 24 or 32'
 		assert nbytes in (16,24,32), 'nbytes must be 16, 24 or 32'
-		from ..crypto import get_random
-		randbytes = get_random(nbytes)
+		from ..crypto import Crypto
+		randbytes = Crypto().get_random(nbytes)
 		if fmt == 'xmrseed':
 		if fmt == 'xmrseed':
 			randbytes = self._xmr_reduce(randbytes)
 			randbytes = self._xmr_reduce(randbytes)
 		from ..opts import opt
 		from ..opts import opt

+ 4 - 4
mmgen/tool/util.py

@@ -92,8 +92,8 @@ class tool_cmd(tool_cmd_base):
 	def randhex(self,
 	def randhex(self,
 			nbytes: 'number of bytes to output' = 32 ):
 			nbytes: 'number of bytes to output' = 32 ):
 		"print 'n' bytes (default 32) of random data in hex format"
 		"print 'n' bytes (default 32) of random data in hex format"
-		from ..crypto import get_random
-		return get_random( nbytes ).hex()
+		from ..crypto import Crypto
+		return Crypto().get_random( nbytes ).hex()
 
 
 	def hexreverse(self,hexstr:'sstr'):
 	def hexreverse(self,hexstr:'sstr'):
 		"reverse bytes of a hexadecimal string"
 		"reverse bytes of a hexadecimal string"
@@ -175,9 +175,9 @@ class tool_cmd(tool_cmd_base):
 			nbytes: 'number of bytes to output' = 32,
 			nbytes: 'number of bytes to output' = 32,
 			pad:    'pad output to this width' = 0 ):
 			pad:    'pad output to this width' = 0 ):
 		"generate random data (default: 32 bytes) and convert it to base 58"
 		"generate random data (default: 32 bytes) and convert it to base 58"
-		from ..crypto import get_random
 		from ..baseconv import baseconv
 		from ..baseconv import baseconv
-		return baseconv('b58').frombytes( get_random(nbytes), pad=pad, tostr=True )
+		from ..crypto import Crypto
+		return baseconv('b58').frombytes( Crypto().get_random(nbytes), pad=pad, tostr=True )
 
 
 	def bytestob58(self,infile:str,pad: 'pad output to this width' = 0):
 	def bytestob58(self,infile:str,pad: 'pad output to this width' = 0):
 		"convert bytes to base 58 (supply data via STDIN)"
 		"convert bytes to base 58 (supply data via STDIN)"

+ 1 - 2
mmgen/wallet/brain.py

@@ -17,7 +17,6 @@ from ..util import msg,qmsg,qmsg_r
 from ..color import yellow
 from ..color import yellow
 from .enc import wallet
 from .enc import wallet
 from .seed import Seed
 from .seed import Seed
-import mmgen.crypto as crypto
 
 
 class wallet(wallet):
 class wallet(wallet):
 
 
@@ -46,7 +45,7 @@ class wallet(wallet):
 			bw_seed_len = opt.seed_len or Seed.dfl_len
 			bw_seed_len = opt.seed_len or Seed.dfl_len
 		qmsg_r('Hashing brainwallet data.  Please wait...')
 		qmsg_r('Hashing brainwallet data.  Please wait...')
 		# Use buflen arg of scrypt.hash() to get seed of desired length
 		# Use buflen arg of scrypt.hash() to get seed of desired length
-		seed = crypto.scrypt_hash_passphrase(
+		seed = self.crypto.scrypt_hash_passphrase(
 			self.brainpasswd.encode(),
 			self.brainpasswd.encode(),
 			b'',
 			b'',
 			d.hash_preset,
 			d.hash_preset,

+ 2 - 2
mmgen/wallet/dieroll.py

@@ -56,8 +56,8 @@ class wallet(wallet):
 		if self.interactive_input and opt.usr_randchars:
 		if self.interactive_input and opt.usr_randchars:
 			from ..ui import keypress_confirm
 			from ..ui import keypress_confirm
 			if keypress_confirm(self.user_entropy_prompt):
 			if keypress_confirm(self.user_entropy_prompt):
-				from ..crypto import add_user_random
-				seed_bytes = add_user_random(
+				from ..crypto import Crypto
+				seed_bytes = Crypto().add_user_random(
 					rand_bytes = seed_bytes,
 					rand_bytes = seed_bytes,
 					desc       = 'gathered from your die rolls' )
 					desc       = 'gathered from your die rolls' )
 				self.desc += ' plus user-supplied entropy'
 				self.desc += ' plus user-supplied entropy'

+ 11 - 9
mmgen/wallet/enc.py

@@ -15,13 +15,15 @@ wallet.enc: encrypted wallet base class
 from ..globalvars import g
 from ..globalvars import g
 from ..opts import opt
 from ..opts import opt
 from ..util import msg,qmsg,make_chksum_8
 from ..util import msg,qmsg,make_chksum_8
-
-import mmgen.crypto as crypto
-
 from .base import wallet
 from .base import wallet
 
 
 class wallet(wallet):
 class wallet(wallet):
 
 
+	def __init__(self,*args,**kwargs):
+		from mmgen.crypto import Crypto
+		self.crypto = Crypto()
+		return super().__init__(*args,**kwargs)
+
 	def _decrypt_retry(self):
 	def _decrypt_retry(self):
 		while True:
 		while True:
 			if self._decrypt():
 			if self._decrypt():
@@ -38,7 +40,7 @@ class wallet(wallet):
 			('',' '+add_desc)[bool(add_desc)],
 			('',' '+add_desc)[bool(add_desc)],
 			('accept the default','reuse the old')[self.op=='pwchg_new'],
 			('accept the default','reuse the old')[self.op=='pwchg_new'],
 			old_preset )
 			old_preset )
-		return crypto.get_hash_preset_from_user( old_preset=old_preset, prompt=prompt )
+		return self.crypto.get_hash_preset_from_user( old_preset=old_preset, prompt=prompt )
 
 
 	def _get_hash_preset(self,add_desc=''):
 	def _get_hash_preset(self,add_desc=''):
 		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'hash_preset'):
 		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'hash_preset'):
@@ -63,14 +65,14 @@ class wallet(wallet):
 		self.ssdata.hash_preset = hp
 		self.ssdata.hash_preset = hp
 
 
 	def _get_new_passphrase(self):
 	def _get_new_passphrase(self):
-		return crypto.get_new_passphrase(
+		return self.crypto.get_new_passphrase(
 			data_desc = ('new ' if self.op in ('new','conv') else '') + self.desc,
 			data_desc = ('new ' if self.op in ('new','conv') else '') + self.desc,
 			hash_preset = self.ssdata.hash_preset,
 			hash_preset = self.ssdata.hash_preset,
 			passwd_file = self.passwd_file,
 			passwd_file = self.passwd_file,
 			pw_desc = ('new ' if self.op=='pwchg_new' else '') + 'passphrase' )
 			pw_desc = ('new ' if self.op=='pwchg_new' else '') + 'passphrase' )
 
 
 	def _get_passphrase(self,add_desc=''):
 	def _get_passphrase(self,add_desc=''):
-		return crypto.get_passphrase(
+		return self.crypto.get_passphrase(
 			data_desc = self.desc + (f' {add_desc}' if add_desc else ''),
 			data_desc = self.desc + (f' {add_desc}' if add_desc else ''),
 			passwd_file = self.passwd_file,
 			passwd_file = self.passwd_file,
 			pw_desc = ('old ' if self.op == 'pwchg_old' else '') + 'passphrase' )
 			pw_desc = ('old ' if self.op == 'pwchg_old' else '') + 'passphrase' )
@@ -92,7 +94,7 @@ class wallet(wallet):
 			d.passwd = self._get_new_passphrase()
 			d.passwd = self._get_new_passphrase()
 
 
 		from hashlib import sha256
 		from hashlib import sha256
-		d.salt     = sha256( crypto.get_random(128) ).digest()[:crypto.salt_len]
-		key        = crypto.make_key( d.passwd, d.salt, d.hash_preset )
+		d.salt     = sha256( self.crypto.get_random(128) ).digest()[:self.crypto.salt_len]
+		key        = self.crypto.make_key( d.passwd, d.salt, d.hash_preset )
 		d.key_id   = make_chksum_8(key)
 		d.key_id   = make_chksum_8(key)
-		d.enc_seed = crypto.encrypt_seed( self.seed.data, key )
+		d.enc_seed = self.crypto.encrypt_seed( self.seed.data, key )

+ 8 - 7
mmgen/wallet/incog_base.py

@@ -17,7 +17,6 @@ from ..opts import opt
 from ..seed import Seed
 from ..seed import Seed
 from ..util import msg,vmsg,qmsg,make_chksum_8
 from ..util import msg,vmsg,qmsg,make_chksum_8
 from .enc import wallet
 from .enc import wallet
-import mmgen.crypto as crypto
 
 
 class wallet(wallet):
 class wallet(wallet):
 
 
@@ -39,9 +38,9 @@ class wallet(wallet):
 
 
 	def _get_incog_data_len(self,seed_len):
 	def _get_incog_data_len(self,seed_len):
 		return (
 		return (
-			crypto.aesctr_iv_len
-			+ crypto.salt_len
-			+ (0 if opt.old_incog_fmt else crypto.hincog_chk_len)
+			self.crypto.aesctr_iv_len
+			+ self.crypto.salt_len
+			+ (0 if opt.old_incog_fmt else self.crypto.hincog_chk_len)
 			+ seed_len//8 )
 			+ seed_len//8 )
 
 
 	def _incog_data_size_chk(self):
 	def _incog_data_size_chk(self):
@@ -67,6 +66,7 @@ class wallet(wallet):
 		if opt.old_incog_fmt:
 		if opt.old_incog_fmt:
 			die(1,'Writing old-format incognito wallets is unsupported')
 			die(1,'Writing old-format incognito wallets is unsupported')
 		d = self.ssdata
 		d = self.ssdata
+		crypto = self.crypto
 
 
 		d.iv = crypto.get_random( crypto.aesctr_iv_len )
 		d.iv = crypto.get_random( crypto.aesctr_iv_len )
 		d.iv_id = self._make_iv_chksum(d.iv)
 		d.iv_id = self._make_iv_chksum(d.iv)
@@ -101,7 +101,7 @@ class wallet(wallet):
 
 
 	def _format(self):
 	def _format(self):
 		d = self.ssdata
 		d = self.ssdata
-		self.fmt_data = d.iv + crypto.encrypt_data(
+		self.fmt_data = d.iv + self.crypto.encrypt_data(
 			data = d.salt + d.enc_seed,
 			data = d.salt + d.enc_seed,
 			key  = d.wrapper_key,
 			key  = d.wrapper_key,
 			iv   = d.iv,
 			iv   = d.iv,
@@ -124,9 +124,9 @@ class wallet(wallet):
 			return False
 			return False
 
 
 		d = self.ssdata
 		d = self.ssdata
-		d.iv             = self.fmt_data[0:crypto.aesctr_iv_len]
+		d.iv             = self.fmt_data[0:self.crypto.aesctr_iv_len]
 		d.incog_id       = self._make_iv_chksum(d.iv)
 		d.incog_id       = self._make_iv_chksum(d.iv)
-		d.enc_incog_data = self.fmt_data[crypto.aesctr_iv_len:]
+		d.enc_incog_data = self.fmt_data[self.crypto.aesctr_iv_len:]
 		msg(f'Incog Wallet ID: {d.incog_id}')
 		msg(f'Incog Wallet ID: {d.incog_id}')
 		qmsg('Check this value against your records')
 		qmsg('Check this value against your records')
 		vmsg('\n  ' + self.msg['check_incog_id'].strip()+'\n')
 		vmsg('\n  ' + self.msg['check_incog_id'].strip()+'\n')
@@ -155,6 +155,7 @@ class wallet(wallet):
 		d = self.ssdata
 		d = self.ssdata
 		self._get_hash_preset(add_desc=d.incog_id)
 		self._get_hash_preset(add_desc=d.incog_id)
 		d.passwd = self._get_passphrase(add_desc=d.incog_id)
 		d.passwd = self._get_passphrase(add_desc=d.incog_id)
+		crypto = self.crypto
 
 
 		# IV is used BOTH to initialize counter and to salt password!
 		# IV is used BOTH to initialize counter and to salt password!
 		wrapper_key = crypto.make_key(
 		wrapper_key = crypto.make_key(

+ 4 - 6
mmgen/wallet/mmgen.py

@@ -21,8 +21,6 @@ from ..util import msg,qmsg,make_timestamp,make_chksum_6,split_into_cols,is_chks
 from ..obj import MMGenWalletLabel,get_obj
 from ..obj import MMGenWalletLabel,get_obj
 from ..baseconv import baseconv
 from ..baseconv import baseconv
 
 
-import mmgen.crypto as crypto
-
 from .enc import wallet
 from .enc import wallet
 
 
 class wallet(wallet):
 class wallet(wallet):
@@ -92,7 +90,7 @@ class wallet(wallet):
 		lines = (
 		lines = (
 			d.label,
 			d.label,
 			'{} {} {} {} {}'.format( s.sid.lower(), d.key_id.lower(), s.bitlen, d.pw_status, d.timestamp ),
 			'{} {} {} {} {}'.format( s.sid.lower(), d.key_id.lower(), s.bitlen, d.pw_status, d.timestamp ),
-			'{}: {} {} {}'.format( d.hash_preset, *crypto.get_hash_params(d.hash_preset) ),
+			'{}: {} {} {}'.format( d.hash_preset, *self.crypto.get_hash_params(d.hash_preset) ),
 			'{} {}'.format( make_chksum_6(slt_fmt), split_into_cols(4,slt_fmt) ),
 			'{} {}'.format( make_chksum_6(slt_fmt), split_into_cols(4,slt_fmt) ),
 			'{} {}'.format( make_chksum_6(es_fmt),  split_into_cols(4,es_fmt) )
 			'{} {}'.format( make_chksum_6(es_fmt),  split_into_cols(4,es_fmt) )
 		)
 		)
@@ -140,7 +138,7 @@ class wallet(wallet):
 
 
 		hash_params = tuple(map(int,hpdata[1:]))
 		hash_params = tuple(map(int,hpdata[1:]))
 
 
-		if hash_params != crypto.get_hash_params(d.hash_preset):
+		if hash_params != self.crypto.get_hash_params(d.hash_preset):
 			msg(f'Hash parameters {" ".join(hash_params)!r} don’t match hash preset {d.hash_preset!r}')
 			msg(f'Hash parameters {" ".join(hash_params)!r} don’t match hash preset {d.hash_preset!r}')
 			return False
 			return False
 
 
@@ -172,8 +170,8 @@ class wallet(wallet):
 		# Needed for multiple transactions with {}-txsign
 		# Needed for multiple transactions with {}-txsign
 		d.passwd = self._get_passphrase(
 		d.passwd = self._get_passphrase(
 			add_desc = os.path.basename(self.infile.name) if opt.quiet else '' )
 			add_desc = os.path.basename(self.infile.name) if opt.quiet else '' )
-		key = crypto.make_key( d.passwd, d.salt, d.hash_preset )
-		ret = crypto.decrypt_seed( d.enc_seed, key, d.seed_id, d.key_id )
+		key = self.crypto.make_key( d.passwd, d.salt, d.hash_preset )
+		ret = self.crypto.decrypt_seed( d.enc_seed, key, d.seed_id, d.key_id )
 		if ret:
 		if ret:
 			self.seed = Seed(ret)
 			self.seed = Seed(ret)
 			return True
 			return True

+ 7 - 5
test/misc/get_passphrase.py

@@ -21,22 +21,24 @@ cmd_args = opts.init({
 		"""
 		"""
 	}})
 	}})
 
 
-from mmgen.crypto import get_passphrase,get_new_passphrase,get_hash_preset_from_user
 from mmgen.wallet import Wallet
 from mmgen.wallet import Wallet
 
 
 def crypto():
 def crypto():
 	desc = 'test data'
 	desc = 'test data'
 
 
-	pw = get_new_passphrase(data_desc=desc,hash_preset=g.dfl_hash_preset,passwd_file=None)
+	from mmgen.crypto import Crypto
+	crypto = Crypto()
+
+	pw = crypto.get_new_passphrase(data_desc=desc,hash_preset=g.dfl_hash_preset,passwd_file=None)
 	msg(f'==> got new passphrase: [{pw}]\n')
 	msg(f'==> got new passphrase: [{pw}]\n')
 
 
-	pw = get_passphrase(data_desc=desc,passwd_file=None)
+	pw = crypto.get_passphrase(data_desc=desc,passwd_file=None)
 	msg(f'==> got passphrase: [{pw}]\n')
 	msg(f'==> got passphrase: [{pw}]\n')
 
 
-	hp = get_hash_preset_from_user(data_desc=desc)
+	hp = crypto.get_hash_preset_from_user(data_desc=desc)
 	msg(f'==> got hash preset: [{hp}]')
 	msg(f'==> got hash preset: [{hp}]')
 
 
-	hp = get_hash_preset_from_user(data_desc=desc)
+	hp = crypto.get_hash_preset_from_user(data_desc=desc)
 	msg(f'==> got hash preset: [{hp}]')
 	msg(f'==> got hash preset: [{hp}]')
 
 
 def seed():
 def seed():

+ 2 - 2
test/misc/oneshot_warning.py

@@ -30,11 +30,11 @@ class wg(oneshot_warning_group):
 
 
 for i in (1,2,3):
 for i in (1,2,3):
 
 
-	from mmgen.crypto import pwfile_reuse_warning
+	from mmgen.crypto import Crypto
 
 
 	msg('\npw')
 	msg('\npw')
 	for k in ('A','B'):
 	for k in ('A','B'):
-		ret = pwfile_reuse_warning(k).warning_shown
+		ret = Crypto.pwfile_reuse_warning(k).warning_shown
 		assert ret == (i != 1), 'warning_shown incorrect'
 		assert ret == (i != 1), 'warning_shown incorrect'
 
 
 	msg('wg1')
 	msg('wg1')

+ 2 - 2
test/misc/term.py

@@ -144,8 +144,8 @@ def _tt_get_char(raw=False,one_char=False,immed_chars=''):
 
 
 def tt_urand():
 def tt_urand():
 	cmsg('Testing _get_random_data_from_user():')
 	cmsg('Testing _get_random_data_from_user():')
-	from mmgen.crypto import _get_random_data_from_user
-	ret = _get_random_data_from_user(10,desc='data').decode()
+	from mmgen.crypto import Crypto
+	ret = Crypto()._get_random_data_from_user(uchars=10,desc='data').decode()
 	msg(f'USER ENTROPY (user input + keystroke timings):\n\n{fmt(ret,"  ")}')
 	msg(f'USER ENTROPY (user input + keystroke timings):\n\n{fmt(ret,"  ")}')
 	times = ret.splitlines()[1:]
 	times = ret.splitlines()[1:]
 	avg_prec = sum(len(t.split('.')[1]) for t in times) // len(times)
 	avg_prec = sum(len(t.split('.')[1]) for t in times) // len(times)

+ 6 - 4
test/unit_tests_d/ut_scrypt.py

@@ -13,7 +13,9 @@ class unit_test(object):
 
 
 		msg_r('Testing password hashing...')
 		msg_r('Testing password hashing...')
 		qmsg('')
 		qmsg('')
-		from mmgen.crypto import scrypt_hash_passphrase,hash_presets
+
+		from mmgen.crypto import Crypto
+		crypto = Crypto()
 
 
 		salt = bytes.fromhex('f00f' * 16)
 		salt = bytes.fromhex('f00f' * 16)
 
 
@@ -37,7 +39,7 @@ class unit_test(object):
 						omsg_r('.')
 						omsg_r('.')
 					else:
 					else:
 						msg_r(f'\n  password {pw_disp:9} ')
 						msg_r(f'\n  password {pw_disp:9} ')
-					ret = scrypt_hash_passphrase(pw,salt,'1').hex()
+					ret = crypto.scrypt_hash_passphrase(pw,salt,'1').hex()
 					assert ret == res, ret
 					assert ret == res, ret
 
 
 		def test_presets(do_presets):
 		def test_presets(do_presets):
@@ -48,9 +50,9 @@ class unit_test(object):
 				if opt.quiet:
 				if opt.quiet:
 					omsg_r('.')
 					omsg_r('.')
 				else:
 				else:
-					msg_r(f'\n  {hp!r:3}: {hash_presets[hp]!r:12}  ')
+					msg_r(f'\n  {hp!r:3}: {crypto.hash_presets[hp]!r:12}  ')
 				st = time.time()
 				st = time.time()
-				ret = scrypt_hash_passphrase(pw,salt,hp).hex()
+				ret = crypto.scrypt_hash_passphrase(pw,salt,hp).hex()
 				t = time.time() - st
 				t = time.time() - st
 				vmsg('' if g.test_suite_deterministic else f'  {t:0.4f} secs')
 				vmsg('' if g.test_suite_deterministic else f'  {t:0.4f} secs')
 				assert ret == res, ret
 				assert ret == res, ret