catacomb/pwsafe.py: Factor database handling out into a StorageBackend.
[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 from __future__ import with_statement
30
31 import catacomb as _C
32 import gdbm as _G
33
34 ###--------------------------------------------------------------------------
35 ### Underlying cryptography.
36
37 class DecryptError (Exception):
38 """
39 I represent a failure to decrypt a message.
40
41 Usually this means that someone used the wrong key, though it can also
42 mean that a ciphertext has been modified.
43 """
44 pass
45
46 class Crypto (object):
47 """
48 I represent a symmetric crypto transform.
49
50 There's currently only one transform implemented, which is the obvious
51 generic-composition construction: given a message m, and keys K0 and K1, we
52 choose an IV v, and compute:
53
54 * y = v || E(K0, v; m)
55 * t = M(K1; y)
56
57 The final ciphertext is t || y.
58 """
59
60 def __init__(me, c, h, m, ck, mk):
61 """
62 Initialize the Crypto object with a given algorithm selection and keys.
63
64 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
65 keys CK and MK for C and M respectively.
66 """
67 me.c = c(ck)
68 me.m = m(mk)
69 me.h = h
70
71 def encrypt(me, pt):
72 """
73 Encrypt the message PT and return the resulting ciphertext.
74 """
75 blksz = me.c.__class__.blksz
76 b = _C.WriteBuffer()
77 if blksz:
78 iv = _C.rand.block(blksz)
79 me.c.setiv(iv)
80 b.put(iv)
81 b.put(me.c.encrypt(pt))
82 t = me.m().hash(b).done()
83 return t + str(buffer(b))
84
85 def decrypt(me, ct):
86 """
87 Decrypt the ciphertext CT, returning the plaintext.
88
89 Raises DecryptError if anything goes wrong.
90 """
91 blksz = me.c.__class__.blksz
92 tagsz = me.m.__class__.tagsz
93 b = _C.ReadBuffer(ct)
94 t = b.get(tagsz)
95 h = me.m()
96 if blksz:
97 iv = b.get(blksz)
98 me.c.setiv(iv)
99 h.hash(iv)
100 x = b.get(b.left)
101 h.hash(x)
102 if t != h.done(): raise DecryptError
103 return me.c.decrypt(x)
104
105 class PPK (Crypto):
106 """
107 I represent a crypto transform whose keys are derived from a passphrase.
108
109 The password is salted and hashed; the salt is available as the `salt'
110 attribute.
111 """
112
113 def __init__(me, pp, c, h, m, salt = None):
114 """
115 Initialize the PPK object with a passphrase and algorithm selection.
116
117 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
118 subclass M, and a SALT. The SALT may be None, if we're generating new
119 keys, indicating that a salt should be chosen randomly.
120 """
121 if not salt: salt = _C.rand.block(h.hashsz)
122 tag = '%s\0%s' % (pp, salt)
123 Crypto.__init__(me, c, h, m,
124 h().hash('cipher:' + tag).done(),
125 h().hash('mac:' + tag).done())
126 me.salt = salt
127
128 ###--------------------------------------------------------------------------
129 ### Backend storage.
130
131 class StorageBackend (object):
132 """
133 I provide a backend for password and metadata storage.
134
135 Backends are responsible for storing and retrieving stuff, but not for the
136 cryptographic details. Backends need to store two kinds of information:
137
138 * metadata, consisting of a number of property names and their values;
139 and
140
141 * password mappings, consisting of a number of binary labels and
142 payloads.
143 """
144
145 FAIL = ['FAIL']
146
147 ## Life cycle methods.
148
149 @classmethod
150 def create(cls, file):
151 """Create a new database in the named FILE, using this backend."""
152 return cls(writep = True, _magic = lambda me: me._create(file))
153 def _create(me, file):
154 me._db = _G.open(file, 'n', 0600)
155
156 def __init__(me, file = None, writep = False, _magic = None, *args, **kw):
157 """
158 Main constructor.
159 """
160 super(StorageBackend, me).__init__(*args, **kw)
161 if _magic is not None: _magic(me)
162 elif file is None: raise ValueError, 'missing file parameter'
163 else: me._db = _G.open(file, writep and 'w' or 'r')
164 me._writep = writep
165 me._livep = True
166
167 def close(me):
168 """
169 Close the database.
170
171 It is harmless to attempt to close a database which has been closed
172 already.
173 """
174 if me._livep:
175 me._livep = False
176 me._db.close()
177
178 ## Utilities.
179
180 def _check_live(me):
181 """Raise an error if the receiver has been closed."""
182 if not me._livep: raise ValueError, 'database is closed'
183
184 def _check_write(me):
185 """Raise an error if the receiver is not open for writing."""
186 me._check_live()
187 if not me._writep: raise ValueError, 'database is read-only'
188
189 def _check_meta_name(me, name):
190 """
191 Raise an error unless NAME is a valid name for a metadata item.
192
193 Metadata names may not start with `$': such names are reserved for
194 password storage.
195 """
196 if name.startswith('$'):
197 raise ValueError, "invalid metadata key `%s'" % name
198
199 ## Context protocol.
200
201 def __enter__(me):
202 """Context protocol: make sure the database is closed on exit."""
203 return me
204 def __exit__(me, exctype, excvalue, exctb):
205 """Context protocol: see `__enter__'."""
206 me.close()
207
208 ## Metadata.
209
210 def get_meta(me, name, default = FAIL):
211 """
212 Fetch the value for the metadata item NAME.
213
214 If no such item exists, then return DEFAULT if that was set; otherwise
215 raise a `KeyError'.
216 """
217 me._check_meta_name(name)
218 me._check_live()
219 try: value = me._db[name]
220 except KeyError: value = default
221 if value is StorageBackend.FAIL: raise KeyError, name
222 return value
223
224 def put_meta(me, name, value):
225 """Store VALUE in the metadata item called NAME."""
226 me._check_meta_name(name)
227 me._check_write()
228 me._db[name] = value
229
230 def del_meta(me, name):
231 """Forget about the metadata item with the given NAME."""
232 me._check_meta_name(name)
233 me._check_write()
234 del me._db[name]
235
236 def iter_meta(me):
237 """Return an iterator over the name/value metadata items."""
238 me._check_live()
239 k = me._db.firstkey()
240 while k is not None:
241 if not k.startswith('$'): yield k, me._db[k]
242 k = me._db.nextkey(k)
243
244 ## Passwords.
245
246 def get_passwd(me, label):
247 """
248 Fetch and return the payload stored with the (opaque, binary) LABEL.
249
250 If there is no such payload then raise `KeyError'.
251 """
252 me._check_live()
253 return me._db['$' + label]
254
255 def put_passwd(me, label, payload):
256 """
257 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
258
259 Any previous payload for LABEL is forgotten.
260 """
261 me._check_write()
262 me._db['$' + label] = payload
263
264 def del_passwd(me, label):
265 """
266 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
267
268 If there is no such payload then raise `KeyError'.
269 """
270 me._check_write()
271 del me._db['$' + label]
272
273 def iter_passwds(me):
274 """Return an iterator over the stored password label/payload pairs."""
275 me._check_live()
276 k = me._db.firstkey()
277 while k is not None:
278 if k.startswith('$'): yield k[1:], me._db[k]
279 k = me._db.nextkey(k)
280
281 ###--------------------------------------------------------------------------
282 ### Password storage.
283
284 class PW (object):
285 """
286 I represent a secure (ish) password store.
287
288 I can store short secrets, associated with textual names, in a way which
289 doesn't leak too much information about them.
290
291 I implement (some of) the Python mapping protocol.
292
293 I keep track of everything using a StorageBackend object. This contains
294 password entries, identified by cryptographic labels, and a number of
295 metadata items.
296
297 cipher Names the Catacomb cipher selected.
298
299 hash Names the Catacomb hash function selected.
300
301 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
302 length and concatenated, encrypted using the master
303 passphrase.
304
305 mac Names the Catacomb message authentication code selected.
306
307 magic A magic string for obscuring password tag names.
308
309 salt The salt for hashing the passphrase.
310
311 tag The master passphrase's tag, for the Pixie's benefit.
312
313 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
314 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
315 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
316 encrypted using the stored keys.
317 """
318
319 def __init__(me, file, writep = False):
320 """
321 Initialize a PW object from the database in FILE.
322
323 If WRITEP is false (the default) then the database is opened read-only;
324 if true then it may be written. Requests the database password from the
325 Pixie, which may cause interaction.
326 """
327
328 ## Open the database.
329 me.db = StorageBackend(file, writep)
330
331 ## Find out what crypto to use.
332 c = _C.gcciphers[me.db.get_meta('cipher')]
333 h = _C.gchashes[me.db.get_meta('hash')]
334 m = _C.gcmacs[me.db.get_meta('mac')]
335
336 ## Request the passphrase and extract the master keys.
337 tag = me.db.get_meta('tag')
338 ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt'))
339 try:
340 b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
341 except DecryptError:
342 _C.ppcancel(tag)
343 raise
344 me.ck = b.getblk16()
345 me.mk = b.getblk16()
346 if not b.endp: raise ValueError, 'trailing junk'
347
348 ## Set the key, and stash it and the tag-hashing secret.
349 me.k = Crypto(c, h, m, me.ck, me.mk)
350 me.magic = me.k.decrypt(me.db.get_meta('magic'))
351
352 @classmethod
353 def create(cls, file, tag, c, h, m):
354 """
355 Create and initialize a new database FILE.
356
357 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
358 and a Pixie passphrase TAG.
359
360 This doesn't return a working object: it just creates the database file
361 and gets out of the way.
362 """
363
364 ## Set up the cryptography.
365 pp = _C.ppread(tag, _C.PMODE_VERIFY)
366 ppk = PPK(pp, c, h, m)
367 ck = _C.rand.block(c.keysz.default)
368 mk = _C.rand.block(c.keysz.default)
369 k = Crypto(c, h, m, ck, mk)
370
371 ## Set up and initialize the database.
372 kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
373 with StorageBackend.create(file) as db:
374 db.put_meta('tag', tag)
375 db.put_meta('salt', ppk.salt)
376 db.put_meta('cipher', c.name)
377 db.put_meta('hash', h.name)
378 db.put_meta('mac', m.name)
379 db.put_meta('key', kct)
380 db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz)))
381
382 def keyxform(me, key):
383 """Transform the KEY (actually a password tag) into a password label."""
384 return me.k.h().hash(me.magic).hash(key).done()
385
386 def changepp(me):
387 """
388 Change the database password.
389
390 Requests the new password from the Pixie, which will probably cause
391 interaction.
392 """
393 tag = me.db.get_meta('tag')
394 _C.ppcancel(tag)
395 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
396 me.k.c.__class__, me.k.h, me.k.m.__class__)
397 kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
398 me.db.put_meta('key', kct)
399 me.db.put_meta('salt', ppk.salt)
400
401 def pack(me, key, value):
402 """Pack the KEY and VALUE into a ciphertext, and return it."""
403 b = _C.WriteBuffer()
404 b.putblk16(key).putblk16(value)
405 b.zero(((b.size + 255) & ~255) - b.size)
406 return me.k.encrypt(b)
407
408 def unpack(me, ct):
409 """
410 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
411
412 Might raise DecryptError, of course.
413 """
414 b = _C.ReadBuffer(me.k.decrypt(ct))
415 key = b.getblk16()
416 value = b.getblk16()
417 return key, value
418
419 ## Mapping protocol.
420
421 def __getitem__(me, key):
422 """Return the password for the given KEY."""
423 try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1]
424 except KeyError: raise KeyError, key
425
426 def __setitem__(me, key, value):
427 """Associate the password VALUE with the KEY."""
428 me.db.put_passwd(me.keyxform(key), me.pack(key, value))
429
430 def __delitem__(me, key):
431 """Forget all about the KEY."""
432 try: me.db.del_passwd(me.keyxform(key))
433 except KeyError: raise KeyError, key
434
435 def __iter__(me):
436 """Iterate over the known password tags."""
437 for _, pld in me.db.iter_passwds():
438 yield me.unpack(pld)[0]
439
440 ## Context protocol.
441
442 def __enter__(me):
443 return me
444 def __exit__(me, excty, excval, exctb):
445 me.db.close()
446
447 ###----- That's all, folks --------------------------------------------------