#!/usr/bin/env python3 """ Simple password store, backed by a JSON file. Also contains an entry point for managing the store. """ import hashlib import json import os from typing import ( TypedDict, ) def gen_salt(length: int = 10) -> str: """Generate a random salt.""" # urandom is stated to be suitable for cryptographic use. return bytearray(os.urandom(length)).hex() # Manual list of entries, to stop someone from executing arbitrary # code by modyfying password database hash_methods = { 'sha256': hashlib.sha256 } class PasswordEntry(TypedDict): """A single entry in the password store.""" hash: str salt: str # One of the keys of hash_methods method: str class Passwords: """ Simple password store. :param fname: Path of json file to load and store from. It's recommended to end this name with ``.json``. """ def __init__(self, fname: str): self.fname = fname self.db: dict[str, PasswordEntry] try: with open(fname) as f: self.db = json.load(f) except Exception: self.db = {} def save(self) -> None: """Dump current data to disk.""" try: with open(os.fspath(self.fname) + '.tmp', 'w') as f: json.dump(self.db, f) f.write('\n') os.rename(os.fspath(self.fname) + '.tmp', self.fname) except Exception as e: print(f'Saving password failed {e}') def add(self, username: str, password: str) -> None: """Add (or modify) entry in store.""" salt = gen_salt() hashed = hashlib.sha256((salt + password).encode('UTF-8')) self.db[username] = { 'hash': hashed.hexdigest(), 'salt': salt, 'method': 'sha256' } def validate(self, username: str, password: str) -> bool: """Check if user exists, and if it has a correct password.""" # These shall fail when key is missing data = self.db.get(username) if not data: return False proc = hash_methods[data['method']] digest = proc((data['salt'] + password).encode('UTF-8')).hexdigest() return data['hash'] == digest # TODO possibly add tests for main def main() -> None: # pragma: no cover """Entry point for directly interfacing with the password store.""" import argparse parser = argparse.ArgumentParser() parser.add_argument('--file', default='passwords.json') subparsers = parser.add_subparsers(dest='cmd') add_parser = subparsers.add_parser('add') add_parser.add_argument('username') add_parser.add_argument('password') val_parser = subparsers.add_parser('validate') val_parser.add_argument('username') val_parser.add_argument('password') args = parser.parse_args() passwords = Passwords(args.file) if args.cmd == 'add': passwords.add(args.username, args.password) passwords.save() elif args.cmd == 'validate': print(passwords.validate(args.username, args.password)) else: parser.print_help() if __name__ == '__main__': # pragma: no cover main()