chiark / gitweb /
Merge remote-tracking branch 'origin/HEAD'
[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)
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 try: import sqlite3 as _Q
545 except ImportError: pass
546 else:
547   class SQLiteStorageBackend (StorageBackend):
548     """
549     I represent a password database stored in SQLite.
550
551     Metadata and password items are stored in separate tables, so there's no
552     conflict.  Some additional metadata is stored in the `meta' table, with
553     names beginning with `$' so as not to conflict with clients:
554
555     $version            The schema version of the table.
556     """
557
558     NAME = 'sqlite'
559     VERSION = 0
560
561     def _open(me, file, writep):
562       try:
563         me._db = _Q.connect(file)
564         ver = me._query_scalar(
565           "SELECT value FROM meta WHERE name = '$version'",
566           "version check")
567       except (_Q.DatabaseError, _Q.OperationalError), e:
568         raise StorageBackendRefusal, e
569       if ver is None: raise ValueError, 'database broken (missing $version)'
570       elif ver < me.VERSION: me._upgrade(ver)
571       elif ver > me.VERSION:
572         raise ValueError, 'unknown database schema version (%d > %d)' % \
573             (ver, me.VERSION)
574
575     def _create(me, file):
576       fd = _OS.open(file, _OS.O_WRONLY | _OS.O_CREAT | _OS.O_EXCL, 0600)
577       _OS.close(fd)
578       try:
579         me._db = _Q.connect(file)
580         c = me._db.cursor()
581         c.execute("""
582           CREATE TABLE meta (
583                   name TEXT PRIMARY KEY NOT NULL,
584                   value BLOB NOT NULL);
585         """)
586         c.execute("""
587           CREATE TABLE passwd (
588                   label BLOB PRIMARY KEY NOT NULL,
589                   payload BLOB NOT NULL);
590         """)
591         c.execute("""
592           INSERT INTO meta (name, value) VALUES ('$version', ?);
593         """, [me.VERSION])
594       except:
595         try: _OS.unlink(file)
596         except OSError: pass
597         raise
598
599     def _upgrade(me, ver):
600       """Upgrade the database from schema version VER."""
601       assert False, 'how embarrassing'
602
603     def _close(me, abruptp):
604       if not abruptp: me._db.commit()
605       me._db.close()
606       me._db = None
607
608     def _fetch_scalar(me, c, what, default = None):
609       try: row = next(c)
610       except StopIteration: val = default
611       else: val, = row
612       try: row = next(c)
613       except StopIteration: pass
614       else: raise ValueError, 'multiple matching records for %s' % what
615       return val
616
617     def _query_scalar(me, query, what, default = None, args = []):
618       c = me._db.cursor()
619       c.execute(query, args)
620       return me._fetch_scalar(c, what, default)
621
622     def _get_meta(me, name, default):
623       v = me._query_scalar("SELECT value FROM meta WHERE name = ?",
624                            "metadata item `%s'" % name,
625                            default = default, args = [name])
626       if v is default: return v
627       else: return str(v)
628
629     def _put_meta(me, name, value):
630       c = me._db.cursor()
631       c.execute("INSERT OR REPLACE INTO meta (name, value) VALUES (?, ?)",
632                 [name, buffer(value)])
633
634     def _del_meta(me, name):
635       c = me._db.cursor()
636       c.execute("DELETE FROM meta WHERE name = ?", [name])
637       if not c.rowcount: raise KeyError, name
638
639     def _iter_meta(me):
640       c = me._db.cursor()
641       c.execute("SELECT name, value FROM meta WHERE name NOT LIKE '$%'")
642       for k, v in c: yield k, str(v)
643
644     def _get_passwd(me, label):
645       pld = me._query_scalar("SELECT payload FROM passwd WHERE label = ?",
646                              "password", default = None,
647                              args = [buffer(label)])
648       if pld is None: raise KeyError, label
649       return str(pld)
650
651     def _put_passwd(me, label, payload):
652       c = me._db.cursor()
653       c.execute("INSERT OR REPLACE INTO passwd (label, payload) "
654                 "VALUES (?, ?)",
655                 [buffer(label), buffer(payload)])
656
657     def _del_passwd(me, label):
658       c = me._db.cursor()
659       c.execute("DELETE FROM passwd WHERE label = ?", [label])
660       if not c.rowcount: raise KeyError, label
661
662     def _iter_passwds(me):
663       c = me._db.cursor()
664       c.execute("SELECT label, payload FROM passwd")
665       for k, v in c: yield str(k), str(v)
666
667 class PlainTextBackend (StorageBackend):
668   """
669   I'm a utility base class for storage backends which use plain text files.
670
671   I provide subclasses with the following capabilities.
672
673     * Creating files, with given modes, optionally ensuring that the file
674       doesn't exist already.
675
676     * Parsing flat text files, checking leading magic, skipping comments, and
677       providing standard encodings of troublesome characters and binary
678       strings in metadata and password records.  See below.
679
680     * Maintenance of metadata and password records in in-memory dictionaries,
681       with ready implementations of the necessary StorageBackend subclass
682       responsibility methods.  (Subclasses can override these if they want to
683       make different arrangements.)
684
685   Metadata records are written with an optional prefix string chosen by the
686   caller, followed by a `NAME=VALUE' pair.  The NAME is form-urlencoded and
687   prefixed with `!' if it contains strange characters; the VALUE is base64-
688   encoded (without the pointless trailing `=' padding) and prefixed with `?'
689   if necessary.
690
691   Password records are written with an optional prefix string chosen by the
692   caller, followed by a LABEL=PAYLOAD pair, both of which are base64-encoded
693   (without padding).
694
695   The following attributes are available for subclasses:
696
697   _meta         Dictionary mapping metadata item names to their values.
698                 Populated by `_parse_meta' and managed by `_get_meta' and
699                 friends.
700
701   _pw           Dictionary mapping password labels to encrypted payloads.
702                 Populated by `_parse_passwd' and managed by `_get_passwd' and
703                 friends.
704
705   _dirtyp       Boolean: set if either of the dictionaries has been modified.
706   """
707
708   def __init__(me, *args, **kw):
709     """
710     Hook for initialization.
711
712     Sets up the published instance attributes.
713     """
714     me._meta = {}
715     me._pw = {}
716     me._dirtyp = False
717     super(PlainTextBackend, me).__init__(*args, **kw)
718
719   def _create_file(me, file, mode = 0600, freshp = False):
720     """
721     Make sure FILE exists, creating it with the given MODE if necessary.
722
723     If FRESHP is true, then make sure the file did not exist previously.
724     Return a file object for the newly created file.
725     """
726     flags = _OS.O_CREAT | _OS.O_WRONLY
727     if freshp: flags |= _OS.O_EXCL
728     else: flags |= _OS.O_TRUNC
729     fd = _OS.open(file, flags, mode)
730     return _OS.fdopen(fd, 'w')
731
732   def _mark_dirty(me):
733     """
734     Set the `_dirtyp' flag.
735
736     Subclasses might find it useful to intercept this method.
737     """
738     me._dirtyp = True
739
740   def _eqsplit(me, line):
741     """
742     Extract the KEY, VALUE pair from a LINE of the form `KEY=VALUE'.
743
744     Raise `ValueError' if there is no `=' in the LINE.
745     """
746     eq = line.index('=')
747     return line[:eq], line[eq + 1:]
748
749   def _parse_file(me, file, magic = None):
750     """
751     Parse a FILE.
752
753     Specifically:
754
755       * Raise `StorageBackendRefusal' if that the first line doesn't match
756         MAGIC (if provided).  MAGIC should not contain the terminating
757         newline.
758
759       * Ignore comments (beginning `#') and blank lines.
760
761       * Call `_parse_line' (provided by the subclass) for other lines.
762     """
763     with open(file, 'r') as f:
764       if magic is not None:
765         if f.readline().rstrip('\n') != magic: raise StorageBackendRefusal
766       for line in f:
767         line = line.rstrip('\n')
768         if not line or line.startswith('#'): continue
769         me._parse_line(line)
770
771   def _write_file(me, file, writebody, mode = 0600, magic = None):
772     """
773     Update FILE atomically.
774
775     The newly created file will have the given MODE.  If MAGIC is given, then
776     write that as the first line.  Calls WRITEBODY(F) to write the main body
777     of the file where F is a file object for the new file.
778     """
779     new = file + '.new'
780     with me._create_file(new, mode) as f:
781       if magic is not None: f.write(magic + '\n')
782       writebody(f)
783     _OS.rename(new, file)
784
785   def _parse_meta(me, line):
786     """Parse LINE as a metadata NAME=VALUE pair, and updates `_meta'."""
787     k, v = me._eqsplit(line)
788     me._meta[_dec_metaname(k)] = _dec_metaval(v)
789
790   def _write_meta(me, f, prefix = ''):
791     """Write the metadata records to F, each with the given PREFIX."""
792     f.write('\n## Metadata.\n')
793     for k, v in me._meta.iteritems():
794       f.write('%s%s=%s\n' % (prefix, _enc_metaname(k), _enc_metaval(v)))
795
796   def _get_meta(me, name, default):
797     return me._meta.get(name, default)
798   def _put_meta(me, name, value):
799     me._mark_dirty()
800     me._meta[name] = value
801   def _del_meta(me, name):
802     me._mark_dirty()
803     del me._meta[name]
804   def _iter_meta(me):
805     return me._meta.iteritems()
806
807   def _parse_passwd(me, line):
808     """Parse LINE as a password LABEL=PAYLOAD pair, and updates `_pw'."""
809     k, v = me._eqsplit(line)
810     me._pw[_unb64(k)] = _unb64(v)
811
812   def _write_passwd(me, f, prefix = ''):
813     """Write the password records to F, each with the given PREFIX."""
814     f.write('\n## Password data.\n')
815     for k, v in me._pw.iteritems():
816       f.write('%s%s=%s\n' % (prefix, _b64(k), _b64(v)))
817
818   def _get_passwd(me, label):
819     return me._pw[str(label)]
820   def _put_passwd(me, label, payload):
821     me._mark_dirty()
822     me._pw[str(label)] = payload
823   def _del_passwd(me, label):
824     me._mark_dirty()
825     del me._pw[str(label)]
826   def _iter_passwds(me):
827     return me._pw.iteritems()
828
829 class FlatFileStorageBackend (PlainTextBackend):
830   """
831   I maintain a password database in a plain text file.
832
833   The text file consists of lines, as follows.
834
835     * Empty lines, and lines beginning with `#' (in the leftmost column only)
836       are ignored.
837
838     * Lines of the form `$LABEL=PAYLOAD' store password data.  Both LABEL and
839       PAYLOAD are base64-encoded, without `=' padding.
840
841     * Lines of the form `NAME=VALUE' store metadata.  If the NAME contains
842       characters other than alphanumerics, hyphens, underscores, and colons,
843       then it is form-urlencoded, and prefixed wth `!'.  If the VALUE
844       contains such characters, then it is base64-encoded, without `='
845       padding, and prefixed with `?'.
846
847     * Other lines are erroneous.
848
849   The file is rewritten from scratch when it's changed: any existing
850   commentary is lost, and items may be reordered.  There is no file locking,
851   but the file is updated atomically, by renaming.
852
853   It is expected that the FlatFileStorageBackend is used mostly for
854   diagnostics and transfer, rather than for a live system.
855   """
856
857   NAME = 'flat'
858   PRIO = 0
859   MAGIC = '### pwsafe password database'
860
861   def _open(me, file, writep):
862     if not _OS.path.isfile(file): raise StorageBackendRefusal
863     me._parse_file(file, magic = me.MAGIC)
864   def _parse_line(me, line):
865     if line.startswith('$'): me._parse_passwd(line[1:])
866     else: me._parse_meta(line)
867
868   def _create(me, file):
869     with me._create_file(file, freshp = True) as f: pass
870     me._file = file
871     me._mark_dirty()
872
873   def _close(me, abruptp):
874     if not abruptp and me._dirtyp:
875       me._write_file(me._file, me._write_body, magic = me.MAGIC)
876
877   def _write_body(me, f):
878     me._write_meta(f)
879     me._write_passwd(f, '$')
880
881 class DirectoryStorageBackend (PlainTextBackend):
882   """
883   I maintain a password database in a directory, with one file per password.
884
885   This makes password databases easy to maintain in a revision-control system
886   such as Git.
887
888   The directory is structured as follows.
889
890   dir/meta      Contains metadata, similar to the `FlatFileBackend'.
891
892   dir/pw/LABEL  Contains the (raw binary) payload for the given password
893                 LABEL (base64-encoded, without the useless `=' padding, and
894                 with `/' replaced by `.').
895
896   dir/tmp/      Contains temporary files used by the implementation.
897   """
898
899   NAME = 'dir'
900   METAMAGIC = '### pwsafe password directory metadata'
901
902   def _open(me, file, writep):
903     if not _OS.path.isdir(file) or \
904           not _OS.path.isdir(_OS.path.join(file, 'pw')) or \
905           not _OS.path.isdir(_OS.path.join(file, 'tmp')) or \
906           not _OS.path.isfile(_OS.path.join(file, 'meta')):
907       raise StorageBackendRefusal
908     me._dir = file
909     me._parse_file(_OS.path.join(file, 'meta'), magic = me.METAMAGIC)
910   def _parse_line(me, line):
911     me._parse_meta(line)
912
913   def _create(me, file):
914     _OS.mkdir(file, 0700)
915     _OS.mkdir(_OS.path.join(file, 'pw'), 0700)
916     _OS.mkdir(_OS.path.join(file, 'tmp'), 0700)
917     me._mark_dirty()
918     me._dir = file
919
920   def _close(me, abruptp):
921     if not abruptp and me._dirtyp:
922       me._write_file(_OS.path.join(me._dir, 'meta'),
923                      me._write_meta, magic = me.METAMAGIC)
924
925   def _pwfile(me, label, dir = 'pw'):
926     return _OS.path.join(me._dir, dir, _b64(label).replace('/', '.'))
927   def _get_passwd(me, label):
928     try:
929       f = open(me._pwfile(label), 'rb')
930     except (OSError, IOError), e:
931       if e.errno == _E.ENOENT: raise KeyError, label
932       else: raise
933     with f: return f.read()
934   def _put_passwd(me, label, payload):
935     new = me._pwfile(label, 'tmp')
936     fd = _OS.open(new, _OS.O_WRONLY | _OS.O_CREAT | _OS.O_TRUNC, 0600)
937     _OS.close(fd)
938     with open(new, 'wb') as f: f.write(payload)
939     _OS.rename(new, me._pwfile(label))
940   def _del_passwd(me, label):
941     try:
942       _OS.remove(me._pwfile(label))
943     except (OSError, IOError), e:
944       if e == _E.ENOENT: raise KeyError, label
945       else: raise
946   def _iter_passwds(me):
947     pw = _OS.path.join(me._dir, 'pw')
948     for i in _OS.listdir(pw):
949       with open(_OS.path.join(pw, i), 'rb') as f: pld = f.read()
950       yield _unb64(i.replace('.', '/')), pld
951
952 ###--------------------------------------------------------------------------
953 ### Password storage.
954
955 class PW (object):
956   """
957   I represent a secure (ish) password store.
958
959   I can store short secrets, associated with textual names, in a way which
960   doesn't leak too much information about them.
961
962   I implement (some of) the Python mapping protocol.
963
964   I keep track of everything using a StorageBackend object.  This contains
965   password entries, identified by cryptographic labels, and a number of
966   metadata items.
967
968   cipher        Names the Catacomb cipher selected.
969
970   hash          Names the Catacomb hash function selected.
971
972   key           Cipher and MAC keys, each prefixed by a 16-bit big-endian
973                 length and concatenated, encrypted using the master
974                 passphrase.
975
976   mac           Names the Catacomb message authentication code selected.
977
978   magic         A magic string for obscuring password tag names.
979
980   salt          The salt for hashing the passphrase.
981
982   tag           The master passphrase's tag, for the Pixie's benefit.
983
984   Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
985   the corresponding value consists of a pair (TAG, PASSWD), prefixed with
986   16-bit lengths, concatenated, padded to a multiple of 256 octets, and
987   encrypted using the stored keys.
988   """
989
990   def __init__(me, file, writep = False):
991     """
992     Initialize a PW object from the database in FILE.
993
994     If WRITEP is false (the default) then the database is opened read-only;
995     if true then it may be written.  Requests the database password from the
996     Pixie, which may cause interaction.
997     """
998
999     ## Open the database.
1000     me.db = StorageBackend.open(file, writep)
1001
1002     ## Find out what crypto to use.
1003     c = _C.gcciphers[me.db.get_meta('cipher')]
1004     h = _C.gchashes[me.db.get_meta('hash')]
1005     m = _C.gcmacs[me.db.get_meta('mac')]
1006
1007     ## Request the passphrase and extract the master keys.
1008     tag = me.db.get_meta('tag')
1009     ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt'))
1010     try:
1011       b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
1012     except DecryptError:
1013       _C.ppcancel(tag)
1014       raise
1015     me.ck = b.getblk16()
1016     me.mk = b.getblk16()
1017     if not b.endp: raise ValueError, 'trailing junk'
1018
1019     ## Set the key, and stash it and the tag-hashing secret.
1020     me.k = Crypto(c, h, m, me.ck, me.mk)
1021     me.magic = me.k.decrypt(me.db.get_meta('magic'))
1022
1023   @classmethod
1024   def create(cls, dbcls, file, tag, c, h, m):
1025     """
1026     Create and initialize a new database FILE using StorageBackend DBCLS.
1027
1028     We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
1029     and a Pixie passphrase TAG.
1030
1031     This doesn't return a working object: it just creates the database file
1032     and gets out of the way.
1033     """
1034
1035     ## Set up the cryptography.
1036     pp = _C.ppread(tag, _C.PMODE_VERIFY)
1037     ppk = PPK(pp, c, h, m)
1038     ck = _C.rand.block(c.keysz.default)
1039     mk = _C.rand.block(c.keysz.default)
1040     k = Crypto(c, h, m, ck, mk)
1041
1042     ## Set up and initialize the database.
1043     kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
1044     with dbcls.create(file) as db:
1045       db.put_meta('tag', tag)
1046       db.put_meta('salt', ppk.salt)
1047       db.put_meta('cipher', c.name)
1048       db.put_meta('hash', h.name)
1049       db.put_meta('mac', m.name)
1050       db.put_meta('key', kct)
1051       db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz)))
1052
1053   def keyxform(me, key):
1054     """Transform the KEY (actually a password tag) into a password label."""
1055     return me.k.h().hash(me.magic).hash(key).done()
1056
1057   def changepp(me):
1058     """
1059     Change the database password.
1060
1061     Requests the new password from the Pixie, which will probably cause
1062     interaction.
1063     """
1064     tag = me.db.get_meta('tag')
1065     _C.ppcancel(tag)
1066     ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
1067               me.k.c.__class__, me.k.h, me.k.m.__class__)
1068     kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
1069     me.db.put_meta('key', kct)
1070     me.db.put_meta('salt', ppk.salt)
1071
1072   def pack(me, key, value):
1073     """Pack the KEY and VALUE into a ciphertext, and return it."""
1074     b = _C.WriteBuffer()
1075     b.putblk16(key).putblk16(value)
1076     b.zero(((b.size + 255) & ~255) - b.size)
1077     return me.k.encrypt(b)
1078
1079   def unpack(me, ct):
1080     """
1081     Unpack a ciphertext CT and return a (KEY, VALUE) pair.
1082
1083     Might raise DecryptError, of course.
1084     """
1085     b = _C.ReadBuffer(me.k.decrypt(ct))
1086     key = b.getblk16()
1087     value = b.getblk16()
1088     return key, value
1089
1090   ## Mapping protocol.
1091
1092   def __getitem__(me, key):
1093     """Return the password for the given KEY."""
1094     try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1]
1095     except KeyError: raise KeyError, key
1096
1097   def __setitem__(me, key, value):
1098     """Associate the password VALUE with the KEY."""
1099     me.db.put_passwd(me.keyxform(key), me.pack(key, value))
1100
1101   def __delitem__(me, key):
1102     """Forget all about the KEY."""
1103     try: me.db.del_passwd(me.keyxform(key))
1104     except KeyError: raise KeyError, key
1105
1106   def __iter__(me):
1107     """Iterate over the known password tags."""
1108     for _, pld in me.db.iter_passwds():
1109       yield me.unpack(pld)[0]
1110
1111   ## Context protocol.
1112
1113   def __enter__(me):
1114     return me
1115   def __exit__(me, excty, excval, exctb):
1116     me.db.close(excval is not None)
1117
1118 ###----- That's all, folks --------------------------------------------------