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 _excval(): return _SYS
.exc_info()[1]
56 ###--------------------------------------------------------------------------
57 ### Text encoding utilities.
61 Answer whether S can be represented literally.
63 If True, then S can be stored literally, as a metadata item name or
64 value; if False, then S requires some kind of encoding.
66 return all(ch
.isalnum() or ch
in '-_:' for ch
in s
)
68 def _enc_metaname(name
):
69 """Encode NAME as a metadata item name, returning the result."""
76 if _literalp(ch
): sio
.write(ch
)
77 elif ch
== ' ': sio
.write('+')
78 else: sio
.write('%%%02x' % ord
(ch
))
81 def _dec_metaname(name
):
82 """Decode NAME as a metadata item name, returning the result."""
83 if not name
.startswith('!'):
94 sio
.write(chr(int(name
[i
:i
+ 2], 16)))
101 """Encode S as base64, without newlines, and trimming `=' padding."""
102 return s
.encode('base64').replace('\n', '').rstrip('=')
104 """Decode S as base64 with trimmed `=' padding."""
105 return (s
+ '='*((4 - len(s
))%4)).decode('base64')
107 def _enc_metaval(val
):
108 """Encode VAL as a metadata item value, returning the result."""
109 if _literalp(val
): return val
110 else: return '?' + _b64(val
)
112 def _dec_metaval(val
):
113 """Decode VAL as a metadata item value, returning the result."""
114 if not val
.startswith('?'): return val
115 else: return _unb64(val
[1:])
117 ###--------------------------------------------------------------------------
118 ### Underlying cryptography.
120 class DecryptError (Exception):
122 I represent a failure to decrypt a message.
124 Usually this means that someone used the wrong key, though it can also
125 mean that a ciphertext has been modified.
129 class Crypto (object):
131 I represent a symmetric crypto transform.
133 There's currently only one transform implemented, which is the obvious
134 generic-composition construction: given a message m, and keys K0 and K1, we
135 choose an IV v, and compute:
137 * y = v || E(K0, v; m)
140 The final ciphertext is t || y.
143 def __init__(me
, c
, h
, m
, ck
, mk
):
145 Initialize the Crypto object with a given algorithm selection and keys.
147 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
148 keys CK and MK for C and M respectively.
156 Encrypt the message PT and return the resulting ciphertext.
158 blksz
= me
.c
.__class__
.blksz
161 iv
= _C
.rand
.block(blksz
)
164 b
.put(me
.c
.encrypt(pt
))
165 t
= me
.m().hash(b
).done()
166 return t
+ str(buffer(b
))
170 Decrypt the ciphertext CT, returning the plaintext.
172 Raises DecryptError if anything goes wrong.
174 blksz
= me
.c
.__class__
.blksz
175 tagsz
= me
.m
.__class__
.tagsz
176 b
= _C
.ReadBuffer(ct
)
185 if t
!= h
.done(): raise DecryptError
186 return me
.c
.decrypt(x
)
190 I represent a crypto transform whose keys are derived from a passphrase.
192 The password is salted and hashed; the salt is available as the `salt'
196 def __init__(me
, pp
, c
, h
, m
, salt
= None):
198 Initialize the PPK object with a passphrase and algorithm selection.
200 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
201 subclass M, and a SALT. The SALT may be None, if we're generating new
202 keys, indicating that a salt should be chosen randomly.
204 if not salt
: salt
= _C
.rand
.block(h
.hashsz
)
205 tag
= pp
+ _NUL
+ salt
206 Crypto
.__init__(me
, c
, h
, m
,
207 h().hash(_CIPHER
).hash(tag
).done(),
208 h().hash(_MAC
).hash(tag
).done())
211 ###--------------------------------------------------------------------------
214 class StorageBackendRefusal (Exception):
216 I signify that a StorageBackend subclass has refused to open a file.
218 This is used by the StorageBackend.open class method.
222 class StorageBackendClass (type):
224 I am a metaclass for StorageBackend classes.
226 My main feature is that I register my concrete instances (with a `NAME'
227 which is not `None') with the StorageBackend class.
229 def __init__(me
, name
, supers
, dict):
231 Register a new concrete StorageBackend subclass.
233 super(StorageBackendClass
, me
).__init__(name
, supers
, dict)
234 if me
.NAME
is not None: StorageBackend
.register_concrete_subclass(me
)
236 class StorageBackend (object):
238 I provide basic protocol for password storage backends.
240 I'm an abstract class: you want one of my subclasses if you actually want
241 to do something useful. But I maintain a list of my subclasses and can
242 choose an appropriate one to open a database file you've found lying about.
244 Backends are responsible for storing and retrieving stuff, but not for the
245 cryptographic details. Backends need to store two kinds of information:
247 * metadata, consisting of a number of property names and their values;
250 * password mappings, consisting of a number of binary labels and
253 Backends need to implement the following ordinary methods. See the calling
254 methods for details of the subclass responsibilities.
256 BE._create(FILE) Create a new database in FILE; used by `create'.
258 BE._open(FILE, WRITEP)
259 Open the existing database FILE; used by `open'.
261 BE._close(ABRUPTP) Close the database, freeing up any resources. If
262 ABRUPTP then don't try to commit changes.
264 BE._get_meta(NAME, DEFAULT)
265 Return the value of the metadata item with the given
266 NAME, or DEFAULT if it doesn't exist; used by
269 BE._put_meta(NAME, VALUE)
270 Set the VALUE of the metadata item with the given
271 NAME, creating one if necessary; used by `put_meta'.
273 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
274 `KeyError' if there is no such item; used by
277 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
278 pairs; used by `iter_meta'.
280 BE._get_passwd(LABEL)
281 Return the password payload stored with the (binary)
282 LABEL; used by `get_passwd'.
284 BE._put_passwd(LABEL, PAYLOAD)
285 Associate the (binary) PAYLOAD with the LABEL,
286 forgetting any previous payload for that LABEL; used
289 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
292 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
293 pairs; used by `iter_passwds'.
295 Also, concrete subclasses should define the following class attributes.
297 NAME The name of the backend, so that the user can select
298 it when creating a new database.
300 PRIO An integer priority: backends are tried in decreasing
301 priority order when opening an existing database.
304 __metaclass__
= StorageBackendClass
308 ## The registry of subclasses.
314 def register_concrete_subclass(sub
):
315 """Register a concrete subclass, so that `open' can try it."""
316 StorageBackend
.CLASSES
[sub
.NAME
] = sub
321 Return the concrete subclass with the given NAME.
323 Raise `KeyError' if the name isn't found.
325 return StorageBackend
.CLASSES
[name
]
329 """Return an iterator over the concrete subclasses."""
330 return _itervalues(StorageBackend
.CLASSES
)
333 def open(file, writep
= False):
334 """Open a database FILE, using some appropriate backend."""
336 for cls
in sorted(StorageBackend
.CLASSES
.values(), reverse
= True,
337 key
= lambda cls
: cls
.PRIO
):
338 try: return cls(file, writep
)
339 except StorageBackendRefusal
: pass
340 raise StorageBackendRefusal
343 def create(cls
, file):
345 Create a new database in the named FILE, using this backend.
347 Subclasses must implement the `_create' instance method.
349 return cls(writep
= True, _magic
= lambda me
: me
._create(file))
351 def __init__(me
, file = None, writep
= False, _magic
= None, *args
, **kw
):
355 Subclasses are not, in general, expected to override this: there's a
356 somewhat hairy protocol between the constructor and some of the class
357 methods. Instead, the main hook for customization is the subclass's
358 `_open' method, which is invoked in the usual case.
360 super(StorageBackend
, me
).__init__(*args
, **kw
)
361 if me
.NAME
is None: raise ValueError('abstract class')
362 if _magic
is not None: _magic(me
)
363 elif file is None: raise ValueError('missing file parameter')
364 else: me
._open(file, writep
)
368 def close(me
, abruptp
= False):
372 It is harmless to attempt to close a database which has been closed
373 already. Calls the subclass's `_close' method.
382 """Raise an error if the receiver has been closed."""
383 if not me
._livep
: raise ValueError('database is closed')
385 def _check_write(me
):
386 """Raise an error if the receiver is not open for writing."""
388 if not me
._writep
: raise ValueError('database is read-only')
390 def _check_meta_name(me
, name
):
392 Raise an error unless NAME is a valid name for a metadata item.
394 Metadata names may not start with `$': such names are reserved for
397 if name
.startswith('$'):
398 raise ValueError("invalid metadata key `%s'" % name
)
403 """Context protocol: make sure the database is closed on exit."""
405 def __exit__(me
, exctype
, excvalue
, exctb
):
406 """Context protocol: see `__enter__'."""
407 me
.close(excvalue
is not None)
411 def get_meta(me
, name
, default
= FAIL
):
413 Fetch the value for the metadata item NAME.
415 If no such item exists, then return DEFAULT if that was set; otherwise
418 This calls the subclass's `_get_meta' method, which should return the
419 requested item or return the given DEFAULT value. It may assume that the
420 name is valid and the database is open.
422 me
._check_meta_name(name
)
424 value
= me
._get_meta(name
, default
)
425 if value
is StorageBackend
.FAIL
: raise KeyError(name
)
428 def put_meta(me
, name
, value
):
430 Store VALUE in the metadata item called NAME.
432 This calls the subclass's `_put_meta' method, which may assume that the
433 name is valid and the database is open for writing.
435 me
._check_meta_name(name
)
437 me
._put_meta(name
, value
)
439 def del_meta(me
, name
):
441 Forget about the metadata item with the given NAME.
443 This calls the subclass's `_del_meta' method, which may assume that the
444 name is valid and the database is open for writing.
446 me
._check_meta_name(name
)
452 Return an iterator over the name/value metadata items.
454 This calls the subclass's `_iter_meta' method, which may assume that the
458 return me
._iter_meta()
460 def get_passwd(me
, label
):
462 Fetch and return the payload stored with the (opaque, binary) LABEL.
464 If there is no such payload then raise `KeyError'.
466 This calls the subclass's `_get_passwd' method, which may assume that the
470 return me
._get_passwd(label
)
472 def put_passwd(me
, label
, payload
):
474 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
476 Any previous payload for LABEL is forgotten.
478 This calls the subclass's `_put_passwd' method, which may assume that the
479 database is open for writing.
482 me
._put_passwd(label
, payload
)
484 def del_passwd(me
, label
):
486 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
488 If there is no such payload then raise `KeyError'.
490 This calls the subclass's `_del_passwd' method, which may assume that the
491 database is open for writing.
494 me
._del_passwd(label
)
496 def iter_passwds(me
):
498 Return an iterator over the stored password label/payload pairs.
500 This calls the subclass's `_iter_passwds' method, which may assume that
501 the database is open.
504 return me
._iter_passwds()
506 try: import gdbm
as _G
507 except ImportError: pass
509 class GDBMStorageBackend (StorageBackend
):
511 My instances store password data in a GDBM database.
513 Metadata and password entries are mixed into the same database. The key
514 for a metadata item is simply its name; the key for a password entry is
515 the entry's label prefixed by `$', since we're guaranteed that no
516 metadata item name begins with `$'.
521 def _open(me
, file, writep
):
522 try: me
._db
= _G
.open(file, writep
and 'w' or 'r')
523 except _G
.error
: raise StorageBackendRefusal(_excval())
525 def _create(me
, file):
526 me
._db
= _G
.open(file, 'n', _M600
)
528 def _close(me
, abruptp
):
532 def _get_meta(me
, name
, default
):
533 try: return me
._db
[name
]
534 except KeyError: return default
536 def _put_meta(me
, name
, value
):
539 def _del_meta(me
, name
):
543 k
= me
._db
.firstkey()
545 if not k
.startswith('$'): yield k
, me
._db
[k
]
546 k
= me
._db
.nextkey(k
)
548 def _get_passwd(me
, label
):
549 return me
._db
['$' + label
]
551 def _put_passwd(me
, label
, payload
):
552 me
._db
['$' + label
] = payload
554 def _del_passwd(me
, label
):
555 del me
._db
['$' + label
]
557 def _iter_passwds(me
):
558 k
= me
._db
.firstkey()
560 if k
.startswith('$'): yield k
[1:], me
._db
[k
]
561 k
= me
._db
.nextkey(k
)
563 try: import sqlite3
as _Q
564 except ImportError: pass
566 class SQLiteStorageBackend (StorageBackend
):
568 I represent a password database stored in SQLite.
570 Metadata and password items are stored in separate tables, so there's no
571 conflict. Some additional metadata is stored in the `meta' table, with
572 names beginning with `$' so as not to conflict with clients:
574 $version The schema version of the table.
580 def _open(me
, file, writep
):
582 me
._db
= _Q
.connect(file)
583 ver
= me
._query_scalar(
584 "SELECT value FROM meta WHERE name = '$version'",
586 except (_Q
.DatabaseError
, _Q
.OperationalError
):
587 raise StorageBackendRefusal(_excval())
588 if ver
is None: raise ValueError('database broken (missing $version)')
589 elif ver
< me
.VERSION
: me
._upgrade(ver
)
590 elif ver
> me
.VERSION
: raise ValueError \
591 ('unknown database schema version (%d > %d)' %
(ver
, me
.VERSION
))
593 def _create(me
, file):
594 fd
= _OS
.open(file, _OS
.O_WRONLY | _OS
.O_CREAT | _OS
.O_EXCL
, _M600
)
597 me
._db
= _Q
.connect(file)
601 name TEXT PRIMARY KEY NOT NULL,
602 value BLOB NOT NULL);
605 CREATE TABLE passwd (
606 label BLOB PRIMARY KEY NOT NULL,
607 payload BLOB NOT NULL);
610 INSERT INTO meta (name, value) VALUES ('$version', ?);
613 try: _OS
.unlink(file)
617 def _upgrade(me
, ver
):
618 """Upgrade the database from schema version VER."""
619 assert False, 'how embarrassing'
621 def _close(me
, abruptp
):
622 if not abruptp
: me
._db
.commit()
626 def _fetch_scalar(me
, c
, what
, default
= None):
628 except StopIteration: val
= default
631 except StopIteration: pass
632 else: raise ValueError('multiple matching records for %s' % what
)
635 def _query_scalar(me
, query
, what
, default
= None, args
= []):
637 c
.execute(query
, args
)
638 return me
._fetch_scalar(c
, what
, default
)
640 def _get_meta(me
, name
, default
):
641 v
= me
._query_scalar("SELECT value FROM meta WHERE name = ?",
642 "metadata item `%s'" % name
,
643 default
= default
, args
= [name
])
644 if v
is default
: return v
647 def _put_meta(me
, name
, value
):
649 c
.execute("INSERT OR REPLACE INTO meta (name, value) VALUES (?, ?)",
650 [name
, buffer(value
)])
652 def _del_meta(me
, name
):
654 c
.execute("DELETE FROM meta WHERE name = ?", [name
])
655 if not c
.rowcount
: raise KeyError(name
)
659 c
.execute("SELECT name, value FROM meta WHERE name NOT LIKE '$%'")
660 for k
, v
in c
: yield k
, str(v
)
662 def _get_passwd(me
, label
):
663 pld
= me
._query_scalar("SELECT payload FROM passwd WHERE label = ?",
664 "password", default
= None,
665 args
= [buffer(label
)])
666 if pld
is None: raise KeyError(label
)
669 def _put_passwd(me
, label
, payload
):
671 c
.execute("INSERT OR REPLACE INTO passwd (label, payload) "
673 [buffer(label
), buffer(payload
)])
675 def _del_passwd(me
, label
):
677 c
.execute("DELETE FROM passwd WHERE label = ?", [label
])
678 if not c
.rowcount
: raise KeyError(label
)
680 def _iter_passwds(me
):
682 c
.execute("SELECT label, payload FROM passwd")
683 for k
, v
in c
: yield str(k
), str(v
)
685 class PlainTextBackend (StorageBackend
):
687 I'm a utility base class for storage backends which use plain text files.
689 I provide subclasses with the following capabilities.
691 * Creating files, with given modes, optionally ensuring that the file
692 doesn't exist already.
694 * Parsing flat text files, checking leading magic, skipping comments, and
695 providing standard encodings of troublesome characters and binary
696 strings in metadata and password records. See below.
698 * Maintenance of metadata and password records in in-memory dictionaries,
699 with ready implementations of the necessary StorageBackend subclass
700 responsibility methods. (Subclasses can override these if they want to
701 make different arrangements.)
703 Metadata records are written with an optional prefix string chosen by the
704 caller, followed by a `NAME=VALUE' pair. The NAME is form-urlencoded and
705 prefixed with `!' if it contains strange characters; the VALUE is base64-
706 encoded (without the pointless trailing `=' padding) and prefixed with `?'
709 Password records are written with an optional prefix string chosen by the
710 caller, followed by a LABEL=PAYLOAD pair, both of which are base64-encoded
713 The following attributes are available for subclasses:
715 _meta Dictionary mapping metadata item names to their values.
716 Populated by `_parse_meta' and managed by `_get_meta' and
719 _pw Dictionary mapping password labels to encrypted payloads.
720 Populated by `_parse_passwd' and managed by `_get_passwd' and
723 _dirtyp Boolean: set if either of the dictionaries has been modified.
726 def __init__(me
, *args
, **kw
):
728 Hook for initialization.
730 Sets up the published instance attributes.
735 super(PlainTextBackend
, me
).__init__(*args
, **kw
)
737 def _create_file(me
, file, mode
= _M600
, freshp
= False):
739 Make sure FILE exists, creating it with the given MODE if necessary.
741 If FRESHP is true, then make sure the file did not exist previously.
742 Return a file object for the newly created file.
744 flags
= _OS
.O_CREAT | _OS
.O_WRONLY
745 if freshp
: flags |
= _OS
.O_EXCL
746 else: flags |
= _OS
.O_TRUNC
747 fd
= _OS
.open(file, flags
, mode
)
748 return _OS
.fdopen(fd
, 'w')
752 Set the `_dirtyp' flag.
754 Subclasses might find it useful to intercept this method.
758 def _eqsplit(me
, line
):
760 Extract the KEY, VALUE pair from a LINE of the form `KEY=VALUE'.
762 Raise `ValueError' if there is no `=' in the LINE.
765 return line
[:eq
], line
[eq
+ 1:]
767 def _parse_file(me
, file, magic
= None):
773 * Raise `StorageBackendRefusal' if that the first line doesn't match
774 MAGIC (if provided). MAGIC should not contain the terminating
777 * Ignore comments (beginning `#') and blank lines.
779 * Call `_parse_line' (provided by the subclass) for other lines.
781 with
open(file, 'r') as f
:
782 if magic
is not None:
783 if f
.readline().rstrip('\n') != magic
: raise StorageBackendRefusal
785 line
= line
.rstrip('\n')
786 if not line
or line
.startswith('#'): continue
789 def _write_file(me
, file, writebody
, mode
= _M600
, magic
= None):
791 Update FILE atomically.
793 The newly created file will have the given MODE. If MAGIC is given, then
794 write that as the first line. Calls WRITEBODY(F) to write the main body
795 of the file where F is a file object for the new file.
798 with me
._create_file(new
, mode
) as f
:
799 if magic
is not None: f
.write(magic
+ '\n')
801 _OS
.rename(new
, file)
803 def _parse_meta(me
, line
):
804 """Parse LINE as a metadata NAME=VALUE pair, and updates `_meta'."""
805 k
, v
= me
._eqsplit(line
)
806 me
._meta
[_dec_metaname(k
)] = _dec_metaval(v
)
808 def _write_meta(me
, f
, prefix
= ''):
809 """Write the metadata records to F, each with the given PREFIX."""
810 f
.write('\n## Metadata.\n')
811 for k
, v
in _iteritems(me
._meta
):
812 f
.write('%s%s=%s\n' %
(prefix
, _enc_metaname(k
), _enc_metaval(v
)))
814 def _get_meta(me
, name
, default
):
815 return me
._meta
.get(name
, default
)
816 def _put_meta(me
, name
, value
):
818 me
._meta
[name
] = value
819 def _del_meta(me
, name
):
823 return _iteritems(me
._meta
)
825 def _parse_passwd(me
, line
):
826 """Parse LINE as a password LABEL=PAYLOAD pair, and updates `_pw'."""
827 k
, v
= me
._eqsplit(line
)
828 me
._pw
[_unb64(k
)] = _unb64(v
)
830 def _write_passwd(me
, f
, prefix
= ''):
831 """Write the password records to F, each with the given PREFIX."""
832 f
.write('\n## Password data.\n')
833 for k
, v
in _iteritems(me
._pw
):
834 f
.write('%s%s=%s\n' %
(prefix
, _b64(k
), _b64(v
)))
836 def _get_passwd(me
, label
):
837 return me
._pw
[str(label
)]
838 def _put_passwd(me
, label
, payload
):
840 me
._pw
[str(label
)] = payload
841 def _del_passwd(me
, label
):
843 del me
._pw
[str(label
)]
844 def _iter_passwds(me
):
845 return _iteritems(me
._pw
)
847 class FlatFileStorageBackend (PlainTextBackend
):
849 I maintain a password database in a plain text file.
851 The text file consists of lines, as follows.
853 * Empty lines, and lines beginning with `#' (in the leftmost column only)
856 * Lines of the form `$LABEL=PAYLOAD' store password data. Both LABEL and
857 PAYLOAD are base64-encoded, without `=' padding.
859 * Lines of the form `NAME=VALUE' store metadata. If the NAME contains
860 characters other than alphanumerics, hyphens, underscores, and colons,
861 then it is form-urlencoded, and prefixed wth `!'. If the VALUE
862 contains such characters, then it is base64-encoded, without `='
863 padding, and prefixed with `?'.
865 * Other lines are erroneous.
867 The file is rewritten from scratch when it's changed: any existing
868 commentary is lost, and items may be reordered. There is no file locking,
869 but the file is updated atomically, by renaming.
871 It is expected that the FlatFileStorageBackend is used mostly for
872 diagnostics and transfer, rather than for a live system.
877 MAGIC
= '### pwsafe password database'
879 def _open(me
, file, writep
):
880 if not _OS
.path
.isfile(file): raise StorageBackendRefusal
881 me
._parse_file(file, magic
= me
.MAGIC
)
882 def _parse_line(me
, line
):
883 if line
.startswith('$'): me
._parse_passwd(line
[1:])
884 else: me
._parse_meta(line
)
886 def _create(me
, file):
887 with me
._create_file(file, freshp
= True) as f
: pass
891 def _close(me
, abruptp
):
892 if not abruptp
and me
._dirtyp
:
893 me
._write_file(me
._file
, me
._write_body
, magic
= me
.MAGIC
)
895 def _write_body(me
, f
):
897 me
._write_passwd(f
, '$')
899 class DirectoryStorageBackend (PlainTextBackend
):
901 I maintain a password database in a directory, with one file per password.
903 This makes password databases easy to maintain in a revision-control system
906 The directory is structured as follows.
908 dir/meta Contains metadata, similar to the `FlatFileBackend'.
910 dir/pw/LABEL Contains the (raw binary) payload for the given password
911 LABEL (base64-encoded, without the useless `=' padding, and
912 with `/' replaced by `.').
914 dir/tmp/ Contains temporary files used by the implementation.
918 METAMAGIC
= '### pwsafe password directory metadata'
920 def _open(me
, file, writep
):
921 if not _OS
.path
.isdir(file) or \
922 not _OS
.path
.isdir(_OS
.path
.join(file, 'pw')) or \
923 not _OS
.path
.isdir(_OS
.path
.join(file, 'tmp')) or \
924 not _OS
.path
.isfile(_OS
.path
.join(file, 'meta')):
925 raise StorageBackendRefusal
927 me
._parse_file(_OS
.path
.join(file, 'meta'), magic
= me
.METAMAGIC
)
928 def _parse_line(me
, line
):
931 def _create(me
, file):
932 _OS
.mkdir(file, _M700
)
933 _OS
.mkdir(_OS
.path
.join(file, 'pw'), _M700
)
934 _OS
.mkdir(_OS
.path
.join(file, 'tmp'), _M700
)
938 def _close(me
, abruptp
):
939 if not abruptp
and me
._dirtyp
:
940 me
._write_file(_OS
.path
.join(me
._dir
, 'meta'),
941 me
._write_meta
, magic
= me
.METAMAGIC
)
943 def _pwfile(me
, label
, dir = 'pw'):
944 return _OS
.path
.join(me
._dir
, dir, _b64(label
).replace('/', '.'))
945 def _get_passwd(me
, label
):
947 f
= open(me
._pwfile(label
), 'rb')
948 except (OSError, IOError):
949 if _excval().errno
== _E
.ENOENT
: raise KeyError(label
)
951 with f
: return f
.read()
952 def _put_passwd(me
, label
, payload
):
953 new
= me
._pwfile(label
, 'tmp')
954 fd
= _OS
.open(new
, _OS
.O_WRONLY | _OS
.O_CREAT | _OS
.O_TRUNC
, _M600
)
956 with
open(new
, 'wb') as f
: f
.write(payload
)
957 _OS
.rename(new
, me
._pwfile(label
))
958 def _del_passwd(me
, label
):
960 _OS
.remove(me
._pwfile(label
))
961 except (OSError, IOError):
962 if _excval().errno
== _E
.ENOENT
: raise KeyError(label
)
964 def _iter_passwds(me
):
965 pw
= _OS
.path
.join(me
._dir
, 'pw')
966 for i
in _OS
.listdir(pw
):
967 with
open(_OS
.path
.join(pw
, i
), 'rb') as f
: pld
= f
.read()
968 yield _unb64(i
.replace('.', '/')), pld
970 ###--------------------------------------------------------------------------
971 ### Password storage.
975 I represent a secure (ish) password store.
977 I can store short secrets, associated with textual names, in a way which
978 doesn't leak too much information about them.
980 I implement (some of) the Python mapping protocol.
982 I keep track of everything using a StorageBackend object. This contains
983 password entries, identified by cryptographic labels, and a number of
986 cipher Names the Catacomb cipher selected.
988 hash Names the Catacomb hash function selected.
990 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
991 length and concatenated, encrypted using the master
994 mac Names the Catacomb message authentication code selected.
996 magic A magic string for obscuring password tag names.
998 salt The salt for hashing the passphrase.
1000 tag The master passphrase's tag, for the Pixie's benefit.
1002 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
1003 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
1004 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
1005 encrypted using the stored keys.
1008 def __init__(me
, file, writep
= False):
1010 Initialize a PW object from the database in FILE.
1012 If WRITEP is false (the default) then the database is opened read-only;
1013 if true then it may be written. Requests the database password from the
1014 Pixie, which may cause interaction.
1017 ## Open the database.
1018 me
.db
= StorageBackend
.open(file, writep
)
1020 ## Find out what crypto to use.
1021 c
= _C
.gcciphers
[me
.db
.get_meta('cipher')]
1022 h
= _C
.gchashes
[me
.db
.get_meta('hash')]
1023 m
= _C
.gcmacs
[me
.db
.get_meta('mac')]
1025 ## Request the passphrase and extract the master keys.
1026 tag
= me
.db
.get_meta('tag')
1027 ppk
= PPK(_C
.ppread(tag
), c
, h
, m
, me
.db
.get_meta('salt'))
1029 b
= _C
.ReadBuffer(ppk
.decrypt(me
.db
.get_meta('key')))
1030 except DecryptError
:
1033 me
.ck
= b
.getblk16()
1034 me
.mk
= b
.getblk16()
1035 if not b
.endp
: raise ValueError('trailing junk')
1037 ## Set the key, and stash it and the tag-hashing secret.
1038 me
.k
= Crypto(c
, h
, m
, me
.ck
, me
.mk
)
1039 me
.magic
= me
.k
.decrypt(me
.db
.get_meta('magic'))
1042 def create(cls
, dbcls
, file, tag
, c
, h
, m
):
1044 Create and initialize a new database FILE using StorageBackend DBCLS.
1046 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
1047 and a Pixie passphrase TAG.
1049 This doesn't return a working object: it just creates the database file
1050 and gets out of the way.
1053 ## Set up the cryptography.
1054 pp
= _C
.ppread(tag
, _C
.PMODE_VERIFY
)
1055 ppk
= PPK(pp
, c
, h
, m
)
1056 ck
= _C
.rand
.block(c
.keysz
.default
)
1057 mk
= _C
.rand
.block(c
.keysz
.default
)
1058 k
= Crypto(c
, h
, m
, ck
, mk
)
1060 ## Set up and initialize the database.
1061 kct
= ppk
.encrypt(_C
.WriteBuffer().putblk16(ck
).putblk16(mk
))
1062 with dbcls
.create(file) as db
:
1063 db
.put_meta('tag', tag
)
1064 db
.put_meta('salt', ppk
.salt
)
1065 db
.put_meta('cipher', c
.name
)
1066 db
.put_meta('hash', h
.name
)
1067 db
.put_meta('mac', m
.name
)
1068 db
.put_meta('key', kct
)
1069 db
.put_meta('magic', k
.encrypt(_C
.rand
.block(h
.hashsz
)))
1071 def keyxform(me
, key
):
1072 """Transform the KEY (actually a password tag) into a password label."""
1073 return me
.k
.h().hash(me
.magic
).hash(key
).done()
1077 Change the database password.
1079 Requests the new password from the Pixie, which will probably cause
1082 tag
= me
.db
.get_meta('tag')
1084 ppk
= PPK(_C
.ppread(tag
, _C
.PMODE_VERIFY
),
1085 me
.k
.c
.__class__
, me
.k
.h
, me
.k
.m
.__class__
)
1086 kct
= ppk
.encrypt(_C
.WriteBuffer().putblk16(me
.ck
).putblk16(me
.mk
))
1087 me
.db
.put_meta('key', kct
)
1088 me
.db
.put_meta('salt', ppk
.salt
)
1090 def pack(me
, key
, value
):
1091 """Pack the KEY and VALUE into a ciphertext, and return it."""
1092 b
= _C
.WriteBuffer()
1093 b
.putblk16(key
).putblk16(value
)
1094 b
.zero(((b
.size
+ 255) & ~
255) - b
.size
)
1095 return me
.k
.encrypt(b
)
1099 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
1101 Might raise DecryptError, of course.
1103 b
= _C
.ReadBuffer(me
.k
.decrypt(ct
))
1105 value
= b
.getblk16()
1108 ## Mapping protocol.
1110 def __getitem__(me
, key
):
1111 """Return the password for the given KEY."""
1112 try: return me
.unpack(me
.db
.get_passwd(me
.keyxform(key
)))[1]
1113 except KeyError: raise KeyError(key
)
1115 def __setitem__(me
, key
, value
):
1116 """Associate the password VALUE with the KEY."""
1117 me
.db
.put_passwd(me
.keyxform(key
), me
.pack(key
, value
))
1119 def __delitem__(me
, key
):
1120 """Forget all about the KEY."""
1121 try: me
.db
.del_passwd(me
.keyxform(key
))
1122 except KeyError: raise KeyError(key
)
1125 """Iterate over the known password tags."""
1126 for _
, pld
in me
.db
.iter_passwds():
1127 yield me
.unpack(pld
)[0]
1129 ## Context protocol.
1133 def __exit__(me
, excty
, excval
, exctb
):
1134 me
.db
.close(excval
is not None)
1136 ###----- That's all, folks --------------------------------------------------