chiark / gitweb /
catacomb/pwsafe.py, pwsafe: Dispatching for multiple backends.
authorMark Wooding <mdw@distorted.org.uk>
Sun, 24 May 2015 18:22:29 +0000 (19:22 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Sun, 31 May 2015 21:42:53 +0000 (22:42 +0100)
This commit introduces a number of tightly related changes.

  * Have concrete backends declare a `NAME' attribute.  This lets users
    name them, and lets us determine which classes are concrete.

  * Introduce a metaclass which registers concrete StorageClass
    subclasses.

  * Extend the `_open' protocol, so that it can raise the new
    StorageBackendRefusal exception to indicate that some other backend
    should try to open the given file.

  * Introduce a `StorageBackend.open' method which examines all
    registered backends and gives each of them an opportunity to open
    the file in some priority order.

  * Add a new method for looking up backends by name.

  * introduce a new DBCLS parameter to `PW.create', which is the backend
    class to use when creating a new database.

  * Introduce a new option to the `create' command to choose the
    database backend by name.

There's only one backend at the moment, though that will change soon.

catacomb/pwsafe.py
pwsafe

index 67b249e747bd11ef1304287501010a915ac1e329..44382e76056b69a0dd6d9e577885331cbc0a5d86 100644 (file)
@@ -28,6 +28,8 @@
 
 from __future__ import with_statement
 
+import os as _OS
+
 import catacomb as _C
 import gdbm as _G
 
@@ -128,12 +130,35 @@ class PPK (Crypto):
 ###--------------------------------------------------------------------------
 ### Backend storage.
 
+class StorageBackendRefusal (Exception):
+  """
+  I signify that a StorageBackend subclass has refused to open a file.
+
+  This is used by the StorageBackend.open class method.
+  """
+  pass
+
+class StorageBackendClass (type):
+  """
+  I am a metaclass for StorageBackend classes.
+
+  My main feature is that I register my concrete instances (with a `NAME'
+  which is not `None') with the StorageBackend class.
+  """
+  def __init__(me, name, supers, dict):
+    """
+    Register a new concrete StorageBackend subclass.
+    """
+    super(StorageBackendClass, me).__init__(name, supers, dict)
+    if me.NAME is not None: StorageBackend.register_concrete_subclass(me)
+
 class StorageBackend (object):
   """
   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.
+  to do something useful.  But I maintain a list of my subclasses and can
+  choose an appropriate one to open a database file you've found lying about.
 
   Backends are responsible for storing and retrieving stuff, but not for the
   cryptographic details.  Backends need to store two kinds of information:
@@ -184,11 +209,53 @@ class StorageBackend (object):
 
   BE._iter_passwds()    Return an iterator over the password (LABEL, PAYLOAD)
                         pairs; used by `iter_passwds'.
+
+  Also, concrete subclasses should define the following class attributes.
+
+  NAME                  The name of the backend, so that the user can select
+                        it when creating a new database.
+
+  PRIO                  An integer priority: backends are tried in decreasing
+                        priority order when opening an existing database.
   """
 
+  __metaclass__ = StorageBackendClass
+  NAME = None
+  PRIO = 10
+
+  ## The registry of subclasses.
+  CLASSES = {}
+
   FAIL = ['FAIL']
 
-  ## Life cycle methods.
+  @staticmethod
+  def register_concrete_subclass(sub):
+    """Register a concrete subclass, so that `open' can try it."""
+    StorageBackend.CLASSES[sub.NAME] = sub
+
+  @staticmethod
+  def byname(name):
+    """
+    Return the concrete subclass with the given NAME.
+
+    Raise `KeyError' if the name isn't found.
+    """
+    return StorageBackend.CLASSES[name]
+
+  @staticmethod
+  def classes():
+    """Return an iterator over the concrete subclasses."""
+    return StorageBackend.CLASSES.itervalues()
+
+  @staticmethod
+  def open(file, writep = False):
+    """Open a database FILE, using some appropriate backend."""
+    _OS.stat(file)
+    for cls in sorted(StorageBackend.CLASSES.values(), reverse = True,
+                      key = lambda cls: cls.PRIO):
+      try: return cls(file, writep)
+      except StorageBackendRefusal: pass
+    raise StorageBackendRefusal
 
   @classmethod
   def create(cls, file):
@@ -209,6 +276,7 @@ class StorageBackend (object):
     `_open' method, which is invoked in the usual case.
     """
     super(StorageBackend, me).__init__(*args, **kw)
+    if me.NAME is None: raise ValueError, 'abstract class'
     if _magic is not None: _magic(me)
     elif file is None: raise ValueError, 'missing file parameter'
     else: me._open(file, writep)
@@ -363,6 +431,8 @@ class GDBMStorageBackend (StorageBackend):
   metadata item name begins with `$'.
   """
 
+  NAME = 'gdbm'
+
   def _open(me, file, writep):
     try: me._db = _G.open(file, writep and 'w' or 'r')
     except _G.error, e: raise StorageBackendRefusal, e
@@ -453,7 +523,7 @@ class PW (object):
     """
 
     ## Open the database.
-    me.db = GDBMStorageBackend(file, writep)
+    me.db = StorageBackend.open(file, writep)
 
     ## Find out what crypto to use.
     c = _C.gcciphers[me.db.get_meta('cipher')]
@@ -477,9 +547,9 @@ class PW (object):
     me.magic = me.k.decrypt(me.db.get_meta('magic'))
 
   @classmethod
-  def create(cls, file, tag, c, h, m):
+  def create(cls, dbcls, file, tag, c, h, m):
     """
-    Create and initialize a new database FILE.
+    Create and initialize a new database FILE using StorageBackend DBCLS.
 
     We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
     and a Pixie passphrase TAG.
@@ -497,7 +567,7 @@ class PW (object):
 
     ## Set up and initialize the database.
     kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
-    with GDBM.StorageBackend.create(file) as db:
+    with dbcls.create(file) as db:
       db.put_meta('tag', tag)
       db.put_meta('salt', ppk.salt)
       db.put_meta('cipher', c.name)
diff --git a/pwsafe b/pwsafe
index 50a58780f6e1bb565cffa0805adde996a1a07c15..153ee54b56b55b3856dde83abcdcff01a472e5f9 100644 (file)
--- a/pwsafe
+++ b/pwsafe
@@ -65,13 +65,16 @@ def cmd_create(av):
 
   ## Parse the options.
   try:
-    opts, args = getopt(av, 'c:h:m:', ['cipher=', 'mac=', 'hash='])
+    opts, args = getopt(av, 'c:d:h:m:',
+                        ['cipher=', 'database=', 'mac=', 'hash='])
   except GetoptError:
     return 1
+  dbty = 'gdbm'
   for o, a in opts:
     if o in ('-c', '--cipher'): cipher = a
     elif o in ('-m', '--mac'): mac = a
     elif o in ('-h', '--hash'): hash = a
+    elif o in ('-d', '--database'): dbty = a
     else: raise 'Barf!'
   if len(args) > 2: return 1
   if len(args) >= 1: tag = args[0]
@@ -79,7 +82,10 @@ def cmd_create(av):
 
   ## Set up the database.
   if mac is None: mac = hash + '-hmac'
-  PW.create(file, C.gcciphers[cipher], C.gchashes[hash], C.gcmacs[mac], tag)
+  try: dbcls = StorageBackend.byname(dbty)
+  except KeyError: die("Unknown database backend `%s'" % dbty)
+  PW.create(dbcls, file, tag,
+            C.gcciphers[cipher], C.gchashes[hash], C.gcmacs[mac])
 
 def cmd_changepp(av):
   if len(av) != 0: return 1
@@ -142,7 +148,7 @@ def cmd_del(av):
     except KeyError, exc: die("Password `%s' not found" % exc.args[0])
 
 commands = { 'create': [cmd_create,
-                        '[-c CIPHER] [-h HASH] [-m MAC] [PP-TAG]'],
+                        '[-c CIPHER] [-d DBTYPE] [-h HASH] [-m MAC] [PP-TAG]'],
              'find' : [cmd_find, 'LABEL'],
              'store' : [cmd_store, 'LABEL [VALUE]'],
              'list' : [cmd_list, '[GLOB-PATTERN]'],
@@ -156,6 +162,8 @@ commands = { 'create': [cmd_create,
 
 def version():
   print '%s 1.0.0' % prog
+  print 'Backend types: %s' % \
+      ' '.join([c.NAME for c in StorageBackend.classes()])
 
 def usage(fp):
   print >>fp, 'Usage: %s COMMAND [ARGS...]' % prog