chiark / gitweb /
6b36bf4d8efffabfe8d33e047615903f565c00f5
[catacomb-python] / pwsafe
1 #! /usr/bin/python2.2
2 # -*-python-*-
3
4 import catacomb as C
5 import gdbm, struct
6 from sys import argv, exit, stdin, stdout, stderr
7 from getopt import getopt, GetoptError
8 from os import environ
9 from fnmatch import fnmatch
10
11 if 'PWSAFE' in environ:
12   file = environ['PWSAFE']
13 else:
14   file = '%s/.pwsafe' % environ['HOME']
15
16 class DecryptError (Exception):
17   pass
18
19 class 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   
42 class 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
51 class 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
71 def wrapstr(s):
72   return struct.pack('>H', len(s)) + s
73
74 class 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]
88 class 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
134 def 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
175 def cmd_changepp(av):
176   if len(av) != 0:
177     return 1
178   pw = PW(file, 'w')
179   pw.changepp()
180
181 def cmd_find(av):
182   if len(av) != 1:
183     return 1
184   pw = PW(file)
185   print pw[av[0]]
186
187 def 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
203 def 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
216 def 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
228 def 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
239 def 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
246 def asciip(s):
247   for ch in s:
248     if ch < ' ' or ch > '~': return False
249   return True
250 def present(s):
251   if asciip(s): return s
252   return C.ByteString(s)
253 def 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
261 commands = { '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]'],
269              'delete' : [cmd_del, 'TAG'],
270              'dump' : [cmd_dump, '']}
271
272 def version():
273   print 'pwsafe 1.0.0'
274 def usage(fp):
275   print >>fp, 'Usage: pwsafe COMMAND [ARGS...]'
276 def help():
277   version()
278   print
279   usage(stdout)  
280   print '''
281 Maintains passwords or other short secrets securely.
282
283 Options:
284
285 -h, --help              Show this help text.
286 -v, --version           Show program version number.
287 -u, --usage             Show short usage message.
288
289 Commands provided:
290 '''
291   for c in commands:
292     print '%s %s' % (c, commands[c][1])
293
294 try:
295   opts, argv = getopt(argv[1:],
296                       'hvuf:',
297                       ['help', 'version', 'usage', 'file='])
298 except GetoptError:
299   usage(stderr)
300   exit(1)
301 for 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!'
315 if len(argv) < 1:
316   usage(stderr)
317   exit(1)
318
319 if argv[0] in commands:
320   c = argv[0]
321   argv = argv[1:]
322 else:
323   c = 'find'
324 if commands[c][0](argv):
325   print >>stderr, 'Usage: pwsafe %s %s' % (c, commands[c][1])
326   exit(1)