chiark / gitweb /
catacomb/pwsafe.py, pwsafe: Make GDBM support conditional.
[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 errno as _E
32 import os as _OS
33 from cStringIO import StringIO as _StringIO
34
35 import catacomb as _C
36
37 ###--------------------------------------------------------------------------
38 ### Text encoding utilities.
39
40 def _literalp(s):
41   """
42   Answer whether S can be represented literally.
43
44   If True, then S can be stored literally, as a metadata item name or
45   value; if False, then S requires some kind of encoding.
46   """
47   return all(ch.isalnum() or ch in '-_:' for ch in s)
48
49 def _enc_metaname(name):
50   """Encode NAME as a metadata item name, returning the result."""
51   if _literalp(name):
52     return name
53   else:
54     sio = _StringIO()
55     sio.write('!')
56     for ch in name:
57       if _literalp(ch): sio.write(ch)
58       elif ch == ' ': sio.write('+')
59       else: sio.write('%%%02x' % ord(ch))
60     return sio.getvalue()
61
62 def _dec_metaname(name):
63   """Decode NAME as a metadata item name, returning the result."""
64   if not name.startswith('!'):
65     return name
66   else:
67     sio = _StringIO()
68     i, n = 1, len(name)
69     while i < n:
70       ch = name[i]
71       i += 1
72       if ch == '+':
73         sio.write(' ')
74       elif ch == '%':
75         sio.write(chr(int(name[i:i + 2], 16)))
76         i += 2
77       else:
78         sio.write(ch)
79     return sio.getvalue()
80
81 def _b64(s):
82   """Encode S as base64, without newlines, and trimming `=' padding."""
83   return s.encode('base64').translate(None, '\n=')
84 def _unb64(s):
85   """Decode S as base64 with trimmed `=' padding."""
86   return (s + '='*((4 - len(s))%4)).decode('base64')
87
88 def _enc_metaval(val):
89   """Encode VAL as a metadata item value, returning the result."""
90   if _literalp(val): return val
91   else: return '?' + _b64(val)
92
93 def _dec_metaval(val):
94   """Decode VAL as a metadata item value, returning the result."""
95   if not val.startswith('?'): return val
96   else: return _unb64(val[1:])
97
98 ###--------------------------------------------------------------------------
99 ### Underlying cryptography.
100
101 class DecryptError (Exception):
102   """
103   I represent a failure to decrypt a message.
104
105   Usually this means that someone used the wrong key, though it can also
106   mean that a ciphertext has been modified.
107   """
108   pass
109
110 class Crypto (object):
111   """
112   I represent a symmetric crypto transform.
113
114   There's currently only one transform implemented, which is the obvious
115   generic-composition construction: given a message m, and keys K0 and K1, we
116   choose an IV v, and compute:
117
118     * y = v || E(K0, v; m)
119     * t = M(K1; y)
120
121   The final ciphertext is t || y.
122   """
123
124   def __init__(me, c, h, m, ck, mk):
125     """
126     Initialize the Crypto object with a given algorithm selection and keys.
127
128     We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
129     keys CK and MK for C and M respectively.
130     """
131     me.c = c(ck)
132     me.m = m(mk)
133     me.h = h
134
135   def encrypt(me, pt):
136     """
137     Encrypt the message PT and return the resulting ciphertext.
138     """
139     blksz = me.c.__class__.blksz
140     b = _C.WriteBuffer()
141     if blksz:
142       iv = _C.rand.block(blksz)
143       me.c.setiv(iv)
144       b.put(iv)
145     b.put(me.c.encrypt(pt))
146     t = me.m().hash(b).done()
147     return t + str(buffer(b))
148
149   def decrypt(me, ct):
150     """
151     Decrypt the ciphertext CT, returning the plaintext.
152
153     Raises DecryptError if anything goes wrong.
154     """
155     blksz = me.c.__class__.blksz
156     tagsz = me.m.__class__.tagsz
157     b = _C.ReadBuffer(ct)
158     t = b.get(tagsz)
159     h = me.m()
160     if blksz:
161       iv = b.get(blksz)
162       me.c.setiv(iv)
163       h.hash(iv)
164     x = b.get(b.left)
165     h.hash(x)
166     if t != h.done(): raise DecryptError
167     return me.c.decrypt(x)
168
169 class PPK (Crypto):
170   """
171   I represent a crypto transform whose keys are derived from a passphrase.
172
173   The password is salted and hashed; the salt is available as the `salt'
174   attribute.
175   """
176
177   def __init__(me, pp, c, h, m, salt = None):
178     """
179     Initialize the PPK object with a passphrase and algorithm selection.
180
181     We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
182     subclass M, and a SALT.  The SALT may be None, if we're generating new
183     keys, indicating that a salt should be chosen randomly.
184     """
185     if not salt: salt = _C.rand.block(h.hashsz)
186     tag = '%s\0%s' % (pp, salt)
187     Crypto.__init__(me, c, h, m,
188                     h().hash('cipher:' + tag).done(),
189                     h().hash('mac:' + tag).done())
190     me.salt = salt
191
192 ###--------------------------------------------------------------------------
193 ### Backend storage.
194
195 class StorageBackendRefusal (Exception):
196   """
197   I signify that a StorageBackend subclass has refused to open a file.
198
199   This is used by the StorageBackend.open class method.
200   """
201   pass
202
203 class StorageBackendClass (type):
204   """
205   I am a metaclass for StorageBackend classes.
206
207   My main feature is that I register my concrete instances (with a `NAME'
208   which is not `None') with the StorageBackend class.
209   """
210   def __init__(me, name, supers, dict):
211     """
212     Register a new concrete StorageBackend subclass.
213     """
214     super(StorageBackendClass, me).__init__(name, supers, dict)
215     if me.NAME is not None: StorageBackend.register_concrete_subclass(me)
216
217 class StorageBackend (object):
218   """
219   I provide basic protocol for password storage backends.
220
221   I'm an abstract class: you want one of my subclasses if you actually want
222   to do something useful.  But I maintain a list of my subclasses and can
223   choose an appropriate one to open a database file you've found lying about.
224
225   Backends are responsible for storing and retrieving stuff, but not for the
226   cryptographic details.  Backends need to store two kinds of information:
227
228     * metadata, consisting of a number of property names and their values;
229       and
230
231     * password mappings, consisting of a number of binary labels and
232       payloads.
233
234   Backends need to implement the following ordinary methods.  See the calling
235   methods for details of the subclass responsibilities.
236
237   BE._create(FILE)      Create a new database in FILE; used by `create'.
238
239   BE._open(FILE, WRITEP)
240                         Open the existing database FILE; used by `open'.
241
242   BE._close(ABRUPTP)    Close the database, freeing up any resources.  If
243                         ABRUPTP then don't try to commit changes.
244
245   BE._get_meta(NAME, DEFAULT)
246                         Return the value of the metadata item with the given
247                         NAME, or DEFAULT if it doesn't exist; used by
248                         `get_meta'.
249
250   BE._put_meta(NAME, VALUE)
251                         Set the VALUE of the metadata item with the given
252                         NAME, creating one if necessary; used by `put_meta'.
253
254   BE._del_meta(NAME)    Forget the metadata item with the given NAME; raise
255                         `KeyError' if there is no such item; used by
256                         `del_meta'.
257
258   BE._iter_meta()       Return an iterator over the metadata (NAME, VALUE)
259                         pairs; used by `iter_meta'.
260
261   BE._get_passwd(LABEL)
262                         Return the password payload stored with the (binary)
263                         LABEL; used by `get_passwd'.
264
265   BE._put_passwd(LABEL, PAYLOAD)
266                         Associate the (binary) PAYLOAD with the LABEL,
267                         forgetting any previous payload for that LABEL; used
268                         by `put_passwd'.
269
270   BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
271                         by `_del_passwd'.
272
273   BE._iter_passwds()    Return an iterator over the password (LABEL, PAYLOAD)
274                         pairs; used by `iter_passwds'.
275
276   Also, concrete subclasses should define the following class attributes.
277
278   NAME                  The name of the backend, so that the user can select
279                         it when creating a new database.
280
281   PRIO                  An integer priority: backends are tried in decreasing
282                         priority order when opening an existing database.
283   """
284
285   __metaclass__ = StorageBackendClass
286   NAME = None
287   PRIO = 10
288
289   ## The registry of subclasses.
290   CLASSES = {}
291
292   FAIL = ['FAIL']
293
294   @staticmethod
295   def register_concrete_subclass(sub):
296     """Register a concrete subclass, so that `open' can try it."""
297     StorageBackend.CLASSES[sub.NAME] = sub
298
299   @staticmethod
300   def byname(name):
301     """
302     Return the concrete subclass with the given NAME.
303
304     Raise `KeyError' if the name isn't found.
305     """
306     return StorageBackend.CLASSES[name]
307
308   @staticmethod
309   def classes():
310     """Return an iterator over the concrete subclasses."""
311     return StorageBackend.CLASSES.itervalues()
312
313   @staticmethod
314   def open(file, writep = False):
315     """Open a database FILE, using some appropriate backend."""
316     _OS.stat(file)
317     for cls in sorted(StorageBackend.CLASSES.values(), reverse = True,
318                       key = lambda cls: cls.PRIO):
319       try: return cls(file, writep)
320       except StorageBackendRefusal: pass
321     raise StorageBackendRefusal
322
323   @classmethod
324   def create(cls, file):
325     """
326     Create a new database in the named FILE, using this backend.
327
328     Subclasses must implement the `_create' instance method.
329     """
330     return cls(writep = True, _magic = lambda me: me._create(file))
331
332   def __init__(me, file = None, writep = False, _magic = None, *args, **kw):
333     """
334     Main constructor.
335
336     Subclasses are not, in general, expected to override this: there's a
337     somewhat hairy protocol between the constructor and some of the class
338     methods.  Instead, the main hook for customization is the subclass's
339     `_open' method, which is invoked in the usual case.
340     """
341     super(StorageBackend, me).__init__(*args, **kw)
342     if me.NAME is None: raise ValueError, 'abstract class'
343     if _magic is not None: _magic(me)
344     elif file is None: raise ValueError, 'missing file parameter'
345     else: me._open(file, writep)
346     me._writep = writep
347     me._livep = True
348
349   def close(me, abruptp = False):
350     """
351     Close the database.
352
353     It is harmless to attempt to close a database which has been closed
354     already.  Calls the subclass's `_close' method.
355     """
356     if me._livep:
357       me._livep = False
358       me._close(abruptp)
359
360   ## Utilities.
361
362   def _check_live(me):
363     """Raise an error if the receiver has been closed."""
364     if not me._livep: raise ValueError, 'database is closed'
365
366   def _check_write(me):
367     """Raise an error if the receiver is not open for writing."""
368     me._check_live()
369     if not me._writep: raise ValueError, 'database is read-only'
370
371   def _check_meta_name(me, name):
372     """
373     Raise an error unless NAME is a valid name for a metadata item.
374
375     Metadata names may not start with `$': such names are reserved for
376     password storage.
377     """
378     if name.startswith('$'):
379       raise ValueError, "invalid metadata key `%s'" % name
380
381   ## Context protocol.
382
383   def __enter__(me):
384     """Context protocol: make sure the database is closed on exit."""
385     return me
386   def __exit__(me, exctype, excvalue, exctb):
387     """Context protocol: see `__enter__'."""
388     me.close(excvalue is not None)
389
390   ## Metadata.
391
392   def get_meta(me, name, default = FAIL):
393     """
394     Fetch the value for the metadata item NAME.
395
396     If no such item exists, then return DEFAULT if that was set; otherwise
397     raise a `KeyError'.
398
399     This calls the subclass's `_get_meta' method, which should return the
400     requested item or return the given DEFAULT value.  It may assume that the
401     name is valid and the database is open.
402     """
403     me._check_meta_name(name)
404     me._check_live()
405     value = me._get_meta(name, default)
406     if value is StorageBackend.FAIL: raise KeyError, name
407     return value
408
409   def put_meta(me, name, value):
410     """
411     Store VALUE in the metadata item called NAME.
412
413     This calls the subclass's `_put_meta' method, which may assume that the
414     name is valid and the database is open for writing.
415     """
416     me._check_meta_name(name)
417     me._check_write()
418     me._put_meta(name, value)
419
420   def del_meta(me, name):
421     """
422     Forget about the metadata item with the given NAME.
423
424     This calls the subclass's `_del_meta' method, which may assume that the
425     name is valid and the database is open for writing.
426     """
427     me._check_meta_name(name)
428     me._check_write()
429     me._del_meta(name)
430
431   def iter_meta(me):
432     """
433     Return an iterator over the name/value metadata items.
434
435     This calls the subclass's `_iter_meta' method, which may assume that the
436     database is open.
437     """
438     me._check_live()
439     return me._iter_meta()
440
441   def get_passwd(me, label):
442     """
443     Fetch and return the payload stored with the (opaque, binary) LABEL.
444
445     If there is no such payload then raise `KeyError'.
446
447     This calls the subclass's `_get_passwd' method, which may assume that the
448     database is open.
449     """
450     me._check_live()
451     return me._get_passwd(label)
452
453   def put_passwd(me, label, payload):
454     """
455     Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
456
457     Any previous payload for LABEL is forgotten.
458
459     This calls the subclass's `_put_passwd' method, which may assume that the
460     database is open for writing.
461     """
462     me._check_write()
463     me._put_passwd(label, payload)
464
465   def del_passwd(me, label):
466     """
467     Forget any PAYLOAD associated with the (opaque, binary) LABEL.
468
469     If there is no such payload then raise `KeyError'.
470
471     This calls the subclass's `_del_passwd' method, which may assume that the
472     database is open for writing.
473     """
474     me._check_write()
475     me._del_passwd(label, payload)
476
477   def iter_passwds(me):
478     """
479     Return an iterator over the stored password label/payload pairs.
480
481     This calls the subclass's `_iter_passwds' method, which may assume that
482     the database is open.
483     """
484     me._check_live()
485     return me._iter_passwds()
486
487 try: import gdbm as _G
488 except ImportError: pass
489 else:
490   class GDBMStorageBackend (StorageBackend):
491     """
492     My instances store password data in a GDBM database.
493
494     Metadata and password entries are mixed into the same database.  The key
495     for a metadata item is simply its name; the key for a password entry is
496     the entry's label prefixed by `$', since we're guaranteed that no
497     metadata item name begins with `$'.
498     """
499
500     NAME = 'gdbm'
501
502     def _open(me, file, writep):
503       try: me._db = _G.open(file, writep and 'w' or 'r')
504       except _G.error, e: raise StorageBackendRefusal, e
505
506     def _create(me, file):
507       me._db = _G.open(file, 'n', 0600)
508
509     def _close(me, abruptp):
510       me._db.close()
511       me._db = None
512
513     def _get_meta(me, name, default):
514       try: return me._db[name]
515       except KeyError: return default
516
517     def _put_meta(me, name, value):
518       me._db[name] = value
519
520     def _del_meta(me, name):
521       del me._db[name]
522
523     def _iter_meta(me):
524       k = me._db.firstkey()
525       while k is not None:
526         if not k.startswith('$'): yield k, me._db[k]
527         k = me._db.nextkey(k)
528
529     def _get_passwd(me, label):
530       return me._db['$' + label]
531
532     def _put_passwd(me, label, payload):
533       me._db['$' + label] = payload
534
535     def _del_passwd(me, label):
536       del me._db['$' + label]
537
538     def _iter_passwds(me):
539       k = me._db.firstkey()
540       while k is not None:
541         if k.startswith('$'): yield k[1:], me._db[k]
542         k = me._db.nextkey(k)
543
544 class PlainTextBackend (StorageBackend):
545   """
546   I'm a utility base class for storage backends which use plain text files.
547
548   I provide subclasses with the following capabilities.
549
550     * Creating files, with given modes, optionally ensuring that the file
551       doesn't exist already.
552
553     * Parsing flat text files, checking leading magic, skipping comments, and
554       providing standard encodings of troublesome characters and binary
555       strings in metadata and password records.  See below.
556
557     * Maintenance of metadata and password records in in-memory dictionaries,
558       with ready implementations of the necessary StorageBackend subclass
559       responsibility methods.  (Subclasses can override these if they want to
560       make different arrangements.)
561
562   Metadata records are written with an optional prefix string chosen by the
563   caller, followed by a `NAME=VALUE' pair.  The NAME is form-urlencoded and
564   prefixed with `!' if it contains strange characters; the VALUE is base64-
565   encoded (without the pointless trailing `=' padding) and prefixed with `?'
566   if necessary.
567
568   Password records are written with an optional prefix string chosen by the
569   caller, followed by a LABEL=PAYLOAD pair, both of which are base64-encoded
570   (without padding).
571
572   The following attributes are available for subclasses:
573
574   _meta         Dictionary mapping metadata item names to their values.
575                 Populated by `_parse_meta' and managed by `_get_meta' and
576                 friends.
577
578   _pw           Dictionary mapping password labels to encrypted payloads.
579                 Populated by `_parse_passwd' and managed by `_get_passwd' and
580                 friends.
581
582   _dirtyp       Boolean: set if either of the dictionaries has been modified.
583   """
584
585   def __init__(me, *args, **kw):
586     """
587     Hook for initialization.
588
589     Sets up the published instance attributes.
590     """
591     me._meta = {}
592     me._pw = {}
593     me._dirtyp = False
594     super(PlainTextBackend, me).__init__(*args, **kw)
595
596   def _create_file(me, file, mode = 0600, freshp = False):
597     """
598     Make sure FILE exists, creating it with the given MODE if necessary.
599
600     If FRESHP is true, then make sure the file did not exist previously.
601     Return a file object for the newly created file.
602     """
603     flags = _OS.O_CREAT | _OS.O_WRONLY
604     if freshp: flags |= _OS.O_EXCL
605     else: flags |= _OS.O_TRUNC
606     fd = _OS.open(file, flags, mode)
607     return _OS.fdopen(fd, 'w')
608
609   def _mark_dirty(me):
610     """
611     Set the `_dirtyp' flag.
612
613     Subclasses might find it useful to intercept this method.
614     """
615     me._dirtyp = True
616
617   def _eqsplit(me, line):
618     """
619     Extract the KEY, VALUE pair from a LINE of the form `KEY=VALUE'.
620
621     Raise `ValueError' if there is no `=' in the LINE.
622     """
623     eq = line.index('=')
624     return line[:eq], line[eq + 1:]
625
626   def _parse_file(me, file, magic = None):
627     """
628     Parse a FILE.
629
630     Specifically:
631
632       * Raise `StorageBackendRefusal' if that the first line doesn't match
633         MAGIC (if provided).  MAGIC should not contain the terminating
634         newline.
635
636       * Ignore comments (beginning `#') and blank lines.
637
638       * Call `_parse_line' (provided by the subclass) for other lines.
639     """
640     with open(file, 'r') as f:
641       if magic is not None:
642         if f.readline().rstrip('\n') != magic: raise StorageBackendRefusal
643       for line in f:
644         line = line.rstrip('\n')
645         if not line or line.startswith('#'): continue
646         me._parse_line(line)
647
648   def _write_file(me, file, writebody, mode = 0600, magic = None):
649     """
650     Update FILE atomically.
651
652     The newly created file will have the given MODE.  If MAGIC is given, then
653     write that as the first line.  Calls WRITEBODY(F) to write the main body
654     of the file where F is a file object for the new file.
655     """
656     new = file + '.new'
657     with me._create_file(new, mode) as f:
658       if magic is not None: f.write(magic + '\n')
659       writebody(f)
660     _OS.rename(new, file)
661
662   def _parse_meta(me, line):
663     """Parse LINE as a metadata NAME=VALUE pair, and updates `_meta'."""
664     k, v = me._eqsplit(line)
665     me._meta[_dec_metaname(k)] = _dec_metaval(v)
666
667   def _write_meta(me, f, prefix = ''):
668     """Write the metadata records to F, each with the given PREFIX."""
669     f.write('\n## Metadata.\n')
670     for k, v in me._meta.iteritems():
671       f.write('%s%s=%s\n' % (prefix, _enc_metaname(k), _enc_metaval(v)))
672
673   def _get_meta(me, name, default):
674     return me._meta.get(name, default)
675   def _put_meta(me, name, value):
676     me._mark_dirty()
677     me._meta[name] = value
678   def _del_meta(me, name):
679     me._mark_dirty()
680     del me._meta[name]
681   def _iter_meta(me):
682     return me._meta.iteritems()
683
684   def _parse_passwd(me, line):
685     """Parse LINE as a password LABEL=PAYLOAD pair, and updates `_pw'."""
686     k, v = me._eqsplit(line)
687     me._pw[_unb64(k)] = _unb64(v)
688
689   def _write_passwd(me, f, prefix = ''):
690     """Write the password records to F, each with the given PREFIX."""
691     f.write('\n## Password data.\n')
692     for k, v in me._pw.iteritems():
693       f.write('%s%s=%s\n' % (prefix, _b64(k), _b64(v)))
694
695   def _get_passwd(me, label):
696     return me._pw[str(label)]
697   def _put_passwd(me, label, payload):
698     me._mark_dirty()
699     me._pw[str(label)] = payload
700   def _del_passwd(me, label):
701     me._mark_dirty()
702     del me._pw[str(label)]
703   def _iter_passwds(me):
704     return me._pw.iteritems()
705
706 class FlatFileStorageBackend (PlainTextBackend):
707   """
708   I maintain a password database in a plain text file.
709
710   The text file consists of lines, as follows.
711
712     * Empty lines, and lines beginning with `#' (in the leftmost column only)
713       are ignored.
714
715     * Lines of the form `$LABEL=PAYLOAD' store password data.  Both LABEL and
716       PAYLOAD are base64-encoded, without `=' padding.
717
718     * Lines of the form `NAME=VALUE' store metadata.  If the NAME contains
719       characters other than alphanumerics, hyphens, underscores, and colons,
720       then it is form-urlencoded, and prefixed wth `!'.  If the VALUE
721       contains such characters, then it is base64-encoded, without `='
722       padding, and prefixed with `?'.
723
724     * Other lines are erroneous.
725
726   The file is rewritten from scratch when it's changed: any existing
727   commentary is lost, and items may be reordered.  There is no file locking,
728   but the file is updated atomically, by renaming.
729
730   It is expected that the FlatFileStorageBackend is used mostly for
731   diagnostics and transfer, rather than for a live system.
732   """
733
734   NAME = 'flat'
735   PRIO = 0
736   MAGIC = '### pwsafe password database'
737
738   def _open(me, file, writep):
739     if not _OS.path.isfile(file): raise StorageBackendRefusal
740     me._parse_file(file, magic = me.MAGIC)
741   def _parse_line(me, line):
742     if line.startswith('$'): me._parse_passwd(line[1:])
743     else: me._parse_meta(line)
744
745   def _create(me, file):
746     with me._create_file(file, freshp = True) as f: pass
747     me._file = file
748     me._mark_dirty()
749
750   def _close(me, abruptp):
751     if not abruptp and me._dirtyp:
752       me._write_file(me._file, me._write_body, magic = me.MAGIC)
753
754   def _write_body(me, f):
755     me._write_meta(f)
756     me._write_passwd(f, '$')
757
758 class DirectoryStorageBackend (PlainTextBackend):
759   """
760   I maintain a password database in a directory, with one file per password.
761
762   This makes password databases easy to maintain in a revision-control system
763   such as Git.
764
765   The directory is structured as follows.
766
767   dir/meta      Contains metadata, similar to the `FlatFileBackend'.
768
769   dir/pw/LABEL  Contains the (raw binary) payload for the given password
770                 LABEL (base64-encoded, without the useless `=' padding, and
771                 with `/' replaced by `.').
772
773   dir/tmp/      Contains temporary files used by the implementation.
774   """
775
776   NAME = 'dir'
777   METAMAGIC = '### pwsafe password directory metadata'
778
779   def _open(me, file, writep):
780     if not _OS.path.isdir(file) or \
781           not _OS.path.isdir(_OS.path.join(file, 'pw')) or \
782           not _OS.path.isdir(_OS.path.join(file, 'tmp')) or \
783           not _OS.path.isfile(_OS.path.join(file, 'meta')):
784       raise StorageBackendRefusal
785     me._dir = file
786     me._parse_file(_OS.path.join(file, 'meta'), magic = me.METAMAGIC)
787   def _parse_line(me, line):
788     me._parse_meta(line)
789
790   def _create(me, file):
791     _OS.mkdir(file, 0700)
792     _OS.mkdir(_OS.path.join(file, 'pw'), 0700)
793     _OS.mkdir(_OS.path.join(file, 'tmp'), 0700)
794     me._mark_dirty()
795     me._dir = file
796
797   def _close(me, abruptp):
798     if not abruptp and me._dirtyp:
799       me._write_file(_OS.path.join(me._dir, 'meta'),
800                      me._write_meta, magic = me.METAMAGIC)
801
802   def _pwfile(me, label, dir = 'pw'):
803     return _OS.path.join(me._dir, dir, _b64(label).replace('/', '.'))
804   def _get_passwd(me, label):
805     try:
806       f = open(me._pwfile(label), 'rb')
807     except (OSError, IOError), e:
808       if e.errno == _E.ENOENT: raise KeyError, label
809       else: raise
810     with f: return f.read()
811   def _put_passwd(me, label, payload):
812     new = me._pwfile(label, 'tmp')
813     fd = _OS.open(new, _OS.O_WRONLY | _OS.O_CREAT | _OS.O_TRUNC, 0600)
814     _OS.close(fd)
815     with open(new, 'wb') as f: f.write(payload)
816     _OS.rename(new, me._pwfile(label))
817   def _del_passwd(me, label):
818     try:
819       _OS.remove(me._pwfile(label))
820     except (OSError, IOError), e:
821       if e == _E.ENOENT: raise KeyError, label
822       else: raise
823   def _iter_passwds(me):
824     pw = _OS.path.join(me._dir, 'pw')
825     for i in _OS.listdir(pw):
826       with open(_OS.path.join(pw, i), 'rb') as f: pld = f.read()
827       yield _unb64(i.replace('.', '/')), pld
828
829 ###--------------------------------------------------------------------------
830 ### Password storage.
831
832 class PW (object):
833   """
834   I represent a secure (ish) password store.
835
836   I can store short secrets, associated with textual names, in a way which
837   doesn't leak too much information about them.
838
839   I implement (some of) the Python mapping protocol.
840
841   I keep track of everything using a StorageBackend object.  This contains
842   password entries, identified by cryptographic labels, and a number of
843   metadata items.
844
845   cipher        Names the Catacomb cipher selected.
846
847   hash          Names the Catacomb hash function selected.
848
849   key           Cipher and MAC keys, each prefixed by a 16-bit big-endian
850                 length and concatenated, encrypted using the master
851                 passphrase.
852
853   mac           Names the Catacomb message authentication code selected.
854
855   magic         A magic string for obscuring password tag names.
856
857   salt          The salt for hashing the passphrase.
858
859   tag           The master passphrase's tag, for the Pixie's benefit.
860
861   Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
862   the corresponding value consists of a pair (TAG, PASSWD), prefixed with
863   16-bit lengths, concatenated, padded to a multiple of 256 octets, and
864   encrypted using the stored keys.
865   """
866
867   def __init__(me, file, writep = False):
868     """
869     Initialize a PW object from the database in FILE.
870
871     If WRITEP is false (the default) then the database is opened read-only;
872     if true then it may be written.  Requests the database password from the
873     Pixie, which may cause interaction.
874     """
875
876     ## Open the database.
877     me.db = StorageBackend.open(file, writep)
878
879     ## Find out what crypto to use.
880     c = _C.gcciphers[me.db.get_meta('cipher')]
881     h = _C.gchashes[me.db.get_meta('hash')]
882     m = _C.gcmacs[me.db.get_meta('mac')]
883
884     ## Request the passphrase and extract the master keys.
885     tag = me.db.get_meta('tag')
886     ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt'))
887     try:
888       b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
889     except DecryptError:
890       _C.ppcancel(tag)
891       raise
892     me.ck = b.getblk16()
893     me.mk = b.getblk16()
894     if not b.endp: raise ValueError, 'trailing junk'
895
896     ## Set the key, and stash it and the tag-hashing secret.
897     me.k = Crypto(c, h, m, me.ck, me.mk)
898     me.magic = me.k.decrypt(me.db.get_meta('magic'))
899
900   @classmethod
901   def create(cls, dbcls, file, tag, c, h, m):
902     """
903     Create and initialize a new database FILE using StorageBackend DBCLS.
904
905     We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
906     and a Pixie passphrase TAG.
907
908     This doesn't return a working object: it just creates the database file
909     and gets out of the way.
910     """
911
912     ## Set up the cryptography.
913     pp = _C.ppread(tag, _C.PMODE_VERIFY)
914     ppk = PPK(pp, c, h, m)
915     ck = _C.rand.block(c.keysz.default)
916     mk = _C.rand.block(c.keysz.default)
917     k = Crypto(c, h, m, ck, mk)
918
919     ## Set up and initialize the database.
920     kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
921     with dbcls.create(file) as db:
922       db.put_meta('tag', tag)
923       db.put_meta('salt', ppk.salt)
924       db.put_meta('cipher', c.name)
925       db.put_meta('hash', h.name)
926       db.put_meta('mac', m.name)
927       db.put_meta('key', kct)
928       db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz)))
929
930   def keyxform(me, key):
931     """Transform the KEY (actually a password tag) into a password label."""
932     return me.k.h().hash(me.magic).hash(key).done()
933
934   def changepp(me):
935     """
936     Change the database password.
937
938     Requests the new password from the Pixie, which will probably cause
939     interaction.
940     """
941     tag = me.db.get_meta('tag')
942     _C.ppcancel(tag)
943     ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
944               me.k.c.__class__, me.k.h, me.k.m.__class__)
945     kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
946     me.db.put_meta('key', kct)
947     me.db.put_meta('salt', ppk.salt)
948
949   def pack(me, key, value):
950     """Pack the KEY and VALUE into a ciphertext, and return it."""
951     b = _C.WriteBuffer()
952     b.putblk16(key).putblk16(value)
953     b.zero(((b.size + 255) & ~255) - b.size)
954     return me.k.encrypt(b)
955
956   def unpack(me, ct):
957     """
958     Unpack a ciphertext CT and return a (KEY, VALUE) pair.
959
960     Might raise DecryptError, of course.
961     """
962     b = _C.ReadBuffer(me.k.decrypt(ct))
963     key = b.getblk16()
964     value = b.getblk16()
965     return key, value
966
967   ## Mapping protocol.
968
969   def __getitem__(me, key):
970     """Return the password for the given KEY."""
971     try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1]
972     except KeyError: raise KeyError, key
973
974   def __setitem__(me, key, value):
975     """Associate the password VALUE with the KEY."""
976     me.db.put_passwd(me.keyxform(key), me.pack(key, value))
977
978   def __delitem__(me, key):
979     """Forget all about the KEY."""
980     try: me.db.del_passwd(me.keyxform(key))
981     except KeyError: raise KeyError, key
982
983   def __iter__(me):
984     """Iterate over the known password tags."""
985     for _, pld in me.db.iter_passwds():
986       yield me.unpack(pld)[0]
987
988   ## Context protocol.
989
990   def __enter__(me):
991     return me
992   def __exit__(me, excty, excval, exctb):
993     me.db.close(excval is not None)
994
995 ###----- That's all, folks --------------------------------------------------