chiark / gitweb /
Initial check-in of catacomb-python.
[catacomb-python] / pwsafe
1 #! /usr/bin/python2.2
2
3 import catacomb as C
4 import gdbm, struct
5 from sys import argv, exit, stdin, stdout, stderr
6 from getopt import getopt, GetoptError
7 from os import environ
8 from fnmatch import fnmatch
9
10 file = '%s/.pwsafe' % environ['HOME']
11
12 class DecryptError (Exception):
13   pass
14
15 class 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   
38 class 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
47 class 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
67 def wrapstr(s):
68   return struct.pack('>H', len(s)) + s
69
70 class 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]
84 class 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
130 def 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
171 def cmd_changepp(av):
172   if len(av) != 0:
173     return 1
174   pw = PW(file, 'w')
175   pw.changepp()
176
177 def cmd_find(av):
178   if len(av) != 1:
179     return 1
180   pw = PW(file)
181   print pw[av[0]]
182
183 def 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
199 def 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
212 def 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
224 def 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
235 def asciip(s):
236   for ch in s:
237     if ch < ' ' or ch > '~': return False
238   return True
239 def present(s):
240   if asciip(s): return s
241   return C.ByteString(s)
242 def 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
250 commands = { '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
260 def version():
261   print 'pwsafe 1.0.0'
262 def usage(fp):
263   print >>fp, 'Usage: pwsafe COMMAND [ARGS...]'
264 def help():
265   version()
266   print
267   usage(stdout)  
268   print '''
269 Maintains passwords or other short secrets securely.
270
271 Options:
272
273 -h, --help              Show this help text.
274 -v, --version           Show program version number.
275 -u, --usage             Show short usage message.
276
277 Commands provided:
278 '''
279   for c in commands:
280     print '%s %s' % (c, commands[c][1])
281
282 try:
283   opts, argv = getopt(argv[1:],
284                       'hvuf:',
285                       ['help', 'version', 'usage', 'file='])
286 except GetoptError:
287   usage(stderr)
288   exit(1)
289 for 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!'
303 if len(argv) < 1:
304   usage(stderr)
305   exit(1)
306
307 if argv[0] in commands:
308   c = argv[0]
309   argv = argv[1:]
310 else:
311   c = 'find'
312 if commands[c][0](argv):
313   print >>stderr, 'Usage: pwsafe %s %s' % (c, commands[c][1])
314   exit(1)