From: Mark Wooding Date: Sun, 24 May 2015 17:59:55 +0000 (+0100) Subject: catacomb/pwsafe.py: Split out the GDBM-specifics from StorageBackend. X-Git-Tag: 1.1.0~10 X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/catacomb-python/commitdiff_plain/1726ab403eaf0c44e1a1a4643b256e1e214b792d?ds=sidebyside catacomb/pwsafe.py: Split out the GDBM-specifics from StorageBackend. For now, use the GDBM-based backend explicitly and unconditionally, because there isn't another one anyway. --- diff --git a/catacomb/pwsafe.py b/catacomb/pwsafe.py index b728e57..67b249e 100644 --- a/catacomb/pwsafe.py +++ b/catacomb/pwsafe.py @@ -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)