###--------------------------------------------------------------------------
### Imported modules.
+from __future__ import with_statement
+
import catacomb as _C
import gdbm as _G
h().hash('mac:' + tag).done())
me.salt = salt
+###--------------------------------------------------------------------------
+### Backend storage.
+
+class StorageBackend (object):
+ """
+ I provide a backend for password and metadata storage.
+
+ Backends are responsible for storing and retrieving stuff, but not for the
+ cryptographic details. Backends need to store two kinds of information:
+
+ * metadata, consisting of a number of property names and their values;
+ and
+
+ * password mappings, consisting of a number of binary labels and
+ payloads.
+ """
+
+ FAIL = ['FAIL']
+
+ ## Life cycle methods.
+
+ @classmethod
+ def create(cls, file):
+ """Create a new database in the named FILE, using this backend."""
+ 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.
+ """
+ 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')
+ me._writep = writep
+ me._livep = True
+
+ def close(me):
+ """
+ Close the database.
+
+ It is harmless to attempt to close a database which has been closed
+ already.
+ """
+ if me._livep:
+ me._livep = False
+ me._db.close()
+
+ ## Utilities.
+
+ def _check_live(me):
+ """Raise an error if the receiver has been closed."""
+ if not me._livep: raise ValueError, 'database is closed'
+
+ def _check_write(me):
+ """Raise an error if the receiver is not open for writing."""
+ me._check_live()
+ if not me._writep: raise ValueError, 'database is read-only'
+
+ def _check_meta_name(me, name):
+ """
+ Raise an error unless NAME is a valid name for a metadata item.
+
+ Metadata names may not start with `$': such names are reserved for
+ password storage.
+ """
+ if name.startswith('$'):
+ raise ValueError, "invalid metadata key `%s'" % name
+
+ ## Context protocol.
+
+ def __enter__(me):
+ """Context protocol: make sure the database is closed on exit."""
+ return me
+ def __exit__(me, exctype, excvalue, exctb):
+ """Context protocol: see `__enter__'."""
+ me.close()
+
+ ## Metadata.
+
+ def get_meta(me, name, default = FAIL):
+ """
+ Fetch the value for the metadata item NAME.
+
+ If no such item exists, then return DEFAULT if that was set; otherwise
+ raise a `KeyError'.
+ """
+ me._check_meta_name(name)
+ me._check_live()
+ try: value = me._db[name]
+ except KeyError: value = 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."""
+ me._check_meta_name(name)
+ me._check_write()
+ me._db[name] = value
+
+ def del_meta(me, name):
+ """Forget about the metadata item with the given NAME."""
+ me._check_meta_name(name)
+ me._check_write()
+ del me._db[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)
+
+ ## Passwords.
+
+ 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'.
+ """
+ me._check_live()
+ return me._db['$' + label]
+
+ def put_passwd(me, label, payload):
+ """
+ Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
+
+ Any previous payload for LABEL is forgotten.
+ """
+ me._check_write()
+ me._db['$' + 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'.
+ """
+ me._check_write()
+ del me._db['$' + label]
+
+ def iter_passwds(me):
+ """Return an iterator over the stored password label/payload pairs."""
+ me._check_live()
+ k = me._db.firstkey()
+ while k is not None:
+ if k.startswith('$'): yield k[1:], me._db[k]
+ k = me._db.nextkey(k)
+
###--------------------------------------------------------------------------
### Password storage.
I implement (some of) the Python mapping protocol.
- Here's how we use the underlying GDBM key/value storage to keep track of
- the necessary things. Password entries have keys whose name begins with
- `$'; other keys have specific meanings, as follows.
+ I keep track of everything using a StorageBackend object. This contains
+ password entries, identified by cryptographic labels, and a number of
+ metadata items.
cipher Names the Catacomb cipher selected.
tag The master passphrase's tag, for the Pixie's benefit.
- Password entries are assigned keys of the form `$' || H(MAGIC || TAG); the
- corresponding value consists of a pair (TAG, PASSWD), prefixed with 16-bit
- lengths, concatenated, padded to a multiple of 256 octets, and encrypted
- using the stored keys.
+ Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
+ the corresponding value consists of a pair (TAG, PASSWD), prefixed with
+ 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
+ encrypted using the stored keys.
"""
def __init__(me, file, writep = False):
"""
- Initialize a PW object from the GDBM database in FILE.
+ Initialize a PW object from the database in FILE.
- If WRITEP is true, then allow write-access to the database; otherwise
- allow read access only. Requests the database password from the Pixie,
- which may cause interaction.
+ If WRITEP is false (the default) then the database is opened read-only;
+ if true then it may be written. Requests the database password from the
+ Pixie, which may cause interaction.
"""
## Open the database.
- me.db = _G.open(file, writep and 'w' or 'r')
+ me.db = StorageBackend(file, writep)
## Find out what crypto to use.
- c = _C.gcciphers[me.db['cipher']]
- h = _C.gchashes[me.db['hash']]
- m = _C.gcmacs[me.db['mac']]
+ c = _C.gcciphers[me.db.get_meta('cipher')]
+ h = _C.gchashes[me.db.get_meta('hash')]
+ m = _C.gcmacs[me.db.get_meta('mac')]
## Request the passphrase and extract the master keys.
- tag = me.db['tag']
- ppk = PPK(_C.ppread(tag), c, h, m, me.db['salt'])
+ tag = me.db.get_meta('tag')
+ ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt'))
try:
- b = _C.ReadBuffer(ppk.decrypt(me.db['key']))
+ b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
except DecryptError:
_C.ppcancel(tag)
raise
## Set the key, and stash it and the tag-hashing secret.
me.k = Crypto(c, h, m, me.ck, me.mk)
- me.magic = me.k.decrypt(me.db['magic'])
+ me.magic = me.k.decrypt(me.db.get_meta('magic'))
@classmethod
- def create(cls, file, c, h, m, tag):
+ def create(cls, file, tag, c, h, m):
"""
- Create and initialize a new, empty, database FILE.
+ Create and initialize a new database FILE.
We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
and a Pixie passphrase TAG.
k = Crypto(c, h, m, ck, mk)
## Set up and initialize the database.
- db = _G.open(file, 'n', 0600)
- db['tag'] = tag
- db['salt'] = ppk.salt
- db['cipher'] = c.name
- db['hash'] = h.name
- db['mac'] = m.name
- db['key'] = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
- db['magic'] = k.encrypt(_C.rand.block(h.hashsz))
+ kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
+ with StorageBackend.create(file) as db:
+ db.put_meta('tag', tag)
+ db.put_meta('salt', ppk.salt)
+ db.put_meta('cipher', c.name)
+ db.put_meta('hash', h.name)
+ db.put_meta('mac', m.name)
+ db.put_meta('key', kct)
+ db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz)))
def keyxform(me, key):
- """
- Transform the KEY (actually a password tag) into a GDBM record key.
- """
- return '$' + me.k.h().hash(me.magic).hash(key).done()
+ """Transform the KEY (actually a password tag) into a password label."""
+ return me.k.h().hash(me.magic).hash(key).done()
def changepp(me):
"""
Requests the new password from the Pixie, which will probably cause
interaction.
"""
- tag = me.db['tag']
+ tag = me.db.get_meta('tag')
_C.ppcancel(tag)
ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
me.k.c.__class__, me.k.h, me.k.m.__class__)
- me.db['key'] = \
- ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
- me.db['salt'] = ppk.salt
+ kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
+ me.db.put_meta('key', kct)
+ me.db.put_meta('salt', ppk.salt)
def pack(me, key, value):
- """
- Pack the KEY and VALUE into a ciphertext, and return it.
- """
+ """Pack the KEY and VALUE into a ciphertext, and return it."""
b = _C.WriteBuffer()
b.putblk16(key).putblk16(value)
b.zero(((b.size + 255) & ~255) - b.size)
## Mapping protocol.
def __getitem__(me, key):
- """
- Return the password for the given KEY.
- """
- try:
- return me.unpack(me.db[me.keyxform(key)])[1]
- except KeyError:
- raise KeyError, key
+ """Return the password for the given KEY."""
+ try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1]
+ except KeyError: raise KeyError, key
def __setitem__(me, key, value):
- """
- Associate the password VALUE with the KEY.
- """
- me.db[me.keyxform(key)] = me.pack(key, value)
+ """Associate the password VALUE with the KEY."""
+ me.db.put_passwd(me.keyxform(key), me.pack(key, value))
def __delitem__(me, key):
- """
- Forget all about the KEY.
- """
- try:
- del me.db[me.keyxform(key)]
- except KeyError:
- raise KeyError, key
+ """Forget all about the KEY."""
+ try: me.db.del_passwd(me.keyxform(key))
+ except KeyError: raise KeyError, key
def __iter__(me):
- """
- Iterate over the known password tags.
- """
- k = me.db.firstkey()
- while k is not None:
- if k[0] == '$': yield me.unpack(me.db[k])[0]
- k = me.db.nextkey(k)
+ """Iterate over the known password tags."""
+ for _, pld in me.db.iter_passwds():
+ yield me.unpack(pld)[0]
## Context protocol.