catacomb/pwsafe.py, pwsafe: Dispatching for multiple backends.
[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 os as _OS
32
33 import catacomb as _C
34 import gdbm as _G
35
36 ###--------------------------------------------------------------------------
37 ### Underlying cryptography.
38
39 class DecryptError (Exception):
40 """
41 I represent a failure to decrypt a message.
42
43 Usually this means that someone used the wrong key, though it can also
44 mean that a ciphertext has been modified.
45 """
46 pass
47
48 class Crypto (object):
49 """
50 I represent a symmetric crypto transform.
51
52 There's currently only one transform implemented, which is the obvious
53 generic-composition construction: given a message m, and keys K0 and K1, we
54 choose an IV v, and compute:
55
56 * y = v || E(K0, v; m)
57 * t = M(K1; y)
58
59 The final ciphertext is t || y.
60 """
61
62 def __init__(me, c, h, m, ck, mk):
63 """
64 Initialize the Crypto object with a given algorithm selection and keys.
65
66 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
67 keys CK and MK for C and M respectively.
68 """
69 me.c = c(ck)
70 me.m = m(mk)
71 me.h = h
72
73 def encrypt(me, pt):
74 """
75 Encrypt the message PT and return the resulting ciphertext.
76 """
77 blksz = me.c.__class__.blksz
78 b = _C.WriteBuffer()
79 if blksz:
80 iv = _C.rand.block(blksz)
81 me.c.setiv(iv)
82 b.put(iv)
83 b.put(me.c.encrypt(pt))
84 t = me.m().hash(b).done()
85 return t + str(buffer(b))
86
87 def decrypt(me, ct):
88 """
89 Decrypt the ciphertext CT, returning the plaintext.
90
91 Raises DecryptError if anything goes wrong.
92 """
93 blksz = me.c.__class__.blksz
94 tagsz = me.m.__class__.tagsz
95 b = _C.ReadBuffer(ct)
96 t = b.get(tagsz)
97 h = me.m()
98 if blksz:
99 iv = b.get(blksz)
100 me.c.setiv(iv)
101 h.hash(iv)
102 x = b.get(b.left)
103 h.hash(x)
104 if t != h.done(): raise DecryptError
105 return me.c.decrypt(x)
106
107 class PPK (Crypto):
108 """
109 I represent a crypto transform whose keys are derived from a passphrase.
110
111 The password is salted and hashed; the salt is available as the `salt'
112 attribute.
113 """
114
115 def __init__(me, pp, c, h, m, salt = None):
116 """
117 Initialize the PPK object with a passphrase and algorithm selection.
118
119 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
120 subclass M, and a SALT. The SALT may be None, if we're generating new
121 keys, indicating that a salt should be chosen randomly.
122 """
123 if not salt: salt = _C.rand.block(h.hashsz)
124 tag = '%s\0%s' % (pp, salt)
125 Crypto.__init__(me, c, h, m,
126 h().hash('cipher:' + tag).done(),
127 h().hash('mac:' + tag).done())
128 me.salt = salt
129
130 ###--------------------------------------------------------------------------
131 ### Backend storage.
132
133 class StorageBackendRefusal (Exception):
134 """
135 I signify that a StorageBackend subclass has refused to open a file.
136
137 This is used by the StorageBackend.open class method.
138 """
139 pass
140
141 class StorageBackendClass (type):
142 """
143 I am a metaclass for StorageBackend classes.
144
145 My main feature is that I register my concrete instances (with a `NAME'
146 which is not `None') with the StorageBackend class.
147 """
148 def __init__(me, name, supers, dict):
149 """
150 Register a new concrete StorageBackend subclass.
151 """
152 super(StorageBackendClass, me).__init__(name, supers, dict)
153 if me.NAME is not None: StorageBackend.register_concrete_subclass(me)
154
155 class StorageBackend (object):
156 """
157 I provide basic protocol for password storage backends.
158
159 I'm an abstract class: you want one of my subclasses if you actually want
160 to do something useful. But I maintain a list of my subclasses and can
161 choose an appropriate one to open a database file you've found lying about.
162
163 Backends are responsible for storing and retrieving stuff, but not for the
164 cryptographic details. Backends need to store two kinds of information:
165
166 * metadata, consisting of a number of property names and their values;
167 and
168
169 * password mappings, consisting of a number of binary labels and
170 payloads.
171
172 Backends need to implement the following ordinary methods. See the calling
173 methods for details of the subclass responsibilities.
174
175 BE._create(FILE) Create a new database in FILE; used by `create'.
176
177 BE._open(FILE, WRITEP)
178 Open the existing database FILE; used by `open'.
179
180 BE._close() Close the database, freeing up any resources.
181
182 BE._get_meta(NAME, DEFAULT)
183 Return the value of the metadata item with the given
184 NAME, or DEFAULT if it doesn't exist; used by
185 `get_meta'.
186
187 BE._put_meta(NAME, VALUE)
188 Set the VALUE of the metadata item with the given
189 NAME, creating one if necessary; used by `put_meta'.
190
191 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
192 `KeyError' if there is no such item; used by
193 `del_meta'.
194
195 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
196 pairs; used by `iter_meta'.
197
198 BE._get_passwd(LABEL)
199 Return the password payload stored with the (binary)
200 LABEL; used by `get_passwd'.
201
202 BE._put_passwd(LABEL, PAYLOAD)
203 Associate the (binary) PAYLOAD with the LABEL,
204 forgetting any previous payload for that LABEL; used
205 by `put_passwd'.
206
207 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
208 by `_del_passwd'.
209
210 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
211 pairs; used by `iter_passwds'.
212
213 Also, concrete subclasses should define the following class attributes.
214
215 NAME The name of the backend, so that the user can select
216 it when creating a new database.
217
218 PRIO An integer priority: backends are tried in decreasing
219 priority order when opening an existing database.
220 """
221
222 __metaclass__ = StorageBackendClass
223 NAME = None
224 PRIO = 10
225
226 ## The registry of subclasses.
227 CLASSES = {}
228
229 FAIL = ['FAIL']
230
231 @staticmethod
232 def register_concrete_subclass(sub):
233 """Register a concrete subclass, so that `open' can try it."""
234 StorageBackend.CLASSES[sub.NAME] = sub
235
236 @staticmethod
237 def byname(name):
238 """
239 Return the concrete subclass with the given NAME.
240
241 Raise `KeyError' if the name isn't found.
242 """
243 return StorageBackend.CLASSES[name]
244
245 @staticmethod
246 def classes():
247 """Return an iterator over the concrete subclasses."""
248 return StorageBackend.CLASSES.itervalues()
249
250 @staticmethod
251 def open(file, writep = False):
252 """Open a database FILE, using some appropriate backend."""
253 _OS.stat(file)
254 for cls in sorted(StorageBackend.CLASSES.values(), reverse = True,
255 key = lambda cls: cls.PRIO):
256 try: return cls(file, writep)
257 except StorageBackendRefusal: pass
258 raise StorageBackendRefusal
259
260 @classmethod
261 def create(cls, file):
262 """
263 Create a new database in the named FILE, using this backend.
264
265 Subclasses must implement the `_create' instance method.
266 """
267 return cls(writep = True, _magic = lambda me: me._create(file))
268
269 def __init__(me, file = None, writep = False, _magic = None, *args, **kw):
270 """
271 Main constructor.
272
273 Subclasses are not, in general, expected to override this: there's a
274 somewhat hairy protocol between the constructor and some of the class
275 methods. Instead, the main hook for customization is the subclass's
276 `_open' method, which is invoked in the usual case.
277 """
278 super(StorageBackend, me).__init__(*args, **kw)
279 if me.NAME is None: raise ValueError, 'abstract class'
280 if _magic is not None: _magic(me)
281 elif file is None: raise ValueError, 'missing file parameter'
282 else: me._open(file, writep)
283 me._writep = writep
284 me._livep = True
285
286 def close(me):
287 """
288 Close the database.
289
290 It is harmless to attempt to close a database which has been closed
291 already. Calls the subclass's `_close' method.
292 """
293 if me._livep:
294 me._livep = False
295 me._close()
296
297 ## Utilities.
298
299 def _check_live(me):
300 """Raise an error if the receiver has been closed."""
301 if not me._livep: raise ValueError, 'database is closed'
302
303 def _check_write(me):
304 """Raise an error if the receiver is not open for writing."""
305 me._check_live()
306 if not me._writep: raise ValueError, 'database is read-only'
307
308 def _check_meta_name(me, name):
309 """
310 Raise an error unless NAME is a valid name for a metadata item.
311
312 Metadata names may not start with `$': such names are reserved for
313 password storage.
314 """
315 if name.startswith('$'):
316 raise ValueError, "invalid metadata key `%s'" % name
317
318 ## Context protocol.
319
320 def __enter__(me):
321 """Context protocol: make sure the database is closed on exit."""
322 return me
323 def __exit__(me, exctype, excvalue, exctb):
324 """Context protocol: see `__enter__'."""
325 me.close()
326
327 ## Metadata.
328
329 def get_meta(me, name, default = FAIL):
330 """
331 Fetch the value for the metadata item NAME.
332
333 If no such item exists, then return DEFAULT if that was set; otherwise
334 raise a `KeyError'.
335
336 This calls the subclass's `_get_meta' method, which should return the
337 requested item or return the given DEFAULT value. It may assume that the
338 name is valid and the database is open.
339 """
340 me._check_meta_name(name)
341 me._check_live()
342 value = me._get_meta(name, default)
343 if value is StorageBackend.FAIL: raise KeyError, name
344 return value
345
346 def put_meta(me, name, value):
347 """
348 Store VALUE in the metadata item called NAME.
349
350 This calls the subclass's `_put_meta' method, which may assume that the
351 name is valid and the database is open for writing.
352 """
353 me._check_meta_name(name)
354 me._check_write()
355 me._put_meta(name, value)
356
357 def del_meta(me, name):
358 """
359 Forget about the metadata item with the given NAME.
360
361 This calls the subclass's `_del_meta' method, which may assume that the
362 name is valid and the database is open for writing.
363 """
364 me._check_meta_name(name)
365 me._check_write()
366 me._del_meta(name)
367
368 def iter_meta(me):
369 """
370 Return an iterator over the name/value metadata items.
371
372 This calls the subclass's `_iter_meta' method, which may assume that the
373 database is open.
374 """
375 me._check_live()
376 return me._iter_meta()
377
378 def get_passwd(me, label):
379 """
380 Fetch and return the payload stored with the (opaque, binary) LABEL.
381
382 If there is no such payload then raise `KeyError'.
383
384 This calls the subclass's `_get_passwd' method, which may assume that the
385 database is open.
386 """
387 me._check_live()
388 return me._get_passwd(label)
389
390 def put_passwd(me, label, payload):
391 """
392 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
393
394 Any previous payload for LABEL is forgotten.
395
396 This calls the subclass's `_put_passwd' method, which may assume that the
397 database is open for writing.
398 """
399 me._check_write()
400 me._put_passwd(label, payload)
401
402 def del_passwd(me, label):
403 """
404 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
405
406 If there is no such payload then raise `KeyError'.
407
408 This calls the subclass's `_del_passwd' method, which may assume that the
409 database is open for writing.
410 """
411 me._check_write()
412 me._del_passwd(label, payload)
413
414 def iter_passwds(me):
415 """
416 Return an iterator over the stored password label/payload pairs.
417
418 This calls the subclass's `_iter_passwds' method, which may assume that
419 the database is open.
420 """
421 me._check_live()
422 return me._iter_passwds()
423
424 class GDBMStorageBackend (StorageBackend):
425 """
426 My instances store password data in a GDBM database.
427
428 Metadata and password entries are mixed into the same database. The key
429 for a metadata item is simply its name; the key for a password entry is
430 the entry's label prefixed by `$', since we're guaranteed that no
431 metadata item name begins with `$'.
432 """
433
434 NAME = 'gdbm'
435
436 def _open(me, file, writep):
437 try: me._db = _G.open(file, writep and 'w' or 'r')
438 except _G.error, e: raise StorageBackendRefusal, e
439
440 def _create(me, file):
441 me._db = _G.open(file, 'n', 0600)
442
443 def _close(me):
444 me._db.close()
445 me._db = None
446
447 def _get_meta(me, name, default):
448 try: return me._db[name]
449 except KeyError: return default
450
451 def _put_meta(me, name, value):
452 me._db[name] = value
453
454 def _del_meta(me, name):
455 del me._db[name]
456
457 def _iter_meta(me):
458 k = me._db.firstkey()
459 while k is not None:
460 if not k.startswith('$'): yield k, me._db[k]
461 k = me._db.nextkey(k)
462
463 def _get_passwd(me, label):
464 return me._db['$' + label]
465
466 def _put_passwd(me, label, payload):
467 me._db['$' + label] = payload
468
469 def _del_passwd(me, label):
470 del me._db['$' + label]
471
472 def _iter_passwds(me):
473 k = me._db.firstkey()
474 while k is not None:
475 if k.startswith('$'): yield k[1:], me._db[k]
476 k = me._db.nextkey(k)
477
478 ###--------------------------------------------------------------------------
479 ### Password storage.
480
481 class PW (object):
482 """
483 I represent a secure (ish) password store.
484
485 I can store short secrets, associated with textual names, in a way which
486 doesn't leak too much information about them.
487
488 I implement (some of) the Python mapping protocol.
489
490 I keep track of everything using a StorageBackend object. This contains
491 password entries, identified by cryptographic labels, and a number of
492 metadata items.
493
494 cipher Names the Catacomb cipher selected.
495
496 hash Names the Catacomb hash function selected.
497
498 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
499 length and concatenated, encrypted using the master
500 passphrase.
501
502 mac Names the Catacomb message authentication code selected.
503
504 magic A magic string for obscuring password tag names.
505
506 salt The salt for hashing the passphrase.
507
508 tag The master passphrase's tag, for the Pixie's benefit.
509
510 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
511 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
512 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
513 encrypted using the stored keys.
514 """
515
516 def __init__(me, file, writep = False):
517 """
518 Initialize a PW object from the database in FILE.
519
520 If WRITEP is false (the default) then the database is opened read-only;
521 if true then it may be written. Requests the database password from the
522 Pixie, which may cause interaction.
523 """
524
525 ## Open the database.
526 me.db = StorageBackend.open(file, writep)
527
528 ## Find out what crypto to use.
529 c = _C.gcciphers[me.db.get_meta('cipher')]
530 h = _C.gchashes[me.db.get_meta('hash')]
531 m = _C.gcmacs[me.db.get_meta('mac')]
532
533 ## Request the passphrase and extract the master keys.
534 tag = me.db.get_meta('tag')
535 ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt'))
536 try:
537 b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
538 except DecryptError:
539 _C.ppcancel(tag)
540 raise
541 me.ck = b.getblk16()
542 me.mk = b.getblk16()
543 if not b.endp: raise ValueError, 'trailing junk'
544
545 ## Set the key, and stash it and the tag-hashing secret.
546 me.k = Crypto(c, h, m, me.ck, me.mk)
547 me.magic = me.k.decrypt(me.db.get_meta('magic'))
548
549 @classmethod
550 def create(cls, dbcls, file, tag, c, h, m):
551 """
552 Create and initialize a new database FILE using StorageBackend DBCLS.
553
554 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
555 and a Pixie passphrase TAG.
556
557 This doesn't return a working object: it just creates the database file
558 and gets out of the way.
559 """
560
561 ## Set up the cryptography.
562 pp = _C.ppread(tag, _C.PMODE_VERIFY)
563 ppk = PPK(pp, c, h, m)
564 ck = _C.rand.block(c.keysz.default)
565 mk = _C.rand.block(c.keysz.default)
566 k = Crypto(c, h, m, ck, mk)
567
568 ## Set up and initialize the database.
569 kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
570 with dbcls.create(file) as db:
571 db.put_meta('tag', tag)
572 db.put_meta('salt', ppk.salt)
573 db.put_meta('cipher', c.name)
574 db.put_meta('hash', h.name)
575 db.put_meta('mac', m.name)
576 db.put_meta('key', kct)
577 db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz)))
578
579 def keyxform(me, key):
580 """Transform the KEY (actually a password tag) into a password label."""
581 return me.k.h().hash(me.magic).hash(key).done()
582
583 def changepp(me):
584 """
585 Change the database password.
586
587 Requests the new password from the Pixie, which will probably cause
588 interaction.
589 """
590 tag = me.db.get_meta('tag')
591 _C.ppcancel(tag)
592 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
593 me.k.c.__class__, me.k.h, me.k.m.__class__)
594 kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
595 me.db.put_meta('key', kct)
596 me.db.put_meta('salt', ppk.salt)
597
598 def pack(me, key, value):
599 """Pack the KEY and VALUE into a ciphertext, and return it."""
600 b = _C.WriteBuffer()
601 b.putblk16(key).putblk16(value)
602 b.zero(((b.size + 255) & ~255) - b.size)
603 return me.k.encrypt(b)
604
605 def unpack(me, ct):
606 """
607 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
608
609 Might raise DecryptError, of course.
610 """
611 b = _C.ReadBuffer(me.k.decrypt(ct))
612 key = b.getblk16()
613 value = b.getblk16()
614 return key, value
615
616 ## Mapping protocol.
617
618 def __getitem__(me, key):
619 """Return the password for the given KEY."""
620 try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1]
621 except KeyError: raise KeyError, key
622
623 def __setitem__(me, key, value):
624 """Associate the password VALUE with the KEY."""
625 me.db.put_passwd(me.keyxform(key), me.pack(key, value))
626
627 def __delitem__(me, key):
628 """Forget all about the KEY."""
629 try: me.db.del_passwd(me.keyxform(key))
630 except KeyError: raise KeyError, key
631
632 def __iter__(me):
633 """Iterate over the known password tags."""
634 for _, pld in me.db.iter_passwds():
635 yield me.unpack(pld)[0]
636
637 ## Context protocol.
638
639 def __enter__(me):
640 return me
641 def __exit__(me, excty, excval, exctb):
642 me.db.close()
643
644 ###----- That's all, folks --------------------------------------------------