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
33 from cStringIO import StringIO as _StringIO
38 ###--------------------------------------------------------------------------
39 ### Text encoding utilities.
43 Answer whether S can be represented literally.
45 If True, then S can be stored literally, as a metadata item name or
46 value; if False, then S requires some kind of encoding.
48 return all(ch.isalnum() or ch in '-_:' for ch in s)
50 def _enc_metaname(name):
51 """Encode NAME as a metadata item name, returning the result."""
58 if _literalp(ch): sio.write(ch)
59 elif ch == ' ': sio.write('+')
60 else: sio.write('%%%02x' % ord(ch))
63 def _dec_metaname(name):
64 """Decode NAME as a metadata item name, returning the result."""
65 if not name.startswith('!'):
76 sio.write(chr(int(name[i:i + 2], 16)))
83 """Encode S as base64, without newlines, and trimming `=' padding."""
84 return s.encode('base64').translate(None, '\n=')
86 """Decode S as base64 with trimmed `=' padding."""
87 return (s + '='*((4 - len(s))%4)).decode('base64')
89 def _enc_metaval(val):
90 """Encode VAL as a metadata item value, returning the result."""
91 if _literalp(val): return val
92 else: return '?' + _b64(val)
94 def _dec_metaval(val):
95 """Decode VAL as a metadata item value, returning the result."""
96 if not val.startswith('?'): return val
97 else: return _unb64(val[1:])
99 ###--------------------------------------------------------------------------
100 ### Underlying cryptography.
102 class DecryptError (Exception):
104 I represent a failure to decrypt a message.
106 Usually this means that someone used the wrong key, though it can also
107 mean that a ciphertext has been modified.
111 class Crypto (object):
113 I represent a symmetric crypto transform.
115 There's currently only one transform implemented, which is the obvious
116 generic-composition construction: given a message m, and keys K0 and K1, we
117 choose an IV v, and compute:
119 * y = v || E(K0, v; m)
122 The final ciphertext is t || y.
125 def __init__(me, c, h, m, ck, mk):
127 Initialize the Crypto object with a given algorithm selection and keys.
129 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
130 keys CK and MK for C and M respectively.
138 Encrypt the message PT and return the resulting ciphertext.
140 blksz = me.c.__class__.blksz
143 iv = _C.rand.block(blksz)
146 b.put(me.c.encrypt(pt))
147 t = me.m().hash(b).done()
148 return t + str(buffer(b))
152 Decrypt the ciphertext CT, returning the plaintext.
154 Raises DecryptError if anything goes wrong.
156 blksz = me.c.__class__.blksz
157 tagsz = me.m.__class__.tagsz
158 b = _C.ReadBuffer(ct)
167 if t != h.done(): raise DecryptError
168 return me.c.decrypt(x)
172 I represent a crypto transform whose keys are derived from a passphrase.
174 The password is salted and hashed; the salt is available as the `salt'
178 def __init__(me, pp, c, h, m, salt = None):
180 Initialize the PPK object with a passphrase and algorithm selection.
182 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
183 subclass M, and a SALT. The SALT may be None, if we're generating new
184 keys, indicating that a salt should be chosen randomly.
186 if not salt: salt = _C.rand.block(h.hashsz)
187 tag = '%s\0%s' % (pp, salt)
188 Crypto.__init__(me, c, h, m,
189 h().hash('cipher:' + tag).done(),
190 h().hash('mac:' + tag).done())
193 ###--------------------------------------------------------------------------
196 class StorageBackendRefusal (Exception):
198 I signify that a StorageBackend subclass has refused to open a file.
200 This is used by the StorageBackend.open class method.
204 class StorageBackendClass (type):
206 I am a metaclass for StorageBackend classes.
208 My main feature is that I register my concrete instances (with a `NAME'
209 which is not `None') with the StorageBackend class.
211 def __init__(me, name, supers, dict):
213 Register a new concrete StorageBackend subclass.
215 super(StorageBackendClass, me).__init__(name, supers, dict)
216 if me.NAME is not None: StorageBackend.register_concrete_subclass(me)
218 class StorageBackend (object):
220 I provide basic protocol for password storage backends.
222 I'm an abstract class: you want one of my subclasses if you actually want
223 to do something useful. But I maintain a list of my subclasses and can
224 choose an appropriate one to open a database file you've found lying about.
226 Backends are responsible for storing and retrieving stuff, but not for the
227 cryptographic details. Backends need to store two kinds of information:
229 * metadata, consisting of a number of property names and their values;
232 * password mappings, consisting of a number of binary labels and
235 Backends need to implement the following ordinary methods. See the calling
236 methods for details of the subclass responsibilities.
238 BE._create(FILE) Create a new database in FILE; used by `create'.
240 BE._open(FILE, WRITEP)
241 Open the existing database FILE; used by `open'.
243 BE._close(ABRUPTP) Close the database, freeing up any resources. If
244 ABRUPTP then don't try to commit changes.
246 BE._get_meta(NAME, DEFAULT)
247 Return the value of the metadata item with the given
248 NAME, or DEFAULT if it doesn't exist; used by
251 BE._put_meta(NAME, VALUE)
252 Set the VALUE of the metadata item with the given
253 NAME, creating one if necessary; used by `put_meta'.
255 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
256 `KeyError' if there is no such item; used by
259 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
260 pairs; used by `iter_meta'.
262 BE._get_passwd(LABEL)
263 Return the password payload stored with the (binary)
264 LABEL; used by `get_passwd'.
266 BE._put_passwd(LABEL, PAYLOAD)
267 Associate the (binary) PAYLOAD with the LABEL,
268 forgetting any previous payload for that LABEL; used
271 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
274 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
275 pairs; used by `iter_passwds'.
277 Also, concrete subclasses should define the following class attributes.
279 NAME The name of the backend, so that the user can select
280 it when creating a new database.
282 PRIO An integer priority: backends are tried in decreasing
283 priority order when opening an existing database.
286 __metaclass__ = StorageBackendClass
290 ## The registry of subclasses.
296 def register_concrete_subclass(sub):
297 """Register a concrete subclass, so that `open' can try it."""
298 StorageBackend.CLASSES[sub.NAME] = sub
303 Return the concrete subclass with the given NAME.
305 Raise `KeyError' if the name isn't found.
307 return StorageBackend.CLASSES[name]
311 """Return an iterator over the concrete subclasses."""
312 return StorageBackend.CLASSES.itervalues()
315 def open(file, writep = False):
316 """Open a database FILE, using some appropriate backend."""
318 for cls in sorted(StorageBackend.CLASSES.values(), reverse = True,
319 key = lambda cls: cls.PRIO):
320 try: return cls(file, writep)
321 except StorageBackendRefusal: pass
322 raise StorageBackendRefusal
325 def create(cls, file):
327 Create a new database in the named FILE, using this backend.
329 Subclasses must implement the `_create' instance method.
331 return cls(writep = True, _magic = lambda me: me._create(file))
333 def __init__(me, file = None, writep = False, _magic = None, *args, **kw):
337 Subclasses are not, in general, expected to override this: there's a
338 somewhat hairy protocol between the constructor and some of the class
339 methods. Instead, the main hook for customization is the subclass's
340 `_open' method, which is invoked in the usual case.
342 super(StorageBackend, me).__init__(*args, **kw)
343 if me.NAME is None: raise ValueError, 'abstract class'
344 if _magic is not None: _magic(me)
345 elif file is None: raise ValueError, 'missing file parameter'
346 else: me._open(file, writep)
350 def close(me, abruptp = False):
354 It is harmless to attempt to close a database which has been closed
355 already. Calls the subclass's `_close' method.
364 """Raise an error if the receiver has been closed."""
365 if not me._livep: raise ValueError, 'database is closed'
367 def _check_write(me):
368 """Raise an error if the receiver is not open for writing."""
370 if not me._writep: raise ValueError, 'database is read-only'
372 def _check_meta_name(me, name):
374 Raise an error unless NAME is a valid name for a metadata item.
376 Metadata names may not start with `$': such names are reserved for
379 if name.startswith('$'):
380 raise ValueError, "invalid metadata key `%s'" % name
385 """Context protocol: make sure the database is closed on exit."""
387 def __exit__(me, exctype, excvalue, exctb):
388 """Context protocol: see `__enter__'."""
389 me.close(excvalue is not None)
393 def get_meta(me, name, default = FAIL):
395 Fetch the value for the metadata item NAME.
397 If no such item exists, then return DEFAULT if that was set; otherwise
400 This calls the subclass's `_get_meta' method, which should return the
401 requested item or return the given DEFAULT value. It may assume that the
402 name is valid and the database is open.
404 me._check_meta_name(name)
406 value = me._get_meta(name, default)
407 if value is StorageBackend.FAIL: raise KeyError, name
410 def put_meta(me, name, value):
412 Store VALUE in the metadata item called NAME.
414 This calls the subclass's `_put_meta' method, which may assume that the
415 name is valid and the database is open for writing.
417 me._check_meta_name(name)
419 me._put_meta(name, value)
421 def del_meta(me, name):
423 Forget about the metadata item with the given NAME.
425 This calls the subclass's `_del_meta' method, which may assume that the
426 name is valid and the database is open for writing.
428 me._check_meta_name(name)
434 Return an iterator over the name/value metadata items.
436 This calls the subclass's `_iter_meta' method, which may assume that the
440 return me._iter_meta()
442 def get_passwd(me, label):
444 Fetch and return the payload stored with the (opaque, binary) LABEL.
446 If there is no such payload then raise `KeyError'.
448 This calls the subclass's `_get_passwd' method, which may assume that the
452 return me._get_passwd(label)
454 def put_passwd(me, label, payload):
456 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
458 Any previous payload for LABEL is forgotten.
460 This calls the subclass's `_put_passwd' method, which may assume that the
461 database is open for writing.
464 me._put_passwd(label, payload)
466 def del_passwd(me, label):
468 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
470 If there is no such payload then raise `KeyError'.
472 This calls the subclass's `_del_passwd' method, which may assume that the
473 database is open for writing.
476 me._del_passwd(label, payload)
478 def iter_passwds(me):
480 Return an iterator over the stored password label/payload pairs.
482 This calls the subclass's `_iter_passwds' method, which may assume that
483 the database is open.
486 return me._iter_passwds()
488 class GDBMStorageBackend (StorageBackend):
490 My instances store password data in a GDBM database.
492 Metadata and password entries are mixed into the same database. The key
493 for a metadata item is simply its name; the key for a password entry is
494 the entry's label prefixed by `$', since we're guaranteed that no
495 metadata item name begins with `$'.
500 def _open(me, file, writep):
501 try: me._db = _G.open(file, writep and 'w' or 'r')
502 except _G.error, e: raise StorageBackendRefusal, e
504 def _create(me, file):
505 me._db = _G.open(file, 'n', 0600)
507 def _close(me, abruptp):
511 def _get_meta(me, name, default):
512 try: return me._db[name]
513 except KeyError: return default
515 def _put_meta(me, name, value):
518 def _del_meta(me, name):
522 k = me._db.firstkey()
524 if not k.startswith('$'): yield k, me._db[k]
525 k = me._db.nextkey(k)
527 def _get_passwd(me, label):
528 return me._db['$' + label]
530 def _put_passwd(me, label, payload):
531 me._db['$' + label] = payload
533 def _del_passwd(me, label):
534 del me._db['$' + label]
536 def _iter_passwds(me):
537 k = me._db.firstkey()
539 if k.startswith('$'): yield k[1:], me._db[k]
540 k = me._db.nextkey(k)
542 class PlainTextBackend (StorageBackend):
544 I'm a utility base class for storage backends which use plain text files.
546 I provide subclasses with the following capabilities.
548 * Creating files, with given modes, optionally ensuring that the file
549 doesn't exist already.
551 * Parsing flat text files, checking leading magic, skipping comments, and
552 providing standard encodings of troublesome characters and binary
553 strings in metadata and password records. See below.
555 * Maintenance of metadata and password records in in-memory dictionaries,
556 with ready implementations of the necessary StorageBackend subclass
557 responsibility methods. (Subclasses can override these if they want to
558 make different arrangements.)
560 Metadata records are written with an optional prefix string chosen by the
561 caller, followed by a `NAME=VALUE' pair. The NAME is form-urlencoded and
562 prefixed with `!' if it contains strange characters; the VALUE is base64-
563 encoded (without the pointless trailing `=' padding) and prefixed with `?'
566 Password records are written with an optional prefix string chosen by the
567 caller, followed by a LABEL=PAYLOAD pair, both of which are base64-encoded
570 The following attributes are available for subclasses:
572 _meta Dictionary mapping metadata item names to their values.
573 Populated by `_parse_meta' and managed by `_get_meta' and
576 _pw Dictionary mapping password labels to encrypted payloads.
577 Populated by `_parse_passwd' and managed by `_get_passwd' and
580 _dirtyp Boolean: set if either of the dictionaries has been modified.
583 def __init__(me, *args, **kw):
585 Hook for initialization.
587 Sets up the published instance attributes.
592 super(PlainTextBackend, me).__init__(*args, **kw)
594 def _create_file(me, file, mode = 0600, freshp = False):
596 Make sure FILE exists, creating it with the given MODE if necessary.
598 If FRESHP is true, then make sure the file did not exist previously.
599 Return a file object for the newly created file.
601 flags = _OS.O_CREAT | _OS.O_WRONLY
602 if freshp: flags |= _OS.O_EXCL
603 else: flags |= _OS.O_TRUNC
604 fd = _OS.open(file, flags, mode)
605 return _OS.fdopen(fd, 'w')
609 Set the `_dirtyp' flag.
611 Subclasses might find it useful to intercept this method.
615 def _eqsplit(me, line):
617 Extract the KEY, VALUE pair from a LINE of the form `KEY=VALUE'.
619 Raise `ValueError' if there is no `=' in the LINE.
622 return line[:eq], line[eq + 1:]
624 def _parse_file(me, file, magic = None):
630 * Raise `StorageBackendRefusal' if that the first line doesn't match
631 MAGIC (if provided). MAGIC should not contain the terminating
634 * Ignore comments (beginning `#') and blank lines.
636 * Call `_parse_line' (provided by the subclass) for other lines.
638 with open(file, 'r') as f:
639 if magic is not None:
640 if f.readline().rstrip('\n') != magic: raise StorageBackendRefusal
642 line = line.rstrip('\n')
643 if not line or line.startswith('#'): continue
646 def _write_file(me, file, writebody, mode = 0600, magic = None):
648 Update FILE atomically.
650 The newly created file will have the given MODE. If MAGIC is given, then
651 write that as the first line. Calls WRITEBODY(F) to write the main body
652 of the file where F is a file object for the new file.
655 with me._create_file(new, mode) as f:
656 if magic is not None: f.write(magic + '\n')
658 _OS.rename(new, file)
660 def _parse_meta(me, line):
661 """Parse LINE as a metadata NAME=VALUE pair, and updates `_meta'."""
662 k, v = me._eqsplit(line)
663 me._meta[_dec_metaname(k)] = _dec_metaval(v)
665 def _write_meta(me, f, prefix = ''):
666 """Write the metadata records to F, each with the given PREFIX."""
667 f.write('\n## Metadata.\n')
668 for k, v in me._meta.iteritems():
669 f.write('%s%s=%s\n' % (prefix, _enc_metaname(k), _enc_metaval(v)))
671 def _get_meta(me, name, default):
672 return me._meta.get(name, default)
673 def _put_meta(me, name, value):
675 me._meta[name] = value
676 def _del_meta(me, name):
680 return me._meta.iteritems()
682 def _parse_passwd(me, line):
683 """Parse LINE as a password LABEL=PAYLOAD pair, and updates `_pw'."""
684 k, v = me._eqsplit(line)
685 me._pw[_unb64(k)] = _unb64(v)
687 def _write_passwd(me, f, prefix = ''):
688 """Write the password records to F, each with the given PREFIX."""
689 f.write('\n## Password data.\n')
690 for k, v in me._pw.iteritems():
691 f.write('%s%s=%s\n' % (prefix, _b64(k), _b64(v)))
693 def _get_passwd(me, label):
694 return me._pw[str(label)]
695 def _put_passwd(me, label, payload):
697 me._pw[str(label)] = payload
698 def _del_passwd(me, label):
700 del me._pw[str(label)]
701 def _iter_passwds(me):
702 return me._pw.iteritems()
704 class FlatFileStorageBackend (PlainTextBackend):
706 I maintain a password database in a plain text file.
708 The text file consists of lines, as follows.
710 * Empty lines, and lines beginning with `#' (in the leftmost column only)
713 * Lines of the form `$LABEL=PAYLOAD' store password data. Both LABEL and
714 PAYLOAD are base64-encoded, without `=' padding.
716 * Lines of the form `NAME=VALUE' store metadata. If the NAME contains
717 characters other than alphanumerics, hyphens, underscores, and colons,
718 then it is form-urlencoded, and prefixed wth `!'. If the VALUE
719 contains such characters, then it is base64-encoded, without `='
720 padding, and prefixed with `?'.
722 * Other lines are erroneous.
724 The file is rewritten from scratch when it's changed: any existing
725 commentary is lost, and items may be reordered. There is no file locking,
726 but the file is updated atomically, by renaming.
728 It is expected that the FlatFileStorageBackend is used mostly for
729 diagnostics and transfer, rather than for a live system.
734 MAGIC = '### pwsafe password database'
736 def _open(me, file, writep):
737 if not _OS.path.isfile(file): raise StorageBackendRefusal
738 me._parse_file(file, magic = me.MAGIC)
739 def _parse_line(me, line):
740 if line.startswith('$'): me._parse_passwd(line[1:])
741 else: me._parse_meta(line)
743 def _create(me, file):
744 with me._create_file(file, freshp = True) as f: pass
748 def _close(me, abruptp):
749 if not abruptp and me._dirtyp:
750 me._write_file(me._file, me._write_body, magic = me.MAGIC)
752 def _write_body(me, f):
754 me._write_passwd(f, '$')
756 ###--------------------------------------------------------------------------
757 ### Password storage.
761 I represent a secure (ish) password store.
763 I can store short secrets, associated with textual names, in a way which
764 doesn't leak too much information about them.
766 I implement (some of) the Python mapping protocol.
768 I keep track of everything using a StorageBackend object. This contains
769 password entries, identified by cryptographic labels, and a number of
772 cipher Names the Catacomb cipher selected.
774 hash Names the Catacomb hash function selected.
776 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
777 length and concatenated, encrypted using the master
780 mac Names the Catacomb message authentication code selected.
782 magic A magic string for obscuring password tag names.
784 salt The salt for hashing the passphrase.
786 tag The master passphrase's tag, for the Pixie's benefit.
788 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
789 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
790 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
791 encrypted using the stored keys.
794 def __init__(me, file, writep = False):
796 Initialize a PW object from the database in FILE.
798 If WRITEP is false (the default) then the database is opened read-only;
799 if true then it may be written. Requests the database password from the
800 Pixie, which may cause interaction.
803 ## Open the database.
804 me.db = StorageBackend.open(file, writep)
806 ## Find out what crypto to use.
807 c = _C.gcciphers[me.db.get_meta('cipher')]
808 h = _C.gchashes[me.db.get_meta('hash')]
809 m = _C.gcmacs[me.db.get_meta('mac')]
811 ## Request the passphrase and extract the master keys.
812 tag = me.db.get_meta('tag')
813 ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt'))
815 b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
821 if not b.endp: raise ValueError, 'trailing junk'
823 ## Set the key, and stash it and the tag-hashing secret.
824 me.k = Crypto(c, h, m, me.ck, me.mk)
825 me.magic = me.k.decrypt(me.db.get_meta('magic'))
828 def create(cls, dbcls, file, tag, c, h, m):
830 Create and initialize a new database FILE using StorageBackend DBCLS.
832 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
833 and a Pixie passphrase TAG.
835 This doesn't return a working object: it just creates the database file
836 and gets out of the way.
839 ## Set up the cryptography.
840 pp = _C.ppread(tag, _C.PMODE_VERIFY)
841 ppk = PPK(pp, c, h, m)
842 ck = _C.rand.block(c.keysz.default)
843 mk = _C.rand.block(c.keysz.default)
844 k = Crypto(c, h, m, ck, mk)
846 ## Set up and initialize the database.
847 kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
848 with dbcls.create(file) as db:
849 db.put_meta('tag', tag)
850 db.put_meta('salt', ppk.salt)
851 db.put_meta('cipher', c.name)
852 db.put_meta('hash', h.name)
853 db.put_meta('mac', m.name)
854 db.put_meta('key', kct)
855 db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz)))
857 def keyxform(me, key):
858 """Transform the KEY (actually a password tag) into a password label."""
859 return me.k.h().hash(me.magic).hash(key).done()
863 Change the database password.
865 Requests the new password from the Pixie, which will probably cause
868 tag = me.db.get_meta('tag')
870 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
871 me.k.c.__class__, me.k.h, me.k.m.__class__)
872 kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
873 me.db.put_meta('key', kct)
874 me.db.put_meta('salt', ppk.salt)
876 def pack(me, key, value):
877 """Pack the KEY and VALUE into a ciphertext, and return it."""
879 b.putblk16(key).putblk16(value)
880 b.zero(((b.size + 255) & ~255) - b.size)
881 return me.k.encrypt(b)
885 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
887 Might raise DecryptError, of course.
889 b = _C.ReadBuffer(me.k.decrypt(ct))
896 def __getitem__(me, key):
897 """Return the password for the given KEY."""
898 try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1]
899 except KeyError: raise KeyError, key
901 def __setitem__(me, key, value):
902 """Associate the password VALUE with the KEY."""
903 me.db.put_passwd(me.keyxform(key), me.pack(key, value))
905 def __delitem__(me, key):
906 """Forget all about the KEY."""
907 try: me.db.del_passwd(me.keyxform(key))
908 except KeyError: raise KeyError, key
911 """Iterate over the known password tags."""
912 for _, pld in me.db.iter_passwds():
913 yield me.unpack(pld)[0]
919 def __exit__(me, excty, excval, exctb):
920 me.db.close(excval is not None)
922 ###----- That's all, folks --------------------------------------------------