catacomb/pwsafe.py: Make `PW' be a context manager, and use it.
[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 ### Utilities.
43
44 ## The program name.
45 prog = re.sub(r'^.*[/\\]', '', argv[0])
46
47 def moan(msg):
48 """Issue a warning message MSG."""
49 print >>stderr, '%s: %s' % (prog, msg)
50
51 def die(msg):
52 """Report MSG as a fatal error, and exit."""
53 moan(msg)
54 exit(1)
55
56 def chomp(pp):
57 """Return the string PP, without its trailing newline if it has one."""
58 if len(pp) > 0 and pp[-1] == '\n':
59 pp = pp[:-1]
60 return pp
61
62 ###--------------------------------------------------------------------------
63 ### Subcommand implementations.
64
65 def cmd_create(av):
66
67 ## Default crypto-primitive selections.
68 cipher = 'blowfish-cbc'
69 hash = 'rmd160'
70 mac = None
71
72 ## Parse the options.
73 try:
74 opts, args = getopt(av, 'c:h:m:', ['cipher=', 'mac=', 'hash='])
75 except GetoptError:
76 return 1
77 for o, a in opts:
78 if o in ('-c', '--cipher'): cipher = a
79 elif o in ('-m', '--mac'): mac = a
80 elif o in ('-h', '--hash'): hash = a
81 else: raise 'Barf!'
82 if len(args) > 2: return 1
83 if len(args) >= 1: tag = args[0]
84 else: tag = 'pwsafe'
85
86 ## Set up the database.
87 if mac is None: mac = hash + '-hmac'
88 PW.create(file, C.gcciphers[cipher], C.gchashes[hash], C.gcmacs[mac], tag)
89
90 def cmd_changepp(av):
91 if len(av) != 0: return 1
92 with PW(file, writep = True) as pw: pw.changepp()
93
94 def cmd_find(av):
95 if len(av) != 1: return 1
96 with PW(file) as pw:
97 try: print pw[av[0]]
98 except KeyError, exc: die("Password `%s' not found" % exc.args[0])
99
100 def cmd_store(av):
101 if len(av) < 1 or len(av) > 2: return 1
102 tag = av[0]
103 with PW(file, writep = True) as pw:
104 if len(av) < 2:
105 pp = C.getpass("Enter passphrase `%s': " % tag)
106 vpp = C.getpass("Confirm passphrase `%s': " % tag)
107 if pp != vpp: die("passphrases don't match")
108 elif av[1] == '-':
109 pp = stdin.readline()
110 else:
111 pp = av[1]
112 pw[av[0]] = chomp(pp)
113
114 def cmd_copy(av):
115 if len(av) < 1 or len(av) > 2: return 1
116 with PW(file) as pw_in:
117 with PW(av[0], writep = True) as pw_out:
118 if len(av) >= 3: pat = av[1]
119 else: pat = None
120 for k in pw_in:
121 if pat is None or fnmatch(k, pat): pw_out[k] = pw_in[k]
122
123 def cmd_list(av):
124 if len(av) > 1: return 1
125 with PW(file) as pw:
126 if len(av) >= 1: pat = av[0]
127 else: pat = None
128 for k in pw:
129 if pat is None or fnmatch(k, pat): print k
130
131 def cmd_topixie(av):
132 if len(av) > 2: return 1
133 with PW(file) as pw:
134 pix = C.Pixie()
135 if len(av) == 0:
136 for tag in pw: pix.set(tag, pw[tag])
137 else:
138 tag = av[0]
139 if len(av) >= 2: pptag = av[1]
140 else: pptag = av[0]
141 pix.set(pptag, pw[tag])
142
143 def cmd_del(av):
144 if len(av) != 1: return 1
145 with PW(file, writep = True) as pw:
146 tag = av[0]
147 try: del pw[tag]
148 except KeyError, exc: die("Password `%s' not found" % exc.args[0])
149
150 commands = { 'create': [cmd_create,
151 '[-c CIPHER] [-h HASH] [-m MAC] [PP-TAG]'],
152 'find' : [cmd_find, 'LABEL'],
153 'store' : [cmd_store, 'LABEL [VALUE]'],
154 'list' : [cmd_list, '[GLOB-PATTERN]'],
155 'changepp' : [cmd_changepp, ''],
156 'copy' : [cmd_copy, 'DEST-FILE [GLOB-PATTERN]'],
157 'to-pixie' : [cmd_topixie, '[TAG [PIXIE-TAG]]'],
158 'delete' : [cmd_del, 'TAG']}
159
160 ###--------------------------------------------------------------------------
161 ### Command-line handling and dispatch.
162
163 def version():
164 print '%s 1.0.0' % prog
165
166 def usage(fp):
167 print >>fp, 'Usage: %s COMMAND [ARGS...]' % prog
168
169 def help():
170 version()
171 print
172 usage(stdout)
173 print '''
174 Maintains passwords or other short secrets securely.
175
176 Options:
177
178 -h, --help Show this help text.
179 -v, --version Show program version number.
180 -u, --usage Show short usage message.
181
182 -f, --file=FILE Where to find the password-safe file.
183
184 Commands provided:
185 '''
186 for c in sorted(commands):
187 print '%s %s' % (c, commands[c][1])
188
189 ## Choose a default database file.
190 if 'PWSAFE' in environ:
191 file = environ['PWSAFE']
192 else:
193 file = '%s/.pwsafe' % environ['HOME']
194
195 ## Parse the command-line options.
196 try:
197 opts, argv = getopt(argv[1:], 'hvuf:',
198 ['help', 'version', 'usage', 'file='])
199 except GetoptError:
200 usage(stderr)
201 exit(1)
202 for o, a in opts:
203 if o in ('-h', '--help'):
204 help()
205 exit(0)
206 elif o in ('-v', '--version'):
207 version()
208 exit(0)
209 elif o in ('-u', '--usage'):
210 usage(stdout)
211 exit(0)
212 elif o in ('-f', '--file'):
213 file = a
214 else:
215 raise 'Barf!'
216 if len(argv) < 1:
217 usage(stderr)
218 exit(1)
219
220 ## Dispatch to a command handler.
221 if argv[0] in commands:
222 c = argv[0]
223 argv = argv[1:]
224 else:
225 c = 'find'
226 try:
227 if commands[c][0](argv):
228 print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1])
229 exit(1)
230 except DecryptError:
231 die("decryption failure")
232
233 ###----- That's all, folks --------------------------------------------------