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