chiark / gitweb /
8b68182e098b1df3231bfa03f435fb0829bea48e
[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(ABRUPTP)    Close the database, freeing up any resources.  If
181                         ABRUPTP then don't try to commit changes.
182
183   BE._get_meta(NAME, DEFAULT)
184                         Return the value of the metadata item with the given
185                         NAME, or DEFAULT if it doesn't exist; used by
186                         `get_meta'.
187
188   BE._put_meta(NAME, VALUE)
189                         Set the VALUE of the metadata item with the given
190                         NAME, creating one if necessary; used by `put_meta'.
191
192   BE._del_meta(NAME)    Forget the metadata item with the given NAME; raise
193                         `KeyError' if there is no such item; used by
194                         `del_meta'.
195
196   BE._iter_meta()       Return an iterator over the metadata (NAME, VALUE)
197                         pairs; used by `iter_meta'.
198
199   BE._get_passwd(LABEL)
200                         Return the password payload stored with the (binary)
201                         LABEL; used by `get_passwd'.
202
203   BE._put_passwd(LABEL, PAYLOAD)
204                         Associate the (binary) PAYLOAD with the LABEL,
205                         forgetting any previous payload for that LABEL; used
206                         by `put_passwd'.
207
208   BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
209                         by `_del_passwd'.
210
211   BE._iter_passwds()    Return an iterator over the password (LABEL, PAYLOAD)
212                         pairs; used by `iter_passwds'.
213
214   Also, concrete subclasses should define the following class attributes.
215
216   NAME                  The name of the backend, so that the user can select
217                         it when creating a new database.
218
219   PRIO                  An integer priority: backends are tried in decreasing
220                         priority order when opening an existing database.
221   """
222
223   __metaclass__ = StorageBackendClass
224   NAME = None
225   PRIO = 10
226
227   ## The registry of subclasses.
228   CLASSES = {}
229
230   FAIL = ['FAIL']
231
232   @staticmethod
233   def register_concrete_subclass(sub):
234     """Register a concrete subclass, so that `open' can try it."""
235     StorageBackend.CLASSES[sub.NAME] = sub
236
237   @staticmethod
238   def byname(name):
239     """
240     Return the concrete subclass with the given NAME.
241
242     Raise `KeyError' if the name isn't found.
243     """
244     return StorageBackend.CLASSES[name]
245
246   @staticmethod
247   def classes():
248     """Return an iterator over the concrete subclasses."""
249     return StorageBackend.CLASSES.itervalues()
250
251   @staticmethod
252   def open(file, writep = False):
253     """Open a database FILE, using some appropriate backend."""
254     _OS.stat(file)
255     for cls in sorted(StorageBackend.CLASSES.values(), reverse = True,
256                       key = lambda cls: cls.PRIO):
257       try: return cls(file, writep)
258       except StorageBackendRefusal: pass
259     raise StorageBackendRefusal
260
261   @classmethod
262   def create(cls, file):
263     """
264     Create a new database in the named FILE, using this backend.
265
266     Subclasses must implement the `_create' instance method.
267     """
268     return cls(writep = True, _magic = lambda me: me._create(file))
269
270   def __init__(me, file = None, writep = False, _magic = None, *args, **kw):
271     """
272     Main constructor.
273
274     Subclasses are not, in general, expected to override this: there's a
275     somewhat hairy protocol between the constructor and some of the class
276     methods.  Instead, the main hook for customization is the subclass's
277     `_open' method, which is invoked in the usual case.
278     """
279     super(StorageBackend, me).__init__(*args, **kw)
280     if me.NAME is None: raise ValueError, 'abstract class'
281     if _magic is not None: _magic(me)
282     elif file is None: raise ValueError, 'missing file parameter'
283     else: me._open(file, writep)
284     me._writep = writep
285     me._livep = True
286
287   def close(me, abruptp = False):
288     """
289     Close the database.
290
291     It is harmless to attempt to close a database which has been closed
292     already.  Calls the subclass's `_close' method.
293     """
294     if me._livep:
295       me._livep = False
296       me._close(abruptp)
297
298   ## Utilities.
299
300   def _check_live(me):
301     """Raise an error if the receiver has been closed."""
302     if not me._livep: raise ValueError, 'database is closed'
303
304   def _check_write(me):
305     """Raise an error if the receiver is not open for writing."""
306     me._check_live()
307     if not me._writep: raise ValueError, 'database is read-only'
308
309   def _check_meta_name(me, name):
310     """
311     Raise an error unless NAME is a valid name for a metadata item.
312
313     Metadata names may not start with `$': such names are reserved for
314     password storage.
315     """
316     if name.startswith('$'):
317       raise ValueError, "invalid metadata key `%s'" % name
318
319   ## Context protocol.
320
321   def __enter__(me):
322     """Context protocol: make sure the database is closed on exit."""
323     return me
324   def __exit__(me, exctype, excvalue, exctb):
325     """Context protocol: see `__enter__'."""
326     me.close(excvalue is not None)
327
328   ## Metadata.
329
330   def get_meta(me, name, default = FAIL):
331     """
332     Fetch the value for the metadata item NAME.
333
334     If no such item exists, then return DEFAULT if that was set; otherwise
335     raise a `KeyError'.
336
337     This calls the subclass's `_get_meta' method, which should return the
338     requested item or return the given DEFAULT value.  It may assume that the
339     name is valid and the database is open.
340     """
341     me._check_meta_name(name)
342     me._check_live()
343     value = me._get_meta(name, default)
344     if value is StorageBackend.FAIL: raise KeyError, name
345     return value
346
347   def put_meta(me, name, value):
348     """
349     Store VALUE in the metadata item called NAME.
350
351     This calls the subclass's `_put_meta' method, which may assume that the
352     name is valid and the database is open for writing.
353     """
354     me._check_meta_name(name)
355     me._check_write()
356     me._put_meta(name, value)
357
358   def del_meta(me, name):
359     """
360     Forget about the metadata item with the given NAME.
361
362     This calls the subclass's `_del_meta' method, which may assume that the
363     name is valid and the database is open for writing.
364     """
365     me._check_meta_name(name)
366     me._check_write()
367     me._del_meta(name)
368
369   def iter_meta(me):
370     """
371     Return an iterator over the name/value metadata items.
372
373     This calls the subclass's `_iter_meta' method, which may assume that the
374     database is open.
375     """
376     me._check_live()
377     return me._iter_meta()
378
379   def get_passwd(me, label):
380     """
381     Fetch and return the payload stored with the (opaque, binary) LABEL.
382
383     If there is no such payload then raise `KeyError'.
384
385     This calls the subclass's `_get_passwd' method, which may assume that the
386     database is open.
387     """
388     me._check_live()
389     return me._get_passwd(label)
390
391   def put_passwd(me, label, payload):
392     """
393     Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
394
395     Any previous payload for LABEL is forgotten.
396
397     This calls the subclass's `_put_passwd' method, which may assume that the
398     database is open for writing.
399     """
400     me._check_write()
401     me._put_passwd(label, payload)
402
403   def del_passwd(me, label):
404     """
405     Forget any PAYLOAD associated with the (opaque, binary) LABEL.
406
407     If there is no such payload then raise `KeyError'.
408
409     This calls the subclass's `_del_passwd' method, which may assume that the
410     database is open for writing.
411     """
412     me._check_write()
413     me._del_passwd(label, payload)
414
415   def iter_passwds(me):
416     """
417     Return an iterator over the stored password label/payload pairs.
418
419     This calls the subclass's `_iter_passwds' method, which may assume that
420     the database is open.
421     """
422     me._check_live()
423     return me._iter_passwds()
424
425 class GDBMStorageBackend (StorageBackend):
426   """
427   My instances store password data in a GDBM database.
428
429   Metadata and password entries are mixed into the same database.  The key
430   for a metadata item is simply its name; the key for a password entry is
431   the entry's label prefixed by `$', since we're guaranteed that no
432   metadata item name begins with `$'.
433   """
434
435   NAME = 'gdbm'
436
437   def _open(me, file, writep):
438     try: me._db = _G.open(file, writep and 'w' or 'r')
439     except _G.error, e: raise StorageBackendRefusal, e
440
441   def _create(me, file):
442     me._db = _G.open(file, 'n', 0600)
443
444   def _close(me, abruptp):
445     me._db.close()
446     me._db = None
447
448   def _get_meta(me, name, default):
449     try: return me._db[name]
450     except KeyError: return default
451
452   def _put_meta(me, name, value):
453     me._db[name] = value
454
455   def _del_meta(me, name):
456     del me._db[name]
457
458   def _iter_meta(me):
459     k = me._db.firstkey()
460     while k is not None:
461       if not k.startswith('$'): yield k, me._db[k]
462       k = me._db.nextkey(k)
463
464   def _get_passwd(me, label):
465     return me._db['$' + label]
466
467   def _put_passwd(me, label, payload):
468     me._db['$' + label] = payload
469
470   def _del_passwd(me, label):
471     del me._db['$' + label]
472
473   def _iter_passwds(me):
474     k = me._db.firstkey()
475     while k is not None:
476       if k.startswith('$'): yield k[1:], me._db[k]
477       k = me._db.nextkey(k)
478
479 ###--------------------------------------------------------------------------
480 ### Password storage.
481
482 class PW (object):
483   """
484   I represent a secure (ish) password store.
485
486   I can store short secrets, associated with textual names, in a way which
487   doesn't leak too much information about them.
488
489   I implement (some of) the Python mapping protocol.
490
491   I keep track of everything using a StorageBackend object.  This contains
492   password entries, identified by cryptographic labels, and a number of
493   metadata items.
494
495   cipher        Names the Catacomb cipher selected.
496
497   hash          Names the Catacomb hash function selected.
498
499   key           Cipher and MAC keys, each prefixed by a 16-bit big-endian
500                 length and concatenated, encrypted using the master
501                 passphrase.
502
503   mac           Names the Catacomb message authentication code selected.
504
505   magic         A magic string for obscuring password tag names.
506
507   salt          The salt for hashing the passphrase.
508
509   tag           The master passphrase's tag, for the Pixie's benefit.
510
511   Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
512   the corresponding value consists of a pair (TAG, PASSWD), prefixed with
513   16-bit lengths, concatenated, padded to a multiple of 256 octets, and
514   encrypted using the stored keys.
515   """
516
517   def __init__(me, file, writep = False):
518     """
519     Initialize a PW object from the database in FILE.
520
521     If WRITEP is false (the default) then the database is opened read-only;
522     if true then it may be written.  Requests the database password from the
523     Pixie, which may cause interaction.
524     """
525
526     ## Open the database.
527     me.db = StorageBackend.open(file, writep)
528
529     ## Find out what crypto to use.
530     c = _C.gcciphers[me.db.get_meta('cipher')]
531     h = _C.gchashes[me.db.get_meta('hash')]
532     m = _C.gcmacs[me.db.get_meta('mac')]
533
534     ## Request the passphrase and extract the master keys.
535     tag = me.db.get_meta('tag')
536     ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt'))
537     try:
538       b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
539     except DecryptError:
540       _C.ppcancel(tag)
541       raise
542     me.ck = b.getblk16()
543     me.mk = b.getblk16()
544     if not b.endp: raise ValueError, 'trailing junk'
545
546     ## Set the key, and stash it and the tag-hashing secret.
547     me.k = Crypto(c, h, m, me.ck, me.mk)
548     me.magic = me.k.decrypt(me.db.get_meta('magic'))
549
550   @classmethod
551   def create(cls, dbcls, file, tag, c, h, m):
552     """
553     Create and initialize a new database FILE using StorageBackend DBCLS.
554
555     We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
556     and a Pixie passphrase TAG.
557
558     This doesn't return a working object: it just creates the database file
559     and gets out of the way.
560     """
561
562     ## Set up the cryptography.
563     pp = _C.ppread(tag, _C.PMODE_VERIFY)
564     ppk = PPK(pp, c, h, m)
565     ck = _C.rand.block(c.keysz.default)
566     mk = _C.rand.block(c.keysz.default)
567     k = Crypto(c, h, m, ck, mk)
568
569     ## Set up and initialize the database.
570     kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
571     with dbcls.create(file) as db:
572       db.put_meta('tag', tag)
573       db.put_meta('salt', ppk.salt)
574       db.put_meta('cipher', c.name)
575       db.put_meta('hash', h.name)
576       db.put_meta('mac', m.name)
577       db.put_meta('key', kct)
578       db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz)))
579
580   def keyxform(me, key):
581     """Transform the KEY (actually a password tag) into a password label."""
582     return me.k.h().hash(me.magic).hash(key).done()
583
584   def changepp(me):
585     """
586     Change the database password.
587
588     Requests the new password from the Pixie, which will probably cause
589     interaction.
590     """
591     tag = me.db.get_meta('tag')
592     _C.ppcancel(tag)
593     ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
594               me.k.c.__class__, me.k.h, me.k.m.__class__)
595     kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
596     me.db.put_meta('key', kct)
597     me.db.put_meta('salt', ppk.salt)
598
599   def pack(me, key, value):
600     """Pack the KEY and VALUE into a ciphertext, and return it."""
601     b = _C.WriteBuffer()
602     b.putblk16(key).putblk16(value)
603     b.zero(((b.size + 255) & ~255) - b.size)
604     return me.k.encrypt(b)
605
606   def unpack(me, ct):
607     """
608     Unpack a ciphertext CT and return a (KEY, VALUE) pair.
609
610     Might raise DecryptError, of course.
611     """
612     b = _C.ReadBuffer(me.k.decrypt(ct))
613     key = b.getblk16()
614     value = b.getblk16()
615     return key, value
616
617   ## Mapping protocol.
618
619   def __getitem__(me, key):
620     """Return the password for the given KEY."""
621     try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1]
622     except KeyError: raise KeyError, key
623
624   def __setitem__(me, key, value):
625     """Associate the password VALUE with the KEY."""
626     me.db.put_passwd(me.keyxform(key), me.pack(key, value))
627
628   def __delitem__(me, key):
629     """Forget all about the KEY."""
630     try: me.db.del_passwd(me.keyxform(key))
631     except KeyError: raise KeyError, key
632
633   def __iter__(me):
634     """Iterate over the known password tags."""
635     for _, pld in me.db.iter_passwds():
636       yield me.unpack(pld)[0]
637
638   ## Context protocol.
639
640   def __enter__(me):
641     return me
642   def __exit__(me, excty, excval, exctb):
643     me.db.close(excval is not None)
644
645 ###----- That's all, folks --------------------------------------------------