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