chiark / gitweb /
catacomb/__init__.py: Generalize rationals to fields of fractions.
[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
32 ###--------------------------------------------------------------------------
33 ### Underlying cryptography.
34
35 class DecryptError (Exception):
36   """
37   I represent a failure to decrypt a message.
38
39   Usually this means that someone used the wrong key, though it can also
40   mean that a ciphertext has been modified.
41   """
42   pass
43
44 class Crypto (object):
45   """
46   I represent a symmetric crypto transform.
47
48   There's currently only one transform implemented, which is the obvious
49   generic-composition construction: given a message m, and keys K0 and K1, we
50   choose an IV v, and compute:
51
52     * y = v || E(K0, v; m)
53     * t = M(K1; y)
54
55   The final ciphertext is t || y.
56   """
57
58   def __init__(me, c, h, m, ck, mk):
59     """
60     Initialize the Crypto object with a given algorithm selection and keys.
61
62     We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
63     keys CK and MK for C and M respectively.
64     """
65     me.c = c(ck)
66     me.m = m(mk)
67     me.h = h
68
69   def encrypt(me, pt):
70     """
71     Encrypt the message PT and return the resulting ciphertext.
72     """
73     blksz = me.c.__class__.blksz
74     b = _C.WriteBuffer()
75     if blksz:
76       iv = _C.rand.block(blksz)
77       me.c.setiv(iv)
78       b.put(iv)
79     b.put(me.c.encrypt(pt))
80     t = me.m().hash(b).done()
81     return t + str(buffer(b))
82
83   def decrypt(me, ct):
84     """
85     Decrypt the ciphertext CT, returning the plaintext.
86
87     Raises DecryptError if anything goes wrong.
88     """
89     blksz = me.c.__class__.blksz
90     tagsz = me.m.__class__.tagsz
91     b = _C.ReadBuffer(ct)
92     t = b.get(tagsz)
93     h = me.m()
94     if blksz:
95       iv = b.get(blksz)
96       me.c.setiv(iv)
97       h.hash(iv)
98     x = b.get(b.left)
99     h.hash(x)
100     if t != h.done(): raise DecryptError
101     return me.c.decrypt(x)
102
103 class PPK (Crypto):
104   """
105   I represent a crypto transform whose keys are derived from a passphrase.
106
107   The password is salted and hashed; the salt is available as the `salt'
108   attribute.
109   """
110
111   def __init__(me, pp, c, h, m, salt = None):
112     """
113     Initialize the PPK object with a passphrase and algorithm selection.
114
115     We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
116     subclass M, and a SALT.  The SALT may be None, if we're generating new
117     keys, indicating that a salt should be chosen randomly.
118     """
119     if not salt: salt = _C.rand.block(h.hashsz)
120     tag = '%s\0%s' % (pp, salt)
121     Crypto.__init__(me, c, h, m,
122                     h().hash('cipher:' + tag).done(),
123                     h().hash('mac:' + tag).done())
124     me.salt = salt
125
126 ###--------------------------------------------------------------------------
127 ### Password storage.
128
129 class PWIter (object):
130   """
131   I am an iterator over items in a password database.
132
133   I implement the usual Python iteration protocol.
134   """
135
136   def __init__(me, pw):
137     """
138     Initialize a PWIter object, to fetch items from PW.
139     """
140     me.pw = pw
141     me.k = me.pw.db.firstkey()
142
143   def next(me):
144     """
145     Return the next tag from the database.
146
147     Raises StopIteration if there are no more tags.
148     """
149     k = me.k
150     while True:
151       if k is None:
152         raise StopIteration
153       if k[0] == '$':
154         break
155       k = me.pw.db.nextkey(k)
156     me.k = me.pw.db.nextkey(k)
157     return me.pw.unpack(me.pw.db[k])[0]
158
159 class PW (object):
160   """
161   I represent a secure (ish) password store.
162
163   I can store short secrets, associated with textual names, in a way which
164   doesn't leak too much information about them.
165
166   I implement (some of the) Python mapping protocol.
167
168   Here's how we use the underlying GDBM key/value storage to keep track of
169   the necessary things.  Password entries have keys whose name begins with
170   `$'; other keys have specific meanings, as follows.
171
172   cipher        Names the Catacomb cipher selected.
173
174   hash          Names the Catacomb hash function selected.
175
176   key           Cipher and MAC keys, each prefixed by a 16-bit big-endian
177                 length and concatenated, encrypted using the master
178                 passphrase.
179
180   mac           Names the Catacomb message authentication code selected.
181
182   magic         A magic string for obscuring password tag names.
183
184   salt          The salt for hashing the passphrase.
185
186   tag           The master passphrase's tag, for the Pixie's benefit.
187
188   Password entries are assigned keys of the form `$' || H(MAGIC || TAG); the
189   corresponding value consists of a pair (TAG, PASSWD), prefixed with 16-bit
190   lengths, concatenated, padded to a multiple of 256 octets, and encrypted
191   using the stored keys.
192   """
193
194   def __init__(me, file, mode = 'r'):
195     """
196     Initialize a PW object from the GDBM database in FILE.
197
198     MODE can be `r' for read-only access to the underlying database, or `w'
199     for read-write access.  Requests the database password from the Pixie,
200     which may cause interaction.
201     """
202
203     ## Open the database.
204     me.db = _G.open(file, mode)
205
206     ## Find out what crypto to use.
207     c = _C.gcciphers[me.db['cipher']]
208     h = _C.gchashes[me.db['hash']]
209     m = _C.gcmacs[me.db['mac']]
210
211     ## Request the passphrase and extract the master keys.
212     tag = me.db['tag']
213     ppk = PPK(_C.ppread(tag), c, h, m, me.db['salt'])
214     try:
215       b = _C.ReadBuffer(ppk.decrypt(me.db['key']))
216     except DecryptError:
217       _C.ppcancel(tag)
218       raise
219     me.ck = b.getblk16()
220     me.mk = b.getblk16()
221     if not b.endp: raise ValueError, 'trailing junk'
222
223     ## Set the key, and stash it and the tag-hashing secret.
224     me.k = Crypto(c, h, m, me.ck, me.mk)
225     me.magic = me.k.decrypt(me.db['magic'])
226
227   @classmethod
228   def create(cls, file, c, h, m, tag):
229     """
230     Create and initialize a new, empty, database FILE.
231
232     We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
233     and a Pixie passphrase TAG.
234
235     This doesn't return a working object: it just creates the database file
236     and gets out of the way.
237     """
238
239     ## Set up the cryptography.
240     pp = _C.ppread(tag, _C.PMODE_VERIFY)
241     ppk = PPK(pp, c, h, m)
242     ck = _C.rand.block(c.keysz.default)
243     mk = _C.rand.block(c.keysz.default)
244     k = Crypto(c, h, m, ck, mk)
245
246     ## Set up and initialize the database.
247     db = _G.open(file, 'n', 0600)
248     db['tag'] = tag
249     db['salt'] = ppk.salt
250     db['cipher'] = c.name
251     db['hash'] = h.name
252     db['mac'] = m.name
253     db['key'] = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
254     db['magic'] = k.encrypt(_C.rand.block(h.hashsz))
255
256   def keyxform(me, key):
257     """
258     Transform the KEY (actually a password tag) into a GDBM record key.
259     """
260     return '$' + me.k.h().hash(me.magic).hash(key).done()
261
262   def changepp(me):
263     """
264     Change the database password.
265
266     Requests the new password from the Pixie, which will probably cause
267     interaction.
268     """
269     tag = me.db['tag']
270     _C.ppcancel(tag)
271     ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
272               me.k.c.__class__, me.k.h, me.k.m.__class__)
273     me.db['key'] = \
274         ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
275     me.db['salt'] = ppk.salt
276
277   def pack(me, key, value):
278     """
279     Pack the KEY and VALUE into a ciphertext, and return it.
280     """
281     b = _C.WriteBuffer()
282     b.putblk16(key).putblk16(value)
283     b.zero(((b.size + 255) & ~255) - b.size)
284     return me.k.encrypt(b)
285
286   def unpack(me, ct):
287     """
288     Unpack a ciphertext CT and return a (KEY, VALUE) pair.
289
290     Might raise DecryptError, of course.
291     """
292     b = _C.ReadBuffer(me.k.decrypt(ct))
293     key = b.getblk16()
294     value = b.getblk16()
295     return key, value
296
297   ## Mapping protocol.
298
299   def __getitem__(me, key):
300     """
301     Return the password for the given KEY.
302     """
303     try:
304       return me.unpack(me.db[me.keyxform(key)])[1]
305     except KeyError:
306       raise KeyError, key
307
308   def __setitem__(me, key, value):
309     """
310     Associate the password VALUE with the KEY.
311     """
312     me.db[me.keyxform(key)] = me.pack(key, value)
313
314   def __delitem__(me, key):
315     """
316     Forget all about the KEY.
317     """
318     try:
319       del me.db[me.keyxform(key)]
320     except KeyError:
321       raise KeyError, key
322
323   def __iter__(me):
324     """
325     Iterate over the known password tags.
326     """
327     return PWIter(me)
328
329 ###----- That's all, folks --------------------------------------------------