#!/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)