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