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 | ||
9486cf9d | 27 | ###-------------------------------------------------------------------------- |
d1c45f5c | 28 | ### Imported modules. |
d7ab1bab | 29 | |
85f15f07 MW |
30 | from __future__ import with_statement |
31 | ||
43c09851 | 32 | from os import environ |
d7ab1bab | 33 | from sys import argv, exit, stdin, stdout, stderr |
34 | from getopt import getopt, GetoptError | |
d7ab1bab | 35 | from fnmatch import fnmatch |
80dc9c94 | 36 | import re |
2e6a3fda | 37 | |
d1c45f5c MW |
38 | import catacomb as C |
39 | from catacomb.pwsafe import * | |
40 | ||
41 | ###-------------------------------------------------------------------------- | |
f6d012db MW |
42 | ### Python version portability. |
43 | ||
467c2619 MW |
44 | def _text(bin): return bin |
45 | def _bin(text): return text | |
46 | ||
f6d012db MW |
47 | def _excval(): return SYS.exc_info()[1] |
48 | ||
49 | ###-------------------------------------------------------------------------- | |
d1c45f5c MW |
50 | ### Utilities. |
51 | ||
52 | ## The program name. | |
2e6a3fda | 53 | prog = re.sub(r'^.*[/\\]', '', argv[0]) |
d1c45f5c | 54 | |
2e6a3fda | 55 | def moan(msg): |
d1c45f5c | 56 | """Issue a warning message MSG.""" |
bbb113f6 | 57 | stderr.write('%s: %s\n' % (prog, msg)) |
d1c45f5c | 58 | |
2e6a3fda | 59 | def die(msg): |
d1c45f5c | 60 | """Report MSG as a fatal error, and exit.""" |
2e6a3fda | 61 | moan(msg) |
62 | exit(1) | |
d7ab1bab | 63 | |
d1c45f5c MW |
64 | ###-------------------------------------------------------------------------- |
65 | ### Subcommand implementations. | |
d7ab1bab | 66 | |
d7ab1bab | 67 | def cmd_create(av): |
d1c45f5c MW |
68 | |
69 | ## Default crypto-primitive selections. | |
6af1255c MW |
70 | cipher = 'rijndael-cbc' |
71 | hash = 'sha256' | |
d7ab1bab | 72 | mac = None |
d1c45f5c MW |
73 | |
74 | ## Parse the options. | |
d7ab1bab | 75 | try: |
6baae405 MW |
76 | opts, args = getopt(av, 'c:d:h:m:', |
77 | ['cipher=', 'database=', 'mac=', 'hash=']) | |
d7ab1bab | 78 | except GetoptError: |
79 | return 1 | |
8501dc39 | 80 | dbty = 'flat' |
d7ab1bab | 81 | for o, a in opts: |
9d0d0a7b MW |
82 | if o in ('-c', '--cipher'): cipher = a |
83 | elif o in ('-m', '--mac'): mac = a | |
84 | elif o in ('-h', '--hash'): hash = a | |
6baae405 | 85 | elif o in ('-d', '--database'): dbty = a |
1c7419c9 | 86 | else: raise Exception("barf") |
9d0d0a7b MW |
87 | if len(args) > 2: return 1 |
88 | if len(args) >= 1: tag = args[0] | |
89 | else: tag = 'pwsafe' | |
d1c45f5c | 90 | |
09b8041d MW |
91 | ## Set up the database. |
92 | if mac is None: mac = hash + '-hmac' | |
6baae405 MW |
93 | try: dbcls = StorageBackend.byname(dbty) |
94 | except KeyError: die("Unknown database backend `%s'" % dbty) | |
95 | PW.create(dbcls, file, tag, | |
96 | C.gcciphers[cipher], C.gchashes[hash], C.gcmacs[mac]) | |
d7ab1bab | 97 | |
98 | def cmd_changepp(av): | |
9d0d0a7b | 99 | if len(av) != 0: return 1 |
5bf6e9f5 | 100 | with PW(file, writep = True) as pw: pw.changepp() |
d7ab1bab | 101 | |
102 | def cmd_find(av): | |
9d0d0a7b | 103 | if len(av) != 1: return 1 |
5bf6e9f5 | 104 | with PW(file) as pw: |
467c2619 | 105 | try: print(_text(pw[_bin(av[0])])) |
f6d012db | 106 | except KeyError: die("Password `%s' not found" % _excval().args[0]) |
d7ab1bab | 107 | |
108 | def cmd_store(av): | |
5bf6e9f5 | 109 | if len(av) < 1 or len(av) > 2: return 1 |
d7ab1bab | 110 | tag = av[0] |
5bf6e9f5 MW |
111 | with PW(file, writep = True) as pw: |
112 | if len(av) < 2: | |
113 | pp = C.getpass("Enter passphrase `%s': " % tag) | |
114 | vpp = C.getpass("Confirm passphrase `%s': " % tag) | |
115 | if pp != vpp: die("passphrases don't match") | |
116 | elif av[1] == '-': | |
467c2619 | 117 | pp = _bin(stdin.readline().rstrip('\n')) |
5bf6e9f5 | 118 | else: |
467c2619 MW |
119 | pp = _bin(av[1]) |
120 | pw[_bin(av[0])] = pp | |
d7ab1bab | 121 | |
122 | def cmd_copy(av): | |
9d0d0a7b | 123 | if len(av) < 1 or len(av) > 2: return 1 |
5bf6e9f5 MW |
124 | with PW(file) as pw_in: |
125 | with PW(av[0], writep = True) as pw_out: | |
126 | if len(av) >= 3: pat = av[1] | |
127 | else: pat = None | |
128 | for k in pw_in: | |
467c2619 MW |
129 | ktext = _text(k) |
130 | if pat is None or fnmatch(ktext, pat): pw_out[k] = pw_in[k] | |
d7ab1bab | 131 | |
132 | def cmd_list(av): | |
9d0d0a7b | 133 | if len(av) > 1: return 1 |
5bf6e9f5 MW |
134 | with PW(file) as pw: |
135 | if len(av) >= 1: pat = av[0] | |
136 | else: pat = None | |
137 | for k in pw: | |
467c2619 MW |
138 | ktext = _text(k) |
139 | if pat is None or fnmatch(ktext, pat): print(ktext) | |
d7ab1bab | 140 | |
141 | def cmd_topixie(av): | |
9d0d0a7b | 142 | if len(av) > 2: return 1 |
5bf6e9f5 MW |
143 | with PW(file) as pw: |
144 | pix = C.Pixie() | |
145 | if len(av) == 0: | |
467c2619 | 146 | for tag in pw: pix.set(tag, pw[_bin(tag)]) |
5bf6e9f5 | 147 | else: |
467c2619 | 148 | tag = _bin(av[0]) |
5bf6e9f5 MW |
149 | if len(av) >= 2: pptag = av[1] |
150 | else: pptag = av[0] | |
151 | pix.set(pptag, pw[tag]) | |
d7ab1bab | 152 | |
3aa33042 | 153 | def cmd_del(av): |
9d0d0a7b | 154 | if len(av) != 1: return 1 |
5bf6e9f5 | 155 | with PW(file, writep = True) as pw: |
467c2619 | 156 | tag = _bin(av[0]) |
5bf6e9f5 | 157 | try: del pw[tag] |
f6d012db | 158 | except KeyError: die("Password `%s' not found" % _excval().args[0]) |
3aa33042 | 159 | |
dab03511 MW |
160 | def cmd_xfer(av): |
161 | ||
162 | ## Parse the command line. | |
163 | try: opts, args = getopt(av, 'd:', ['database=']) | |
164 | except GetoptError: return 1 | |
165 | dbty = 'flat' | |
166 | for o, a in opts: | |
167 | if o in ('-d', '--database'): dbty = a | |
1c7419c9 | 168 | else: raise Exception("barf") |
dab03511 MW |
169 | if len(args) != 1: return 1 |
170 | try: dbcls = StorageBackend.byname(dbty) | |
171 | except KeyError: die("Unknown database backend `%s'" % dbty) | |
172 | ||
173 | ## Create the target database. | |
174 | with StorageBackend.open(file) as db_in: | |
175 | with dbcls.create(args[0]) as db_out: | |
176 | for k, v in db_in.iter_meta(): db_out.put_meta(k, v) | |
177 | for k, v in db_in.iter_passwds(): db_out.put_passwd(k, v) | |
178 | ||
d7ab1bab | 179 | commands = { 'create': [cmd_create, |
6baae405 | 180 | '[-c CIPHER] [-d DBTYPE] [-h HASH] [-m MAC] [PP-TAG]'], |
d1c45f5c MW |
181 | 'find' : [cmd_find, 'LABEL'], |
182 | 'store' : [cmd_store, 'LABEL [VALUE]'], | |
183 | 'list' : [cmd_list, '[GLOB-PATTERN]'], | |
184 | 'changepp' : [cmd_changepp, ''], | |
185 | 'copy' : [cmd_copy, 'DEST-FILE [GLOB-PATTERN]'], | |
186 | 'to-pixie' : [cmd_topixie, '[TAG [PIXIE-TAG]]'], | |
dab03511 MW |
187 | 'delete' : [cmd_del, 'TAG'], |
188 | 'xfer': [cmd_xfer, '[-d DBTYPE] DEST-FILE'] } | |
d1c45f5c MW |
189 | |
190 | ###-------------------------------------------------------------------------- | |
191 | ### Command-line handling and dispatch. | |
d7ab1bab | 192 | |
193 | def version(): | |
bbb113f6 MW |
194 | print('%s 1.0.0' % prog) |
195 | print('Backend types: %s' % | |
196 | ' '.join([c.NAME for c in StorageBackend.classes()])) | |
d1c45f5c | 197 | |
d7ab1bab | 198 | def usage(fp): |
bbb113f6 | 199 | fp.write('Usage: %s COMMAND [ARGS...]\n' % prog) |
d1c45f5c | 200 | |
d7ab1bab | 201 | def help(): |
202 | version() | |
bbb113f6 | 203 | print('') |
b2687a0a | 204 | usage(stdout) |
bbb113f6 | 205 | print(''' |
d7ab1bab | 206 | Maintains passwords or other short secrets securely. |
207 | ||
208 | Options: | |
209 | ||
210 | -h, --help Show this help text. | |
211 | -v, --version Show program version number. | |
212 | -u, --usage Show short usage message. | |
213 | ||
2e6a3fda | 214 | -f, --file=FILE Where to find the password-safe file. |
215 | ||
d7ab1bab | 216 | Commands provided: |
bbb113f6 | 217 | ''') |
05a82542 | 218 | for c in sorted(commands): |
bbb113f6 | 219 | print('%s %s' % (c, commands[c][1])) |
d7ab1bab | 220 | |
d1c45f5c MW |
221 | ## Choose a default database file. |
222 | if 'PWSAFE' in environ: | |
223 | file = environ['PWSAFE'] | |
224 | else: | |
225 | file = '%s/.pwsafe' % environ['HOME'] | |
226 | ||
227 | ## Parse the command-line options. | |
d7ab1bab | 228 | try: |
d1c45f5c MW |
229 | opts, argv = getopt(argv[1:], 'hvuf:', |
230 | ['help', 'version', 'usage', 'file=']) | |
d7ab1bab | 231 | except GetoptError: |
232 | usage(stderr) | |
233 | exit(1) | |
234 | for o, a in opts: | |
235 | if o in ('-h', '--help'): | |
236 | help() | |
237 | exit(0) | |
238 | elif o in ('-v', '--version'): | |
239 | version() | |
240 | exit(0) | |
241 | elif o in ('-u', '--usage'): | |
242 | usage(stdout) | |
243 | exit(0) | |
244 | elif o in ('-f', '--file'): | |
245 | file = a | |
246 | else: | |
1c7419c9 | 247 | raise Exception("barf") |
d7ab1bab | 248 | if len(argv) < 1: |
249 | usage(stderr) | |
250 | exit(1) | |
251 | ||
d1c45f5c | 252 | ## Dispatch to a command handler. |
d7ab1bab | 253 | if argv[0] in commands: |
254 | c = argv[0] | |
255 | argv = argv[1:] | |
256 | else: | |
257 | c = 'find' | |
40b16f0c MW |
258 | try: |
259 | if commands[c][0](argv): | |
bbb113f6 | 260 | stderr.write('Usage: %s %s %s\n' % (prog, c, commands[c][1])) |
40b16f0c MW |
261 | exit(1) |
262 | except DecryptError: | |
263 | die("decryption failure") | |
d1c45f5c MW |
264 | |
265 | ###----- That's all, folks -------------------------------------------------- |