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.
This commit is contained in:
2021-11-10 23:29:01 +01:00
parent ea5abfe149
commit 5e074c6ab9
3 changed files with 146 additions and 21 deletions

View File

@ -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))

View File

@ -1,5 +1,6 @@
pysrc = files(
'__init__.py',
'__main__.py',
'passgeny.py',
'bhash.py',
'phogen.py')

View File

@ -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,7 +28,24 @@ PASSGENY_ARGON2_PARALLEL = 4
# Hash length - 512 bits by default
PASSGENY_ARGON2_HASH_LEN = 64
def argon2_hash(message):
class PassgenyInvalidPattern(Exception):
pass
class Passgeny:
def __init__(self, master_password):
self.master_password = hashlib.sha256(master_password.encode()).digest()
self.pattern = PASSGENY_DEFAULT_PATTERN
def generate(self, domain, user, *tokens):
bh = bhash.Bhash()
bh.from_bytes(self.__hash([domain, user, *tokens]))
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,
@ -33,15 +55,80 @@ def argon2_hash(message):
parallelism = PASSGENY_ARGON2_PARALLEL,
hash_len = PASSGENY_ARGON2_HASH_LEN)
def passgeny_hash(master_password, token_list):
def __hash(self, token_list):
message = b''
# Construct the message to be hashed
for x in token_list:
message += x.encode() + b'\0'
message += master_password.encode() + b'\0'
return argon2_hash(message)
# 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")))