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