initial commit
Dave Cinege
3 years ago
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() |