Dave Cinege Git Repo pwfile / 6488c89
initial commit Dave Cinege 2 years ago
1 changed file(s) with 279 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 #!/usr/bin/env python3
1 """
2 pwfile 2019-01-19
3
4 Copyright (c) 2019 Dave Cinege
5 Licence: GPL2, Copyright notice may not be altered.
6
7 Create, manage, and authenticate simple password files
8
9 Works with python 2.7 -> python3.6+.
10
11 # openvpn example
12 auth-user-pass-verify "/usr/local/bin/pwfile --quiet --pwfile ./openvpn.pwfile --check --file" via-file
13
14 """
15 from __future__ import print_function
16
17 import sys, os, argparse, getpass, crypt
18
19 from thesaurus import thes
20 G = thes()
21
22 G.VERSION = '20191111'
23
24 G.FIELDDELIM = ':'
25 G.MINLEN = 8
26
27 def getargs ():
28 ap = argparse.ArgumentParser(description='')
29
30 ap.add_argument('--pwfile', action='store', type=str, required=True,
31 help='username%(FIELDDELIM)spasshash filename.' % G, metavar='FILENAME')
32
33
34 ap.add_argument('-c', '--check', action='store_true',
35 help='check authentication.')
36
37 ap.add_argument('-a', '--add', action='store_true',
38 help='add/replace user in PWFILE.')
39
40 ap.add_argument('-r', '--rand', nargs='?', type=int, const=16,
41 help='generate random password for user. (default: %(const)s)', metavar='LENGTH')
42
43 ap.add_argument('-l', '--lock', action='store_true',
44 help='lock (disable) user.')
45
46 ap.add_argument('-n', '--unlock', action='store_true',
47 help='unlock (re-enable) user.')
48
49
50 ap.add_argument('-d', '--delete', action='store_true',
51 help='delete user from PWFILE.')
52
53
54 ap.add_argument('--stdin', action='store_true',
55 help='Read username and password from stdin.')
56
57 ap.add_argument('-f', '--file', action='store', type=str,
58 help='Read username and password from file. Overrides --stdin.', metavar='FILENAME')
59
60 ap.add_argument('-u', '--username', action='store', type=str,
61 help='username. Overrides --stdin.and --file.')
62
63 ap.add_argument('-p', '--password', action='store', type=str,
64 help='password. Overrides --stdin and --file. WARNING: not recommended.')
65
66
67 ap.add_argument('-q', '--quiet', default=False, action='store_true',
68 help='quiet all output. (Override --verbose)')
69
70 ap.add_argument('-v', '--verbose', default=True, action='store_true',
71 help='verbose output enabled.')
72
73 ap.add_argument('--version', action='version', version='%(prog)s ' + G.VERSION,
74 help='print version number and exit. (v%(VERSION)s)' % G)
75
76 G.args = ap.parse_args()
77
78 if G.args.quiet:
79 G.args.verbose = False
80
81 xor = 0
82 if G.args.check:
83 xor += 1
84 if G.args.add:
85 xor += 1
86 if G.args.delete:
87 xor += 1
88 if G.args.lock:
89 xor += 1
90 if G.args.unlock:
91 xor += 1
92 if xor > 1:
93 verb('Conflicting args. Quitting...')
94 quit()
95 if xor == 0:
96 verb('Nothing to do. Quitting...')
97 quit()
98
99
100 def compare_hash (a, b):
101 try:
102 from hmac import compare_digest
103 except ImportError:
104 return a == b
105 else:
106 return compare_digest(a, b)
107
108
109 def genrandpass (length=16):
110 import string
111 try:
112 import secrets
113 except ImportError:
114 import random
115 secrets = random.SystemRandom()
116 alphabet = string.ascii_letters + string.digits
117 while True:
118 s = ''.join(secrets.choice(alphabet) for i in range(length))
119 if (any(c.islower() for c in s)
120 and any(c.isupper() for c in s)
121 and sum(c.isdigit() for c in s) >= 3):
122 break
123 return s
124
125
126 def askpass ():
127 a = getpass.getpass(prompt='Enter password: ')
128 b = getpass.getpass(prompt='Retype password: ')
129 if a == b:
130 return a.strip()
131 verb('Passwords do not match. Quitting...')
132 quit()
133
134
135 def verb (*args,**kwargs):
136 if G.args.verbose:
137 print(*args,**kwargs)
138
139
140 def quit (rc=1):
141 sys.exit(rc)
142
143
144 def setpwline ():
145 if G.filehash.startswith('!'):
146 if G.args.unlock and G.passhash.startswith('!'):
147 G.passhash = G.passhash[1:]
148 elif not G.passhash.startswith('!'):
149 G.passhash = '!' + G.passhash
150 if G.args.lock and not G.passhash.startswith('!'):
151 G.passhash = '!' + G.passhash
152
153 return '%(username)s%(FIELDDELIM)s%(passhash)s\n' % G
154
155
156 def pwfile_write ():
157 pwnewlines = list()
158 count = 0
159 s = '%(username)s%(FIELDDELIM)s' % G
160 for line in G.pwlines:
161 line = line.strip()
162 if line.startswith(s):
163 count =+ 1
164 if count > 1:
165 verb('WARN: purging duplicate username entry.')
166 continue
167 pwnewlines.append(line+'\n')
168
169 if not os.access(G.args.pwfile, os.W_OK):
170 verb('Pwfile "%(args.pwfile)s" not writable. Quitting...' % G)
171 quit()
172 open(G.args.pwfile,'w').writelines(pwnewlines)
173
174
175 def pwfile_getuser():
176 G.pwlines = open(G.args.pwfile).readlines()
177 s = '%(username)s%(FIELDDELIM)s' % G
178 x = 0
179 G.userlnum = -1
180 for line in G.pwlines:
181 if line.strip().startswith(s):
182 G.userlnum = x
183 G.filehash = line.split(G.FIELDDELIM,maxsplit=1)[1].strip()
184 return True
185 x +=1
186 return False
187
188
189 def main ():
190 getargs()
191
192 if not os.access(G.args.pwfile, os.R_OK):
193 verb('Pwfile "%(args.pwfile)s" not readable. Quitting...' % G)
194 quit()
195
196 if G.args.stdin:
197 G.username = sys.stdin.readline().strip()
198 G.password = sys.stdin.readline().strip()
199 if G.args.file:
200 if not os.access(G.args.file, os.R_OK):
201 verb('File "%(args.file)s" not readable. Quitting...' % G)
202 quit()
203 f = open(G.args.file)
204 G.username = f.readline().strip()
205 G.password = f.readline().strip()
206 f.close()
207 if G.args.username:
208 G.username = G.args.username.strip()
209 if G.args.password:
210 G.password = G.args.password.strip()
211
212 if 'username' not in G:
213 verb('Username is required. Quitting...')
214 quit()
215 if G.FIELDDELIM in G.username:
216 verb('Char "%(FIELDDELIM)s" not allowed in username. Quitting...' % G)
217 quit()
218
219 if G.args.add:
220 if G.args.rand:
221 G.password = genrandpass(G.args.rand)
222 verb('%(password)s' % G)
223 elif 'password' not in G:
224 G.password = askpass()
225 if G.password == '':
226 verb('Empty password not allowed. Quitting...')
227 quit()
228 if len(G.password) < G.MINLEN:
229 verb('Minimum password length is: %(MINLEN)s. Quitting...' % G)
230 quit()
231 G.passhash = crypt.crypt(G.password, None)
232
233 userfound = pwfile_getuser()
234 if G.args.check or G.args.delete or G.args.lock or G.args.unlock:
235 if not userfound:
236 verb('Username "%(username)s" not found.' % G)
237 quit()
238
239 if G.args.check:
240 if G.filehash.startswith('!'):
241 verb('Username "%(username)s" is locked.' % G)
242 quit()
243 if 'password' not in G:
244 verb('Password is required. Quitting...')
245 quit()
246 if not compare_hash(crypt.crypt(G.password, G.filehash), G.filehash):
247 verb('Password failure.')
248 quit()
249 else:
250 verb('Password success.')
251 quit(0)
252 quit()
253 elif G.args.add:
254 if userfound:
255 G.pwlines[G.userlnum] = setpwline()
256 else:
257 G.filehash = G.passhash
258 G.pwlines.append(setpwline())
259 elif G.args.lock or G.args.unlock:
260 if G.args.lock and G.filehash.startswith('!'):
261 quit(0)
262 elif G.args.unlock and not G.filehash.startswith('!'):
263 quit(0)
264 G.passhash = G.filehash
265 G.pwlines[G.userlnum] = setpwline()
266 elif G.args.delete:
267 del G.pwlines[G.userlnum]
268
269 pwfile_write()
270 quit(0)
271
272 if __name__ == '__main__':
273 try:
274 import setproctitle
275 setproctitle.setproctitle(' '.join(sys.argv[0:]))
276 except:
277 pass
278 main()