Dave Cinege Git Repo pwfile / master pwfile.py
master

Tree @master (Download .tar.gz)

pwfile.py @masterraw · history · blame

#!/usr/bin/env python3
"""
pwfile		2019-01-19

Create, manage, and authenticate simple password files

Copyright (c) 2019 Dave Cinege
Licence: GPL2, Copyright notice may not be altered.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, version 2 of the License.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
    

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