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 basic protocol for password storage backends.
135 I'm an abstract class: you want one of my subclasses if you actually want
136 to do something useful.
138 Backends are responsible for storing and retrieving stuff, but not for the
139 cryptographic details. Backends need to store two kinds of information:
141 * metadata, consisting of a number of property names and their values;
144 * password mappings, consisting of a number of binary labels and
147 Backends need to implement the following ordinary methods. See the calling
148 methods for details of the subclass responsibilities.
150 BE._create(FILE) Create a new database in FILE; used by `create'.
152 BE._open(FILE, WRITEP)
153 Open the existing database FILE; used by `open'.
155 BE._close() Close the database, freeing up any resources.
157 BE._get_meta(NAME, DEFAULT)
158 Return the value of the metadata item with the given
159 NAME, or DEFAULT if it doesn't exist; used by
162 BE._put_meta(NAME, VALUE)
163 Set the VALUE of the metadata item with the given
164 NAME, creating one if necessary; used by `put_meta'.
166 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
167 `KeyError' if there is no such item; used by
170 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
171 pairs; used by `iter_meta'.
173 BE._get_passwd(LABEL)
174 Return the password payload stored with the (binary)
175 LABEL; used by `get_passwd'.
177 BE._put_passwd(LABEL, PAYLOAD)
178 Associate the (binary) PAYLOAD with the LABEL,
179 forgetting any previous payload for that LABEL; used
182 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
185 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
186 pairs; used by `iter_passwds'.
191 ## Life cycle methods.
194 def create(cls, file):
196 Create a new database in the named FILE, using this backend.
198 Subclasses must implement the `_create' instance method.
200 return cls(writep = True, _magic = lambda me: me._create(file))
202 def __init__(me, file = None, writep = False, _magic = None, *args, **kw):
206 Subclasses are not, in general, expected to override this: there's a
207 somewhat hairy protocol between the constructor and some of the class
208 methods. Instead, the main hook for customization is the subclass's
209 `_open' method, which is invoked in the usual case.
211 super(StorageBackend, me).__init__(*args, **kw)
212 if _magic is not None: _magic(me)
213 elif file is None: raise ValueError, 'missing file parameter'
214 else: me._open(file, writep)
222 It is harmless to attempt to close a database which has been closed
223 already. Calls the subclass's `_close' method.
232 """Raise an error if the receiver has been closed."""
233 if not me._livep: raise ValueError, 'database is closed'
235 def _check_write(me):
236 """Raise an error if the receiver is not open for writing."""
238 if not me._writep: raise ValueError, 'database is read-only'
240 def _check_meta_name(me, name):
242 Raise an error unless NAME is a valid name for a metadata item.
244 Metadata names may not start with `$': such names are reserved for
247 if name.startswith('$'):
248 raise ValueError, "invalid metadata key `%s'" % name
253 """Context protocol: make sure the database is closed on exit."""
255 def __exit__(me, exctype, excvalue, exctb):
256 """Context protocol: see `__enter__'."""
261 def get_meta(me, name, default = FAIL):
263 Fetch the value for the metadata item NAME.
265 If no such item exists, then return DEFAULT if that was set; otherwise
268 This calls the subclass's `_get_meta' method, which should return the
269 requested item or return the given DEFAULT value. It may assume that the
270 name is valid and the database is open.
272 me._check_meta_name(name)
274 value = me._get_meta(name, default)
275 if value is StorageBackend.FAIL: raise KeyError, name
278 def put_meta(me, name, value):
280 Store VALUE in the metadata item called NAME.
282 This calls the subclass's `_put_meta' method, which may assume that the
283 name is valid and the database is open for writing.
285 me._check_meta_name(name)
287 me._put_meta(name, value)
289 def del_meta(me, name):
291 Forget about the metadata item with the given NAME.
293 This calls the subclass's `_del_meta' method, which may assume that the
294 name is valid and the database is open for writing.
296 me._check_meta_name(name)
302 Return an iterator over the name/value metadata items.
304 This calls the subclass's `_iter_meta' method, which may assume that the
308 return me._iter_meta()
310 def get_passwd(me, label):
312 Fetch and return the payload stored with the (opaque, binary) LABEL.
314 If there is no such payload then raise `KeyError'.
316 This calls the subclass's `_get_passwd' method, which may assume that the
320 return me._get_passwd(label)
322 def put_passwd(me, label, payload):
324 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
326 Any previous payload for LABEL is forgotten.
328 This calls the subclass's `_put_passwd' method, which may assume that the
329 database is open for writing.
332 me._put_passwd(label, payload)
334 def del_passwd(me, label):
336 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
338 If there is no such payload then raise `KeyError'.
340 This calls the subclass's `_del_passwd' method, which may assume that the
341 database is open for writing.
344 me._del_passwd(label, payload)
346 def iter_passwds(me):
348 Return an iterator over the stored password label/payload pairs.
350 This calls the subclass's `_iter_passwds' method, which may assume that
351 the database is open.
354 return me._iter_passwds()
356 class GDBMStorageBackend (StorageBackend):
358 My instances store password data in a GDBM database.
360 Metadata and password entries are mixed into the same database. The key
361 for a metadata item is simply its name; the key for a password entry is
362 the entry's label prefixed by `$', since we're guaranteed that no
363 metadata item name begins with `$'.
366 def _open(me, file, writep):
367 try: me._db = _G.open(file, writep and 'w' or 'r')
368 except _G.error, e: raise StorageBackendRefusal, e
370 def _create(me, file):
371 me._db = _G.open(file, 'n', 0600)
377 def _get_meta(me, name, default):
378 try: return me._db[name]
379 except KeyError: return default
381 def _put_meta(me, name, value):
384 def _del_meta(me, name):
388 k = me._db.firstkey()
390 if not k.startswith('$'): yield k, me._db[k]
391 k = me._db.nextkey(k)
393 def _get_passwd(me, label):
394 return me._db['$' + label]
396 def _put_passwd(me, label, payload):
397 me._db['$' + label] = payload
399 def _del_passwd(me, label):
400 del me._db['$' + label]
402 def _iter_passwds(me):
403 k = me._db.firstkey()
405 if k.startswith('$'): yield k[1:], me._db[k]
406 k = me._db.nextkey(k)
408 ###--------------------------------------------------------------------------
409 ### Password storage.
413 I represent a secure (ish) password store.
415 I can store short secrets, associated with textual names, in a way which
416 doesn't leak too much information about them.
418 I implement (some of) the Python mapping protocol.
420 I keep track of everything using a StorageBackend object. This contains
421 password entries, identified by cryptographic labels, and a number of
424 cipher Names the Catacomb cipher selected.
426 hash Names the Catacomb hash function selected.
428 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
429 length and concatenated, encrypted using the master
432 mac Names the Catacomb message authentication code selected.
434 magic A magic string for obscuring password tag names.
436 salt The salt for hashing the passphrase.
438 tag The master passphrase's tag, for the Pixie's benefit.
440 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
441 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
442 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
443 encrypted using the stored keys.
446 def __init__(me, file, writep = False):
448 Initialize a PW object from the database in FILE.
450 If WRITEP is false (the default) then the database is opened read-only;
451 if true then it may be written. Requests the database password from the
452 Pixie, which may cause interaction.
455 ## Open the database.
456 me.db = GDBMStorageBackend(file, writep)
458 ## Find out what crypto to use.
459 c = _C.gcciphers[me.db.get_meta('cipher')]
460 h = _C.gchashes[me.db.get_meta('hash')]
461 m = _C.gcmacs[me.db.get_meta('mac')]
463 ## Request the passphrase and extract the master keys.
464 tag = me.db.get_meta('tag')
465 ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt'))
467 b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
473 if not b.endp: raise ValueError, 'trailing junk'
475 ## Set the key, and stash it and the tag-hashing secret.
476 me.k = Crypto(c, h, m, me.ck, me.mk)
477 me.magic = me.k.decrypt(me.db.get_meta('magic'))
480 def create(cls, file, tag, c, h, m):
482 Create and initialize a new database FILE.
484 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
485 and a Pixie passphrase TAG.
487 This doesn't return a working object: it just creates the database file
488 and gets out of the way.
491 ## Set up the cryptography.
492 pp = _C.ppread(tag, _C.PMODE_VERIFY)
493 ppk = PPK(pp, c, h, m)
494 ck = _C.rand.block(c.keysz.default)
495 mk = _C.rand.block(c.keysz.default)
496 k = Crypto(c, h, m, ck, mk)
498 ## Set up and initialize the database.
499 kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
500 with GDBM.StorageBackend.create(file) as db:
501 db.put_meta('tag', tag)
502 db.put_meta('salt', ppk.salt)
503 db.put_meta('cipher', c.name)
504 db.put_meta('hash', h.name)
505 db.put_meta('mac', m.name)
506 db.put_meta('key', kct)
507 db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz)))
509 def keyxform(me, key):
510 """Transform the KEY (actually a password tag) into a password label."""
511 return me.k.h().hash(me.magic).hash(key).done()
515 Change the database password.
517 Requests the new password from the Pixie, which will probably cause
520 tag = me.db.get_meta('tag')
522 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
523 me.k.c.__class__, me.k.h, me.k.m.__class__)
524 kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
525 me.db.put_meta('key', kct)
526 me.db.put_meta('salt', ppk.salt)
528 def pack(me, key, value):
529 """Pack the KEY and VALUE into a ciphertext, and return it."""
531 b.putblk16(key).putblk16(value)
532 b.zero(((b.size + 255) & ~255) - b.size)
533 return me.k.encrypt(b)
537 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
539 Might raise DecryptError, of course.
541 b = _C.ReadBuffer(me.k.decrypt(ct))
548 def __getitem__(me, key):
549 """Return the password for the given KEY."""
550 try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1]
551 except KeyError: raise KeyError, key
553 def __setitem__(me, key, value):
554 """Associate the password VALUE with the KEY."""
555 me.db.put_passwd(me.keyxform(key), me.pack(key, value))
557 def __delitem__(me, key):
558 """Forget all about the KEY."""
559 try: me.db.del_passwd(me.keyxform(key))
560 except KeyError: raise KeyError, key
563 """Iterate over the known password tags."""
564 for _, pld in me.db.iter_passwds():
565 yield me.unpack(pld)[0]
571 def __exit__(me, excty, excval, exctb):
574 ###----- That's all, folks --------------------------------------------------