chiark / gitweb /
catacomb/pwsafe.py: Split out the GDBM-specifics from StorageBackend.
authorMark Wooding <mdw@distorted.org.uk>
Sun, 24 May 2015 17:59:55 +0000 (18:59 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Sun, 31 May 2015 21:42:53 +0000 (22:42 +0100)
For now, use the GDBM-based backend explicitly and unconditionally,
because there isn't another one anyway.

catacomb/pwsafe.py

index b728e57f8fc3eff511d8da0b951656155a4254d0..67b249e747bd11ef1304287501010a915ac1e329 100644 (file)
@@ -130,7 +130,10 @@ class PPK (Crypto):
 
 class StorageBackend (object):
   """
-  I provide a backend for password and metadata storage.
+  I provide basic protocol for password storage backends.
+
+  I'm an abstract class: you want one of my subclasses if you actually want
+  to do something useful.
 
   Backends are responsible for storing and retrieving stuff, but not for the
   cryptographic details.  Backends need to store two kinds of information:
@@ -140,6 +143,47 @@ class StorageBackend (object):
 
     * password mappings, consisting of a number of binary labels and
       payloads.
+
+  Backends need to implement the following ordinary methods.  See the calling
+  methods for details of the subclass responsibilities.
+
+  BE._create(FILE)      Create a new database in FILE; used by `create'.
+
+  BE._open(FILE, WRITEP)
+                        Open the existing database FILE; used by `open'.
+
+  BE._close()           Close the database, freeing up any resources.
+
+  BE._get_meta(NAME, DEFAULT)
+                        Return the value of the metadata item with the given
+                        NAME, or DEFAULT if it doesn't exist; used by
+                        `get_meta'.
+
+  BE._put_meta(NAME, VALUE)
+                        Set the VALUE of the metadata item with the given
+                        NAME, creating one if necessary; used by `put_meta'.
+
+  BE._del_meta(NAME)    Forget the metadata item with the given NAME; raise
+                        `KeyError' if there is no such item; used by
+                        `del_meta'.
+
+  BE._iter_meta()       Return an iterator over the metadata (NAME, VALUE)
+                        pairs; used by `iter_meta'.
+
+  BE._get_passwd(LABEL)
+                        Return the password payload stored with the (binary)
+                        LABEL; used by `get_passwd'.
+
+  BE._put_passwd(LABEL, PAYLOAD)
+                        Associate the (binary) PAYLOAD with the LABEL,
+                        forgetting any previous payload for that LABEL; used
+                        by `put_passwd'.
+
+  BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
+                        by `_del_passwd'.
+
+  BE._iter_passwds()    Return an iterator over the password (LABEL, PAYLOAD)
+                        pairs; used by `iter_passwds'.
   """
 
   FAIL = ['FAIL']
@@ -148,19 +192,26 @@ class StorageBackend (object):
 
   @classmethod
   def create(cls, file):
-    """Create a new database in the named FILE, using this backend."""
+    """
+    Create a new database in the named FILE, using this backend.
+
+    Subclasses must implement the `_create' instance method.
+    """
     return cls(writep = True, _magic = lambda me: me._create(file))
-  def _create(me, file):
-    me._db = _G.open(file, 'n', 0600)
 
   def __init__(me, file = None, writep = False, _magic = None, *args, **kw):
     """
     Main constructor.
+
+    Subclasses are not, in general, expected to override this: there's a
+    somewhat hairy protocol between the constructor and some of the class
+    methods.  Instead, the main hook for customization is the subclass's
+    `_open' method, which is invoked in the usual case.
     """
     super(StorageBackend, me).__init__(*args, **kw)
     if _magic is not None: _magic(me)
     elif file is None: raise ValueError, 'missing file parameter'
-    else: me._db = _G.open(file, writep and 'w' or 'r')
+    else: me._open(file, writep)
     me._writep = writep
     me._livep = True
 
@@ -169,11 +220,11 @@ class StorageBackend (object):
     Close the database.
 
     It is harmless to attempt to close a database which has been closed
-    already.
+    already.  Calls the subclass's `_close' method.
     """
     if me._livep:
       me._livep = False
-      me._db.close()
+      me._close()
 
   ## Utilities.
 
@@ -213,66 +264,142 @@ class StorageBackend (object):
 
     If no such item exists, then return DEFAULT if that was set; otherwise
     raise a `KeyError'.
+
+    This calls the subclass's `_get_meta' method, which should return the
+    requested item or return the given DEFAULT value.  It may assume that the
+    name is valid and the database is open.
     """
     me._check_meta_name(name)
     me._check_live()
-    try: value = me._db[name]
-    except KeyError: value = default
+    value = me._get_meta(name, default)
     if value is StorageBackend.FAIL: raise KeyError, name
     return value
 
   def put_meta(me, name, value):
-    """Store VALUE in the metadata item called NAME."""
+    """
+    Store VALUE in the metadata item called NAME.
+
+    This calls the subclass's `_put_meta' method, which may assume that the
+    name is valid and the database is open for writing.
+    """
     me._check_meta_name(name)
     me._check_write()
-    me._db[name] = value
+    me._put_meta(name, value)
 
   def del_meta(me, name):
-    """Forget about the metadata item with the given NAME."""
+    """
+    Forget about the metadata item with the given NAME.
+
+    This calls the subclass's `_del_meta' method, which may assume that the
+    name is valid and the database is open for writing.
+    """
     me._check_meta_name(name)
     me._check_write()
-    del me._db[name]
+    me._del_meta(name)
 
   def iter_meta(me):
-    """Return an iterator over the name/value metadata items."""
-    me._check_live()
-    k = me._db.firstkey()
-    while k is not None:
-      if not k.startswith('$'): yield k, me._db[k]
-      k = me._db.nextkey(k)
+    """
+    Return an iterator over the name/value metadata items.
 
-  ## Passwords.
+    This calls the subclass's `_iter_meta' method, which may assume that the
+    database is open.
+    """
+    me._check_live()
+    return me._iter_meta()
 
   def get_passwd(me, label):
     """
     Fetch and return the payload stored with the (opaque, binary) LABEL.
 
     If there is no such payload then raise `KeyError'.
+
+    This calls the subclass's `_get_passwd' method, which may assume that the
+    database is open.
     """
     me._check_live()
-    return me._db['$' + label]
+    return me._get_passwd(label)
 
   def put_passwd(me, label, payload):
     """
     Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
 
     Any previous payload for LABEL is forgotten.
+
+    This calls the subclass's `_put_passwd' method, which may assume that the
+    database is open for writing.
     """
     me._check_write()
-    me._db['$' + label] = payload
+    me._put_passwd(label, payload)
 
   def del_passwd(me, label):
     """
     Forget any PAYLOAD associated with the (opaque, binary) LABEL.
 
     If there is no such payload then raise `KeyError'.
+
+    This calls the subclass's `_del_passwd' method, which may assume that the
+    database is open for writing.
     """
     me._check_write()
-    del me._db['$' + label]
+    me._del_passwd(label, payload)
 
   def iter_passwds(me):
-    """Return an iterator over the stored password label/payload pairs."""
+    """
+    Return an iterator over the stored password label/payload pairs.
+
+    This calls the subclass's `_iter_passwds' method, which may assume that
+    the database is open.
+    """
     me._check_live()
+    return me._iter_passwds()
+
+class GDBMStorageBackend (StorageBackend):
+  """
+  My instances store password data in a GDBM database.
+
+  Metadata and password entries are mixed into the same database.  The key
+  for a metadata item is simply its name; the key for a password entry is
+  the entry's label prefixed by `$', since we're guaranteed that no
+  metadata item name begins with `$'.
+  """
+
+  def _open(me, file, writep):
+    try: me._db = _G.open(file, writep and 'w' or 'r')
+    except _G.error, e: raise StorageBackendRefusal, e
+
+  def _create(me, file):
+    me._db = _G.open(file, 'n', 0600)
+
+  def _close(me):
+    me._db.close()
+    me._db = None
+
+  def _get_meta(me, name, default):
+    try: return me._db[name]
+    except KeyError: return default
+
+  def _put_meta(me, name, value):
+    me._db[name] = value
+
+  def _del_meta(me, name):
+    del me._db[name]
+
+  def _iter_meta(me):
+    k = me._db.firstkey()
+    while k is not None:
+      if not k.startswith('$'): yield k, me._db[k]
+      k = me._db.nextkey(k)
+
+  def _get_passwd(me, label):
+    return me._db['$' + label]
+
+  def _put_passwd(me, label, payload):
+    me._db['$' + label] = payload
+
+  def _del_passwd(me, label):
+    del me._db['$' + label]
+
+  def _iter_passwds(me):
     k = me._db.firstkey()
     while k is not None:
       if k.startswith('$'): yield k[1:], me._db[k]
@@ -326,7 +453,7 @@ class PW (object):
     """
 
     ## Open the database.
-    me.db = StorageBackend(file, writep)
+    me.db = GDBMStorageBackend(file, writep)
 
     ## Find out what crypto to use.
     c = _C.gcciphers[me.db.get_meta('cipher')]
@@ -370,7 +497,7 @@ class PW (object):
 
     ## Set up and initialize the database.
     kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
-    with StorageBackend.create(file) as db:
+    with GDBM.StorageBackend.create(file) as db:
       db.put_meta('tag', tag)
       db.put_meta('salt', ppk.salt)
       db.put_meta('cipher', c.name)