X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/catacomb-python/blobdiff_plain/a0580db01b4aaaa99516df243caa85153de65a4e..d1c45f5c79fda35ff9f8fd7d90f345dba3de4eb8:/catacomb/pwsafe.py diff --git a/catacomb/pwsafe.py b/catacomb/pwsafe.py index bfd4e86..da311c7 100644 --- a/catacomb/pwsafe.py +++ b/catacomb/pwsafe.py @@ -1,18 +1,142 @@ -# -*-python-*- +### -*-python-*- +### +### Management of a secure password database +### +### (c) 2005 Straylight/Edgeware +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the Python interface to Catacomb. +### +### Catacomb/Python is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. +### +### Catacomb/Python is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. +### +### You should have received a copy of the GNU General Public License along +### with Catacomb/Python; if not, write to the Free Software Foundation, +### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +###-------------------------------------------------------------------------- +### Imported modules. import catacomb as _C import gdbm as _G import struct as _S +###-------------------------------------------------------------------------- +### Utilities. + +class Buffer (object): + """ + I am a simple gadget for parsing binary strings. + + You should use Catacomb's ReadBuffer instead. + """ + + def __init__(me, s): + """ + Initialize the buffer with a string S. + """ + me.str = s + me.i = 0 + + def get(me, n): + """ + Fetch and return the next N bytes from the buffer. + """ + i = me.i + if n + i > len(me.str): + raise IndexError, 'buffer underflow' + me.i += n + return me.str[i:i + n] + + def getbyte(me): + """ + Fetch and return (as a small integer) the next byte from the buffer. + """ + return ord(me.get(1)) + + def unpack(me, fmt): + """ + Unpack a structure described by FMT from the next bytes of the buffer. + + Return a tuple containing the unpacked items. + """ + return _S.unpack(fmt, me.get(_S.calcsize(fmt))) + + def getstring(me): + """ + Fetch and return a counted string from the buffer. + + The string is expected to be preceded by its 16-bit length, in network + byte order. + """ + return me.get(me.unpack('>H')[0]) + + def checkend(me): + """ + Raise an error if the buffer has not been completely consumed. + """ + if me.i != len(me.str): + raise ValueError, 'junk at end of buffer' + +def _wrapstr(s): + """ + Prefix the string S with its 16-bit length. + + It can be read using Buffer.getstring. You should use Catacomb's + WriteBuffer.putblk16() function instead. + """ + return _S.pack('>H', len(s)) + s + +###-------------------------------------------------------------------------- +### Underlying cryptography. + class DecryptError (Exception): + """ + I represent a failure to decrypt a message. + + Usually this means that someone used the wrong key, though it can also + mean that a ciphertext has been modified. + """ pass class Crypto (object): + """ + I represent a symmetric crypto transform. + + There's currently only one transform implemented, which is the obvious + generic-composition construction: given a message m, and keys K0 and K1, we + choose an IV v, and compute: + + * y = v || E(K0, v; m) + * t = M(K1; y) + + The final ciphertext is t || y. + """ + def __init__(me, c, h, m, ck, mk): + """ + Initialize the Crypto object with a given algorithm selection and keys. + + We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and + keys CK and MK for C and M respectively. + """ me.c = c(ck) me.m = m(mk) me.h = h + def encrypt(me, pt): + """ + Encrypt the message PT and return the resulting ciphertext. + """ if me.c.__class__.blksz: iv = _C.rand.block(me.c.__class__.blksz) me.c.setiv(iv) @@ -22,6 +146,11 @@ class Crypto (object): t = me.m().hash(y).done() return t + y def decrypt(me, ct): + """ + Decrypt the ciphertext CT, returning the plaintext. + + Raises DecryptError if anything goes wrong. + """ t = ct[:me.m.__class__.tagsz] y = ct[me.m.__class__.tagsz:] if t != me.m().hash(y).done(): @@ -31,57 +160,114 @@ class Crypto (object): return me.c.decrypt(y[me.c.__class__.blksz:]) class PPK (Crypto): + """ + I represent a crypto transform whose keys are derived from a passphrase. + + The password is salted and hashed; the salt is available as the `salt' + attribute. + """ + def __init__(me, pp, c, h, m, salt = None): + """ + Initialize the PPK object with a passphrase and algorithm selection. + + We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC + subclass M, and a SALT. The SALT may be None, if we're generating new + keys, indicating that a salt should be chosen randomly. + """ if not salt: salt = _C.rand.block(h.hashsz) tag = '%s\0%s' % (pp, salt) Crypto.__init__(me, c, h, m, - h().hash('cipher:' + tag).done(), - h().hash('mac:' + tag).done()) + h().hash('cipher:' + tag).done(), + h().hash('mac:' + tag).done()) me.salt = salt -class Buffer (object): - def __init__(me, s): - me.str = s - me.i = 0 - def get(me, n): - i = me.i - if n + i > len(me.str): - raise IndexError, 'buffer underflow' - me.i += n - return me.str[i:i + n] - def getbyte(me): - return ord(me.get(1)) - def unpack(me, fmt): - return _S.unpack(fmt, me.get(_S.calcsize(fmt))) - def getstring(me): - return me.get(me.unpack('>H')[0]) - def checkend(me): - if me.i != len(me.str): - raise ValueError, 'junk at end of buffer' - -def _wrapstr(s): - return _S.pack('>H', len(s)) + s +###-------------------------------------------------------------------------- +### Password storage. class PWIter (object): + """ + I am an iterator over items in a password database. + + I implement the usual Python iteration protocol. + """ + def __init__(me, pw): + """ + Initialize a PWIter object, to fetch items from PW. + """ me.pw = pw me.k = me.pw.db.firstkey() + def next(me): + """ + Return the next tag from the database. + + Raises StopIteration if there are no more tags. + """ k = me.k while True: if k is None: - raise StopIteration + raise StopIteration if k[0] == '$': - break + break k = me.pw.db.nextkey(k) me.k = me.pw.db.nextkey(k) return me.pw.unpack(me.pw.db[k])[0] + class PW (object): + """ + I represent a secure (ish) password store. + + I can store short secrets, associated with textual names, in a way which + doesn't leak too much information about them. + + 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. + + cipher Names the Catacomb cipher selected. + + hash Names the Catacomb hash function selected. + + key Cipher and MAC keys, each prefixed by a 16-bit big-endian + length and concatenated, encrypted using the master + passphrase. + + mac Names the Catacomb message authentication code selected. + + magic A magic string for obscuring password tag names. + + salt The salt for hashing the passphrase. + + 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. + """ + def __init__(me, file, mode = 'r'): + """ + Initialize a PW object from the GDBM database in FILE. + + MODE can be `r' for read-only access to the underlying database, or `w' + for read-write access. Requests the database password from the Pixie, + which may cause interaction. + """ + + ## Open the database. me.db = _G.open(file, mode) + + ## Find out what crypto to use. c = _C.gcciphers[me.db['cipher']] h = _C.gchashes[me.db['hash']] m = _C.gcmacs[me.db['mac']] + + ## Request the passphrase and extract the master keys. tag = me.db['tag'] ppk = PPK(_C.ppread(tag), c, h, m, me.db['salt']) try: @@ -92,39 +278,81 @@ class PW (object): me.ck = buf.getstring() me.mk = buf.getstring() buf.checkend() + + ## 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']) + 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() + def changepp(me): + """ + Change the database password. + + Requests the new password from the Pixie, which will probably cause + interaction. + """ tag = me.db['tag'] _C.ppcancel(tag) ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY), - me.k.c.__class__, me.k.h, me.k.m.__class__) + me.k.c.__class__, me.k.h, me.k.m.__class__) me.db['key'] = ppk.encrypt(_wrapstr(me.ck) + _wrapstr(me.mk)) me.db['salt'] = ppk.salt + def pack(me, key, value): + """ + Pack the KEY and VALUE into a ciphertext, and return it. + """ w = _wrapstr(key) + _wrapstr(value) pl = (len(w) + 255) & ~255 w += '\0' * (pl - len(w)) return me.k.encrypt(w) - def unpack(me, p): - buf = Buffer(me.k.decrypt(p)) + + def unpack(me, ct): + """ + Unpack a ciphertext CT and return a (KEY, VALUE) pair. + + Might raise DecryptError, of course. + """ + buf = Buffer(me.k.decrypt(ct)) key = buf.getstring() value = buf.getstring() return key, value + + ## 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 + def __setitem__(me, key, value): + """ + Associate the password VALUE with the KEY. + """ me.db[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 + def __iter__(me): + """ + Iterate over the known password tags. + """ return PWIter(me) +###----- That's all, folks --------------------------------------------------