#!/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()