Commit | Line | Data |
---|---|---|
24b3d57b | 1 | #! /usr/bin/python |
d1c45f5c MW |
2 | ### -*-python-*- |
3 | ### | |
4 | ### Tool for maintaining a secure-ish password database | |
5 | ### | |
6 | ### (c) 2005 Straylight/Edgeware | |
7 | ### | |
8 | ||
9 | ###----- Licensing notice --------------------------------------------------- | |
10 | ### | |
11 | ### This file is part of the Python interface to Catacomb. | |
12 | ### | |
13 | ### Catacomb/Python is free software; you can redistribute it and/or modify | |
14 | ### it under the terms of the GNU General Public License as published by | |
15 | ### the Free Software Foundation; either version 2 of the License, or | |
16 | ### (at your option) any later version. | |
17 | ### | |
18 | ### Catacomb/Python is distributed in the hope that it will be useful, | |
19 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of | |
20 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
21 | ### GNU General Public License for more details. | |
22 | ### | |
23 | ### You should have received a copy of the GNU General Public License | |
24 | ### along with Catacomb/Python; if not, write to the Free Software Foundation, | |
25 | ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | |
26 | ||
27 | ###--------------------------------------------------------------------------- | |
28 | ### Imported modules. | |
d7ab1bab | 29 | |
43c09851 | 30 | import gdbm as G |
31 | from os import environ | |
d7ab1bab | 32 | from sys import argv, exit, stdin, stdout, stderr |
33 | from getopt import getopt, GetoptError | |
d7ab1bab | 34 | from fnmatch import fnmatch |
80dc9c94 | 35 | import re |
2e6a3fda | 36 | |
d1c45f5c MW |
37 | import catacomb as C |
38 | from catacomb.pwsafe import * | |
39 | ||
40 | ###-------------------------------------------------------------------------- | |
41 | ### Utilities. | |
42 | ||
43 | ## The program name. | |
2e6a3fda | 44 | prog = re.sub(r'^.*[/\\]', '', argv[0]) |
d1c45f5c | 45 | |
2e6a3fda | 46 | def moan(msg): |
d1c45f5c | 47 | """Issue a warning message MSG.""" |
2e6a3fda | 48 | print >>stderr, '%s: %s' % (prog, msg) |
d1c45f5c | 49 | |
2e6a3fda | 50 | def die(msg): |
d1c45f5c | 51 | """Report MSG as a fatal error, and exit.""" |
2e6a3fda | 52 | moan(msg) |
53 | exit(1) | |
d7ab1bab | 54 | |
d1c45f5c MW |
55 | def chomp(pp): |
56 | """Return the string PP, without its trailing newline if it has one.""" | |
57 | if len(pp) > 0 and pp[-1] == '\n': | |
58 | pp = pp[:-1] | |
59 | return pp | |
60 | ||
61 | def asciip(s): | |
62 | """Answer whether all of the characters of S are plain ASCII.""" | |
63 | for ch in s: | |
64 | if ch < ' ' or ch > '~': return False | |
65 | return True | |
66 | ||
67 | def present(s): | |
68 | """ | |
69 | Return a presentation form of the string S. | |
70 | ||
71 | If S is plain ASCII, then return S unchanged; otherwise return it as one of | |
72 | Catacomb's ByteString objects. | |
73 | """ | |
74 | if asciip(s): return s | |
75 | return C.ByteString(s) | |
76 | ||
77 | ###-------------------------------------------------------------------------- | |
78 | ### Subcommand implementations. | |
d7ab1bab | 79 | |
d7ab1bab | 80 | def cmd_create(av): |
d1c45f5c MW |
81 | |
82 | ## Default crypto-primitive selections. | |
d7ab1bab | 83 | cipher = 'blowfish-cbc' |
84 | hash = 'rmd160' | |
85 | mac = None | |
d1c45f5c MW |
86 | |
87 | ## Parse the options. | |
d7ab1bab | 88 | try: |
89 | opts, args = getopt(av, 'c:h:m:', ['cipher=', 'mac=', 'hash=']) | |
90 | except GetoptError: | |
91 | return 1 | |
92 | for o, a in opts: | |
93 | if o in ('-c', '--cipher'): | |
94 | cipher = a | |
95 | elif o in ('-m', '--mac'): | |
96 | mac = a | |
97 | elif o in ('-h', '--hash'): | |
98 | hash = a | |
99 | else: | |
100 | raise 'Barf!' | |
101 | if len(args) > 2: | |
102 | return 1 | |
103 | if len(args) >= 1: | |
104 | tag = args[0] | |
105 | else: | |
106 | tag = 'pwsafe' | |
d1c45f5c MW |
107 | |
108 | ## Choose a passphrase, and generate master keys. | |
d7ab1bab | 109 | pp = C.ppread(tag, C.PMODE_VERIFY) |
110 | if not mac: mac = hash + '-hmac' | |
111 | c = C.gcciphers[cipher] | |
112 | h = C.gchashes[hash] | |
113 | m = C.gcmacs[mac] | |
43c09851 | 114 | ppk = PW.PPK(pp, c, h, m) |
d7ab1bab | 115 | ck = C.rand.block(c.keysz.default) |
116 | mk = C.rand.block(m.keysz.default) | |
117 | k = Crypto(c, h, m, ck, mk) | |
d1c45f5c MW |
118 | |
119 | ## Set up the database, storing the basic information we need. | |
120 | db = G.open(file, 'n', 0600) | |
d7ab1bab | 121 | db['tag'] = tag |
122 | db['salt'] = ppk.salt | |
123 | db['cipher'] = cipher | |
124 | db['hash'] = hash | |
125 | db['mac'] = mac | |
126 | db['key'] = ppk.encrypt(wrapstr(ck) + wrapstr(mk)) | |
127 | db['magic'] = k.encrypt(C.rand.block(h.hashsz)) | |
128 | ||
129 | def cmd_changepp(av): | |
130 | if len(av) != 0: | |
131 | return 1 | |
132 | pw = PW(file, 'w') | |
133 | pw.changepp() | |
134 | ||
135 | def cmd_find(av): | |
136 | if len(av) != 1: | |
137 | return 1 | |
138 | pw = PW(file) | |
2e6a3fda | 139 | try: |
140 | print pw[av[0]] | |
141 | except KeyError, exc: | |
142 | die('Password `%s\' not found.' % exc.args[0]) | |
d7ab1bab | 143 | |
144 | def cmd_store(av): | |
145 | if len(av) < 1 or len(av) > 2: | |
146 | return 1 | |
147 | tag = av[0] | |
148 | if len(av) < 2: | |
149 | pp = C.getpass("Enter passphrase `%s': " % tag) | |
150 | vpp = C.getpass("Confirm passphrase `%s': " % tag) | |
151 | if pp != vpp: | |
152 | raise ValueError, "passphrases don't match" | |
153 | elif av[1] == '-': | |
154 | pp = stdin.readline() | |
155 | else: | |
156 | pp = av[1] | |
157 | pw = PW(file, 'w') | |
43c09851 | 158 | pw[av[0]] = chomp(pp) |
d7ab1bab | 159 | |
160 | def cmd_copy(av): | |
161 | if len(av) < 1 or len(av) > 2: | |
162 | return 1 | |
163 | pw_in = PW(file) | |
164 | pw_out = PW(av[0], 'w') | |
165 | if len(av) >= 3: | |
166 | pat = av[1] | |
167 | else: | |
168 | pat = None | |
169 | for k in pw_in: | |
170 | if pat is None or fnmatch(k, pat): | |
171 | pw_out[k] = pw_in[k] | |
172 | ||
173 | def cmd_list(av): | |
174 | if len(av) > 1: | |
175 | return 1 | |
176 | pw = PW(file) | |
177 | if len(av) >= 1: | |
178 | pat = av[0] | |
179 | else: | |
180 | pat = None | |
181 | for k in pw: | |
182 | if pat is None or fnmatch(k, pat): | |
183 | print k | |
184 | ||
185 | def cmd_topixie(av): | |
43c09851 | 186 | if len(av) > 2: |
d7ab1bab | 187 | return 1 |
188 | pw = PW(file) | |
43c09851 | 189 | pix = C.Pixie() |
190 | if len(av) == 0: | |
191 | for tag in pw: | |
192 | pix.set(tag, pw[tag]) | |
d7ab1bab | 193 | else: |
43c09851 | 194 | tag = av[0] |
195 | if len(av) >= 2: | |
196 | pptag = av[1] | |
197 | else: | |
198 | pptag = av[0] | |
199 | pix.set(pptag, pw[tag]) | |
d7ab1bab | 200 | |
3aa33042 | 201 | def cmd_del(av): |
202 | if len(av) != 1: | |
203 | return 1 | |
204 | pw = PW(file, 'w') | |
205 | tag = av[0] | |
2e6a3fda | 206 | try: |
207 | del pw[tag] | |
208 | except KeyError, exc: | |
209 | die('Password `%s\' not found.' % exc.args[0]) | |
3aa33042 | 210 | |
d7ab1bab | 211 | def cmd_dump(av): |
212 | db = gdbm.open(file, 'r') | |
213 | k = db.firstkey() | |
214 | while True: | |
215 | if k is None: break | |
216 | print '%r: %r' % (present(k), present(db[k])) | |
217 | k = db.nextkey(k) | |
218 | ||
219 | commands = { 'create': [cmd_create, | |
d1c45f5c MW |
220 | '[-c CIPHER] [-h HASH] [-m MAC] [PP-TAG]'], |
221 | 'find' : [cmd_find, 'LABEL'], | |
222 | 'store' : [cmd_store, 'LABEL [VALUE]'], | |
223 | 'list' : [cmd_list, '[GLOB-PATTERN]'], | |
224 | 'changepp' : [cmd_changepp, ''], | |
225 | 'copy' : [cmd_copy, 'DEST-FILE [GLOB-PATTERN]'], | |
226 | 'to-pixie' : [cmd_topixie, '[TAG [PIXIE-TAG]]'], | |
227 | 'delete' : [cmd_del, 'TAG'], | |
228 | 'dump' : [cmd_dump, '']} | |
229 | ||
230 | ###-------------------------------------------------------------------------- | |
231 | ### Command-line handling and dispatch. | |
d7ab1bab | 232 | |
233 | def version(): | |
2e6a3fda | 234 | print '%s 1.0.0' % prog |
d1c45f5c | 235 | |
d7ab1bab | 236 | def usage(fp): |
2e6a3fda | 237 | print >>fp, 'Usage: %s COMMAND [ARGS...]' % prog |
d1c45f5c | 238 | |
d7ab1bab | 239 | def help(): |
240 | version() | |
241 | ||
b2687a0a | 242 | usage(stdout) |
d7ab1bab | 243 | print ''' |
244 | Maintains passwords or other short secrets securely. | |
245 | ||
246 | Options: | |
247 | ||
248 | -h, --help Show this help text. | |
249 | -v, --version Show program version number. | |
250 | -u, --usage Show short usage message. | |
251 | ||
2e6a3fda | 252 | -f, --file=FILE Where to find the password-safe file. |
253 | ||
d7ab1bab | 254 | Commands provided: |
255 | ''' | |
256 | for c in commands: | |
257 | print '%s %s' % (c, commands[c][1]) | |
258 | ||
d1c45f5c MW |
259 | ## Choose a default database file. |
260 | if 'PWSAFE' in environ: | |
261 | file = environ['PWSAFE'] | |
262 | else: | |
263 | file = '%s/.pwsafe' % environ['HOME'] | |
264 | ||
265 | ## Parse the command-line options. | |
d7ab1bab | 266 | try: |
d1c45f5c MW |
267 | opts, argv = getopt(argv[1:], 'hvuf:', |
268 | ['help', 'version', 'usage', 'file=']) | |
d7ab1bab | 269 | except GetoptError: |
270 | usage(stderr) | |
271 | exit(1) | |
272 | for o, a in opts: | |
273 | if o in ('-h', '--help'): | |
274 | help() | |
275 | exit(0) | |
276 | elif o in ('-v', '--version'): | |
277 | version() | |
278 | exit(0) | |
279 | elif o in ('-u', '--usage'): | |
280 | usage(stdout) | |
281 | exit(0) | |
282 | elif o in ('-f', '--file'): | |
283 | file = a | |
284 | else: | |
285 | raise 'Barf!' | |
286 | if len(argv) < 1: | |
287 | usage(stderr) | |
288 | exit(1) | |
289 | ||
d1c45f5c | 290 | ## Dispatch to a command handler. |
d7ab1bab | 291 | if argv[0] in commands: |
292 | c = argv[0] | |
293 | argv = argv[1:] | |
294 | else: | |
295 | c = 'find' | |
296 | if commands[c][0](argv): | |
2e6a3fda | 297 | print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1]) |
d7ab1bab | 298 | exit(1) |
d1c45f5c MW |
299 | |
300 | ###----- That's all, folks -------------------------------------------------- |