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 class DirectoryStorageBackend (PlainTextBackend):
758 I maintain a password database in a directory, with one file per password.
760 This makes password databases easy to maintain in a revision-control system
763 The directory is structured as follows.
765 dir/meta Contains metadata, similar to the `FlatFileBackend'.
767 dir/pw/LABEL Contains the (raw binary) payload for the given password
768 LABEL (base64-encoded, without the useless `=' padding, and
769 with `/' replaced by `.').
771 dir/tmp/ Contains temporary files used by the implementation.
775 METAMAGIC = '### pwsafe password directory metadata'
777 def _open(me, file, writep):
778 if not _OS.path.isdir(file) or \
779 not _OS.path.isdir(_OS.path.join(file, 'pw')) or \
780 not _OS.path.isdir(_OS.path.join(file, 'tmp')) or \
781 not _OS.path.isfile(_OS.path.join(file, 'meta')):
782 raise StorageBackendRefusal
784 me._parse_file(_OS.path.join(file, 'meta'), magic = me.METAMAGIC)
785 def _parse_line(me, line):
788 def _create(me, file):
789 _OS.mkdir(file, 0700)
790 _OS.mkdir(_OS.path.join(file, 'pw'), 0700)
791 _OS.mkdir(_OS.path.join(file, 'tmp'), 0700)
795 def _close(me, abruptp):
796 if not abruptp and me._dirtyp:
797 me._write_file(_OS.path.join(me._dir, 'meta'),
798 me._write_meta, magic = me.METAMAGIC)
800 def _pwfile(me, label, dir = 'pw'):
801 return _OS.path.join(me._dir, dir, _b64(label).replace('/', '.'))
802 def _get_passwd(me, label):
804 f = open(me._pwfile(label), 'rb')
805 except (OSError, IOError), e:
806 if e.errno == _E.ENOENT: raise KeyError, label
808 with f: return f.read()
809 def _put_passwd(me, label, payload):
810 new = me._pwfile(label, 'tmp')
811 fd = _OS.open(new, _OS.O_WRONLY | _OS.O_CREAT | _OS.O_TRUNC, 0600)
813 with open(new, 'wb') as f: f.write(payload)
814 _OS.rename(new, me._pwfile(label))
815 def _del_passwd(me, label):
817 _OS.remove(me._pwfile(label))
818 except (OSError, IOError), e:
819 if e == _E.ENOENT: raise KeyError, label
821 def _iter_passwds(me):
822 pw = _OS.path.join(me._dir, 'pw')
823 for i in _OS.listdir(pw):
824 with open(_OS.path.join(pw, i), 'rb') as f: pld = f.read()
825 yield _unb64(i.replace('.', '/')), pld
827 ###--------------------------------------------------------------------------
828 ### Password storage.
832 I represent a secure (ish) password store.
834 I can store short secrets, associated with textual names, in a way which
835 doesn't leak too much information about them.
837 I implement (some of) the Python mapping protocol.
839 I keep track of everything using a StorageBackend object. This contains
840 password entries, identified by cryptographic labels, and a number of
843 cipher Names the Catacomb cipher selected.
845 hash Names the Catacomb hash function selected.
847 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
848 length and concatenated, encrypted using the master
851 mac Names the Catacomb message authentication code selected.
853 magic A magic string for obscuring password tag names.
855 salt The salt for hashing the passphrase.
857 tag The master passphrase's tag, for the Pixie's benefit.
859 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
860 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
861 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
862 encrypted using the stored keys.
865 def __init__(me, file, writep = False):
867 Initialize a PW object from the database in FILE.
869 If WRITEP is false (the default) then the database is opened read-only;
870 if true then it may be written. Requests the database password from the
871 Pixie, which may cause interaction.
874 ## Open the database.
875 me.db = StorageBackend.open(file, writep)
877 ## Find out what crypto to use.
878 c = _C.gcciphers[me.db.get_meta('cipher')]
879 h = _C.gchashes[me.db.get_meta('hash')]
880 m = _C.gcmacs[me.db.get_meta('mac')]
882 ## Request the passphrase and extract the master keys.
883 tag = me.db.get_meta('tag')
884 ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt'))
886 b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
892 if not b.endp: raise ValueError, 'trailing junk'
894 ## Set the key, and stash it and the tag-hashing secret.
895 me.k = Crypto(c, h, m, me.ck, me.mk)
896 me.magic = me.k.decrypt(me.db.get_meta('magic'))
899 def create(cls, dbcls, file, tag, c, h, m):
901 Create and initialize a new database FILE using StorageBackend DBCLS.
903 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
904 and a Pixie passphrase TAG.
906 This doesn't return a working object: it just creates the database file
907 and gets out of the way.
910 ## Set up the cryptography.
911 pp = _C.ppread(tag, _C.PMODE_VERIFY)
912 ppk = PPK(pp, c, h, m)
913 ck = _C.rand.block(c.keysz.default)
914 mk = _C.rand.block(c.keysz.default)
915 k = Crypto(c, h, m, ck, mk)
917 ## Set up and initialize the database.
918 kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
919 with dbcls.create(file) as db:
920 db.put_meta('tag', tag)
921 db.put_meta('salt', ppk.salt)
922 db.put_meta('cipher', c.name)
923 db.put_meta('hash', h.name)
924 db.put_meta('mac', m.name)
925 db.put_meta('key', kct)
926 db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz)))
928 def keyxform(me, key):
929 """Transform the KEY (actually a password tag) into a password label."""
930 return me.k.h().hash(me.magic).hash(key).done()
934 Change the database password.
936 Requests the new password from the Pixie, which will probably cause
939 tag = me.db.get_meta('tag')
941 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
942 me.k.c.__class__, me.k.h, me.k.m.__class__)
943 kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
944 me.db.put_meta('key', kct)
945 me.db.put_meta('salt', ppk.salt)
947 def pack(me, key, value):
948 """Pack the KEY and VALUE into a ciphertext, and return it."""
950 b.putblk16(key).putblk16(value)
951 b.zero(((b.size + 255) & ~255) - b.size)
952 return me.k.encrypt(b)
956 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
958 Might raise DecryptError, of course.
960 b = _C.ReadBuffer(me.k.decrypt(ct))
967 def __getitem__(me, key):
968 """Return the password for the given KEY."""
969 try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1]
970 except KeyError: raise KeyError, key
972 def __setitem__(me, key, value):
973 """Associate the password VALUE with the KEY."""
974 me.db.put_passwd(me.keyxform(key), me.pack(key, value))
976 def __delitem__(me, key):
977 """Forget all about the KEY."""
978 try: me.db.del_passwd(me.keyxform(key))
979 except KeyError: raise KeyError, key
982 """Iterate over the known password tags."""
983 for _, pld in me.db.iter_passwds():
984 yield me.unpack(pld)[0]
990 def __exit__(me, excty, excval, exctb):
991 me.db.close(excval is not None)
993 ###----- That's all, folks --------------------------------------------------