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 ### Python version portability.
40 def _iterkeys(dict): return dict.iterkeys()
41 def _itervalues(dict): return dict.itervalues()
42 def _iteritems(dict): return dict.iteritems()
44 def _bin(text
): return text
45 def _text(bin
): return bin
48 _CIPHER
= _bin('cipher:')
51 def _with_metaclass(meta
, *supers
):
52 return meta("#<anonymous base %s>" % meta
.__name__
,
53 supers
or (object,), dict())
55 def _excval(): return SYS
.exc_info()[1]
60 ###--------------------------------------------------------------------------
61 ### Text encoding utilities.
65 Answer whether S can be represented literally.
67 If True, then S can be stored literally, as a metadata item name or
68 value; if False, then S requires some kind of encoding.
70 return all(ch
.isalnum() or ch
in '-_:' for ch
in s
)
72 def _enc_metaname(name
):
73 """Encode NAME as a metadata item name, returning the result."""
80 if _literalp(ch
): sio
.write(ch
)
81 elif ch
== ' ': sio
.write('+')
82 else: sio
.write('%%%02x' % ord
(ch
))
85 def _dec_metaname(name
):
86 """Decode NAME as a metadata item name, returning the result."""
87 if not name
.startswith('!'):
98 sio
.write(chr(int(name
[i
:i
+ 2], 16)))
102 return sio
.getvalue()
105 """Encode S as base64, without newlines, and trimming `=' padding."""
106 return s
.encode('base64').replace('\n', '').rstrip('=')
108 """Decode S as base64 with trimmed `=' padding."""
109 return (s
+ '='*((4 - len(s
))%4)).decode('base64')
111 def _enc_metaval(val
):
112 """Encode VAL as a metadata item value, returning the result."""
113 if _literalp(val
): return val
114 else: return '?' + _b64(val
)
116 def _dec_metaval(val
):
117 """Decode VAL as a metadata item value, returning the result."""
118 if not val
.startswith('?'): return val
119 else: return _unb64(val
[1:])
121 ###--------------------------------------------------------------------------
122 ### Underlying cryptography.
124 class DecryptError (Exception):
126 I represent a failure to decrypt a message.
128 Usually this means that someone used the wrong key, though it can also
129 mean that a ciphertext has been modified.
133 class Crypto (object):
135 I represent a symmetric crypto transform.
137 There's currently only one transform implemented, which is the obvious
138 generic-composition construction: given a message m, and keys K0 and K1, we
139 choose an IV v, and compute:
141 * y = v || E(K0, v; m)
144 The final ciphertext is t || y.
147 def __init__(me
, c
, h
, m
, ck
, mk
):
149 Initialize the Crypto object with a given algorithm selection and keys.
151 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
152 keys CK and MK for C and M respectively.
160 Encrypt the message PT and return the resulting ciphertext.
162 blksz
= me
.c
.__class__
.blksz
165 iv
= _C
.rand
.block(blksz
)
168 b
.put(me
.c
.encrypt(pt
))
169 t
= me
.m().hash(b
).done()
170 return t
+ str(buffer(b
))
174 Decrypt the ciphertext CT, returning the plaintext.
176 Raises DecryptError if anything goes wrong.
178 blksz
= me
.c
.__class__
.blksz
179 tagsz
= me
.m
.__class__
.tagsz
180 b
= _C
.ReadBuffer(ct
)
189 if t
!= h
.done(): raise DecryptError
190 return me
.c
.decrypt(x
)
194 I represent a crypto transform whose keys are derived from a passphrase.
196 The password is salted and hashed; the salt is available as the `salt'
200 def __init__(me
, pp
, c
, h
, m
, salt
= None):
202 Initialize the PPK object with a passphrase and algorithm selection.
204 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
205 subclass M, and a SALT. The SALT may be None, if we're generating new
206 keys, indicating that a salt should be chosen randomly.
208 if not salt
: salt
= _C
.rand
.block(h
.hashsz
)
209 tag
= pp
+ _NUL
+ salt
210 Crypto
.__init__(me
, c
, h
, m
,
211 h().hash(_CIPHER
).hash(tag
).done(),
212 h().hash(_MAC
).hash(tag
).done())
215 ###--------------------------------------------------------------------------
218 class StorageBackendRefusal (Exception):
220 I signify that a StorageBackend subclass has refused to open a file.
222 This is used by the StorageBackend.open class method.
226 class StorageBackendClass (type):
228 I am a metaclass for StorageBackend classes.
230 My main feature is that I register my concrete instances (with a `NAME'
231 which is not `None') with the StorageBackend class.
233 def __init__(me
, name
, supers
, dict):
235 Register a new concrete StorageBackend subclass.
237 super(StorageBackendClass
, me
).__init__(name
, supers
, dict)
239 except AttributeError: pass
240 else: StorageBackend
.register_concrete_subclass(me
)
242 class StorageBackend (_with_metaclass(StorageBackendClass
)):
244 I provide basic protocol for password storage backends.
246 I'm an abstract class: you want one of my subclasses if you actually want
247 to do something useful. But I maintain a list of my subclasses and can
248 choose an appropriate one to open a database file you've found lying about.
250 Backends are responsible for storing and retrieving stuff, but not for the
251 cryptographic details. Backends need to store two kinds of information:
253 * metadata, consisting of a number of property names and their values;
256 * password mappings, consisting of a number of binary labels and
259 Backends need to implement the following ordinary methods. See the calling
260 methods for details of the subclass responsibilities.
262 BE._create(FILE) Create a new database in FILE; used by `create'.
264 BE._open(FILE, WRITEP)
265 Open the existing database FILE; used by `open'.
267 BE._close(ABRUPTP) Close the database, freeing up any resources. If
268 ABRUPTP then don't try to commit changes.
270 BE._get_meta(NAME, DEFAULT)
271 Return the value of the metadata item with the given
272 NAME, or DEFAULT if it doesn't exist; used by
275 BE._put_meta(NAME, VALUE)
276 Set the VALUE of the metadata item with the given
277 NAME, creating one if necessary; used by `put_meta'.
279 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
280 `KeyError' if there is no such item; used by
283 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
284 pairs; used by `iter_meta'.
286 BE._get_passwd(LABEL)
287 Return the password payload stored with the (binary)
288 LABEL; used by `get_passwd'.
290 BE._put_passwd(LABEL, PAYLOAD)
291 Associate the (binary) PAYLOAD with the LABEL,
292 forgetting any previous payload for that LABEL; used
295 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
298 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
299 pairs; used by `iter_passwds'.
301 Also, concrete subclasses should define the following class attributes.
303 NAME The name of the backend, so that the user can select
304 it when creating a new database.
306 PRIO An integer priority: backends are tried in decreasing
307 priority order when opening an existing database.
312 ## The registry of subclasses.
318 def register_concrete_subclass(sub
):
319 """Register a concrete subclass, so that `open' can try it."""
320 StorageBackend
.CLASSES
[sub
.NAME
] = sub
325 Return the concrete subclass with the given NAME.
327 Raise `KeyError' if the name isn't found.
329 return StorageBackend
.CLASSES
[name
]
333 """Return an iterator over the concrete subclasses."""
334 return _itervalues(StorageBackend
.CLASSES
)
337 def open(file, writep
= False):
338 """Open a database FILE, using some appropriate backend."""
340 for cls
in sorted(StorageBackend
.CLASSES
.values(), reverse
= True,
341 key
= lambda cls
: cls
.PRIO
):
342 try: return cls(file, writep
)
343 except StorageBackendRefusal
: pass
344 raise StorageBackendRefusal
347 def create(cls
, file):
349 Create a new database in the named FILE, using this backend.
351 Subclasses must implement the `_create' instance method.
353 return cls(writep
= True, _magic
= lambda me
: me
._create(file))
355 def __init__(me
, file = None, writep
= False, _magic
= None, *args
, **kw
):
359 Subclasses are not, in general, expected to override this: there's a
360 somewhat hairy protocol between the constructor and some of the class
361 methods. Instead, the main hook for customization is the subclass's
362 `_open' method, which is invoked in the usual case.
364 super(StorageBackend
, me
).__init__(*args
, **kw
)
365 if me
.NAME
is None: raise ValueError('abstract class')
366 if _magic
is not None: _magic(me
)
367 elif file is None: raise ValueError('missing file parameter')
368 else: me
._open(file, writep
)
372 def close(me
, abruptp
= False):
376 It is harmless to attempt to close a database which has been closed
377 already. Calls the subclass's `_close' method.
386 """Raise an error if the receiver has been closed."""
387 if not me
._livep
: raise ValueError('database is closed')
389 def _check_write(me
):
390 """Raise an error if the receiver is not open for writing."""
392 if not me
._writep
: raise ValueError('database is read-only')
394 def _check_meta_name(me
, name
):
396 Raise an error unless NAME is a valid name for a metadata item.
398 Metadata names may not start with `$': such names are reserved for
401 if name
.startswith('$'):
402 raise ValueError("invalid metadata key `%s'" % name
)
407 """Context protocol: make sure the database is closed on exit."""
409 def __exit__(me
, exctype
, excvalue
, exctb
):
410 """Context protocol: see `__enter__'."""
411 me
.close(excvalue
is not None)
415 def get_meta(me
, name
, default
= FAIL
):
417 Fetch the value for the metadata item NAME.
419 If no such item exists, then return DEFAULT if that was set; otherwise
422 This calls the subclass's `_get_meta' method, which should return the
423 requested item or return the given DEFAULT value. It may assume that the
424 name is valid and the database is open.
426 me
._check_meta_name(name
)
428 value
= me
._get_meta(name
, default
)
429 if value
is StorageBackend
.FAIL
: raise KeyError(name
)
432 def put_meta(me
, name
, value
):
434 Store VALUE in the metadata item called NAME.
436 This calls the subclass's `_put_meta' method, which may assume that the
437 name is valid and the database is open for writing.
439 me
._check_meta_name(name
)
441 me
._put_meta(name
, value
)
443 def del_meta(me
, name
):
445 Forget about the metadata item with the given NAME.
447 This calls the subclass's `_del_meta' method, which may assume that the
448 name is valid and the database is open for writing.
450 me
._check_meta_name(name
)
456 Return an iterator over the name/value metadata items.
458 This calls the subclass's `_iter_meta' method, which may assume that the
462 return me
._iter_meta()
464 def get_passwd(me
, label
):
466 Fetch and return the payload stored with the (opaque, binary) LABEL.
468 If there is no such payload then raise `KeyError'.
470 This calls the subclass's `_get_passwd' method, which may assume that the
474 return me
._get_passwd(label
)
476 def put_passwd(me
, label
, payload
):
478 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
480 Any previous payload for LABEL is forgotten.
482 This calls the subclass's `_put_passwd' method, which may assume that the
483 database is open for writing.
486 me
._put_passwd(label
, payload
)
488 def del_passwd(me
, label
):
490 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
492 If there is no such payload then raise `KeyError'.
494 This calls the subclass's `_del_passwd' method, which may assume that the
495 database is open for writing.
498 me
._del_passwd(label
)
500 def iter_passwds(me
):
502 Return an iterator over the stored password label/payload pairs.
504 This calls the subclass's `_iter_passwds' method, which may assume that
505 the database is open.
508 return me
._iter_passwds()
510 try: import gdbm
as _G
511 except ImportError: pass
513 class GDBMStorageBackend (StorageBackend
):
515 My instances store password data in a GDBM database.
517 Metadata and password entries are mixed into the same database. The key
518 for a metadata item is simply its name; the key for a password entry is
519 the entry's label prefixed by `$', since we're guaranteed that no
520 metadata item name begins with `$'.
525 def _open(me
, file, writep
):
526 try: me
._db
= _G
.open(file, writep
and 'w' or 'r')
527 except _G
.error
: raise StorageBackendRefusal(_excval())
529 def _create(me
, file):
530 me
._db
= _G
.open(file, 'n', _M600
)
532 def _close(me
, abruptp
):
536 def _get_meta(me
, name
, default
):
537 try: return me
._db
[name
]
538 except KeyError: return default
540 def _put_meta(me
, name
, value
):
543 def _del_meta(me
, name
):
547 k
= me
._db
.firstkey()
549 if not k
.startswith('$'): yield k
, me
._db
[k
]
550 k
= me
._db
.nextkey(k
)
552 def _get_passwd(me
, label
):
553 return me
._db
['$' + label
]
555 def _put_passwd(me
, label
, payload
):
556 me
._db
['$' + label
] = payload
558 def _del_passwd(me
, label
):
559 del me
._db
['$' + label
]
561 def _iter_passwds(me
):
562 k
= me
._db
.firstkey()
564 if k
.startswith('$'): yield k
[1:], me
._db
[k
]
565 k
= me
._db
.nextkey(k
)
567 try: import sqlite3
as _Q
568 except ImportError: pass
570 class SQLiteStorageBackend (StorageBackend
):
572 I represent a password database stored in SQLite.
574 Metadata and password items are stored in separate tables, so there's no
575 conflict. Some additional metadata is stored in the `meta' table, with
576 names beginning with `$' so as not to conflict with clients:
578 $version The schema version of the table.
584 def _open(me
, file, writep
):
586 me
._db
= _Q
.connect(file)
587 ver
= me
._query_scalar(
588 "SELECT value FROM meta WHERE name = '$version'",
590 except (_Q
.DatabaseError
, _Q
.OperationalError
):
591 raise StorageBackendRefusal(_excval())
592 if ver
is None: raise ValueError('database broken (missing $version)')
593 elif ver
< me
.VERSION
: me
._upgrade(ver
)
594 elif ver
> me
.VERSION
: raise ValueError \
595 ('unknown database schema version (%d > %d)' %
(ver
, me
.VERSION
))
597 def _create(me
, file):
598 fd
= _OS
.open(file, _OS
.O_WRONLY | _OS
.O_CREAT | _OS
.O_EXCL
, _M600
)
601 me
._db
= _Q
.connect(file)
605 name TEXT PRIMARY KEY NOT NULL,
606 value BLOB NOT NULL);
609 CREATE TABLE passwd (
610 label BLOB PRIMARY KEY NOT NULL,
611 payload BLOB NOT NULL);
614 INSERT INTO meta (name, value) VALUES ('$version', ?);
617 try: _OS
.unlink(file)
621 def _upgrade(me
, ver
):
622 """Upgrade the database from schema version VER."""
623 assert False, 'how embarrassing'
625 def _close(me
, abruptp
):
626 if not abruptp
: me
._db
.commit()
630 def _fetch_scalar(me
, c
, what
, default
= None):
632 except StopIteration: val
= default
635 except StopIteration: pass
636 else: raise ValueError('multiple matching records for %s' % what
)
639 def _query_scalar(me
, query
, what
, default
= None, args
= []):
641 c
.execute(query
, args
)
642 return me
._fetch_scalar(c
, what
, default
)
644 def _get_meta(me
, name
, default
):
645 v
= me
._query_scalar("SELECT value FROM meta WHERE name = ?",
646 "metadata item `%s'" % name
,
647 default
= default
, args
= [name
])
648 if v
is default
: return v
651 def _put_meta(me
, name
, value
):
653 c
.execute("INSERT OR REPLACE INTO meta (name, value) VALUES (?, ?)",
654 [name
, buffer(value
)])
656 def _del_meta(me
, name
):
658 c
.execute("DELETE FROM meta WHERE name = ?", [name
])
659 if not c
.rowcount
: raise KeyError(name
)
663 c
.execute("SELECT name, value FROM meta WHERE name NOT LIKE '$%'")
664 for k
, v
in c
: yield k
, str(v
)
666 def _get_passwd(me
, label
):
667 pld
= me
._query_scalar("SELECT payload FROM passwd WHERE label = ?",
668 "password", default
= None,
669 args
= [buffer(label
)])
670 if pld
is None: raise KeyError(label
)
673 def _put_passwd(me
, label
, payload
):
675 c
.execute("INSERT OR REPLACE INTO passwd (label, payload) "
677 [buffer(label
), buffer(payload
)])
679 def _del_passwd(me
, label
):
681 c
.execute("DELETE FROM passwd WHERE label = ?", [label
])
682 if not c
.rowcount
: raise KeyError(label
)
684 def _iter_passwds(me
):
686 c
.execute("SELECT label, payload FROM passwd")
687 for k
, v
in c
: yield str(k
), str(v
)
689 class PlainTextBackend (StorageBackend
):
691 I'm a utility base class for storage backends which use plain text files.
693 I provide subclasses with the following capabilities.
695 * Creating files, with given modes, optionally ensuring that the file
696 doesn't exist already.
698 * Parsing flat text files, checking leading magic, skipping comments, and
699 providing standard encodings of troublesome characters and binary
700 strings in metadata and password records. See below.
702 * Maintenance of metadata and password records in in-memory dictionaries,
703 with ready implementations of the necessary StorageBackend subclass
704 responsibility methods. (Subclasses can override these if they want to
705 make different arrangements.)
707 Metadata records are written with an optional prefix string chosen by the
708 caller, followed by a `NAME=VALUE' pair. The NAME is form-urlencoded and
709 prefixed with `!' if it contains strange characters; the VALUE is base64-
710 encoded (without the pointless trailing `=' padding) and prefixed with `?'
713 Password records are written with an optional prefix string chosen by the
714 caller, followed by a LABEL=PAYLOAD pair, both of which are base64-encoded
717 The following attributes are available for subclasses:
719 _meta Dictionary mapping metadata item names to their values.
720 Populated by `_parse_meta' and managed by `_get_meta' and
723 _pw Dictionary mapping password labels to encrypted payloads.
724 Populated by `_parse_passwd' and managed by `_get_passwd' and
727 _dirtyp Boolean: set if either of the dictionaries has been modified.
730 def __init__(me
, *args
, **kw
):
732 Hook for initialization.
734 Sets up the published instance attributes.
739 super(PlainTextBackend
, me
).__init__(*args
, **kw
)
741 def _create_file(me
, file, mode
= _M600
, freshp
= False):
743 Make sure FILE exists, creating it with the given MODE if necessary.
745 If FRESHP is true, then make sure the file did not exist previously.
746 Return a file object for the newly created file.
748 flags
= _OS
.O_CREAT | _OS
.O_WRONLY
749 if freshp
: flags |
= _OS
.O_EXCL
750 else: flags |
= _OS
.O_TRUNC
751 fd
= _OS
.open(file, flags
, mode
)
752 return _OS
.fdopen(fd
, 'w')
756 Set the `_dirtyp' flag.
758 Subclasses might find it useful to intercept this method.
762 def _eqsplit(me
, line
):
764 Extract the KEY, VALUE pair from a LINE of the form `KEY=VALUE'.
766 Raise `ValueError' if there is no `=' in the LINE.
769 return line
[:eq
], line
[eq
+ 1:]
771 def _parse_file(me
, file, magic
= None):
777 * Raise `StorageBackendRefusal' if that the first line doesn't match
778 MAGIC (if provided). MAGIC should not contain the terminating
781 * Ignore comments (beginning `#') and blank lines.
783 * Call `_parse_line' (provided by the subclass) for other lines.
785 with
open(file, 'r') as f
:
786 if magic
is not None:
787 if f
.readline().rstrip('\n') != magic
: raise StorageBackendRefusal
789 line
= line
.rstrip('\n')
790 if not line
or line
.startswith('#'): continue
793 def _write_file(me
, file, writebody
, mode
= _M600
, magic
= None):
795 Update FILE atomically.
797 The newly created file will have the given MODE. If MAGIC is given, then
798 write that as the first line. Calls WRITEBODY(F) to write the main body
799 of the file where F is a file object for the new file.
802 with me
._create_file(new
, mode
) as f
:
803 if magic
is not None: f
.write(magic
+ '\n')
805 _OS
.rename(new
, file)
807 def _parse_meta(me
, line
):
808 """Parse LINE as a metadata NAME=VALUE pair, and updates `_meta'."""
809 k
, v
= me
._eqsplit(line
)
810 me
._meta
[_dec_metaname(k
)] = _dec_metaval(v
)
812 def _write_meta(me
, f
, prefix
= ''):
813 """Write the metadata records to F, each with the given PREFIX."""
814 f
.write('\n## Metadata.\n')
815 for k
, v
in _iteritems(me
._meta
):
816 f
.write('%s%s=%s\n' %
(prefix
, _enc_metaname(k
), _enc_metaval(v
)))
818 def _get_meta(me
, name
, default
):
819 return me
._meta
.get(name
, default
)
820 def _put_meta(me
, name
, value
):
822 me
._meta
[name
] = value
823 def _del_meta(me
, name
):
827 return _iteritems(me
._meta
)
829 def _parse_passwd(me
, line
):
830 """Parse LINE as a password LABEL=PAYLOAD pair, and updates `_pw'."""
831 k
, v
= me
._eqsplit(line
)
832 me
._pw
[_unb64(k
)] = _unb64(v
)
834 def _write_passwd(me
, f
, prefix
= ''):
835 """Write the password records to F, each with the given PREFIX."""
836 f
.write('\n## Password data.\n')
837 for k
, v
in _iteritems(me
._pw
):
838 f
.write('%s%s=%s\n' %
(prefix
, _b64(k
), _b64(v
)))
840 def _get_passwd(me
, label
):
841 return me
._pw
[str(label
)]
842 def _put_passwd(me
, label
, payload
):
844 me
._pw
[str(label
)] = payload
845 def _del_passwd(me
, label
):
847 del me
._pw
[str(label
)]
848 def _iter_passwds(me
):
849 return _iteritems(me
._pw
)
851 class FlatFileStorageBackend (PlainTextBackend
):
853 I maintain a password database in a plain text file.
855 The text file consists of lines, as follows.
857 * Empty lines, and lines beginning with `#' (in the leftmost column only)
860 * Lines of the form `$LABEL=PAYLOAD' store password data. Both LABEL and
861 PAYLOAD are base64-encoded, without `=' padding.
863 * Lines of the form `NAME=VALUE' store metadata. If the NAME contains
864 characters other than alphanumerics, hyphens, underscores, and colons,
865 then it is form-urlencoded, and prefixed wth `!'. If the VALUE
866 contains such characters, then it is base64-encoded, without `='
867 padding, and prefixed with `?'.
869 * Other lines are erroneous.
871 The file is rewritten from scratch when it's changed: any existing
872 commentary is lost, and items may be reordered. There is no file locking,
873 but the file is updated atomically, by renaming.
875 It is expected that the FlatFileStorageBackend is used mostly for
876 diagnostics and transfer, rather than for a live system.
881 MAGIC
= '### pwsafe password database'
883 def _open(me
, file, writep
):
884 if not _OS
.path
.isfile(file): raise StorageBackendRefusal
885 me
._parse_file(file, magic
= me
.MAGIC
)
886 def _parse_line(me
, line
):
887 if line
.startswith('$'): me
._parse_passwd(line
[1:])
888 else: me
._parse_meta(line
)
890 def _create(me
, file):
891 with me
._create_file(file, freshp
= True) as f
: pass
895 def _close(me
, abruptp
):
896 if not abruptp
and me
._dirtyp
:
897 me
._write_file(me
._file
, me
._write_body
, magic
= me
.MAGIC
)
899 def _write_body(me
, f
):
901 me
._write_passwd(f
, '$')
903 class DirectoryStorageBackend (PlainTextBackend
):
905 I maintain a password database in a directory, with one file per password.
907 This makes password databases easy to maintain in a revision-control system
910 The directory is structured as follows.
912 dir/meta Contains metadata, similar to the `FlatFileBackend'.
914 dir/pw/LABEL Contains the (raw binary) payload for the given password
915 LABEL (base64-encoded, without the useless `=' padding, and
916 with `/' replaced by `.').
918 dir/tmp/ Contains temporary files used by the implementation.
922 METAMAGIC
= '### pwsafe password directory metadata'
924 def _open(me
, file, writep
):
925 if not _OS
.path
.isdir(file) or \
926 not _OS
.path
.isdir(_OS
.path
.join(file, 'pw')) or \
927 not _OS
.path
.isdir(_OS
.path
.join(file, 'tmp')) or \
928 not _OS
.path
.isfile(_OS
.path
.join(file, 'meta')):
929 raise StorageBackendRefusal
931 me
._parse_file(_OS
.path
.join(file, 'meta'), magic
= me
.METAMAGIC
)
932 def _parse_line(me
, line
):
935 def _create(me
, file):
936 _OS
.mkdir(file, _M700
)
937 _OS
.mkdir(_OS
.path
.join(file, 'pw'), _M700
)
938 _OS
.mkdir(_OS
.path
.join(file, 'tmp'), _M700
)
942 def _close(me
, abruptp
):
943 if not abruptp
and me
._dirtyp
:
944 me
._write_file(_OS
.path
.join(me
._dir
, 'meta'),
945 me
._write_meta
, magic
= me
.METAMAGIC
)
947 def _pwfile(me
, label
, dir = 'pw'):
948 return _OS
.path
.join(me
._dir
, dir, _b64(label
).replace('/', '.'))
949 def _get_passwd(me
, label
):
951 f
= open(me
._pwfile(label
), 'rb')
952 except (OSError, IOError):
953 if _excval().errno
== _E
.ENOENT
: raise KeyError(label
)
955 with f
: return f
.read()
956 def _put_passwd(me
, label
, payload
):
957 new
= me
._pwfile(label
, 'tmp')
958 fd
= _OS
.open(new
, _OS
.O_WRONLY | _OS
.O_CREAT | _OS
.O_TRUNC
, _M600
)
960 with
open(new
, 'wb') as f
: f
.write(payload
)
961 _OS
.rename(new
, me
._pwfile(label
))
962 def _del_passwd(me
, label
):
964 _OS
.remove(me
._pwfile(label
))
965 except (OSError, IOError):
966 if _excval().errno
== _E
.ENOENT
: raise KeyError(label
)
968 def _iter_passwds(me
):
969 pw
= _OS
.path
.join(me
._dir
, 'pw')
970 for i
in _OS
.listdir(pw
):
971 with
open(_OS
.path
.join(pw
, i
), 'rb') as f
: pld
= f
.read()
972 yield _unb64(i
.replace('.', '/')), pld
974 ###--------------------------------------------------------------------------
975 ### Password storage.
979 I represent a secure (ish) password store.
981 I can store short secrets, associated with textual names, in a way which
982 doesn't leak too much information about them.
984 I implement (some of) the Python mapping protocol.
986 I keep track of everything using a StorageBackend object. This contains
987 password entries, identified by cryptographic labels, and a number of
990 cipher Names the Catacomb cipher selected.
992 hash Names the Catacomb hash function selected.
994 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
995 length and concatenated, encrypted using the master
998 mac Names the Catacomb message authentication code selected.
1000 magic A magic string for obscuring password tag names.
1002 salt The salt for hashing the passphrase.
1004 tag The master passphrase's tag, for the Pixie's benefit.
1006 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
1007 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
1008 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
1009 encrypted using the stored keys.
1012 def __init__(me
, file, writep
= False):
1014 Initialize a PW object from the database in FILE.
1016 If WRITEP is false (the default) then the database is opened read-only;
1017 if true then it may be written. Requests the database password from the
1018 Pixie, which may cause interaction.
1021 ## Open the database.
1022 me
.db
= StorageBackend
.open(file, writep
)
1024 ## Find out what crypto to use.
1025 c
= _C
.gcciphers
[me
.db
.get_meta('cipher')]
1026 h
= _C
.gchashes
[me
.db
.get_meta('hash')]
1027 m
= _C
.gcmacs
[me
.db
.get_meta('mac')]
1029 ## Request the passphrase and extract the master keys.
1030 tag
= me
.db
.get_meta('tag')
1031 ppk
= PPK(_C
.ppread(tag
), c
, h
, m
, me
.db
.get_meta('salt'))
1033 b
= _C
.ReadBuffer(ppk
.decrypt(me
.db
.get_meta('key')))
1034 except DecryptError
:
1037 me
.ck
= b
.getblk16()
1038 me
.mk
= b
.getblk16()
1039 if not b
.endp
: raise ValueError('trailing junk')
1041 ## Set the key, and stash it and the tag-hashing secret.
1042 me
.k
= Crypto(c
, h
, m
, me
.ck
, me
.mk
)
1043 me
.magic
= me
.k
.decrypt(me
.db
.get_meta('magic'))
1046 def create(cls
, dbcls
, file, tag
, c
, h
, m
):
1048 Create and initialize a new database FILE using StorageBackend DBCLS.
1050 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
1051 and a Pixie passphrase TAG.
1053 This doesn't return a working object: it just creates the database file
1054 and gets out of the way.
1057 ## Set up the cryptography.
1058 pp
= _C
.ppread(tag
, _C
.PMODE_VERIFY
)
1059 ppk
= PPK(pp
, c
, h
, m
)
1060 ck
= _C
.rand
.block(c
.keysz
.default
)
1061 mk
= _C
.rand
.block(c
.keysz
.default
)
1062 k
= Crypto(c
, h
, m
, ck
, mk
)
1064 ## Set up and initialize the database.
1065 kct
= ppk
.encrypt(_C
.WriteBuffer().putblk16(ck
).putblk16(mk
))
1066 with dbcls
.create(file) as db
:
1067 db
.put_meta('tag', tag
)
1068 db
.put_meta('salt', ppk
.salt
)
1069 db
.put_meta('cipher', c
.name
)
1070 db
.put_meta('hash', h
.name
)
1071 db
.put_meta('mac', m
.name
)
1072 db
.put_meta('key', kct
)
1073 db
.put_meta('magic', k
.encrypt(_C
.rand
.block(h
.hashsz
)))
1075 def keyxform(me
, key
):
1076 """Transform the KEY (actually a password tag) into a password label."""
1077 return me
.k
.h().hash(me
.magic
).hash(key
).done()
1081 Change the database password.
1083 Requests the new password from the Pixie, which will probably cause
1086 tag
= me
.db
.get_meta('tag')
1088 ppk
= PPK(_C
.ppread(tag
, _C
.PMODE_VERIFY
),
1089 me
.k
.c
.__class__
, me
.k
.h
, me
.k
.m
.__class__
)
1090 kct
= ppk
.encrypt(_C
.WriteBuffer().putblk16(me
.ck
).putblk16(me
.mk
))
1091 me
.db
.put_meta('key', kct
)
1092 me
.db
.put_meta('salt', ppk
.salt
)
1094 def pack(me
, key
, value
):
1095 """Pack the KEY and VALUE into a ciphertext, and return it."""
1096 b
= _C
.WriteBuffer()
1097 b
.putblk16(key
).putblk16(value
)
1098 b
.zero(((b
.size
+ 255) & ~
255) - b
.size
)
1099 return me
.k
.encrypt(b
)
1103 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
1105 Might raise DecryptError, of course.
1107 b
= _C
.ReadBuffer(me
.k
.decrypt(ct
))
1109 value
= b
.getblk16()
1112 ## Mapping protocol.
1114 def __getitem__(me
, key
):
1115 """Return the password for the given KEY."""
1116 try: return me
.unpack(me
.db
.get_passwd(me
.keyxform(key
)))[1]
1117 except KeyError: raise KeyError(key
)
1119 def __setitem__(me
, key
, value
):
1120 """Associate the password VALUE with the KEY."""
1121 me
.db
.put_passwd(me
.keyxform(key
), me
.pack(key
, value
))
1123 def __delitem__(me
, key
):
1124 """Forget all about the KEY."""
1125 try: me
.db
.del_passwd(me
.keyxform(key
))
1126 except KeyError: raise KeyError(key
)
1129 """Iterate over the known password tags."""
1130 for _
, pld
in me
.db
.iter_passwds():
1131 yield me
.unpack(pld
)[0]
1133 ## Context protocol.
1137 def __exit__(me
, excty
, excval
, exctb
):
1138 me
.db
.close(excval
is not None)
1140 ###----- That's all, folks --------------------------------------------------