chiark / gitweb /
pwsafe, catacomb/pwsafe.py: Documentation and cleanup.
[catacomb-python] / pwsafe
CommitLineData
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
27###---------------------------------------------------------------------------
28### Imported modules.
d7ab1bab 29
43c09851 30import gdbm as G
31from os import environ
d7ab1bab 32from sys import argv, exit, stdin, stdout, stderr
33from getopt import getopt, GetoptError
d7ab1bab 34from fnmatch import fnmatch
80dc9c94 35import re
2e6a3fda 36
d1c45f5c
MW
37import catacomb as C
38from catacomb.pwsafe import *
39
40###--------------------------------------------------------------------------
41### Utilities.
42
43## The program name.
2e6a3fda 44prog = re.sub(r'^.*[/\\]', '', argv[0])
d1c45f5c 45
2e6a3fda 46def moan(msg):
d1c45f5c 47 """Issue a warning message MSG."""
2e6a3fda 48 print >>stderr, '%s: %s' % (prog, msg)
d1c45f5c 49
2e6a3fda 50def die(msg):
d1c45f5c 51 """Report MSG as a fatal error, and exit."""
2e6a3fda 52 moan(msg)
53 exit(1)
d7ab1bab 54
d1c45f5c
MW
55def 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
61def 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
67def 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.
d7ab1bab 79
d7ab1bab 80def cmd_create(av):
d1c45f5c
MW
81
82 ## Default crypto-primitive selections.
d7ab1bab 83 cipher = 'blowfish-cbc'
84 hash = 'rmd160'
85 mac = None
d1c45f5c
MW
86
87 ## Parse the options.
d7ab1bab 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'
d1c45f5c
MW
107
108 ## Choose a passphrase, and generate master keys.
d7ab1bab 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]
43c09851 114 ppk = PW.PPK(pp, c, h, m)
d7ab1bab 115 ck = C.rand.block(c.keysz.default)
116 mk = C.rand.block(m.keysz.default)
117 k = Crypto(c, h, m, ck, mk)
d1c45f5c
MW
118
119 ## Set up the database, storing the basic information we need.
120 db = G.open(file, 'n', 0600)
d7ab1bab 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
129def cmd_changepp(av):
130 if len(av) != 0:
131 return 1
132 pw = PW(file, 'w')
133 pw.changepp()
134
135def cmd_find(av):
136 if len(av) != 1:
137 return 1
138 pw = PW(file)
2e6a3fda 139 try:
140 print pw[av[0]]
141 except KeyError, exc:
142 die('Password `%s\' not found.' % exc.args[0])
d7ab1bab 143
144def 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')
43c09851 158 pw[av[0]] = chomp(pp)
d7ab1bab 159
160def 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
173def 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
185def cmd_topixie(av):
43c09851 186 if len(av) > 2:
d7ab1bab 187 return 1
188 pw = PW(file)
43c09851 189 pix = C.Pixie()
190 if len(av) == 0:
191 for tag in pw:
192 pix.set(tag, pw[tag])
d7ab1bab 193 else:
43c09851 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])
d7ab1bab 200
3aa33042 201def cmd_del(av):
202 if len(av) != 1:
203 return 1
204 pw = PW(file, 'w')
205 tag = av[0]
2e6a3fda 206 try:
207 del pw[tag]
208 except KeyError, exc:
209 die('Password `%s\' not found.' % exc.args[0])
3aa33042 210
d7ab1bab 211def 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
219commands = { 'create': [cmd_create,
d1c45f5c
MW
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.
d7ab1bab 232
233def version():
2e6a3fda 234 print '%s 1.0.0' % prog
d1c45f5c 235
d7ab1bab 236def usage(fp):
2e6a3fda 237 print >>fp, 'Usage: %s COMMAND [ARGS...]' % prog
d1c45f5c 238
d7ab1bab 239def help():
240 version()
241 print
b2687a0a 242 usage(stdout)
d7ab1bab 243 print '''
244Maintains passwords or other short secrets securely.
245
246Options:
247
248-h, --help Show this help text.
249-v, --version Show program version number.
250-u, --usage Show short usage message.
251
2e6a3fda 252-f, --file=FILE Where to find the password-safe file.
253
d7ab1bab 254Commands provided:
255'''
256 for c in commands:
257 print '%s %s' % (c, commands[c][1])
258
d1c45f5c
MW
259## Choose a default database file.
260if 'PWSAFE' in environ:
261 file = environ['PWSAFE']
262else:
263 file = '%s/.pwsafe' % environ['HOME']
264
265## Parse the command-line options.
d7ab1bab 266try:
d1c45f5c
MW
267 opts, argv = getopt(argv[1:], 'hvuf:',
268 ['help', 'version', 'usage', 'file='])
d7ab1bab 269except GetoptError:
270 usage(stderr)
271 exit(1)
272for 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!'
286if len(argv) < 1:
287 usage(stderr)
288 exit(1)
289
d1c45f5c 290## Dispatch to a command handler.
d7ab1bab 291if argv[0] in commands:
292 c = argv[0]
293 argv = argv[1:]
294else:
295 c = 'find'
296if commands[c][0](argv):
2e6a3fda 297 print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1])
d7ab1bab 298 exit(1)
d1c45f5c
MW
299
300###----- That's all, folks --------------------------------------------------