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
37 ###--------------------------------------------------------------------------
38 ### Text encoding utilities.
42 Answer whether S can be represented literally.
44 If True, then S can be stored literally, as a metadata item name or
45 value; if False, then S requires some kind of encoding.
47 return all(ch.isalnum() or ch in '-_:' for ch in s)
49 def _enc_metaname(name):
50 """Encode NAME as a metadata item name, returning the result."""
57 if _literalp(ch): sio.write(ch)
58 elif ch == ' ': sio.write('+')
59 else: sio.write('%%%02x' % ord(ch))
62 def _dec_metaname(name):
63 """Decode NAME as a metadata item name, returning the result."""
64 if not name.startswith('!'):
75 sio.write(chr(int(name[i:i + 2], 16)))
82 """Encode S as base64, without newlines, and trimming `=' padding."""
83 return s.encode('base64').translate(None, '\n=')
85 """Decode S as base64 with trimmed `=' padding."""
86 return (s + '='*((4 - len(s))%4)).decode('base64')
88 def _enc_metaval(val):
89 """Encode VAL as a metadata item value, returning the result."""
90 if _literalp(val): return val
91 else: return '?' + _b64(val)
93 def _dec_metaval(val):
94 """Decode VAL as a metadata item value, returning the result."""
95 if not val.startswith('?'): return val
96 else: return _unb64(val[1:])
98 ###--------------------------------------------------------------------------
99 ### Underlying cryptography.
101 class DecryptError (Exception):
103 I represent a failure to decrypt a message.
105 Usually this means that someone used the wrong key, though it can also
106 mean that a ciphertext has been modified.
110 class Crypto (object):
112 I represent a symmetric crypto transform.
114 There's currently only one transform implemented, which is the obvious
115 generic-composition construction: given a message m, and keys K0 and K1, we
116 choose an IV v, and compute:
118 * y = v || E(K0, v; m)
121 The final ciphertext is t || y.
124 def __init__(me, c, h, m, ck, mk):
126 Initialize the Crypto object with a given algorithm selection and keys.
128 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
129 keys CK and MK for C and M respectively.
137 Encrypt the message PT and return the resulting ciphertext.
139 blksz = me.c.__class__.blksz
142 iv = _C.rand.block(blksz)
145 b.put(me.c.encrypt(pt))
146 t = me.m().hash(b).done()
147 return t + str(buffer(b))
151 Decrypt the ciphertext CT, returning the plaintext.
153 Raises DecryptError if anything goes wrong.
155 blksz = me.c.__class__.blksz
156 tagsz = me.m.__class__.tagsz
157 b = _C.ReadBuffer(ct)
166 if t != h.done(): raise DecryptError
167 return me.c.decrypt(x)
171 I represent a crypto transform whose keys are derived from a passphrase.
173 The password is salted and hashed; the salt is available as the `salt'
177 def __init__(me, pp, c, h, m, salt = None):
179 Initialize the PPK object with a passphrase and algorithm selection.
181 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
182 subclass M, and a SALT. The SALT may be None, if we're generating new
183 keys, indicating that a salt should be chosen randomly.
185 if not salt: salt = _C.rand.block(h.hashsz)
186 tag = '%s\0%s' % (pp, salt)
187 Crypto.__init__(me, c, h, m,
188 h().hash('cipher:' + tag).done(),
189 h().hash('mac:' + tag).done())
192 ###--------------------------------------------------------------------------
195 class StorageBackendRefusal (Exception):
197 I signify that a StorageBackend subclass has refused to open a file.
199 This is used by the StorageBackend.open class method.
203 class StorageBackendClass (type):
205 I am a metaclass for StorageBackend classes.
207 My main feature is that I register my concrete instances (with a `NAME'
208 which is not `None') with the StorageBackend class.
210 def __init__(me, name, supers, dict):
212 Register a new concrete StorageBackend subclass.
214 super(StorageBackendClass, me).__init__(name, supers, dict)
215 if me.NAME is not None: StorageBackend.register_concrete_subclass(me)
217 class StorageBackend (object):
219 I provide basic protocol for password storage backends.
221 I'm an abstract class: you want one of my subclasses if you actually want
222 to do something useful. But I maintain a list of my subclasses and can
223 choose an appropriate one to open a database file you've found lying about.
225 Backends are responsible for storing and retrieving stuff, but not for the
226 cryptographic details. Backends need to store two kinds of information:
228 * metadata, consisting of a number of property names and their values;
231 * password mappings, consisting of a number of binary labels and
234 Backends need to implement the following ordinary methods. See the calling
235 methods for details of the subclass responsibilities.
237 BE._create(FILE) Create a new database in FILE; used by `create'.
239 BE._open(FILE, WRITEP)
240 Open the existing database FILE; used by `open'.
242 BE._close(ABRUPTP) Close the database, freeing up any resources. If
243 ABRUPTP then don't try to commit changes.
245 BE._get_meta(NAME, DEFAULT)
246 Return the value of the metadata item with the given
247 NAME, or DEFAULT if it doesn't exist; used by
250 BE._put_meta(NAME, VALUE)
251 Set the VALUE of the metadata item with the given
252 NAME, creating one if necessary; used by `put_meta'.
254 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
255 `KeyError' if there is no such item; used by
258 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
259 pairs; used by `iter_meta'.
261 BE._get_passwd(LABEL)
262 Return the password payload stored with the (binary)
263 LABEL; used by `get_passwd'.
265 BE._put_passwd(LABEL, PAYLOAD)
266 Associate the (binary) PAYLOAD with the LABEL,
267 forgetting any previous payload for that LABEL; used
270 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
273 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
274 pairs; used by `iter_passwds'.
276 Also, concrete subclasses should define the following class attributes.
278 NAME The name of the backend, so that the user can select
279 it when creating a new database.
281 PRIO An integer priority: backends are tried in decreasing
282 priority order when opening an existing database.
285 __metaclass__ = StorageBackendClass
289 ## The registry of subclasses.
295 def register_concrete_subclass(sub):
296 """Register a concrete subclass, so that `open' can try it."""
297 StorageBackend.CLASSES[sub.NAME] = sub
302 Return the concrete subclass with the given NAME.
304 Raise `KeyError' if the name isn't found.
306 return StorageBackend.CLASSES[name]
310 """Return an iterator over the concrete subclasses."""
311 return StorageBackend.CLASSES.itervalues()
314 def open(file, writep = False):
315 """Open a database FILE, using some appropriate backend."""
317 for cls in sorted(StorageBackend.CLASSES.values(), reverse = True,
318 key = lambda cls: cls.PRIO):
319 try: return cls(file, writep)
320 except StorageBackendRefusal: pass
321 raise StorageBackendRefusal
324 def create(cls, file):
326 Create a new database in the named FILE, using this backend.
328 Subclasses must implement the `_create' instance method.
330 return cls(writep = True, _magic = lambda me: me._create(file))
332 def __init__(me, file = None, writep = False, _magic = None, *args, **kw):
336 Subclasses are not, in general, expected to override this: there's a
337 somewhat hairy protocol between the constructor and some of the class
338 methods. Instead, the main hook for customization is the subclass's
339 `_open' method, which is invoked in the usual case.
341 super(StorageBackend, me).__init__(*args, **kw)
342 if me.NAME is None: raise ValueError, 'abstract class'
343 if _magic is not None: _magic(me)
344 elif file is None: raise ValueError, 'missing file parameter'
345 else: me._open(file, writep)
349 def close(me, abruptp = False):
353 It is harmless to attempt to close a database which has been closed
354 already. Calls the subclass's `_close' method.
363 """Raise an error if the receiver has been closed."""
364 if not me._livep: raise ValueError, 'database is closed'
366 def _check_write(me):
367 """Raise an error if the receiver is not open for writing."""
369 if not me._writep: raise ValueError, 'database is read-only'
371 def _check_meta_name(me, name):
373 Raise an error unless NAME is a valid name for a metadata item.
375 Metadata names may not start with `$': such names are reserved for
378 if name.startswith('$'):
379 raise ValueError, "invalid metadata key `%s'" % name
384 """Context protocol: make sure the database is closed on exit."""
386 def __exit__(me, exctype, excvalue, exctb):
387 """Context protocol: see `__enter__'."""
388 me.close(excvalue is not None)
392 def get_meta(me, name, default = FAIL):
394 Fetch the value for the metadata item NAME.
396 If no such item exists, then return DEFAULT if that was set; otherwise
399 This calls the subclass's `_get_meta' method, which should return the
400 requested item or return the given DEFAULT value. It may assume that the
401 name is valid and the database is open.
403 me._check_meta_name(name)
405 value = me._get_meta(name, default)
406 if value is StorageBackend.FAIL: raise KeyError, name
409 def put_meta(me, name, value):
411 Store VALUE in the metadata item called NAME.
413 This calls the subclass's `_put_meta' method, which may assume that the
414 name is valid and the database is open for writing.
416 me._check_meta_name(name)
418 me._put_meta(name, value)
420 def del_meta(me, name):
422 Forget about the metadata item with the given NAME.
424 This calls the subclass's `_del_meta' method, which may assume that the
425 name is valid and the database is open for writing.
427 me._check_meta_name(name)
433 Return an iterator over the name/value metadata items.
435 This calls the subclass's `_iter_meta' method, which may assume that the
439 return me._iter_meta()
441 def get_passwd(me, label):
443 Fetch and return the payload stored with the (opaque, binary) LABEL.
445 If there is no such payload then raise `KeyError'.
447 This calls the subclass's `_get_passwd' method, which may assume that the
451 return me._get_passwd(label)
453 def put_passwd(me, label, payload):
455 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
457 Any previous payload for LABEL is forgotten.
459 This calls the subclass's `_put_passwd' method, which may assume that the
460 database is open for writing.
463 me._put_passwd(label, payload)
465 def del_passwd(me, label):
467 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
469 If there is no such payload then raise `KeyError'.
471 This calls the subclass's `_del_passwd' method, which may assume that the
472 database is open for writing.
475 me._del_passwd(label, payload)
477 def iter_passwds(me):
479 Return an iterator over the stored password label/payload pairs.
481 This calls the subclass's `_iter_passwds' method, which may assume that
482 the database is open.
485 return me._iter_passwds()
487 try: import gdbm as _G
488 except ImportError: pass
490 class GDBMStorageBackend (StorageBackend):
492 My instances store password data in a GDBM database.
494 Metadata and password entries are mixed into the same database. The key
495 for a metadata item is simply its name; the key for a password entry is
496 the entry's label prefixed by `$', since we're guaranteed that no
497 metadata item name begins with `$'.
502 def _open(me, file, writep):
503 try: me._db = _G.open(file, writep and 'w' or 'r')
504 except _G.error, e: raise StorageBackendRefusal, e
506 def _create(me, file):
507 me._db = _G.open(file, 'n', 0600)
509 def _close(me, abruptp):
513 def _get_meta(me, name, default):
514 try: return me._db[name]
515 except KeyError: return default
517 def _put_meta(me, name, value):
520 def _del_meta(me, name):
524 k = me._db.firstkey()
526 if not k.startswith('$'): yield k, me._db[k]
527 k = me._db.nextkey(k)
529 def _get_passwd(me, label):
530 return me._db['$' + label]
532 def _put_passwd(me, label, payload):
533 me._db['$' + label] = payload
535 def _del_passwd(me, label):
536 del me._db['$' + label]
538 def _iter_passwds(me):
539 k = me._db.firstkey()
541 if k.startswith('$'): yield k[1:], me._db[k]
542 k = me._db.nextkey(k)
544 class PlainTextBackend (StorageBackend):
546 I'm a utility base class for storage backends which use plain text files.
548 I provide subclasses with the following capabilities.
550 * Creating files, with given modes, optionally ensuring that the file
551 doesn't exist already.
553 * Parsing flat text files, checking leading magic, skipping comments, and
554 providing standard encodings of troublesome characters and binary
555 strings in metadata and password records. See below.
557 * Maintenance of metadata and password records in in-memory dictionaries,
558 with ready implementations of the necessary StorageBackend subclass
559 responsibility methods. (Subclasses can override these if they want to
560 make different arrangements.)
562 Metadata records are written with an optional prefix string chosen by the
563 caller, followed by a `NAME=VALUE' pair. The NAME is form-urlencoded and
564 prefixed with `!' if it contains strange characters; the VALUE is base64-
565 encoded (without the pointless trailing `=' padding) and prefixed with `?'
568 Password records are written with an optional prefix string chosen by the
569 caller, followed by a LABEL=PAYLOAD pair, both of which are base64-encoded
572 The following attributes are available for subclasses:
574 _meta Dictionary mapping metadata item names to their values.
575 Populated by `_parse_meta' and managed by `_get_meta' and
578 _pw Dictionary mapping password labels to encrypted payloads.
579 Populated by `_parse_passwd' and managed by `_get_passwd' and
582 _dirtyp Boolean: set if either of the dictionaries has been modified.
585 def __init__(me, *args, **kw):
587 Hook for initialization.
589 Sets up the published instance attributes.
594 super(PlainTextBackend, me).__init__(*args, **kw)
596 def _create_file(me, file, mode = 0600, freshp = False):
598 Make sure FILE exists, creating it with the given MODE if necessary.
600 If FRESHP is true, then make sure the file did not exist previously.
601 Return a file object for the newly created file.
603 flags = _OS.O_CREAT | _OS.O_WRONLY
604 if freshp: flags |= _OS.O_EXCL
605 else: flags |= _OS.O_TRUNC
606 fd = _OS.open(file, flags, mode)
607 return _OS.fdopen(fd, 'w')
611 Set the `_dirtyp' flag.
613 Subclasses might find it useful to intercept this method.
617 def _eqsplit(me, line):
619 Extract the KEY, VALUE pair from a LINE of the form `KEY=VALUE'.
621 Raise `ValueError' if there is no `=' in the LINE.
624 return line[:eq], line[eq + 1:]
626 def _parse_file(me, file, magic = None):
632 * Raise `StorageBackendRefusal' if that the first line doesn't match
633 MAGIC (if provided). MAGIC should not contain the terminating
636 * Ignore comments (beginning `#') and blank lines.
638 * Call `_parse_line' (provided by the subclass) for other lines.
640 with open(file, 'r') as f:
641 if magic is not None:
642 if f.readline().rstrip('\n') != magic: raise StorageBackendRefusal
644 line = line.rstrip('\n')
645 if not line or line.startswith('#'): continue
648 def _write_file(me, file, writebody, mode = 0600, magic = None):
650 Update FILE atomically.
652 The newly created file will have the given MODE. If MAGIC is given, then
653 write that as the first line. Calls WRITEBODY(F) to write the main body
654 of the file where F is a file object for the new file.
657 with me._create_file(new, mode) as f:
658 if magic is not None: f.write(magic + '\n')
660 _OS.rename(new, file)
662 def _parse_meta(me, line):
663 """Parse LINE as a metadata NAME=VALUE pair, and updates `_meta'."""
664 k, v = me._eqsplit(line)
665 me._meta[_dec_metaname(k)] = _dec_metaval(v)
667 def _write_meta(me, f, prefix = ''):
668 """Write the metadata records to F, each with the given PREFIX."""
669 f.write('\n## Metadata.\n')
670 for k, v in me._meta.iteritems():
671 f.write('%s%s=%s\n' % (prefix, _enc_metaname(k), _enc_metaval(v)))
673 def _get_meta(me, name, default):
674 return me._meta.get(name, default)
675 def _put_meta(me, name, value):
677 me._meta[name] = value
678 def _del_meta(me, name):
682 return me._meta.iteritems()
684 def _parse_passwd(me, line):
685 """Parse LINE as a password LABEL=PAYLOAD pair, and updates `_pw'."""
686 k, v = me._eqsplit(line)
687 me._pw[_unb64(k)] = _unb64(v)
689 def _write_passwd(me, f, prefix = ''):
690 """Write the password records to F, each with the given PREFIX."""
691 f.write('\n## Password data.\n')
692 for k, v in me._pw.iteritems():
693 f.write('%s%s=%s\n' % (prefix, _b64(k), _b64(v)))
695 def _get_passwd(me, label):
696 return me._pw[str(label)]
697 def _put_passwd(me, label, payload):
699 me._pw[str(label)] = payload
700 def _del_passwd(me, label):
702 del me._pw[str(label)]
703 def _iter_passwds(me):
704 return me._pw.iteritems()
706 class FlatFileStorageBackend (PlainTextBackend):
708 I maintain a password database in a plain text file.
710 The text file consists of lines, as follows.
712 * Empty lines, and lines beginning with `#' (in the leftmost column only)
715 * Lines of the form `$LABEL=PAYLOAD' store password data. Both LABEL and
716 PAYLOAD are base64-encoded, without `=' padding.
718 * Lines of the form `NAME=VALUE' store metadata. If the NAME contains
719 characters other than alphanumerics, hyphens, underscores, and colons,
720 then it is form-urlencoded, and prefixed wth `!'. If the VALUE
721 contains such characters, then it is base64-encoded, without `='
722 padding, and prefixed with `?'.
724 * Other lines are erroneous.
726 The file is rewritten from scratch when it's changed: any existing
727 commentary is lost, and items may be reordered. There is no file locking,
728 but the file is updated atomically, by renaming.
730 It is expected that the FlatFileStorageBackend is used mostly for
731 diagnostics and transfer, rather than for a live system.
736 MAGIC = '### pwsafe password database'
738 def _open(me, file, writep):
739 if not _OS.path.isfile(file): raise StorageBackendRefusal
740 me._parse_file(file, magic = me.MAGIC)
741 def _parse_line(me, line):
742 if line.startswith('$'): me._parse_passwd(line[1:])
743 else: me._parse_meta(line)
745 def _create(me, file):
746 with me._create_file(file, freshp = True) as f: pass
750 def _close(me, abruptp):
751 if not abruptp and me._dirtyp:
752 me._write_file(me._file, me._write_body, magic = me.MAGIC)
754 def _write_body(me, f):
756 me._write_passwd(f, '$')
758 class DirectoryStorageBackend (PlainTextBackend):
760 I maintain a password database in a directory, with one file per password.
762 This makes password databases easy to maintain in a revision-control system
765 The directory is structured as follows.
767 dir/meta Contains metadata, similar to the `FlatFileBackend'.
769 dir/pw/LABEL Contains the (raw binary) payload for the given password
770 LABEL (base64-encoded, without the useless `=' padding, and
771 with `/' replaced by `.').
773 dir/tmp/ Contains temporary files used by the implementation.
777 METAMAGIC = '### pwsafe password directory metadata'
779 def _open(me, file, writep):
780 if not _OS.path.isdir(file) or \
781 not _OS.path.isdir(_OS.path.join(file, 'pw')) or \
782 not _OS.path.isdir(_OS.path.join(file, 'tmp')) or \
783 not _OS.path.isfile(_OS.path.join(file, 'meta')):
784 raise StorageBackendRefusal
786 me._parse_file(_OS.path.join(file, 'meta'), magic = me.METAMAGIC)
787 def _parse_line(me, line):
790 def _create(me, file):
791 _OS.mkdir(file, 0700)
792 _OS.mkdir(_OS.path.join(file, 'pw'), 0700)
793 _OS.mkdir(_OS.path.join(file, 'tmp'), 0700)
797 def _close(me, abruptp):
798 if not abruptp and me._dirtyp:
799 me._write_file(_OS.path.join(me._dir, 'meta'),
800 me._write_meta, magic = me.METAMAGIC)
802 def _pwfile(me, label, dir = 'pw'):
803 return _OS.path.join(me._dir, dir, _b64(label).replace('/', '.'))
804 def _get_passwd(me, label):
806 f = open(me._pwfile(label), 'rb')
807 except (OSError, IOError), e:
808 if e.errno == _E.ENOENT: raise KeyError, label
810 with f: return f.read()
811 def _put_passwd(me, label, payload):
812 new = me._pwfile(label, 'tmp')
813 fd = _OS.open(new, _OS.O_WRONLY | _OS.O_CREAT | _OS.O_TRUNC, 0600)
815 with open(new, 'wb') as f: f.write(payload)
816 _OS.rename(new, me._pwfile(label))
817 def _del_passwd(me, label):
819 _OS.remove(me._pwfile(label))
820 except (OSError, IOError), e:
821 if e == _E.ENOENT: raise KeyError, label
823 def _iter_passwds(me):
824 pw = _OS.path.join(me._dir, 'pw')
825 for i in _OS.listdir(pw):
826 with open(_OS.path.join(pw, i), 'rb') as f: pld = f.read()
827 yield _unb64(i.replace('.', '/')), pld
829 ###--------------------------------------------------------------------------
830 ### Password storage.
834 I represent a secure (ish) password store.
836 I can store short secrets, associated with textual names, in a way which
837 doesn't leak too much information about them.
839 I implement (some of) the Python mapping protocol.
841 I keep track of everything using a StorageBackend object. This contains
842 password entries, identified by cryptographic labels, and a number of
845 cipher Names the Catacomb cipher selected.
847 hash Names the Catacomb hash function selected.
849 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
850 length and concatenated, encrypted using the master
853 mac Names the Catacomb message authentication code selected.
855 magic A magic string for obscuring password tag names.
857 salt The salt for hashing the passphrase.
859 tag The master passphrase's tag, for the Pixie's benefit.
861 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
862 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
863 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
864 encrypted using the stored keys.
867 def __init__(me, file, writep = False):
869 Initialize a PW object from the database in FILE.
871 If WRITEP is false (the default) then the database is opened read-only;
872 if true then it may be written. Requests the database password from the
873 Pixie, which may cause interaction.
876 ## Open the database.
877 me.db = StorageBackend.open(file, writep)
879 ## Find out what crypto to use.
880 c = _C.gcciphers[me.db.get_meta('cipher')]
881 h = _C.gchashes[me.db.get_meta('hash')]
882 m = _C.gcmacs[me.db.get_meta('mac')]
884 ## Request the passphrase and extract the master keys.
885 tag = me.db.get_meta('tag')
886 ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt'))
888 b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
894 if not b.endp: raise ValueError, 'trailing junk'
896 ## Set the key, and stash it and the tag-hashing secret.
897 me.k = Crypto(c, h, m, me.ck, me.mk)
898 me.magic = me.k.decrypt(me.db.get_meta('magic'))
901 def create(cls, dbcls, file, tag, c, h, m):
903 Create and initialize a new database FILE using StorageBackend DBCLS.
905 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
906 and a Pixie passphrase TAG.
908 This doesn't return a working object: it just creates the database file
909 and gets out of the way.
912 ## Set up the cryptography.
913 pp = _C.ppread(tag, _C.PMODE_VERIFY)
914 ppk = PPK(pp, c, h, m)
915 ck = _C.rand.block(c.keysz.default)
916 mk = _C.rand.block(c.keysz.default)
917 k = Crypto(c, h, m, ck, mk)
919 ## Set up and initialize the database.
920 kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
921 with dbcls.create(file) as db:
922 db.put_meta('tag', tag)
923 db.put_meta('salt', ppk.salt)
924 db.put_meta('cipher', c.name)
925 db.put_meta('hash', h.name)
926 db.put_meta('mac', m.name)
927 db.put_meta('key', kct)
928 db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz)))
930 def keyxform(me, key):
931 """Transform the KEY (actually a password tag) into a password label."""
932 return me.k.h().hash(me.magic).hash(key).done()
936 Change the database password.
938 Requests the new password from the Pixie, which will probably cause
941 tag = me.db.get_meta('tag')
943 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
944 me.k.c.__class__, me.k.h, me.k.m.__class__)
945 kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
946 me.db.put_meta('key', kct)
947 me.db.put_meta('salt', ppk.salt)
949 def pack(me, key, value):
950 """Pack the KEY and VALUE into a ciphertext, and return it."""
952 b.putblk16(key).putblk16(value)
953 b.zero(((b.size + 255) & ~255) - b.size)
954 return me.k.encrypt(b)
958 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
960 Might raise DecryptError, of course.
962 b = _C.ReadBuffer(me.k.decrypt(ct))
969 def __getitem__(me, key):
970 """Return the password for the given KEY."""
971 try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1]
972 except KeyError: raise KeyError, key
974 def __setitem__(me, key, value):
975 """Associate the password VALUE with the KEY."""
976 me.db.put_passwd(me.keyxform(key), me.pack(key, value))
978 def __delitem__(me, key):
979 """Forget all about the KEY."""
980 try: me.db.del_passwd(me.keyxform(key))
981 except KeyError: raise KeyError, key
984 """Iterate over the known password tags."""
985 for _, pld in me.db.iter_passwds():
986 yield me.unpack(pld)[0]
992 def __exit__(me, excty, excval, exctb):
993 me.db.close(excval is not None)
995 ###----- That's all, folks --------------------------------------------------