pwsafe, catacomb/pwsafe.py: Documentation and cleanup.
[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 import gdbm as G
31 from os import environ
32 from sys import argv, exit, stdin, stdout, stderr
33 from getopt import getopt, GetoptError
34 from fnmatch import fnmatch
35 import re
36
37 import catacomb as C
38 from catacomb.pwsafe import *
39
40 ###--------------------------------------------------------------------------
41 ### Utilities.
42
43 ## The program name.
44 prog = re.sub(r'^.*[/\\]', '', argv[0])
45
46 def moan(msg):
47 """Issue a warning message MSG."""
48 print >>stderr, '%s: %s' % (prog, msg)
49
50 def die(msg):
51 """Report MSG as a fatal error, and exit."""
52 moan(msg)
53 exit(1)
54
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.
79
80 def cmd_create(av):
81
82 ## Default crypto-primitive selections.
83 cipher = 'blowfish-cbc'
84 hash = 'rmd160'
85 mac = None
86
87 ## Parse the options.
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'
107
108 ## Choose a passphrase, and generate master keys.
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]
114 ppk = PW.PPK(pp, c, h, m)
115 ck = C.rand.block(c.keysz.default)
116 mk = C.rand.block(m.keysz.default)
117 k = Crypto(c, h, m, ck, mk)
118
119 ## Set up the database, storing the basic information we need.
120 db = G.open(file, 'n', 0600)
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)
139 try:
140 print pw[av[0]]
141 except KeyError, exc:
142 die('Password `%s\' not found.' % exc.args[0])
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')
158 pw[av[0]] = chomp(pp)
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):
186 if len(av) > 2:
187 return 1
188 pw = PW(file)
189 pix = C.Pixie()
190 if len(av) == 0:
191 for tag in pw:
192 pix.set(tag, pw[tag])
193 else:
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])
200
201 def cmd_del(av):
202 if len(av) != 1:
203 return 1
204 pw = PW(file, 'w')
205 tag = av[0]
206 try:
207 del pw[tag]
208 except KeyError, exc:
209 die('Password `%s\' not found.' % exc.args[0])
210
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,
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.
232
233 def version():
234 print '%s 1.0.0' % prog
235
236 def usage(fp):
237 print >>fp, 'Usage: %s COMMAND [ARGS...]' % prog
238
239 def help():
240 version()
241 print
242 usage(stdout)
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
252 -f, --file=FILE Where to find the password-safe file.
253
254 Commands provided:
255 '''
256 for c in commands:
257 print '%s %s' % (c, commands[c][1])
258
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.
266 try:
267 opts, argv = getopt(argv[1:], 'hvuf:',
268 ['help', 'version', 'usage', 'file='])
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
290 ## Dispatch to a command handler.
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):
297 print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1])
298 exit(1)
299
300 ###----- That's all, folks --------------------------------------------------