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:
37
python/passgeny/__main__.py
Normal file
37
python/passgeny/__main__.py
Normal 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))
|
||||||
@ -1,5 +1,6 @@
|
|||||||
pysrc = files(
|
pysrc = files(
|
||||||
'__init__.py',
|
'__init__.py',
|
||||||
|
'__main__.py',
|
||||||
'passgeny.py',
|
'passgeny.py',
|
||||||
'bhash.py',
|
'bhash.py',
|
||||||
'phogen.py')
|
'phogen.py')
|
||||||
|
|||||||
@ -1,15 +1,20 @@
|
|||||||
#
|
#
|
||||||
# Requirements: argon2-cffi
|
# Requirements: argon2-cffi
|
||||||
#
|
#
|
||||||
|
|
||||||
import argon2
|
import argon2
|
||||||
import argparse
|
import argparse
|
||||||
import getpass
|
import hashlib
|
||||||
import sys
|
import re
|
||||||
|
|
||||||
|
from . import bhash, phogen
|
||||||
|
|
||||||
#
|
#
|
||||||
# WARNING: Changing any of the parameters below will affect password generation
|
# 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
|
# Default SALT to use; this cannot be random since passgeny must
|
||||||
# generate predictable passwords
|
# generate predictable passwords
|
||||||
@ -23,25 +28,107 @@ PASSGENY_ARGON2_PARALLEL = 4
|
|||||||
# Hash length - 512 bits by default
|
# Hash length - 512 bits by default
|
||||||
PASSGENY_ARGON2_HASH_LEN = 64
|
PASSGENY_ARGON2_HASH_LEN = 64
|
||||||
|
|
||||||
def argon2_hash(message):
|
class PassgenyInvalidPattern(Exception):
|
||||||
return argon2.low_level.hash_secret_raw(
|
pass
|
||||||
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 passgeny_hash(master_password, token_list):
|
class Passgeny:
|
||||||
message = b''
|
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
|
def generate(self, domain, user, *tokens):
|
||||||
for x in token_list:
|
bh = bhash.Bhash()
|
||||||
message += x.encode() + b'\0'
|
bh.from_bytes(self.__hash([domain, user, *tokens]))
|
||||||
|
|
||||||
message += master_password.encode() + b'\0'
|
return self.__generate_from_pattern(bh)
|
||||||
return argon2_hash(message)
|
|
||||||
|
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")))
|
|
||||||
|
|||||||
Reference in New Issue
Block a user