diff --git a/pwfile.py b/pwfile.py new file mode 100644 index 0000000..bb46da2 --- /dev/null +++ b/pwfile.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +""" +pwfile 2019-01-19 + +Copyright (c) 2019 Dave Cinege +Licence: GPL2, Copyright notice may not be altered. + +Create, manage, and authenticate simple password files + +Works with python 2.7 -> python3.6+. + +# openvpn example +auth-user-pass-verify "/usr/local/bin/pwfile --quiet --pwfile ./openvpn.pwfile --check --file" via-file + +""" +from __future__ import print_function + +import sys, os, argparse, getpass, crypt + +from thesaurus import thes +G = thes() + +G.VERSION = '20191111' + +G.FIELDDELIM = ':' +G.MINLEN = 8 + +def getargs (): + ap = argparse.ArgumentParser(description='') + + ap.add_argument('--pwfile', action='store', type=str, required=True, + help='username%(FIELDDELIM)spasshash filename.' % G, metavar='FILENAME') + + + ap.add_argument('-c', '--check', action='store_true', + help='check authentication.') + + ap.add_argument('-a', '--add', action='store_true', + help='add/replace user in PWFILE.') + + ap.add_argument('-r', '--rand', nargs='?', type=int, const=16, + help='generate random password for user. (default: %(const)s)', metavar='LENGTH') + + ap.add_argument('-l', '--lock', action='store_true', + help='lock (disable) user.') + + ap.add_argument('-n', '--unlock', action='store_true', + help='unlock (re-enable) user.') + + + ap.add_argument('-d', '--delete', action='store_true', + help='delete user from PWFILE.') + + + ap.add_argument('--stdin', action='store_true', + help='Read username and password from stdin.') + + ap.add_argument('-f', '--file', action='store', type=str, + help='Read username and password from file. Overrides --stdin.', metavar='FILENAME') + + ap.add_argument('-u', '--username', action='store', type=str, + help='username. Overrides --stdin.and --file.') + + ap.add_argument('-p', '--password', action='store', type=str, + help='password. Overrides --stdin and --file. WARNING: not recommended.') + + + ap.add_argument('-q', '--quiet', default=False, action='store_true', + help='quiet all output. (Override --verbose)') + + ap.add_argument('-v', '--verbose', default=True, action='store_true', + help='verbose output enabled.') + + ap.add_argument('--version', action='version', version='%(prog)s ' + G.VERSION, + help='print version number and exit. (v%(VERSION)s)' % G) + + G.args = ap.parse_args() + + if G.args.quiet: + G.args.verbose = False + + xor = 0 + if G.args.check: + xor += 1 + if G.args.add: + xor += 1 + if G.args.delete: + xor += 1 + if G.args.lock: + xor += 1 + if G.args.unlock: + xor += 1 + if xor > 1: + verb('Conflicting args. Quitting...') + quit() + if xor == 0: + verb('Nothing to do. Quitting...') + quit() + + +def compare_hash (a, b): + try: + from hmac import compare_digest + except ImportError: + return a == b + else: + return compare_digest(a, b) + + +def genrandpass (length=16): + import string + try: + import secrets + except ImportError: + import random + secrets = random.SystemRandom() + alphabet = string.ascii_letters + string.digits + while True: + s = ''.join(secrets.choice(alphabet) for i in range(length)) + if (any(c.islower() for c in s) + and any(c.isupper() for c in s) + and sum(c.isdigit() for c in s) >= 3): + break + return s + + +def askpass (): + a = getpass.getpass(prompt='Enter password: ') + b = getpass.getpass(prompt='Retype password: ') + if a == b: + return a.strip() + verb('Passwords do not match. Quitting...') + quit() + + +def verb (*args,**kwargs): + if G.args.verbose: + print(*args,**kwargs) + + +def quit (rc=1): + sys.exit(rc) + + +def setpwline (): + if G.filehash.startswith('!'): + if G.args.unlock and G.passhash.startswith('!'): + G.passhash = G.passhash[1:] + elif not G.passhash.startswith('!'): + G.passhash = '!' + G.passhash + if G.args.lock and not G.passhash.startswith('!'): + G.passhash = '!' + G.passhash + + return '%(username)s%(FIELDDELIM)s%(passhash)s\n' % G + + +def pwfile_write (): + pwnewlines = list() + count = 0 + s = '%(username)s%(FIELDDELIM)s' % G + for line in G.pwlines: + line = line.strip() + if line.startswith(s): + count =+ 1 + if count > 1: + verb('WARN: purging duplicate username entry.') + continue + pwnewlines.append(line+'\n') + + if not os.access(G.args.pwfile, os.W_OK): + verb('Pwfile "%(args.pwfile)s" not writable. Quitting...' % G) + quit() + open(G.args.pwfile,'w').writelines(pwnewlines) + + +def pwfile_getuser(): + G.pwlines = open(G.args.pwfile).readlines() + s = '%(username)s%(FIELDDELIM)s' % G + x = 0 + G.userlnum = -1 + for line in G.pwlines: + if line.strip().startswith(s): + G.userlnum = x + G.filehash = line.split(G.FIELDDELIM,maxsplit=1)[1].strip() + return True + x +=1 + return False + + +def main (): + getargs() + + if not os.access(G.args.pwfile, os.R_OK): + verb('Pwfile "%(args.pwfile)s" not readable. Quitting...' % G) + quit() + + if G.args.stdin: + G.username = sys.stdin.readline().strip() + G.password = sys.stdin.readline().strip() + if G.args.file: + if not os.access(G.args.file, os.R_OK): + verb('File "%(args.file)s" not readable. Quitting...' % G) + quit() + f = open(G.args.file) + G.username = f.readline().strip() + G.password = f.readline().strip() + f.close() + if G.args.username: + G.username = G.args.username.strip() + if G.args.password: + G.password = G.args.password.strip() + + if 'username' not in G: + verb('Username is required. Quitting...') + quit() + if G.FIELDDELIM in G.username: + verb('Char "%(FIELDDELIM)s" not allowed in username. Quitting...' % G) + quit() + + if G.args.add: + if G.args.rand: + G.password = genrandpass(G.args.rand) + verb('%(password)s' % G) + elif 'password' not in G: + G.password = askpass() + if G.password == '': + verb('Empty password not allowed. Quitting...') + quit() + if len(G.password) < G.MINLEN: + verb('Minimum password length is: %(MINLEN)s. Quitting...' % G) + quit() + G.passhash = crypt.crypt(G.password, None) + + userfound = pwfile_getuser() + if G.args.check or G.args.delete or G.args.lock or G.args.unlock: + if not userfound: + verb('Username "%(username)s" not found.' % G) + quit() + + if G.args.check: + if G.filehash.startswith('!'): + verb('Username "%(username)s" is locked.' % G) + quit() + if 'password' not in G: + verb('Password is required. Quitting...') + quit() + if not compare_hash(crypt.crypt(G.password, G.filehash), G.filehash): + verb('Password failure.') + quit() + else: + verb('Password success.') + quit(0) + quit() + elif G.args.add: + if userfound: + G.pwlines[G.userlnum] = setpwline() + else: + G.filehash = G.passhash + G.pwlines.append(setpwline()) + elif G.args.lock or G.args.unlock: + if G.args.lock and G.filehash.startswith('!'): + quit(0) + elif G.args.unlock and not G.filehash.startswith('!'): + quit(0) + G.passhash = G.filehash + G.pwlines[G.userlnum] = setpwline() + elif G.args.delete: + del G.pwlines[G.userlnum] + + pwfile_write() + quit(0) + +if __name__ == '__main__': + try: + import setproctitle + setproctitle.setproctitle(' '.join(sys.argv[0:])) + except: + pass + main()