102 lines
3.0 KiB
Python
Executable File
102 lines
3.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""passdrill: typing drills for practicing passphrases
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from base64 import b64encode, b64decode
|
|
from getpass import getpass
|
|
from hashlib import scrypt
|
|
from typing import Sequence, Tuple
|
|
|
|
HASH_FILENAME = 'passdrill.hash'
|
|
HELP = 'Use -s to save passphrase hash for practice.'
|
|
|
|
|
|
def prompt() -> str:
|
|
print('WARNING: the passphrase WILL BE SHOWN so that you can check it!')
|
|
confirmed = ''
|
|
while confirmed != 'y':
|
|
passphrase = input('Type passphrase to hash (it will be echoed): ')
|
|
if passphrase in ('', 'q'):
|
|
print('ERROR: the passphrase cannot be empty or "q".')
|
|
continue
|
|
print(f'Passphrase to be hashed -> {passphrase}')
|
|
confirmed = input('Confirm (y/n): ').lower()
|
|
return passphrase
|
|
|
|
|
|
def crypto_hash(salt: bytes, passphrase: str) -> bytes:
|
|
octets = passphrase.encode('utf-8')
|
|
# Recommended parameters for interactive logins as of 2017:
|
|
# N=32768, r=8 and p=1 (https://godoc.org/golang.org/x/crypto/scrypt)
|
|
return scrypt(octets, salt=salt, n=32768, r=8, p=1, maxmem=2 ** 26)
|
|
|
|
|
|
def build_hash(passphrase: str) -> bytes:
|
|
salt = os.urandom(32)
|
|
payload = crypto_hash(salt, passphrase)
|
|
return b64encode(salt) + b':' + b64encode(payload)
|
|
|
|
|
|
def save_hash() -> None:
|
|
salted_hash = build_hash(prompt())
|
|
with open(HASH_FILENAME, 'wb') as fp:
|
|
fp.write(salted_hash)
|
|
print(f'Passphrase hash saved to {HASH_FILENAME}')
|
|
|
|
|
|
def load_hash() -> Tuple[bytes, bytes]:
|
|
try:
|
|
with open(HASH_FILENAME, 'rb') as fp:
|
|
salted_hash = fp.read()
|
|
except FileNotFoundError:
|
|
print('ERROR: passphrase hash file not found.', HELP)
|
|
# "standard" exit status codes:
|
|
# https://stackoverflow.com/questions/1101957/are-there-any-standard-exit-status-codes-in-linux/40484670#40484670
|
|
sys.exit(74) # input/output error
|
|
|
|
salt, stored_hash = salted_hash.split(b':')
|
|
return b64decode(salt), b64decode(stored_hash)
|
|
|
|
|
|
def practice() -> None:
|
|
salt, stored_hash = load_hash()
|
|
print('Type q to end practice.')
|
|
turn = 0
|
|
correct = 0
|
|
while True:
|
|
turn += 1
|
|
response = getpass(f'{turn}:')
|
|
if response == '':
|
|
print('Type q to quit.')
|
|
turn -= 1 # don't count this response
|
|
continue
|
|
elif response == 'q':
|
|
turn -= 1 # don't count this response
|
|
break
|
|
if crypto_hash(salt, response) == stored_hash:
|
|
correct += 1
|
|
answer = 'OK'
|
|
else:
|
|
answer = 'wrong'
|
|
print(f' {answer}\thits={correct}\tmisses={turn-correct}')
|
|
|
|
if turn:
|
|
print(f'\n{turn} turns. {correct / turn:.1%} correct.')
|
|
|
|
|
|
def main(argv: Sequence[str]) -> None:
|
|
if len(argv) < 2:
|
|
practice()
|
|
elif len(argv) == 2 and argv[1] == '-s':
|
|
save_hash()
|
|
else:
|
|
print('ERROR: invalid argument.', HELP)
|
|
sys.exit(2) # command line usage error
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv)
|