chiark / gitweb /
d0d1a35b027e131366fe75c34d536e79de5f46b8
[catacomb-python] / catacomb / pwsafe.py
1 ### -*-python-*-
2 ###
3 ### Management of a secure password database
4 ###
5 ### (c) 2005 Straylight/Edgeware
6 ###
7
8 ###----- Licensing notice ---------------------------------------------------
9 ###
10 ### This file is part of the Python interface to Catacomb.
11 ###
12 ### Catacomb/Python is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU General Public License as published by
14 ### the Free Software Foundation; either version 2 of the License, or
15 ### (at your option) any later version.
16 ###
17 ### Catacomb/Python is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 ### GNU General Public License for more details.
21 ###
22 ### You should have received a copy of the GNU General Public License along
23 ### with Catacomb/Python; if not, write to the Free Software Foundation,
24 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25
26 ###--------------------------------------------------------------------------
27 ### Imported modules.
28
29 import catacomb as _C
30 import gdbm as _G
31 import struct as _S
32
33 ###--------------------------------------------------------------------------
34 ### Utilities.
35
36 class Buffer (object):
37   """
38   I am a simple gadget for parsing binary strings.
39
40   You should use Catacomb's ReadBuffer instead.
41   """
42
43   def __init__(me, s):
44     """
45     Initialize the buffer with a string S.
46     """
47     me.str = s
48     me.i = 0
49
50   def get(me, n):
51     """
52     Fetch and return the next N bytes from the buffer.
53     """
54     i = me.i
55     if n + i > len(me.str):
56       raise IndexError, 'buffer underflow'
57     me.i += n
58     return me.str[i:i + n]
59
60   def getbyte(me):
61     """
62     Fetch and return (as a small integer) the next byte from the buffer.
63     """
64     return ord(me.get(1))
65
66   def unpack(me, fmt):
67     """
68     Unpack a structure described by FMT from the next bytes of the buffer.
69
70     Return a tuple containing the unpacked items.
71     """
72     return _S.unpack(fmt, me.get(_S.calcsize(fmt)))
73
74   def getstring(me):
75     """
76     Fetch and return a counted string from the buffer.
77
78     The string is expected to be preceded by its 16-bit length, in network
79     byte order.
80     """
81     return me.get(me.unpack('>H')[0])
82
83   def checkend(me):
84     """
85     Raise an error if the buffer has not been completely consumed.
86     """
87     if me.i != len(me.str):
88       raise ValueError, 'junk at end of buffer'
89
90 def _wrapstr(s):
91   """
92   Prefix the string S with its 16-bit length.
93
94   It can be read using Buffer.getstring.  You should use Catacomb's
95   WriteBuffer.putblk16() function instead.
96   """
97   return _S.pack('>H', len(s)) + s
98
99 ###--------------------------------------------------------------------------
100 ### Underlying cryptography.
101
102 class DecryptError (Exception):
103   """
104   I represent a failure to decrypt a message.
105
106   Usually this means that someone used the wrong key, though it can also
107   mean that a ciphertext has been modified.
108   """
109   pass
110
111 class Crypto (object):
112   """
113   I represent a symmetric crypto transform.
114
115   There's currently only one transform implemented, which is the obvious
116   generic-composition construction: given a message m, and keys K0 and K1, we
117   choose an IV v, and compute:
118
119     * y = v || E(K0, v; m)
120     * t = M(K1; y)
121
122   The final ciphertext is t || y.
123   """
124
125   def __init__(me, c, h, m, ck, mk):
126     """
127     Initialize the Crypto object with a given algorithm selection and keys.
128
129     We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
130     keys CK and MK for C and M respectively.
131     """
132     me.c = c(ck)
133     me.m = m(mk)
134     me.h = h
135
136   def encrypt(me, pt):
137     """
138     Encrypt the message PT and return the resulting ciphertext.
139     """
140     if me.c.__class__.blksz:
141       iv = _C.rand.block(me.c.__class__.blksz)
142       me.c.setiv(iv)
143     else:
144       iv = ''
145     y = iv + me.c.encrypt(pt)
146     t = me.m().hash(y).done()
147     return t + y
148   def decrypt(me, ct):
149     """
150     Decrypt the ciphertext CT, returning the plaintext.
151
152     Raises DecryptError if anything goes wrong.
153     """
154     t = ct[:me.m.__class__.tagsz]
155     y = ct[me.m.__class__.tagsz:]
156     if t != me.m().hash(y).done():
157       raise DecryptError
158     iv = y[:me.c.__class__.blksz]
159     if me.c.__class__.blksz: me.c.setiv(iv)
160     return me.c.decrypt(y[me.c.__class__.blksz:])
161
162 class PPK (Crypto):
163   """
164   I represent a crypto transform whose keys are derived from a passphrase.
165
166   The password is salted and hashed; the salt is available as the `salt'
167   attribute.
168   """
169
170   def __init__(me, pp, c, h, m, salt = None):
171     """
172     Initialize the PPK object with a passphrase and algorithm selection.
173
174     We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
175     subclass M, and a SALT.  The SALT may be None, if we're generating new
176     keys, indicating that a salt should be chosen randomly.
177     """
178     if not salt: salt = _C.rand.block(h.hashsz)
179     tag = '%s\0%s' % (pp, salt)
180     Crypto.__init__(me, c, h, m,
181                     h().hash('cipher:' + tag).done(),
182                     h().hash('mac:' + tag).done())
183     me.salt = salt
184
185 ###--------------------------------------------------------------------------
186 ### Password storage.
187
188 class PWIter (object):
189   """
190   I am an iterator over items in a password database.
191
192   I implement the usual Python iteration protocol.
193   """
194
195   def __init__(me, pw):
196     """
197     Initialize a PWIter object, to fetch items from PW.
198     """
199     me.pw = pw
200     me.k = me.pw.db.firstkey()
201
202   def next(me):
203     """
204     Return the next tag from the database.
205
206     Raises StopIteration if there are no more tags.
207     """
208     k = me.k
209     while True:
210       if k is None:
211         raise StopIteration
212       if k[0] == '$':
213         break
214       k = me.pw.db.nextkey(k)
215     me.k = me.pw.db.nextkey(k)
216     return me.pw.unpack(me.pw.db[k])[0]
217
218 class PW (object):
219   """
220   I represent a secure (ish) password store.
221
222   I can store short secrets, associated with textual names, in a way which
223   doesn't leak too much information about them.
224
225   I implement (some of the) Python mapping protocol.
226
227   Here's how we use the underlying GDBM key/value storage to keep track of
228   the necessary things.  Password entries have keys whose name begins with
229   `$'; other keys have specific meanings, as follows.
230
231   cipher        Names the Catacomb cipher selected.
232
233   hash          Names the Catacomb hash function selected.
234
235   key           Cipher and MAC keys, each prefixed by a 16-bit big-endian
236                 length and concatenated, encrypted using the master
237                 passphrase.
238
239   mac           Names the Catacomb message authentication code selected.
240
241   magic         A magic string for obscuring password tag names.
242
243   salt          The salt for hashing the passphrase.
244
245   tag           The master passphrase's tag, for the Pixie's benefit.
246
247   Password entries are assigned keys of the form `$' || H(MAGIC || TAG); the
248   corresponding value consists of a pair (TAG, PASSWD), prefixed with 16-bit
249   lengths, concatenated, padded to a multiple of 256 octets, and encrypted
250   using the stored keys.
251   """
252
253   def __init__(me, file, mode = 'r'):
254     """
255     Initialize a PW object from the GDBM database in FILE.
256
257     MODE can be `r' for read-only access to the underlying database, or `w'
258     for read-write access.  Requests the database password from the Pixie,
259     which may cause interaction.
260     """
261
262     ## Open the database.
263     me.db = _G.open(file, mode)
264
265     ## Find out what crypto to use.
266     c = _C.gcciphers[me.db['cipher']]
267     h = _C.gchashes[me.db['hash']]
268     m = _C.gcmacs[me.db['mac']]
269
270     ## Request the passphrase and extract the master keys.
271     tag = me.db['tag']
272     ppk = PPK(_C.ppread(tag), c, h, m, me.db['salt'])
273     try:
274       buf = Buffer(ppk.decrypt(me.db['key']))
275     except DecryptError:
276       _C.ppcancel(tag)
277       raise
278     me.ck = buf.getstring()
279     me.mk = buf.getstring()
280     buf.checkend()
281
282     ## Set the key, and stash it and the tag-hashing secret.
283     me.k = Crypto(c, h, m, me.ck, me.mk)
284     me.magic = me.k.decrypt(me.db['magic'])
285
286   @classmethod
287   def create(cls, file, c, h, m, tag):
288     """
289     Create and initialize a new, empty, database FILE.
290
291     We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
292     and a Pixie passphrase TAG.
293
294     This doesn't return a working object: it just creates the database file
295     and gets out of the way.
296     """
297
298     ## Set up the cryptography.
299     pp = _C.ppread(tag, _C.PMODE_VERIFY)
300     ppk = PPK(pp, c, h, m)
301     ck = _C.rand.block(c.keysz.default)
302     mk = _C.rand.block(c.keysz.default)
303     k = Crypto(c, h, m, ck, mk)
304
305     ## Set up and initialize the database.
306     db = _G.open(file, 'n', 0600)
307     db['tag'] = tag
308     db['salt'] = ppk.salt
309     db['cipher'] = c.name
310     db['hash'] = h.name
311     db['mac'] = m.name
312     db['key'] = ppk.encrypt(_wrapstr(ck) + _wrapstr(mk))
313     db['magic'] = k.encrypt(_C.rand.block(h.hashsz))
314
315   def keyxform(me, key):
316     """
317     Transform the KEY (actually a password tag) into a GDBM record key.
318     """
319     return '$' + me.k.h().hash(me.magic).hash(key).done()
320
321   def changepp(me):
322     """
323     Change the database password.
324
325     Requests the new password from the Pixie, which will probably cause
326     interaction.
327     """
328     tag = me.db['tag']
329     _C.ppcancel(tag)
330     ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
331               me.k.c.__class__, me.k.h, me.k.m.__class__)
332     me.db['key'] = ppk.encrypt(_wrapstr(me.ck) + _wrapstr(me.mk))
333     me.db['salt'] = ppk.salt
334
335   def pack(me, key, value):
336     """
337     Pack the KEY and VALUE into a ciphertext, and return it.
338     """
339     w = _wrapstr(key) + _wrapstr(value)
340     pl = (len(w) + 255) & ~255
341     w += '\0' * (pl - len(w))
342     return me.k.encrypt(w)
343
344   def unpack(me, ct):
345     """
346     Unpack a ciphertext CT and return a (KEY, VALUE) pair.
347
348     Might raise DecryptError, of course.
349     """
350     buf = Buffer(me.k.decrypt(ct))
351     key = buf.getstring()
352     value = buf.getstring()
353     return key, value
354
355   ## Mapping protocol.
356
357   def __getitem__(me, key):
358     """
359     Return the password for the given KEY.
360     """
361     try:
362       return me.unpack(me.db[me.keyxform(key)])[1]
363     except KeyError:
364       raise KeyError, key
365
366   def __setitem__(me, key, value):
367     """
368     Associate the password VALUE with the KEY.
369     """
370     me.db[me.keyxform(key)] = me.pack(key, value)
371
372   def __delitem__(me, key):
373     """
374     Forget all about the KEY.
375     """
376     try:
377       del me.db[me.keyxform(key)]
378     except KeyError:
379       raise KeyError, key
380
381   def __iter__(me):
382     """
383     Iterate over the known password tags.
384     """
385     return PWIter(me)
386
387 ###----- That's all, folks --------------------------------------------------