chiark / gitweb /
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 --------------------------------------------------