From 5e074c6ab939d47ecdce8e96d5d68ac5874977f6 Mon Sep 17 00:00:00 2001 From: Mitja HORVAT Date: Wed, 10 Nov 2021 23:29:01 +0100 Subject: [PATCH] python: Implement main module This commit implements the python main cli module. Currently it supports a preliminary set of command line options but should be able to successfully generate passwords. --- python/passgeny/__main__.py | 37 +++++++++++ python/passgeny/meson.build | 1 + python/passgeny/passgeny.py | 129 ++++++++++++++++++++++++++++++------ 3 files changed, 146 insertions(+), 21 deletions(-) create mode 100644 python/passgeny/__main__.py diff --git a/python/passgeny/__main__.py b/python/passgeny/__main__.py new file mode 100644 index 0000000..26a2c0a --- /dev/null +++ b/python/passgeny/__main__.py @@ -0,0 +1,37 @@ +import argparse +import getpass +import os +import sys + +from passgeny import passgeny + +pargs = argparse.ArgumentParser("Passgeny - Password Generator") +pargs.add_argument("domain", help="Domain or unique site identifier") +pargs.add_argument("user", help="Username or unique user identifier") +pargs.add_argument("tokens", nargs='*', help="Additional tokens") +pargs.add_argument("--verbose", "-v", action='store_true', help="Verbose") +pargs.add_argument("--pattern", "-p", help="Set pattern") + +pargs_pass = pargs.add_mutually_exclusive_group() +pargs_pass.add_argument("--stdin", "-s", action='store_true', help="Read password from stdin") +pargs_pass.add_argument("--env", "-e", help="Read password from environment") + +popt = pargs.parse_args() + +# Read master password +if popt.env: + mpw = os.getenv(popt.env) + if mpw is None: + raise Exception("Environment {} not defined.".format(popt.env)) +elif popt.stdin: + mpw = sys.stdin.readline().rstrip('\n\r') +else: + mpw = getpass.getpass("Master password: ") + +pg = passgeny.Passgeny(mpw) +del mpw + +if popt.pattern: + pg.set_pattern(popt.pattern) + +print(pg.generate(popt.domain, popt.user, *popt.tokens)) diff --git a/python/passgeny/meson.build b/python/passgeny/meson.build index b6153e2..48652b4 100644 --- a/python/passgeny/meson.build +++ b/python/passgeny/meson.build @@ -1,5 +1,6 @@ pysrc = files( '__init__.py', + '__main__.py', 'passgeny.py', 'bhash.py', 'phogen.py') diff --git a/python/passgeny/passgeny.py b/python/passgeny/passgeny.py index 5eb7327..a5a1fab 100644 --- a/python/passgeny/passgeny.py +++ b/python/passgeny/passgeny.py @@ -1,15 +1,20 @@ # # Requirements: argon2-cffi # - import argon2 import argparse -import getpass -import sys +import hashlib +import re + +from . import bhash, phogen # # WARNING: Changing any of the parameters below will affect password generation # +PASSGENY_DEFAULT_PATTERN = "^6p^6p^6ps2p100d" + +# List of special characters +PASSGENY_SPECIAL = "-./=_ " # Default SALT to use; this cannot be random since passgeny must # generate predictable passwords @@ -23,25 +28,107 @@ PASSGENY_ARGON2_PARALLEL = 4 # Hash length - 512 bits by default PASSGENY_ARGON2_HASH_LEN = 64 -def argon2_hash(message): - return argon2.low_level.hash_secret_raw( - message, - PASSGENY_SALT_DEFAULT, - type = argon2.Type.ID, - memory_cost = PASSGENY_ARGON2_MEMORY_COST, - time_cost = PASSGENY_ARGON2_TIME_COST, - parallelism = PASSGENY_ARGON2_PARALLEL, - hash_len = PASSGENY_ARGON2_HASH_LEN) +class PassgenyInvalidPattern(Exception): + pass -def passgeny_hash(master_password, token_list): - message = b'' +class Passgeny: + def __init__(self, master_password): + self.master_password = hashlib.sha256(master_password.encode()).digest() + self.pattern = PASSGENY_DEFAULT_PATTERN - # Construct the message to be hashed - for x in token_list: - message += x.encode() + b'\0' + def generate(self, domain, user, *tokens): + bh = bhash.Bhash() + bh.from_bytes(self.__hash([domain, user, *tokens])) - message += master_password.encode() + b'\0' - return argon2_hash(message) + return self.__generate_from_pattern(bh) + + def set_pattern(self, pattern): + self.pattern = pattern + + def __argon2_hash(self, message): + return argon2.low_level.hash_secret_raw( + message, + PASSGENY_SALT_DEFAULT, + type = argon2.Type.ID, + memory_cost = PASSGENY_ARGON2_MEMORY_COST, + time_cost = PASSGENY_ARGON2_TIME_COST, + parallelism = PASSGENY_ARGON2_PARALLEL, + hash_len = PASSGENY_ARGON2_HASH_LEN) + + def __hash(self, token_list): + message = b'' + # Construct the message to be hashed + for x in token_list: + message += x.encode() + b'\0' + + # Append the SHA256 of the master password + message += self.master_password + + # Compute the hash using argon2 + return self.__argon2_hash(message) + + def __generate_from_pattern(self, bh): + pattern = self.pattern + flags = "" + password = "" + + def gen_phogen(match): + plen = 1 if match[1] == "" else int(match[1]) + return phogen.encode(bh, plen) + + def gen_special(match): + slen = 1 if match[1] == "" else int(match[1]) + return PASSGENY_SPECIAL[bh.modulo(len(PASSGENY_SPECIAL))] + + def gen_decimal(match): + return "{}".format(bh.modulo(int(match[1]))) + + def gen_hex(match): + return "{:x}".format(bh.modulo(int(match[1]))) + + def gen_hexstr(match): + x = "" + for l in range(int(match[1])): + x += "{:x}".format(bh.modulo(16)) + + return x + + + def gen_capitalize(match): + nonlocal flags + flags += '^' + return None + + pattern_list = [ + [ '([0-9]*)p', gen_phogen ], + [ '([0-9]*)s', gen_special ], + [ '([0-9]+)d', gen_decimal ], + [ '([0-9]+)x', gen_hex ], + [ '([0-9]+)X', gen_hexstr ], + [ '\^', gen_capitalize ], + ] + + while pattern: + pw = None + + for pm in pattern_list: + if match := re.match(pm[0], pattern): + break + + if not match: + raise PassgenyInvalidPattern("Invalid pattern: {}".format(self.pattern)) + + pw = pm[1](match) + pattern = pattern[len(match[0]):] + + if not pw: continue + + if "^" in flags: + pw = pw.capitalize() + + password += pw + flags = "" + + print("Used bits = {}".format(bh.bits_used)) + return password -mpw = getpass.getpass("Master password: ") -print(passgeny_hash(mpw, ("1", "2", "3")))