3 ### Management of a secure password database
5 ### (c) 2005 Straylight/Edgeware
8 ###----- Licensing notice ---------------------------------------------------
10 ### This file is part of the Python interface to Catacomb.
12 ### Catacomb/Python is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU General Public License as published by
14 ### the Free Software Foundation; either version 2 of the License, or
15 ### (at your option) any later version.
17 ### Catacomb/Python is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ### GNU General Public License for more details.
22 ### You should have received a copy of the GNU General Public License along
23 ### with Catacomb/Python; if not, write to the Free Software Foundation,
24 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26 ###--------------------------------------------------------------------------
29 from __future__
import with_statement
34 from cStringIO
import StringIO
as _StringIO
38 ###--------------------------------------------------------------------------
39 ### Python version portability.
41 def _iterkeys(dict): return dict.iterkeys()
42 def _itervalues(dict): return dict.itervalues()
43 def _iteritems(dict): return dict.iteritems()
45 def _bin(text
): return text
46 def _text(bin
): return bin
49 _CIPHER
= _bin('cipher:')
52 def _with_metaclass(meta
, *supers
):
53 return meta("#<anonymous base %s>" % meta
.__name__
,
54 supers
or (object,), dict())
56 def _excval(): return _SYS
.exc_info()[1]
61 ###--------------------------------------------------------------------------
62 ### Text encoding utilities.
66 Answer whether S can be represented literally.
68 If True, then S can be stored literally, as a metadata item name or
69 value; if False, then S requires some kind of encoding.
71 return all(ch
.isalnum() or ch
in '-_:' for ch
in s
)
73 def _enc_metaname(name
):
74 """Encode NAME as a metadata item name, returning the result."""
81 if _literalp(ch
): sio
.write(ch
)
82 elif ch
== ' ': sio
.write('+')
83 else: sio
.write('%%%02x' % ord
(ch
))
86 def _dec_metaname(name
):
87 """Decode NAME as a metadata item name, returning the result."""
88 if not name
.startswith('!'):
99 sio
.write(chr(int(name
[i
:i
+ 2], 16)))
103 return sio
.getvalue()
106 """Encode S as base64, without newlines, and trimming `=' padding."""
107 return _text(_B
.b2a_base64(s
)).replace('\n', '').rstrip('=')
109 """Decode S as base64 with trimmed `=' padding."""
110 return _B
.a2b_base64(s
+ '='*((4 - len(s
))%4))
112 def _enc_metaval(val
):
113 """Encode VAL as a metadata item value, returning the result."""
114 if _literalp(val
): return val
115 else: return '?' + _b64(val
)
117 def _dec_metaval(val
):
118 """Decode VAL as a metadata item value, returning the result."""
119 if not val
.startswith('?'): return val
120 else: return _unb64(val
[1:])
122 ###--------------------------------------------------------------------------
123 ### Underlying cryptography.
125 class DecryptError (Exception):
127 I represent a failure to decrypt a message.
129 Usually this means that someone used the wrong key, though it can also
130 mean that a ciphertext has been modified.
134 class Crypto (object):
136 I represent a symmetric crypto transform.
138 There's currently only one transform implemented, which is the obvious
139 generic-composition construction: given a message m, and keys K0 and K1, we
140 choose an IV v, and compute:
142 * y = v || E(K0, v; m)
145 The final ciphertext is t || y.
148 def __init__(me
, c
, h
, m
, ck
, mk
):
150 Initialize the Crypto object with a given algorithm selection and keys.
152 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
153 keys CK and MK for C and M respectively.
161 Encrypt the message PT and return the resulting ciphertext.
163 blksz
= me
.c
.__class__
.blksz
166 iv
= _C
.rand
.block(blksz
)
169 b
.put(me
.c
.encrypt(pt
))
170 t
= me
.m().hash(b
).done()
171 return t
+ str(buffer(b
))
175 Decrypt the ciphertext CT, returning the plaintext.
177 Raises DecryptError if anything goes wrong.
179 blksz
= me
.c
.__class__
.blksz
180 tagsz
= me
.m
.__class__
.tagsz
181 b
= _C
.ReadBuffer(ct
)
190 if t
!= h
.done(): raise DecryptError
191 return me
.c
.decrypt(x
)
195 I represent a crypto transform whose keys are derived from a passphrase.
197 The password is salted and hashed; the salt is available as the `salt'
201 def __init__(me
, pp
, c
, h
, m
, salt
= None):
203 Initialize the PPK object with a passphrase and algorithm selection.
205 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
206 subclass M, and a SALT. The SALT may be None, if we're generating new
207 keys, indicating that a salt should be chosen randomly.
209 if not salt
: salt
= _C
.rand
.block(h
.hashsz
)
210 tag
= pp
+ _NUL
+ salt
211 Crypto
.__init__(me
, c
, h
, m
,
212 h().hash(_CIPHER
).hash(tag
).done(),
213 h().hash(_MAC
).hash(tag
).done())
216 ###--------------------------------------------------------------------------
219 class StorageBackendRefusal (Exception):
221 I signify that a StorageBackend subclass has refused to open a file.
223 This is used by the StorageBackend.open class method.
227 class StorageBackendClass (type):
229 I am a metaclass for StorageBackend classes.
231 My main feature is that I register my concrete instances (with a `NAME'
232 which is not `None') with the StorageBackend class.
234 def __init__(me
, name
, supers
, dict):
236 Register a new concrete StorageBackend subclass.
238 super(StorageBackendClass
, me
).__init__(name
, supers
, dict)
240 except AttributeError: pass
241 else: StorageBackend
.register_concrete_subclass(me
)
243 class StorageBackend (_with_metaclass(StorageBackendClass
)):
245 I provide basic protocol for password storage backends.
247 I'm an abstract class: you want one of my subclasses if you actually want
248 to do something useful. But I maintain a list of my subclasses and can
249 choose an appropriate one to open a database file you've found lying about.
251 Backends are responsible for storing and retrieving stuff, but not for the
252 cryptographic details. Backends need to store two kinds of information:
254 * metadata, consisting of a number of property names and their values;
257 * password mappings, consisting of a number of binary labels and
260 Backends need to implement the following ordinary methods. See the calling
261 methods for details of the subclass responsibilities.
263 BE._create(FILE) Create a new database in FILE; used by `create'.
265 BE._open(FILE, WRITEP)
266 Open the existing database FILE; used by `open'.
268 BE._close(ABRUPTP) Close the database, freeing up any resources. If
269 ABRUPTP then don't try to commit changes.
271 BE._get_meta(NAME, DEFAULT)
272 Return the value of the metadata item with the given
273 NAME, or DEFAULT if it doesn't exist; used by
276 BE._put_meta(NAME, VALUE)
277 Set the VALUE of the metadata item with the given
278 NAME, creating one if necessary; used by `put_meta'.
280 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
281 `KeyError' if there is no such item; used by
284 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
285 pairs; used by `iter_meta'.
287 BE._get_passwd(LABEL)
288 Return the password payload stored with the (binary)
289 LABEL; used by `get_passwd'.
291 BE._put_passwd(LABEL, PAYLOAD)
292 Associate the (binary) PAYLOAD with the LABEL,
293 forgetting any previous payload for that LABEL; used
296 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
299 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
300 pairs; used by `iter_passwds'.
302 Also, concrete subclasses should define the following class attributes.
304 NAME The name of the backend, so that the user can select
305 it when creating a new database.
307 PRIO An integer priority: backends are tried in decreasing
308 priority order when opening an existing database.
313 ## The registry of subclasses.
319 def register_concrete_subclass(sub
):
320 """Register a concrete subclass, so that `open' can try it."""
321 StorageBackend
.CLASSES
[sub
.NAME
] = sub
326 Return the concrete subclass with the given NAME.
328 Raise `KeyError' if the name isn't found.
330 return StorageBackend
.CLASSES
[name
]
334 """Return an iterator over the concrete subclasses."""
335 return _itervalues(StorageBackend
.CLASSES
)
338 def open(file, writep
= False):
339 """Open a database FILE, using some appropriate backend."""
341 for cls
in sorted(StorageBackend
.CLASSES
.values(), reverse
= True,
342 key
= lambda cls
: cls
.PRIO
):
343 try: return cls(file, writep
)
344 except StorageBackendRefusal
: pass
345 raise StorageBackendRefusal
348 def create(cls
, file):
350 Create a new database in the named FILE, using this backend.
352 Subclasses must implement the `_create' instance method.
354 return cls(writep
= True, _magic
= lambda me
: me
._create(file))
356 def __init__(me
, file = None, writep
= False, _magic
= None, *args
, **kw
):
360 Subclasses are not, in general, expected to override this: there's a
361 somewhat hairy protocol between the constructor and some of the class
362 methods. Instead, the main hook for customization is the subclass's
363 `_open' method, which is invoked in the usual case.
365 super(StorageBackend
, me
).__init__(*args
, **kw
)
366 if me
.NAME
is None: raise ValueError('abstract class')
367 if _magic
is not None: _magic(me
)
368 elif file is None: raise ValueError('missing file parameter')
369 else: me
._open(file, writep
)
373 def close(me
, abruptp
= False):
377 It is harmless to attempt to close a database which has been closed
378 already. Calls the subclass's `_close' method.
387 """Raise an error if the receiver has been closed."""
388 if not me
._livep
: raise ValueError('database is closed')
390 def _check_write(me
):
391 """Raise an error if the receiver is not open for writing."""
393 if not me
._writep
: raise ValueError('database is read-only')
395 def _check_meta_name(me
, name
):
397 Raise an error unless NAME is a valid name for a metadata item.
399 Metadata names may not start with `$': such names are reserved for
402 if name
.startswith('$'):
403 raise ValueError("invalid metadata key `%s'" % name
)
408 """Context protocol: make sure the database is closed on exit."""
410 def __exit__(me
, exctype
, excvalue
, exctb
):
411 """Context protocol: see `__enter__'."""
412 me
.close(excvalue
is not None)
416 def get_meta(me
, name
, default
= FAIL
):
418 Fetch the value for the metadata item NAME.
420 If no such item exists, then return DEFAULT if that was set; otherwise
423 This calls the subclass's `_get_meta' method, which should return the
424 requested item or return the given DEFAULT value. It may assume that the
425 name is valid and the database is open.
427 me
._check_meta_name(name
)
429 value
= me
._get_meta(name
, default
)
430 if value
is StorageBackend
.FAIL
: raise KeyError(name
)
433 def put_meta(me
, name
, value
):
435 Store VALUE in the metadata item called NAME.
437 This calls the subclass's `_put_meta' method, which may assume that the
438 name is valid and the database is open for writing.
440 me
._check_meta_name(name
)
442 me
._put_meta(name
, value
)
444 def del_meta(me
, name
):
446 Forget about the metadata item with the given NAME.
448 This calls the subclass's `_del_meta' method, which may assume that the
449 name is valid and the database is open for writing.
451 me
._check_meta_name(name
)
457 Return an iterator over the name/value metadata items.
459 This calls the subclass's `_iter_meta' method, which may assume that the
463 return me
._iter_meta()
465 def get_passwd(me
, label
):
467 Fetch and return the payload stored with the (opaque, binary) LABEL.
469 If there is no such payload then raise `KeyError'.
471 This calls the subclass's `_get_passwd' method, which may assume that the
475 return me
._get_passwd(label
)
477 def put_passwd(me
, label
, payload
):
479 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
481 Any previous payload for LABEL is forgotten.
483 This calls the subclass's `_put_passwd' method, which may assume that the
484 database is open for writing.
487 me
._put_passwd(label
, payload
)
489 def del_passwd(me
, label
):
491 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
493 If there is no such payload then raise `KeyError'.
495 This calls the subclass's `_del_passwd' method, which may assume that the
496 database is open for writing.
499 me
._del_passwd(label
)
501 def iter_passwds(me
):
503 Return an iterator over the stored password label/payload pairs.
505 This calls the subclass's `_iter_passwds' method, which may assume that
506 the database is open.
509 return me
._iter_passwds()
511 try: import gdbm
as _G
512 except ImportError: pass
514 class GDBMStorageBackend (StorageBackend
):
516 My instances store password data in a GDBM database.
518 Metadata and password entries are mixed into the same database. The key
519 for a metadata item is simply its name; the key for a password entry is
520 the entry's label prefixed by `$', since we're guaranteed that no
521 metadata item name begins with `$'.
526 def _open(me
, file, writep
):
527 try: me
._db
= _G
.open(file, writep
and 'w' or 'r')
528 except _G
.error
: raise StorageBackendRefusal(_excval())
530 def _create(me
, file):
531 me
._db
= _G
.open(file, 'n', _M600
)
533 def _close(me
, abruptp
):
537 def _get_meta(me
, name
, default
):
538 try: return me
._db
[name
]
539 except KeyError: return default
541 def _put_meta(me
, name
, value
):
544 def _del_meta(me
, name
):
548 k
= me
._db
.firstkey()
550 if not k
.startswith('$'): yield k
, me
._db
[k
]
551 k
= me
._db
.nextkey(k
)
553 def _get_passwd(me
, label
):
554 return me
._db
['$' + label
]
556 def _put_passwd(me
, label
, payload
):
557 me
._db
['$' + label
] = payload
559 def _del_passwd(me
, label
):
560 del me
._db
['$' + label
]
562 def _iter_passwds(me
):
563 k
= me
._db
.firstkey()
565 if k
.startswith('$'): yield k
[1:], me
._db
[k
]
566 k
= me
._db
.nextkey(k
)
568 try: import sqlite3
as _Q
569 except ImportError: pass
571 class SQLiteStorageBackend (StorageBackend
):
573 I represent a password database stored in SQLite.
575 Metadata and password items are stored in separate tables, so there's no
576 conflict. Some additional metadata is stored in the `meta' table, with
577 names beginning with `$' so as not to conflict with clients:
579 $version The schema version of the table.
585 def _open(me
, file, writep
):
587 me
._db
= _Q
.connect(file)
588 ver
= me
._query_scalar(
589 "SELECT value FROM meta WHERE name = '$version'",
591 except (_Q
.DatabaseError
, _Q
.OperationalError
):
592 raise StorageBackendRefusal(_excval())
593 if ver
is None: raise ValueError('database broken (missing $version)')
594 elif ver
< me
.VERSION
: me
._upgrade(ver
)
595 elif ver
> me
.VERSION
: raise ValueError \
596 ('unknown database schema version (%d > %d)' %
(ver
, me
.VERSION
))
598 def _create(me
, file):
599 fd
= _OS
.open(file, _OS
.O_WRONLY | _OS
.O_CREAT | _OS
.O_EXCL
, _M600
)
602 me
._db
= _Q
.connect(file)
606 name TEXT PRIMARY KEY NOT NULL,
607 value BLOB NOT NULL);
610 CREATE TABLE passwd (
611 label BLOB PRIMARY KEY NOT NULL,
612 payload BLOB NOT NULL);
615 INSERT INTO meta (name, value) VALUES ('$version', ?);
618 try: _OS
.unlink(file)
622 def _upgrade(me
, ver
):
623 """Upgrade the database from schema version VER."""
624 assert False, 'how embarrassing'
626 def _close(me
, abruptp
):
627 if not abruptp
: me
._db
.commit()
631 def _fetch_scalar(me
, c
, what
, default
= None):
633 except StopIteration: val
= default
636 except StopIteration: pass
637 else: raise ValueError('multiple matching records for %s' % what
)
640 def _query_scalar(me
, query
, what
, default
= None, args
= []):
642 c
.execute(query
, args
)
643 return me
._fetch_scalar(c
, what
, default
)
645 def _get_meta(me
, name
, default
):
646 v
= me
._query_scalar("SELECT value FROM meta WHERE name = ?",
647 "metadata item `%s'" % name
,
648 default
= default
, args
= [name
])
649 if v
is default
: return v
652 def _put_meta(me
, name
, value
):
654 c
.execute("INSERT OR REPLACE INTO meta (name, value) VALUES (?, ?)",
655 [name
, buffer(value
)])
657 def _del_meta(me
, name
):
659 c
.execute("DELETE FROM meta WHERE name = ?", [name
])
660 if not c
.rowcount
: raise KeyError(name
)
664 c
.execute("SELECT name, value FROM meta WHERE name NOT LIKE '$%'")
665 for k
, v
in c
: yield k
, str(v
)
667 def _get_passwd(me
, label
):
668 pld
= me
._query_scalar("SELECT payload FROM passwd WHERE label = ?",
669 "password", default
= None,
670 args
= [buffer(label
)])
671 if pld
is None: raise KeyError(label
)
674 def _put_passwd(me
, label
, payload
):
676 c
.execute("INSERT OR REPLACE INTO passwd (label, payload) "
678 [buffer(label
), buffer(payload
)])
680 def _del_passwd(me
, label
):
682 c
.execute("DELETE FROM passwd WHERE label = ?", [label
])
683 if not c
.rowcount
: raise KeyError(label
)
685 def _iter_passwds(me
):
687 c
.execute("SELECT label, payload FROM passwd")
688 for k
, v
in c
: yield str(k
), str(v
)
690 class PlainTextBackend (StorageBackend
):
692 I'm a utility base class for storage backends which use plain text files.
694 I provide subclasses with the following capabilities.
696 * Creating files, with given modes, optionally ensuring that the file
697 doesn't exist already.
699 * Parsing flat text files, checking leading magic, skipping comments, and
700 providing standard encodings of troublesome characters and binary
701 strings in metadata and password records. See below.
703 * Maintenance of metadata and password records in in-memory dictionaries,
704 with ready implementations of the necessary StorageBackend subclass
705 responsibility methods. (Subclasses can override these if they want to
706 make different arrangements.)
708 Metadata records are written with an optional prefix string chosen by the
709 caller, followed by a `NAME=VALUE' pair. The NAME is form-urlencoded and
710 prefixed with `!' if it contains strange characters; the VALUE is base64-
711 encoded (without the pointless trailing `=' padding) and prefixed with `?'
714 Password records are written with an optional prefix string chosen by the
715 caller, followed by a LABEL=PAYLOAD pair, both of which are base64-encoded
718 The following attributes are available for subclasses:
720 _meta Dictionary mapping metadata item names to their values.
721 Populated by `_parse_meta' and managed by `_get_meta' and
724 _pw Dictionary mapping password labels to encrypted payloads.
725 Populated by `_parse_passwd' and managed by `_get_passwd' and
728 _dirtyp Boolean: set if either of the dictionaries has been modified.
731 def __init__(me
, *args
, **kw
):
733 Hook for initialization.
735 Sets up the published instance attributes.
740 super(PlainTextBackend
, me
).__init__(*args
, **kw
)
742 def _create_file(me
, file, mode
= _M600
, freshp
= False):
744 Make sure FILE exists, creating it with the given MODE if necessary.
746 If FRESHP is true, then make sure the file did not exist previously.
747 Return a file object for the newly created file.
749 flags
= _OS
.O_CREAT | _OS
.O_WRONLY
750 if freshp
: flags |
= _OS
.O_EXCL
751 else: flags |
= _OS
.O_TRUNC
752 fd
= _OS
.open(file, flags
, mode
)
753 return _OS
.fdopen(fd
, 'w')
757 Set the `_dirtyp' flag.
759 Subclasses might find it useful to intercept this method.
763 def _eqsplit(me
, line
):
765 Extract the KEY, VALUE pair from a LINE of the form `KEY=VALUE'.
767 Raise `ValueError' if there is no `=' in the LINE.
770 return line
[:eq
], line
[eq
+ 1:]
772 def _parse_file(me
, file, magic
= None):
778 * Raise `StorageBackendRefusal' if that the first line doesn't match
779 MAGIC (if provided). MAGIC should not contain the terminating
782 * Ignore comments (beginning `#') and blank lines.
784 * Call `_parse_line' (provided by the subclass) for other lines.
786 with
open(file, 'r') as f
:
787 if magic
is not None:
788 if f
.readline().rstrip('\n') != magic
: raise StorageBackendRefusal
790 line
= line
.rstrip('\n')
791 if not line
or line
.startswith('#'): continue
794 def _write_file(me
, file, writebody
, mode
= _M600
, magic
= None):
796 Update FILE atomically.
798 The newly created file will have the given MODE. If MAGIC is given, then
799 write that as the first line. Calls WRITEBODY(F) to write the main body
800 of the file where F is a file object for the new file.
803 with me
._create_file(new
, mode
) as f
:
804 if magic
is not None: f
.write(magic
+ '\n')
806 _OS
.rename(new
, file)
808 def _parse_meta(me
, line
):
809 """Parse LINE as a metadata NAME=VALUE pair, and updates `_meta'."""
810 k
, v
= me
._eqsplit(line
)
811 me
._meta
[_dec_metaname(k
)] = _dec_metaval(v
)
813 def _write_meta(me
, f
, prefix
= ''):
814 """Write the metadata records to F, each with the given PREFIX."""
815 f
.write('\n## Metadata.\n')
816 for k
, v
in _iteritems(me
._meta
):
817 f
.write('%s%s=%s\n' %
(prefix
, _enc_metaname(k
), _enc_metaval(v
)))
819 def _get_meta(me
, name
, default
):
820 return me
._meta
.get(name
, default
)
821 def _put_meta(me
, name
, value
):
823 me
._meta
[name
] = value
824 def _del_meta(me
, name
):
828 return _iteritems(me
._meta
)
830 def _parse_passwd(me
, line
):
831 """Parse LINE as a password LABEL=PAYLOAD pair, and updates `_pw'."""
832 k
, v
= me
._eqsplit(line
)
833 me
._pw
[_unb64(k
)] = _unb64(v
)
835 def _write_passwd(me
, f
, prefix
= ''):
836 """Write the password records to F, each with the given PREFIX."""
837 f
.write('\n## Password data.\n')
838 for k
, v
in _iteritems(me
._pw
):
839 f
.write('%s%s=%s\n' %
(prefix
, _b64(k
), _b64(v
)))
841 def _get_passwd(me
, label
):
842 return me
._pw
[str(label
)]
843 def _put_passwd(me
, label
, payload
):
845 me
._pw
[str(label
)] = payload
846 def _del_passwd(me
, label
):
848 del me
._pw
[str(label
)]
849 def _iter_passwds(me
):
850 return _iteritems(me
._pw
)
852 class FlatFileStorageBackend (PlainTextBackend
):
854 I maintain a password database in a plain text file.
856 The text file consists of lines, as follows.
858 * Empty lines, and lines beginning with `#' (in the leftmost column only)
861 * Lines of the form `$LABEL=PAYLOAD' store password data. Both LABEL and
862 PAYLOAD are base64-encoded, without `=' padding.
864 * Lines of the form `NAME=VALUE' store metadata. If the NAME contains
865 characters other than alphanumerics, hyphens, underscores, and colons,
866 then it is form-urlencoded, and prefixed wth `!'. If the VALUE
867 contains such characters, then it is base64-encoded, without `='
868 padding, and prefixed with `?'.
870 * Other lines are erroneous.
872 The file is rewritten from scratch when it's changed: any existing
873 commentary is lost, and items may be reordered. There is no file locking,
874 but the file is updated atomically, by renaming.
876 It is expected that the FlatFileStorageBackend is used mostly for
877 diagnostics and transfer, rather than for a live system.
882 MAGIC
= '### pwsafe password database'
884 def _open(me
, file, writep
):
885 if not _OS
.path
.isfile(file): raise StorageBackendRefusal
886 me
._parse_file(file, magic
= me
.MAGIC
)
887 def _parse_line(me
, line
):
888 if line
.startswith('$'): me
._parse_passwd(line
[1:])
889 else: me
._parse_meta(line
)
891 def _create(me
, file):
892 with me
._create_file(file, freshp
= True) as f
: pass
896 def _close(me
, abruptp
):
897 if not abruptp
and me
._dirtyp
:
898 me
._write_file(me
._file
, me
._write_body
, magic
= me
.MAGIC
)
900 def _write_body(me
, f
):
902 me
._write_passwd(f
, '$')
904 class DirectoryStorageBackend (PlainTextBackend
):
906 I maintain a password database in a directory, with one file per password.
908 This makes password databases easy to maintain in a revision-control system
911 The directory is structured as follows.
913 dir/meta Contains metadata, similar to the `FlatFileBackend'.
915 dir/pw/LABEL Contains the (raw binary) payload for the given password
916 LABEL (base64-encoded, without the useless `=' padding, and
917 with `/' replaced by `.').
919 dir/tmp/ Contains temporary files used by the implementation.
923 METAMAGIC
= '### pwsafe password directory metadata'
925 def _open(me
, file, writep
):
926 if not _OS
.path
.isdir(file) or \
927 not _OS
.path
.isdir(_OS
.path
.join(file, 'pw')) or \
928 not _OS
.path
.isdir(_OS
.path
.join(file, 'tmp')) or \
929 not _OS
.path
.isfile(_OS
.path
.join(file, 'meta')):
930 raise StorageBackendRefusal
932 me
._parse_file(_OS
.path
.join(file, 'meta'), magic
= me
.METAMAGIC
)
933 def _parse_line(me
, line
):
936 def _create(me
, file):
937 _OS
.mkdir(file, _M700
)
938 _OS
.mkdir(_OS
.path
.join(file, 'pw'), _M700
)
939 _OS
.mkdir(_OS
.path
.join(file, 'tmp'), _M700
)
943 def _close(me
, abruptp
):
944 if not abruptp
and me
._dirtyp
:
945 me
._write_file(_OS
.path
.join(me
._dir
, 'meta'),
946 me
._write_meta
, magic
= me
.METAMAGIC
)
948 def _pwfile(me
, label
, dir = 'pw'):
949 return _OS
.path
.join(me
._dir
, dir, _b64(label
).replace('/', '.'))
950 def _get_passwd(me
, label
):
952 f
= open(me
._pwfile(label
), 'rb')
953 except (OSError, IOError):
954 if _excval().errno
== _E
.ENOENT
: raise KeyError(label
)
956 with f
: return f
.read()
957 def _put_passwd(me
, label
, payload
):
958 new
= me
._pwfile(label
, 'tmp')
959 fd
= _OS
.open(new
, _OS
.O_WRONLY | _OS
.O_CREAT | _OS
.O_TRUNC
, _M600
)
961 with
open(new
, 'wb') as f
: f
.write(payload
)
962 _OS
.rename(new
, me
._pwfile(label
))
963 def _del_passwd(me
, label
):
965 _OS
.remove(me
._pwfile(label
))
966 except (OSError, IOError):
967 if _excval().errno
== _E
.ENOENT
: raise KeyError(label
)
969 def _iter_passwds(me
):
970 pw
= _OS
.path
.join(me
._dir
, 'pw')
971 for i
in _OS
.listdir(pw
):
972 with
open(_OS
.path
.join(pw
, i
), 'rb') as f
: pld
= f
.read()
973 yield _unb64(i
.replace('.', '/')), pld
975 ###--------------------------------------------------------------------------
976 ### Password storage.
980 I represent a secure (ish) password store.
982 I can store short secrets, associated with textual names, in a way which
983 doesn't leak too much information about them.
985 I implement (some of) the Python mapping protocol.
987 I keep track of everything using a StorageBackend object. This contains
988 password entries, identified by cryptographic labels, and a number of
991 cipher Names the Catacomb cipher selected.
993 hash Names the Catacomb hash function selected.
995 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
996 length and concatenated, encrypted using the master
999 mac Names the Catacomb message authentication code selected.
1001 magic A magic string for obscuring password tag names.
1003 salt The salt for hashing the passphrase.
1005 tag The master passphrase's tag, for the Pixie's benefit.
1007 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
1008 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
1009 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
1010 encrypted using the stored keys.
1013 def __init__(me
, file, writep
= False):
1015 Initialize a PW object from the database in FILE.
1017 If WRITEP is false (the default) then the database is opened read-only;
1018 if true then it may be written. Requests the database password from the
1019 Pixie, which may cause interaction.
1022 ## Open the database.
1023 me
.db
= StorageBackend
.open(file, writep
)
1025 ## Find out what crypto to use.
1026 c
= _C
.gcciphers
[me
.db
.get_meta('cipher')]
1027 h
= _C
.gchashes
[me
.db
.get_meta('hash')]
1028 m
= _C
.gcmacs
[me
.db
.get_meta('mac')]
1030 ## Request the passphrase and extract the master keys.
1031 tag
= me
.db
.get_meta('tag')
1032 ppk
= PPK(_C
.ppread(tag
), c
, h
, m
, me
.db
.get_meta('salt'))
1034 b
= _C
.ReadBuffer(ppk
.decrypt(me
.db
.get_meta('key')))
1035 except DecryptError
:
1038 me
.ck
= b
.getblk16()
1039 me
.mk
= b
.getblk16()
1040 if not b
.endp
: raise ValueError('trailing junk')
1042 ## Set the key, and stash it and the tag-hashing secret.
1043 me
.k
= Crypto(c
, h
, m
, me
.ck
, me
.mk
)
1044 me
.magic
= me
.k
.decrypt(me
.db
.get_meta('magic'))
1047 def create(cls
, dbcls
, file, tag
, c
, h
, m
):
1049 Create and initialize a new database FILE using StorageBackend DBCLS.
1051 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
1052 and a Pixie passphrase TAG.
1054 This doesn't return a working object: it just creates the database file
1055 and gets out of the way.
1058 ## Set up the cryptography.
1059 pp
= _C
.ppread(tag
, _C
.PMODE_VERIFY
)
1060 ppk
= PPK(pp
, c
, h
, m
)
1061 ck
= _C
.rand
.block(c
.keysz
.default
)
1062 mk
= _C
.rand
.block(c
.keysz
.default
)
1063 k
= Crypto(c
, h
, m
, ck
, mk
)
1065 ## Set up and initialize the database.
1066 kct
= ppk
.encrypt(_C
.WriteBuffer().putblk16(ck
).putblk16(mk
))
1067 with dbcls
.create(file) as db
:
1068 db
.put_meta('tag', tag
)
1069 db
.put_meta('salt', ppk
.salt
)
1070 db
.put_meta('cipher', c
.name
)
1071 db
.put_meta('hash', h
.name
)
1072 db
.put_meta('mac', m
.name
)
1073 db
.put_meta('key', kct
)
1074 db
.put_meta('magic', k
.encrypt(_C
.rand
.block(h
.hashsz
)))
1076 def keyxform(me
, key
):
1077 """Transform the KEY (actually a password tag) into a password label."""
1078 return me
.k
.h().hash(me
.magic
).hash(key
).done()
1082 Change the database password.
1084 Requests the new password from the Pixie, which will probably cause
1087 tag
= me
.db
.get_meta('tag')
1089 ppk
= PPK(_C
.ppread(tag
, _C
.PMODE_VERIFY
),
1090 me
.k
.c
.__class__
, me
.k
.h
, me
.k
.m
.__class__
)
1091 kct
= ppk
.encrypt(_C
.WriteBuffer().putblk16(me
.ck
).putblk16(me
.mk
))
1092 me
.db
.put_meta('key', kct
)
1093 me
.db
.put_meta('salt', ppk
.salt
)
1095 def pack(me
, key
, value
):
1096 """Pack the KEY and VALUE into a ciphertext, and return it."""
1097 b
= _C
.WriteBuffer()
1098 b
.putblk16(key
).putblk16(value
)
1099 b
.zero(((b
.size
+ 255) & ~
255) - b
.size
)
1100 return me
.k
.encrypt(b
)
1104 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
1106 Might raise DecryptError, of course.
1108 b
= _C
.ReadBuffer(me
.k
.decrypt(ct
))
1110 value
= b
.getblk16()
1113 ## Mapping protocol.
1115 def __getitem__(me
, key
):
1116 """Return the password for the given KEY."""
1117 try: return me
.unpack(me
.db
.get_passwd(me
.keyxform(key
)))[1]
1118 except KeyError: raise KeyError(key
)
1120 def __setitem__(me
, key
, value
):
1121 """Associate the password VALUE with the KEY."""
1122 me
.db
.put_passwd(me
.keyxform(key
), me
.pack(key
, value
))
1124 def __delitem__(me
, key
):
1125 """Forget all about the KEY."""
1126 try: me
.db
.del_passwd(me
.keyxform(key
))
1127 except KeyError: raise KeyError(key
)
1130 """Iterate over the known password tags."""
1131 for _
, pld
in me
.db
.iter_passwds():
1132 yield me
.unpack(pld
)[0]
1134 ## Context protocol.
1138 def __exit__(me
, excty
, excval
, exctb
):
1139 me
.db
.close(excval
is not None)
1141 ###----- That's all, folks --------------------------------------------------