More fixing for modern Pythons. No longer works with 2.2. Sorry.
[catacomb-python] / pwsafe
CommitLineData
d7ab1bab 1#! /usr/bin/python2.2
3aa33042 2# -*-python-*-
d7ab1bab 3
4import catacomb as C
5import gdbm, struct
6from sys import argv, exit, stdin, stdout, stderr
7from getopt import getopt, GetoptError
8from os import environ
9from fnmatch import fnmatch
10
3aa33042 11if 'PWSAFE' in environ:
12 file = environ['PWSAFE']
13else:
14 file = '%s/.pwsafe' % environ['HOME']
d7ab1bab 15
16class DecryptError (Exception):
17 pass
18
19class Crypto (object):
20 def __init__(me, c, h, m, ck, mk):
21 me.c = c(ck)
22 me.m = m(mk)
23 me.h = h
24 def encrypt(me, pt):
25 if me.c.__class__.blksz:
26 iv = C.rand.block(me.c.__class__.blksz)
27 me.c.setiv(iv)
28 else:
29 iv = ''
30 y = iv + me.c.encrypt(pt)
31 t = me.m().hash(y).done()
32 return t + y
33 def decrypt(me, ct):
34 t = ct[:me.m.__class__.tagsz]
35 y = ct[me.m.__class__.tagsz:]
36 if t != me.m().hash(y).done():
37 raise DecryptError
38 iv = y[:me.c.__class__.blksz]
39 if me.c.__class__.blksz: me.c.setiv(iv)
40 return me.c.decrypt(y[me.c.__class__.blksz:])
41
42class PPK (Crypto):
43 def __init__(me, pp, c, h, m, salt = None):
44 if not salt: salt = C.rand.block(h.hashsz)
45 tag = '%s\0%s' % (pp, salt)
46 Crypto.__init__(me, c, h, m,
47 h().hash('cipher:' + tag).done(),
48 h().hash('mac:' + tag).done())
49 me.salt = salt
50
51class Buffer (object):
52 def __init__(me, s):
53 me.str = s
54 me.i = 0
55 def get(me, n):
56 i = me.i
57 if n + i > len(me.str):
58 raise IndexError, 'buffer underflow'
59 me.i += n
60 return me.str[i:i + n]
61 def getbyte(me):
62 return ord(me.get(1))
63 def unpack(me, fmt):
64 return struct.unpack(fmt, me.get(struct.calcsize(fmt)))
65 def getstring(me):
66 return me.get(me.unpack('>H')[0])
67 def checkend(me):
68 if me.i != len(me.str):
69 raise ValueError, 'junk at end of buffer'
70
71def wrapstr(s):
72 return struct.pack('>H', len(s)) + s
73
74class PWIter (object):
75 def __init__(me, pw):
76 me.pw = pw
77 me.k = me.pw.db.firstkey()
78 def next(me):
79 k = me.k
80 while True:
81 if k is None:
82 raise StopIteration
83 if k[0] == '$':
84 break
85 k = me.pw.db.nextkey(k)
86 me.k = me.pw.db.nextkey(k)
87 return me.pw.unpack(me.pw.db[k])[0]
88class PW (object):
89 def __init__(me, file, mode = 'r'):
90 me.db = gdbm.open(file, mode)
91 c = C.gcciphers[me.db['cipher']]
92 h = C.gchashes[me.db['hash']]
93 m = C.gcmacs[me.db['mac']]
94 tag = me.db['tag']
95 ppk = PPK(C.ppread(tag), c, h, m, me.db['salt'])
96 try:
97 buf = Buffer(ppk.decrypt(me.db['key']))
98 except DecryptError:
99 C.ppcancel(tag)
100 raise
101 me.ck = buf.getstring()
102 me.mk = buf.getstring()
103 buf.checkend()
104 me.k = Crypto(c, h, m, me.ck, me.mk)
105 me.magic = me.k.decrypt(me.db['magic'])
106 def keyxform(me, key):
107 return '$' + me.k.h().hash(me.magic).hash(key).done()
108 def changepp(me):
109 tag = me.db['tag']
110 C.ppcancel(tag)
111 ppk = PPK(C.ppread(tag, C.PMODE_VERIFY),
112 me.k.c.__class__, me.k.h, me.k.m.__class__)
113 me.db['key'] = ppk.encrypt(wrapstr(me.ck) + wrapstr(me.mk))
114 me.db['salt'] = ppk.salt
115 def pack(me, key, value):
116 w = wrapstr(key) + wrapstr(value)
117 pl = (len(w) + 255) & ~255
118 w += '\0' * (pl - len(w))
119 return me.k.encrypt(w)
120 def unpack(me, p):
121 buf = Buffer(me.k.decrypt(p))
122 key = buf.getstring()
123 value = buf.getstring()
124 return key, value
125 def __getitem__(me, key):
126 return me.unpack(me.db[me.keyxform(key)])[1]
127 def __setitem__(me, key, value):
128 me.db[me.keyxform(key)] = me.pack(key, value)
129 def __delitem__(me, key):
130 del me.db[me.keyxform(key)]
131 def __iter__(me):
132 return PWIter(me)
133
134def cmd_create(av):
135 cipher = 'blowfish-cbc'
136 hash = 'rmd160'
137 mac = None
138 try:
139 opts, args = getopt(av, 'c:h:m:', ['cipher=', 'mac=', 'hash='])
140 except GetoptError:
141 return 1
142 for o, a in opts:
143 if o in ('-c', '--cipher'):
144 cipher = a
145 elif o in ('-m', '--mac'):
146 mac = a
147 elif o in ('-h', '--hash'):
148 hash = a
149 else:
150 raise 'Barf!'
151 if len(args) > 2:
152 return 1
153 if len(args) >= 1:
154 tag = args[0]
155 else:
156 tag = 'pwsafe'
157 db = gdbm.open(file, 'n', 0600)
158 pp = C.ppread(tag, C.PMODE_VERIFY)
159 if not mac: mac = hash + '-hmac'
160 c = C.gcciphers[cipher]
161 h = C.gchashes[hash]
162 m = C.gcmacs[mac]
163 ppk = PPK(pp, c, h, m)
164 ck = C.rand.block(c.keysz.default)
165 mk = C.rand.block(m.keysz.default)
166 k = Crypto(c, h, m, ck, mk)
167 db['tag'] = tag
168 db['salt'] = ppk.salt
169 db['cipher'] = cipher
170 db['hash'] = hash
171 db['mac'] = mac
172 db['key'] = ppk.encrypt(wrapstr(ck) + wrapstr(mk))
173 db['magic'] = k.encrypt(C.rand.block(h.hashsz))
174
175def cmd_changepp(av):
176 if len(av) != 0:
177 return 1
178 pw = PW(file, 'w')
179 pw.changepp()
180
181def cmd_find(av):
182 if len(av) != 1:
183 return 1
184 pw = PW(file)
185 print pw[av[0]]
186
187def cmd_store(av):
188 if len(av) < 1 or len(av) > 2:
189 return 1
190 tag = av[0]
191 if len(av) < 2:
192 pp = C.getpass("Enter passphrase `%s': " % tag)
193 vpp = C.getpass("Confirm passphrase `%s': " % tag)
194 if pp != vpp:
195 raise ValueError, "passphrases don't match"
196 elif av[1] == '-':
197 pp = stdin.readline()
198 else:
199 pp = av[1]
200 pw = PW(file, 'w')
201 pw[av[0]] = pp
202
203def cmd_copy(av):
204 if len(av) < 1 or len(av) > 2:
205 return 1
206 pw_in = PW(file)
207 pw_out = PW(av[0], 'w')
208 if len(av) >= 3:
209 pat = av[1]
210 else:
211 pat = None
212 for k in pw_in:
213 if pat is None or fnmatch(k, pat):
214 pw_out[k] = pw_in[k]
215
216def cmd_list(av):
217 if len(av) > 1:
218 return 1
219 pw = PW(file)
220 if len(av) >= 1:
221 pat = av[0]
222 else:
223 pat = None
224 for k in pw:
225 if pat is None or fnmatch(k, pat):
226 print k
227
228def cmd_topixie(av):
229 if len(av) < 1 or len(av) > 2:
230 return 1
231 pw = PW(file)
232 tag = av[0]
233 if len(av) >= 2:
234 pptag = av[1]
235 else:
236 pptag = av[0]
237 C.Pixie().set(pptag, pw[tag])
238
3aa33042 239def cmd_del(av):
240 if len(av) != 1:
241 return 1
242 pw = PW(file, 'w')
243 tag = av[0]
244 del pw[tag]
245
d7ab1bab 246def asciip(s):
247 for ch in s:
248 if ch < ' ' or ch > '~': return False
249 return True
250def present(s):
251 if asciip(s): return s
252 return C.ByteString(s)
253def cmd_dump(av):
254 db = gdbm.open(file, 'r')
255 k = db.firstkey()
256 while True:
257 if k is None: break
258 print '%r: %r' % (present(k), present(db[k]))
259 k = db.nextkey(k)
260
261commands = { 'create': [cmd_create,
262 '[-c CIPHER] [-h HASH] [-m MAC] [PP-TAG]'],
263 'find' : [cmd_find, 'LABEL'],
264 'store' : [cmd_store, 'LABEL [VALUE]'],
265 'list' : [cmd_list, '[GLOB-PATTERN]'],
266 'changepp' : [cmd_changepp, ''],
267 'copy' : [cmd_copy, 'DEST-FILE [GLOB-PATTERN]'],
268 'to-pixie' : [cmd_topixie, 'TAG [PIXIE-TAG]'],
3aa33042 269 'delete' : [cmd_del, 'TAG'],
d7ab1bab 270 'dump' : [cmd_dump, '']}
271
272def version():
273 print 'pwsafe 1.0.0'
274def usage(fp):
275 print >>fp, 'Usage: pwsafe COMMAND [ARGS...]'
276def help():
277 version()
278 print
279 usage(stdout)
280 print '''
281Maintains passwords or other short secrets securely.
282
283Options:
284
285-h, --help Show this help text.
286-v, --version Show program version number.
287-u, --usage Show short usage message.
288
289Commands provided:
290'''
291 for c in commands:
292 print '%s %s' % (c, commands[c][1])
293
294try:
295 opts, argv = getopt(argv[1:],
296 'hvuf:',
297 ['help', 'version', 'usage', 'file='])
298except GetoptError:
299 usage(stderr)
300 exit(1)
301for o, a in opts:
302 if o in ('-h', '--help'):
303 help()
304 exit(0)
305 elif o in ('-v', '--version'):
306 version()
307 exit(0)
308 elif o in ('-u', '--usage'):
309 usage(stdout)
310 exit(0)
311 elif o in ('-f', '--file'):
312 file = a
313 else:
314 raise 'Barf!'
315if len(argv) < 1:
316 usage(stderr)
317 exit(1)
318
319if argv[0] in commands:
320 c = argv[0]
321 argv = argv[1:]
322else:
323 c = 'find'
324if commands[c][0](argv):
325 print >>stderr, 'Usage: pwsafe %s %s' % (c, commands[c][1])
326 exit(1)