crypto.py: reimplement as class

This commit is contained in:
The MMGen Project 2023-03-27 10:50:38 +00:00
commit 19f0730913
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
24 changed files with 430 additions and 419 deletions

View file

@ -49,12 +49,11 @@ class AddrFile(MMGenObject):
self.infile = None
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(),
desc = f'new {self.parent.desc} list' )
self.ext += f'.{mmenc_ext}'
self.ext += f'.{Crypto.mmenc_ext}'
@property
def filename(self):

View file

@ -312,11 +312,11 @@ class AddrList(MMGenObject): # Address info for a single seed ID
scramble_key = self.proto.coin.lower()
else:
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:
scramble_key += ':' + self.proto.network
self.dmsg_sc('str',scramble_key)
return scramble_seed(seed,scramble_key.encode())
return Crypto().scramble_seed(seed,scramble_key.encode())
def idxs(self):
return [e.idx for e in self.data]

View file

@ -39,368 +39,376 @@ from .util import (
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
class Crypto:
# 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),
}
mmenc_ext = 'mmenc'
scramble_hash_rounds = 10
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)
salt_len = 16
aesctr_iv_len = 16
aesctr_dfl_iv = int.to_bytes(1,aesctr_iv_len,'big')
hincog_chk_len = 8
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")
mmenc_salt_len = 32
mmenc_nonce_len = 32
def sha256_rounds(s,n):
from hashlib import sha256
for i in range(n):
s = sha256(s).digest()
return s
# 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),
}
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 )
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 encrypt_seed(data,key,desc='seed'):
return encrypt_data(data,key,desc=desc)
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 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'):
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())
encryptor = c.encryptor()
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.
"""
msg(f'Enter {uchars} random symbols' if opt.quiet else
'\n' + fmt(info1,indent=' ') +
'\n' + fmt(info2) )
import time
from .term import get_char_raw
key_data = ''
time_data = []
for i in range(uchars):
key_data += get_char_raw(f'\rYou may begin typing. {uchars-i} symbols left: ')
time_data.append(time.time())
msg_r( '\r' if opt.quiet else f'\rThank you. That’s enough.{" "*18}\n\n' )
time_data = [f'{t:.22f}'.rstrip('0') for t in time_data]
avg_prec = sum(len(t.split('.')[1]) for t in time_data) // len(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!')
ret = key_data + '\n' + '\n'.join(time_data)
if g.debug:
msg(f'USER ENTROPY (user input + keystroke timings):\n{ret}')
from .ui import line_input
line_input( 'User random data successfully acquired. Press ENTER to continue: ' )
return ret.encode()
def get_random(self,length):
os_rand = os.urandom(length)
assert len(os_rand) == length, f'OS random number generator returned {len(os_rand)} (!= {length}) bytes!'
return self.add_user_random(
rand_bytes = os_rand,
desc = 'from your operating system' )
def add_user_random(
self,
rand_bytes,
desc,
urand = {'data':b'', 'counter':0} ):
assert type(rand_bytes) == bytes, 'add_user_random_chk1'
if opt.usr_randchars:
if not urand['data']:
from hashlib import sha256
urand['data'] = sha256(self._get_random_data_from_user(desc=desc)).digest()
# counter protects against very evil rng that might repeatedly output the same data
urand['counter'] += 1
os_rand = os.urandom(8)
assert len(os_rand) == 8, f'OS random number generator returned {len(os_rand)} (!= 8) bytes!'
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 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')
if pw == '':
qmsg('WARNING: Empty passphrase')
return pw
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:
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:
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}...')
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(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)
return ret
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
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}.
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.
"""
msg(f'Enter {uchars} random symbols' if opt.quiet else
'\n' + fmt(info1,indent=' ') +
'\n' + fmt(info2) )
import time
from .term import get_char_raw
key_data = ''
time_data = []
for i in range(uchars):
key_data += get_char_raw(f'\rYou may begin typing. {uchars-i} symbols left: ')
time_data.append(time.time())
msg_r( '\r' if opt.quiet else f'\rThank you. That’s enough.{" "*18}\n\n' )
time_data = [f'{t:.22f}'.rstrip('0') for t in time_data]
avg_prec = sum(len(t.split('.')[1]) for t in time_data) // len(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!')
ret = key_data + '\n' + '\n'.join(time_data)
if g.debug:
msg(f'USER ENTROPY (user input + keystroke timings):\n{ret}')
from .ui import line_input
line_input('User random data successfully acquired. Press ENTER to continue: ')
return ret.encode()
def get_random(length):
os_rand = os.urandom(length)
assert len(os_rand) == length, f'OS random number generator returned {len(os_rand)} (!= {length}) bytes!'
return add_user_random(
rand_bytes = os_rand,
desc = 'from your operating system' )
def add_user_random(
rand_bytes,
desc,
urand = {'data':b'', 'counter':0} ):
assert type(rand_bytes) == bytes, 'add_user_random_chk1'
if opt.usr_randchars:
if not urand['data']:
from hashlib import sha256
urand['data'] = sha256(_get_random_data_from_user(opt.usr_randchars,desc)).digest()
# counter protects against very evil rng that might repeatedly output the same data
urand['counter'] += 1
os_rand = os.urandom(8)
assert len(os_rand) == 8, f'OS random number generator returned {len(os_rand)} (!= 8) bytes!'
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)))
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}: '))
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...')
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...')

View file

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

View file

@ -322,13 +322,12 @@ def get_lines_from_file(
def decrypt_file_maybe():
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):
m = ('Attempting to decrypt','Decrypting')[have_enc_ext]
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
lines = decrypt_file_maybe().decode().splitlines()

View file

@ -96,11 +96,11 @@ def die_on_incompatible_opts(incompat_list):
def _show_hash_presets():
fs = ' {:<7} {:<6} {:<3} {}'
from .util import msg
from .crypto import hash_presets
from .crypto import Crypto
msg('Available parameters for scrypt.hash():')
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)')
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)
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):
from .crypto import hash_presets
a = val.split(',')
if len(a) != 2:
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)
from .seed import Seed
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):
if val == 0:

View file

@ -239,5 +239,5 @@ class PasswordList(AddrList):
scramble_key = f'hex:{pwlen}:{self.pw_id_str}'
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())

View file

@ -57,10 +57,10 @@ class SeedBase(MMGenObject):
def __init__(self,seed_bin=None,nSubseeds=None):
if not seed_bin:
from .opts import opt
from .crypto import get_random
from .crypto import Crypto
from hashlib import sha256
# 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:
die(3,f'{len(seed_bin)*8}: invalid seed bit length')

View file

@ -24,6 +24,7 @@ from .globalvars import g
from .color import yellow
from .obj import MMGenPWIDString,MMGenIdx
from .subseed import *
from .crypto import Crypto
class SeedShareIdx(MMGenIdx):
max_val = 1024
@ -209,7 +210,7 @@ class SeedShare(SeedShareBase,SubSeed):
b':master:' +
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):
@ -261,14 +262,14 @@ class SeedShareMaster(SeedBase,SeedShareBase):
seed = self.parent_list.parent_seed
# field maximums: idx: 65535 (1024)
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
# by user as an identifier
def make_derived_seed_bin(self,id_str,count):
# field maximums: id_str: none (256 chars), count: 65535 (1024)
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):
psid = self.parent_list.parent_seed.sid

View file

@ -24,7 +24,6 @@ from .color import green
from .util import msg_r,msg,qmsg,die
from .obj import MMGenRange,IndexedDict
from .seed import *
from .crypto import scramble_seed
class SubSeedIdxRange(MMGenRange):
min_idx = 1
@ -75,7 +74,8 @@ class SubSeed(SeedBase):
short = { 'short': True, 'long': False }[length]
# 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')
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):
have_short = True

View file

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

View file

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

View file

@ -24,7 +24,7 @@ import os
from .common import tool_cmd_base
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):
"file utilities"
@ -38,7 +38,7 @@ class tool_cmd(tool_cmd_base):
from hashlib import sha256
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
flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
f = os.open(filename,flgs)
@ -98,7 +98,7 @@ class tool_cmd(tool_cmd_base):
from ..util2 import parse_bytespec
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() )
encryptor = c.encryptor()
while True:
@ -115,7 +115,7 @@ class tool_cmd(tool_cmd_base):
outfile = make_full_path( opt.outdir, outfile )
f = open(outfile,'wb')
key = get_random(32)
key = Crypto().get_random(32)
q1,q2 = ( Queue(), Queue() )
for i in range(max(1,threads-2)):

View file

@ -71,8 +71,8 @@ class tool_cmd(tool_cmd_base):
def _do_random_mn(self,nbytes:int,fmt:str):
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':
randbytes = self._xmr_reduce(randbytes)
from ..opts import opt

View file

@ -92,8 +92,8 @@ class tool_cmd(tool_cmd_base):
def randhex(self,
nbytes: 'number of bytes to output' = 32 ):
"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'):
"reverse bytes of a hexadecimal string"
@ -175,9 +175,9 @@ class tool_cmd(tool_cmd_base):
nbytes: 'number of bytes to output' = 32,
pad: 'pad output to this width' = 0 ):
"generate random data (default: 32 bytes) and convert it to base 58"
from ..crypto import get_random
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):
"convert bytes to base 58 (supply data via STDIN)"

View file

@ -17,7 +17,6 @@ from ..util import msg,qmsg,qmsg_r
from ..color import yellow
from .enc import wallet
from .seed import Seed
import mmgen.crypto as crypto
class wallet(wallet):
@ -46,7 +45,7 @@ class wallet(wallet):
bw_seed_len = opt.seed_len or Seed.dfl_len
qmsg_r('Hashing brainwallet data. Please wait...')
# 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(),
b'',
d.hash_preset,

View file

@ -56,8 +56,8 @@ class wallet(wallet):
if self.interactive_input and opt.usr_randchars:
from ..ui import keypress_confirm
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,
desc = 'gathered from your die rolls' )
self.desc += ' plus user-supplied entropy'

View file

@ -15,13 +15,15 @@ wallet.enc: encrypted wallet base class
from ..globalvars import g
from ..opts import opt
from ..util import msg,qmsg,make_chksum_8
import mmgen.crypto as crypto
from .base import 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):
while True:
if self._decrypt():
@ -38,7 +40,7 @@ class wallet(wallet):
('',' '+add_desc)[bool(add_desc)],
('accept the default','reuse the old')[self.op=='pwchg_new'],
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=''):
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
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,
hash_preset = self.ssdata.hash_preset,
passwd_file = self.passwd_file,
pw_desc = ('new ' if self.op=='pwchg_new' else '') + 'passphrase' )
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 ''),
passwd_file = self.passwd_file,
pw_desc = ('old ' if self.op == 'pwchg_old' else '') + 'passphrase' )
@ -92,7 +94,7 @@ class wallet(wallet):
d.passwd = self._get_new_passphrase()
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.enc_seed = crypto.encrypt_seed( self.seed.data, key )
d.enc_seed = self.crypto.encrypt_seed( self.seed.data, key )

View file

@ -17,7 +17,6 @@ from ..opts import opt
from ..seed import Seed
from ..util import msg,vmsg,qmsg,make_chksum_8
from .enc import wallet
import mmgen.crypto as crypto
class wallet(wallet):
@ -39,9 +38,9 @@ class wallet(wallet):
def _get_incog_data_len(self,seed_len):
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 )
def _incog_data_size_chk(self):
@ -67,6 +66,7 @@ class wallet(wallet):
if opt.old_incog_fmt:
die(1,'Writing old-format incognito wallets is unsupported')
d = self.ssdata
crypto = self.crypto
d.iv = crypto.get_random( crypto.aesctr_iv_len )
d.iv_id = self._make_iv_chksum(d.iv)
@ -101,7 +101,7 @@ class wallet(wallet):
def _format(self):
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,
key = d.wrapper_key,
iv = d.iv,
@ -124,9 +124,9 @@ class wallet(wallet):
return False
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.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}')
qmsg('Check this value against your records')
vmsg('\n ' + self.msg['check_incog_id'].strip()+'\n')
@ -155,6 +155,7 @@ class wallet(wallet):
d = self.ssdata
self._get_hash_preset(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!
wrapper_key = crypto.make_key(

View file

@ -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 ..baseconv import baseconv
import mmgen.crypto as crypto
from .enc import wallet
class wallet(wallet):
@ -92,7 +90,7 @@ class wallet(wallet):
lines = (
d.label,
'{} {} {} {} {}'.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(es_fmt), split_into_cols(4,es_fmt) )
)
@ -140,7 +138,7 @@ class wallet(wallet):
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}')
return False
@ -172,8 +170,8 @@ class wallet(wallet):
# Needed for multiple transactions with {}-txsign
d.passwd = self._get_passphrase(
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:
self.seed = Seed(ret)
return True

View file

@ -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
def crypto():
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')
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')
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}]')
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}]')
def seed():

View file

@ -30,11 +30,11 @@ class wg(oneshot_warning_group):
for i in (1,2,3):
from mmgen.crypto import pwfile_reuse_warning
from mmgen.crypto import Crypto
msg('\npw')
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'
msg('wg1')

View file

@ -144,8 +144,8 @@ def _tt_get_char(raw=False,one_char=False,immed_chars=''):
def tt_urand():
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," ")}')
times = ret.splitlines()[1:]
avg_prec = sum(len(t.split('.')[1]) for t in times) // len(times)

View file

@ -13,7 +13,9 @@ class unit_test(object):
msg_r('Testing password hashing...')
qmsg('')
from mmgen.crypto import scrypt_hash_passphrase,hash_presets
from mmgen.crypto import Crypto
crypto = Crypto()
salt = bytes.fromhex('f00f' * 16)
@ -37,7 +39,7 @@ class unit_test(object):
omsg_r('.')
else:
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
def test_presets(do_presets):
@ -48,9 +50,9 @@ class unit_test(object):
if opt.quiet:
omsg_r('.')
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()
ret = scrypt_hash_passphrase(pw,salt,hp).hex()
ret = crypto.scrypt_hash_passphrase(pw,salt,hp).hex()
t = time.time() - st
vmsg('' if g.test_suite_deterministic else f' {t:0.4f} secs')
assert ret == res, ret