3 ### Management of a secure password database
5 ### (c) 2005 Straylight/Edgeware
8 ###----- Licensing notice ---------------------------------------------------
10 ### This file is part of the Python interface to Catacomb.
12 ### Catacomb/Python is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU General Public License as published by
14 ### the Free Software Foundation; either version 2 of the License, or
15 ### (at your option) any later version.
17 ### Catacomb/Python is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ### GNU General Public License for more details.
22 ### You should have received a copy of the GNU General Public License along
23 ### with Catacomb/Python; if not, write to the Free Software Foundation,
24 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26 ###--------------------------------------------------------------------------
29 from __future__ import with_statement
34 ###--------------------------------------------------------------------------
35 ### Underlying cryptography.
37 class DecryptError (Exception):
39 I represent a failure to decrypt a message.
41 Usually this means that someone used the wrong key, though it can also
42 mean that a ciphertext has been modified.
46 class Crypto (object):
48 I represent a symmetric crypto transform.
50 There's currently only one transform implemented, which is the obvious
51 generic-composition construction: given a message m, and keys K0 and K1, we
52 choose an IV v, and compute:
54 * y = v || E(K0, v; m)
57 The final ciphertext is t || y.
60 def __init__(me, c, h, m, ck, mk):
62 Initialize the Crypto object with a given algorithm selection and keys.
64 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
65 keys CK and MK for C and M respectively.
73 Encrypt the message PT and return the resulting ciphertext.
75 blksz = me.c.__class__.blksz
78 iv = _C.rand.block(blksz)
81 b.put(me.c.encrypt(pt))
82 t = me.m().hash(b).done()
83 return t + str(buffer(b))
87 Decrypt the ciphertext CT, returning the plaintext.
89 Raises DecryptError if anything goes wrong.
91 blksz = me.c.__class__.blksz
92 tagsz = me.m.__class__.tagsz
102 if t != h.done(): raise DecryptError
103 return me.c.decrypt(x)
107 I represent a crypto transform whose keys are derived from a passphrase.
109 The password is salted and hashed; the salt is available as the `salt'
113 def __init__(me, pp, c, h, m, salt = None):
115 Initialize the PPK object with a passphrase and algorithm selection.
117 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
118 subclass M, and a SALT. The SALT may be None, if we're generating new
119 keys, indicating that a salt should be chosen randomly.
121 if not salt: salt = _C.rand.block(h.hashsz)
122 tag = '%s\0%s' % (pp, salt)
123 Crypto.__init__(me, c, h, m,
124 h().hash('cipher:' + tag).done(),
125 h().hash('mac:' + tag).done())
128 ###--------------------------------------------------------------------------
131 class StorageBackend (object):
133 I provide a backend for password and metadata storage.
135 Backends are responsible for storing and retrieving stuff, but not for the
136 cryptographic details. Backends need to store two kinds of information:
138 * metadata, consisting of a number of property names and their values;
141 * password mappings, consisting of a number of binary labels and
147 ## Life cycle methods.
150 def create(cls, file):
151 """Create a new database in the named FILE, using this backend."""
152 return cls(writep = True, _magic = lambda me: me._create(file))
153 def _create(me, file):
154 me._db = _G.open(file, 'n', 0600)
156 def __init__(me, file = None, writep = False, _magic = None, *args, **kw):
160 super(StorageBackend, me).__init__(*args, **kw)
161 if _magic is not None: _magic(me)
162 elif file is None: raise ValueError, 'missing file parameter'
163 else: me._db = _G.open(file, writep and 'w' or 'r')
171 It is harmless to attempt to close a database which has been closed
181 """Raise an error if the receiver has been closed."""
182 if not me._livep: raise ValueError, 'database is closed'
184 def _check_write(me):
185 """Raise an error if the receiver is not open for writing."""
187 if not me._writep: raise ValueError, 'database is read-only'
189 def _check_meta_name(me, name):
191 Raise an error unless NAME is a valid name for a metadata item.
193 Metadata names may not start with `$': such names are reserved for
196 if name.startswith('$'):
197 raise ValueError, "invalid metadata key `%s'" % name
202 """Context protocol: make sure the database is closed on exit."""
204 def __exit__(me, exctype, excvalue, exctb):
205 """Context protocol: see `__enter__'."""
210 def get_meta(me, name, default = FAIL):
212 Fetch the value for the metadata item NAME.
214 If no such item exists, then return DEFAULT if that was set; otherwise
217 me._check_meta_name(name)
219 try: value = me._db[name]
220 except KeyError: value = default
221 if value is StorageBackend.FAIL: raise KeyError, name
224 def put_meta(me, name, value):
225 """Store VALUE in the metadata item called NAME."""
226 me._check_meta_name(name)
230 def del_meta(me, name):
231 """Forget about the metadata item with the given NAME."""
232 me._check_meta_name(name)
237 """Return an iterator over the name/value metadata items."""
239 k = me._db.firstkey()
241 if not k.startswith('$'): yield k, me._db[k]
242 k = me._db.nextkey(k)
246 def get_passwd(me, label):
248 Fetch and return the payload stored with the (opaque, binary) LABEL.
250 If there is no such payload then raise `KeyError'.
253 return me._db['$' + label]
255 def put_passwd(me, label, payload):
257 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
259 Any previous payload for LABEL is forgotten.
262 me._db['$' + label] = payload
264 def del_passwd(me, label):
266 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
268 If there is no such payload then raise `KeyError'.
271 del me._db['$' + label]
273 def iter_passwds(me):
274 """Return an iterator over the stored password label/payload pairs."""
276 k = me._db.firstkey()
278 if k.startswith('$'): yield k[1:], me._db[k]
279 k = me._db.nextkey(k)
281 ###--------------------------------------------------------------------------
282 ### Password storage.
286 I represent a secure (ish) password store.
288 I can store short secrets, associated with textual names, in a way which
289 doesn't leak too much information about them.
291 I implement (some of) the Python mapping protocol.
293 I keep track of everything using a StorageBackend object. This contains
294 password entries, identified by cryptographic labels, and a number of
297 cipher Names the Catacomb cipher selected.
299 hash Names the Catacomb hash function selected.
301 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
302 length and concatenated, encrypted using the master
305 mac Names the Catacomb message authentication code selected.
307 magic A magic string for obscuring password tag names.
309 salt The salt for hashing the passphrase.
311 tag The master passphrase's tag, for the Pixie's benefit.
313 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
314 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
315 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
316 encrypted using the stored keys.
319 def __init__(me, file, writep = False):
321 Initialize a PW object from the database in FILE.
323 If WRITEP is false (the default) then the database is opened read-only;
324 if true then it may be written. Requests the database password from the
325 Pixie, which may cause interaction.
328 ## Open the database.
329 me.db = StorageBackend(file, writep)
331 ## Find out what crypto to use.
332 c = _C.gcciphers[me.db.get_meta('cipher')]
333 h = _C.gchashes[me.db.get_meta('hash')]
334 m = _C.gcmacs[me.db.get_meta('mac')]
336 ## Request the passphrase and extract the master keys.
337 tag = me.db.get_meta('tag')
338 ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt'))
340 b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
346 if not b.endp: raise ValueError, 'trailing junk'
348 ## Set the key, and stash it and the tag-hashing secret.
349 me.k = Crypto(c, h, m, me.ck, me.mk)
350 me.magic = me.k.decrypt(me.db.get_meta('magic'))
353 def create(cls, file, tag, c, h, m):
355 Create and initialize a new database FILE.
357 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
358 and a Pixie passphrase TAG.
360 This doesn't return a working object: it just creates the database file
361 and gets out of the way.
364 ## Set up the cryptography.
365 pp = _C.ppread(tag, _C.PMODE_VERIFY)
366 ppk = PPK(pp, c, h, m)
367 ck = _C.rand.block(c.keysz.default)
368 mk = _C.rand.block(c.keysz.default)
369 k = Crypto(c, h, m, ck, mk)
371 ## Set up and initialize the database.
372 kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
373 with StorageBackend.create(file) as db:
374 db.put_meta('tag', tag)
375 db.put_meta('salt', ppk.salt)
376 db.put_meta('cipher', c.name)
377 db.put_meta('hash', h.name)
378 db.put_meta('mac', m.name)
379 db.put_meta('key', kct)
380 db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz)))
382 def keyxform(me, key):
383 """Transform the KEY (actually a password tag) into a password label."""
384 return me.k.h().hash(me.magic).hash(key).done()
388 Change the database password.
390 Requests the new password from the Pixie, which will probably cause
393 tag = me.db.get_meta('tag')
395 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
396 me.k.c.__class__, me.k.h, me.k.m.__class__)
397 kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
398 me.db.put_meta('key', kct)
399 me.db.put_meta('salt', ppk.salt)
401 def pack(me, key, value):
402 """Pack the KEY and VALUE into a ciphertext, and return it."""
404 b.putblk16(key).putblk16(value)
405 b.zero(((b.size + 255) & ~255) - b.size)
406 return me.k.encrypt(b)
410 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
412 Might raise DecryptError, of course.
414 b = _C.ReadBuffer(me.k.decrypt(ct))
421 def __getitem__(me, key):
422 """Return the password for the given KEY."""
423 try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1]
424 except KeyError: raise KeyError, key
426 def __setitem__(me, key, value):
427 """Associate the password VALUE with the KEY."""
428 me.db.put_passwd(me.keyxform(key), me.pack(key, value))
430 def __delitem__(me, key):
431 """Forget all about the KEY."""
432 try: me.db.del_passwd(me.keyxform(key))
433 except KeyError: raise KeyError, key
436 """Iterate over the known password tags."""
437 for _, pld in me.db.iter_passwds():
438 yield me.unpack(pld)[0]
444 def __exit__(me, excty, excval, exctb):
447 ###----- That's all, folks --------------------------------------------------