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]
53 ###--------------------------------------------------------------------------
54 ### Text encoding utilities.
58 Answer whether S can be represented literally.
60 If True, then S can be stored literally, as a metadata item name or
61 value; if False, then S requires some kind of encoding.
63 return all(ch
.isalnum() or ch
in '-_:' for ch
in s
)
65 def _enc_metaname(name
):
66 """Encode NAME as a metadata item name, returning the result."""
73 if _literalp(ch
): sio
.write(ch
)
74 elif ch
== ' ': sio
.write('+')
75 else: sio
.write('%%%02x' % ord
(ch
))
78 def _dec_metaname(name
):
79 """Decode NAME as a metadata item name, returning the result."""
80 if not name
.startswith('!'):
91 sio
.write(chr(int(name
[i
:i
+ 2], 16)))
98 """Encode S as base64, without newlines, and trimming `=' padding."""
99 return s
.encode('base64').replace('\n', '').rstrip('=')
101 """Decode S as base64 with trimmed `=' padding."""
102 return (s
+ '='*((4 - len(s
))%4)).decode('base64')
104 def _enc_metaval(val
):
105 """Encode VAL as a metadata item value, returning the result."""
106 if _literalp(val
): return val
107 else: return '?' + _b64(val
)
109 def _dec_metaval(val
):
110 """Decode VAL as a metadata item value, returning the result."""
111 if not val
.startswith('?'): return val
112 else: return _unb64(val
[1:])
114 ###--------------------------------------------------------------------------
115 ### Underlying cryptography.
117 class DecryptError (Exception):
119 I represent a failure to decrypt a message.
121 Usually this means that someone used the wrong key, though it can also
122 mean that a ciphertext has been modified.
126 class Crypto (object):
128 I represent a symmetric crypto transform.
130 There's currently only one transform implemented, which is the obvious
131 generic-composition construction: given a message m, and keys K0 and K1, we
132 choose an IV v, and compute:
134 * y = v || E(K0, v; m)
137 The final ciphertext is t || y.
140 def __init__(me
, c
, h
, m
, ck
, mk
):
142 Initialize the Crypto object with a given algorithm selection and keys.
144 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
145 keys CK and MK for C and M respectively.
153 Encrypt the message PT and return the resulting ciphertext.
155 blksz
= me
.c
.__class__
.blksz
158 iv
= _C
.rand
.block(blksz
)
161 b
.put(me
.c
.encrypt(pt
))
162 t
= me
.m().hash(b
).done()
163 return t
+ str(buffer(b
))
167 Decrypt the ciphertext CT, returning the plaintext.
169 Raises DecryptError if anything goes wrong.
171 blksz
= me
.c
.__class__
.blksz
172 tagsz
= me
.m
.__class__
.tagsz
173 b
= _C
.ReadBuffer(ct
)
182 if t
!= h
.done(): raise DecryptError
183 return me
.c
.decrypt(x
)
187 I represent a crypto transform whose keys are derived from a passphrase.
189 The password is salted and hashed; the salt is available as the `salt'
193 def __init__(me
, pp
, c
, h
, m
, salt
= None):
195 Initialize the PPK object with a passphrase and algorithm selection.
197 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
198 subclass M, and a SALT. The SALT may be None, if we're generating new
199 keys, indicating that a salt should be chosen randomly.
201 if not salt
: salt
= _C
.rand
.block(h
.hashsz
)
202 tag
= pp
+ _NUL
+ salt
203 Crypto
.__init__(me
, c
, h
, m
,
204 h().hash(_CIPHER
).hash(tag
).done(),
205 h().hash(_MAC
).hash(tag
).done())
208 ###--------------------------------------------------------------------------
211 class StorageBackendRefusal (Exception):
213 I signify that a StorageBackend subclass has refused to open a file.
215 This is used by the StorageBackend.open class method.
219 class StorageBackendClass (type):
221 I am a metaclass for StorageBackend classes.
223 My main feature is that I register my concrete instances (with a `NAME'
224 which is not `None') with the StorageBackend class.
226 def __init__(me
, name
, supers
, dict):
228 Register a new concrete StorageBackend subclass.
230 super(StorageBackendClass
, me
).__init__(name
, supers
, dict)
231 if me
.NAME
is not None: StorageBackend
.register_concrete_subclass(me
)
233 class StorageBackend (object):
235 I provide basic protocol for password storage backends.
237 I'm an abstract class: you want one of my subclasses if you actually want
238 to do something useful. But I maintain a list of my subclasses and can
239 choose an appropriate one to open a database file you've found lying about.
241 Backends are responsible for storing and retrieving stuff, but not for the
242 cryptographic details. Backends need to store two kinds of information:
244 * metadata, consisting of a number of property names and their values;
247 * password mappings, consisting of a number of binary labels and
250 Backends need to implement the following ordinary methods. See the calling
251 methods for details of the subclass responsibilities.
253 BE._create(FILE) Create a new database in FILE; used by `create'.
255 BE._open(FILE, WRITEP)
256 Open the existing database FILE; used by `open'.
258 BE._close(ABRUPTP) Close the database, freeing up any resources. If
259 ABRUPTP then don't try to commit changes.
261 BE._get_meta(NAME, DEFAULT)
262 Return the value of the metadata item with the given
263 NAME, or DEFAULT if it doesn't exist; used by
266 BE._put_meta(NAME, VALUE)
267 Set the VALUE of the metadata item with the given
268 NAME, creating one if necessary; used by `put_meta'.
270 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
271 `KeyError' if there is no such item; used by
274 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
275 pairs; used by `iter_meta'.
277 BE._get_passwd(LABEL)
278 Return the password payload stored with the (binary)
279 LABEL; used by `get_passwd'.
281 BE._put_passwd(LABEL, PAYLOAD)
282 Associate the (binary) PAYLOAD with the LABEL,
283 forgetting any previous payload for that LABEL; used
286 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
289 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
290 pairs; used by `iter_passwds'.
292 Also, concrete subclasses should define the following class attributes.
294 NAME The name of the backend, so that the user can select
295 it when creating a new database.
297 PRIO An integer priority: backends are tried in decreasing
298 priority order when opening an existing database.
301 __metaclass__
= StorageBackendClass
305 ## The registry of subclasses.
311 def register_concrete_subclass(sub
):
312 """Register a concrete subclass, so that `open' can try it."""
313 StorageBackend
.CLASSES
[sub
.NAME
] = sub
318 Return the concrete subclass with the given NAME.
320 Raise `KeyError' if the name isn't found.
322 return StorageBackend
.CLASSES
[name
]
326 """Return an iterator over the concrete subclasses."""
327 return _itervalues(StorageBackend
.CLASSES
)
330 def open(file, writep
= False):
331 """Open a database FILE, using some appropriate backend."""
333 for cls
in sorted(StorageBackend
.CLASSES
.values(), reverse
= True,
334 key
= lambda cls
: cls
.PRIO
):
335 try: return cls(file, writep
)
336 except StorageBackendRefusal
: pass
337 raise StorageBackendRefusal
340 def create(cls
, file):
342 Create a new database in the named FILE, using this backend.
344 Subclasses must implement the `_create' instance method.
346 return cls(writep
= True, _magic
= lambda me
: me
._create(file))
348 def __init__(me
, file = None, writep
= False, _magic
= None, *args
, **kw
):
352 Subclasses are not, in general, expected to override this: there's a
353 somewhat hairy protocol between the constructor and some of the class
354 methods. Instead, the main hook for customization is the subclass's
355 `_open' method, which is invoked in the usual case.
357 super(StorageBackend
, me
).__init__(*args
, **kw
)
358 if me
.NAME
is None: raise ValueError('abstract class')
359 if _magic
is not None: _magic(me
)
360 elif file is None: raise ValueError('missing file parameter')
361 else: me
._open(file, writep
)
365 def close(me
, abruptp
= False):
369 It is harmless to attempt to close a database which has been closed
370 already. Calls the subclass's `_close' method.
379 """Raise an error if the receiver has been closed."""
380 if not me
._livep
: raise ValueError('database is closed')
382 def _check_write(me
):
383 """Raise an error if the receiver is not open for writing."""
385 if not me
._writep
: raise ValueError('database is read-only')
387 def _check_meta_name(me
, name
):
389 Raise an error unless NAME is a valid name for a metadata item.
391 Metadata names may not start with `$': such names are reserved for
394 if name
.startswith('$'):
395 raise ValueError("invalid metadata key `%s'" % name
)
400 """Context protocol: make sure the database is closed on exit."""
402 def __exit__(me
, exctype
, excvalue
, exctb
):
403 """Context protocol: see `__enter__'."""
404 me
.close(excvalue
is not None)
408 def get_meta(me
, name
, default
= FAIL
):
410 Fetch the value for the metadata item NAME.
412 If no such item exists, then return DEFAULT if that was set; otherwise
415 This calls the subclass's `_get_meta' method, which should return the
416 requested item or return the given DEFAULT value. It may assume that the
417 name is valid and the database is open.
419 me
._check_meta_name(name
)
421 value
= me
._get_meta(name
, default
)
422 if value
is StorageBackend
.FAIL
: raise KeyError(name
)
425 def put_meta(me
, name
, value
):
427 Store VALUE in the metadata item called NAME.
429 This calls the subclass's `_put_meta' method, which may assume that the
430 name is valid and the database is open for writing.
432 me
._check_meta_name(name
)
434 me
._put_meta(name
, value
)
436 def del_meta(me
, name
):
438 Forget about the metadata item with the given NAME.
440 This calls the subclass's `_del_meta' method, which may assume that the
441 name is valid and the database is open for writing.
443 me
._check_meta_name(name
)
449 Return an iterator over the name/value metadata items.
451 This calls the subclass's `_iter_meta' method, which may assume that the
455 return me
._iter_meta()
457 def get_passwd(me
, label
):
459 Fetch and return the payload stored with the (opaque, binary) LABEL.
461 If there is no such payload then raise `KeyError'.
463 This calls the subclass's `_get_passwd' method, which may assume that the
467 return me
._get_passwd(label
)
469 def put_passwd(me
, label
, payload
):
471 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
473 Any previous payload for LABEL is forgotten.
475 This calls the subclass's `_put_passwd' method, which may assume that the
476 database is open for writing.
479 me
._put_passwd(label
, payload
)
481 def del_passwd(me
, label
):
483 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
485 If there is no such payload then raise `KeyError'.
487 This calls the subclass's `_del_passwd' method, which may assume that the
488 database is open for writing.
491 me
._del_passwd(label
)
493 def iter_passwds(me
):
495 Return an iterator over the stored password label/payload pairs.
497 This calls the subclass's `_iter_passwds' method, which may assume that
498 the database is open.
501 return me
._iter_passwds()
503 try: import gdbm
as _G
504 except ImportError: pass
506 class GDBMStorageBackend (StorageBackend
):
508 My instances store password data in a GDBM database.
510 Metadata and password entries are mixed into the same database. The key
511 for a metadata item is simply its name; the key for a password entry is
512 the entry's label prefixed by `$', since we're guaranteed that no
513 metadata item name begins with `$'.
518 def _open(me
, file, writep
):
519 try: me
._db
= _G
.open(file, writep
and 'w' or 'r')
520 except _G
.error
: raise StorageBackendRefusal(_excval())
522 def _create(me
, file):
523 me
._db
= _G
.open(file, 'n', 0600)
525 def _close(me
, abruptp
):
529 def _get_meta(me
, name
, default
):
530 try: return me
._db
[name
]
531 except KeyError: return default
533 def _put_meta(me
, name
, value
):
536 def _del_meta(me
, name
):
540 k
= me
._db
.firstkey()
542 if not k
.startswith('$'): yield k
, me
._db
[k
]
543 k
= me
._db
.nextkey(k
)
545 def _get_passwd(me
, label
):
546 return me
._db
['$' + label
]
548 def _put_passwd(me
, label
, payload
):
549 me
._db
['$' + label
] = payload
551 def _del_passwd(me
, label
):
552 del me
._db
['$' + label
]
554 def _iter_passwds(me
):
555 k
= me
._db
.firstkey()
557 if k
.startswith('$'): yield k
[1:], me
._db
[k
]
558 k
= me
._db
.nextkey(k
)
560 try: import sqlite3
as _Q
561 except ImportError: pass
563 class SQLiteStorageBackend (StorageBackend
):
565 I represent a password database stored in SQLite.
567 Metadata and password items are stored in separate tables, so there's no
568 conflict. Some additional metadata is stored in the `meta' table, with
569 names beginning with `$' so as not to conflict with clients:
571 $version The schema version of the table.
577 def _open(me
, file, writep
):
579 me
._db
= _Q
.connect(file)
580 ver
= me
._query_scalar(
581 "SELECT value FROM meta WHERE name = '$version'",
583 except (_Q
.DatabaseError
, _Q
.OperationalError
):
584 raise StorageBackendRefusal(_excval())
585 if ver
is None: raise ValueError('database broken (missing $version)')
586 elif ver
< me
.VERSION
: me
._upgrade(ver
)
587 elif ver
> me
.VERSION
: raise ValueError \
588 ('unknown database schema version (%d > %d)' %
(ver
, me
.VERSION
))
590 def _create(me
, file):
591 fd
= _OS
.open(file, _OS
.O_WRONLY | _OS
.O_CREAT | _OS
.O_EXCL
, 0600)
594 me
._db
= _Q
.connect(file)
598 name TEXT PRIMARY KEY NOT NULL,
599 value BLOB NOT NULL);
602 CREATE TABLE passwd (
603 label BLOB PRIMARY KEY NOT NULL,
604 payload BLOB NOT NULL);
607 INSERT INTO meta (name, value) VALUES ('$version', ?);
610 try: _OS
.unlink(file)
614 def _upgrade(me
, ver
):
615 """Upgrade the database from schema version VER."""
616 assert False, 'how embarrassing'
618 def _close(me
, abruptp
):
619 if not abruptp
: me
._db
.commit()
623 def _fetch_scalar(me
, c
, what
, default
= None):
625 except StopIteration: val
= default
628 except StopIteration: pass
629 else: raise ValueError('multiple matching records for %s' % what
)
632 def _query_scalar(me
, query
, what
, default
= None, args
= []):
634 c
.execute(query
, args
)
635 return me
._fetch_scalar(c
, what
, default
)
637 def _get_meta(me
, name
, default
):
638 v
= me
._query_scalar("SELECT value FROM meta WHERE name = ?",
639 "metadata item `%s'" % name
,
640 default
= default
, args
= [name
])
641 if v
is default
: return v
644 def _put_meta(me
, name
, value
):
646 c
.execute("INSERT OR REPLACE INTO meta (name, value) VALUES (?, ?)",
647 [name
, buffer(value
)])
649 def _del_meta(me
, name
):
651 c
.execute("DELETE FROM meta WHERE name = ?", [name
])
652 if not c
.rowcount
: raise KeyError(name
)
656 c
.execute("SELECT name, value FROM meta WHERE name NOT LIKE '$%'")
657 for k
, v
in c
: yield k
, str(v
)
659 def _get_passwd(me
, label
):
660 pld
= me
._query_scalar("SELECT payload FROM passwd WHERE label = ?",
661 "password", default
= None,
662 args
= [buffer(label
)])
663 if pld
is None: raise KeyError(label
)
666 def _put_passwd(me
, label
, payload
):
668 c
.execute("INSERT OR REPLACE INTO passwd (label, payload) "
670 [buffer(label
), buffer(payload
)])
672 def _del_passwd(me
, label
):
674 c
.execute("DELETE FROM passwd WHERE label = ?", [label
])
675 if not c
.rowcount
: raise KeyError(label
)
677 def _iter_passwds(me
):
679 c
.execute("SELECT label, payload FROM passwd")
680 for k
, v
in c
: yield str(k
), str(v
)
682 class PlainTextBackend (StorageBackend
):
684 I'm a utility base class for storage backends which use plain text files.
686 I provide subclasses with the following capabilities.
688 * Creating files, with given modes, optionally ensuring that the file
689 doesn't exist already.
691 * Parsing flat text files, checking leading magic, skipping comments, and
692 providing standard encodings of troublesome characters and binary
693 strings in metadata and password records. See below.
695 * Maintenance of metadata and password records in in-memory dictionaries,
696 with ready implementations of the necessary StorageBackend subclass
697 responsibility methods. (Subclasses can override these if they want to
698 make different arrangements.)
700 Metadata records are written with an optional prefix string chosen by the
701 caller, followed by a `NAME=VALUE' pair. The NAME is form-urlencoded and
702 prefixed with `!' if it contains strange characters; the VALUE is base64-
703 encoded (without the pointless trailing `=' padding) and prefixed with `?'
706 Password records are written with an optional prefix string chosen by the
707 caller, followed by a LABEL=PAYLOAD pair, both of which are base64-encoded
710 The following attributes are available for subclasses:
712 _meta Dictionary mapping metadata item names to their values.
713 Populated by `_parse_meta' and managed by `_get_meta' and
716 _pw Dictionary mapping password labels to encrypted payloads.
717 Populated by `_parse_passwd' and managed by `_get_passwd' and
720 _dirtyp Boolean: set if either of the dictionaries has been modified.
723 def __init__(me
, *args
, **kw
):
725 Hook for initialization.
727 Sets up the published instance attributes.
732 super(PlainTextBackend
, me
).__init__(*args
, **kw
)
734 def _create_file(me
, file, mode
= 0600, freshp
= False):
736 Make sure FILE exists, creating it with the given MODE if necessary.
738 If FRESHP is true, then make sure the file did not exist previously.
739 Return a file object for the newly created file.
741 flags
= _OS
.O_CREAT | _OS
.O_WRONLY
742 if freshp
: flags |
= _OS
.O_EXCL
743 else: flags |
= _OS
.O_TRUNC
744 fd
= _OS
.open(file, flags
, mode
)
745 return _OS
.fdopen(fd
, 'w')
749 Set the `_dirtyp' flag.
751 Subclasses might find it useful to intercept this method.
755 def _eqsplit(me
, line
):
757 Extract the KEY, VALUE pair from a LINE of the form `KEY=VALUE'.
759 Raise `ValueError' if there is no `=' in the LINE.
762 return line
[:eq
], line
[eq
+ 1:]
764 def _parse_file(me
, file, magic
= None):
770 * Raise `StorageBackendRefusal' if that the first line doesn't match
771 MAGIC (if provided). MAGIC should not contain the terminating
774 * Ignore comments (beginning `#') and blank lines.
776 * Call `_parse_line' (provided by the subclass) for other lines.
778 with
open(file, 'r') as f
:
779 if magic
is not None:
780 if f
.readline().rstrip('\n') != magic
: raise StorageBackendRefusal
782 line
= line
.rstrip('\n')
783 if not line
or line
.startswith('#'): continue
786 def _write_file(me
, file, writebody
, mode
= 0600, magic
= None):
788 Update FILE atomically.
790 The newly created file will have the given MODE. If MAGIC is given, then
791 write that as the first line. Calls WRITEBODY(F) to write the main body
792 of the file where F is a file object for the new file.
795 with me
._create_file(new
, mode
) as f
:
796 if magic
is not None: f
.write(magic
+ '\n')
798 _OS
.rename(new
, file)
800 def _parse_meta(me
, line
):
801 """Parse LINE as a metadata NAME=VALUE pair, and updates `_meta'."""
802 k
, v
= me
._eqsplit(line
)
803 me
._meta
[_dec_metaname(k
)] = _dec_metaval(v
)
805 def _write_meta(me
, f
, prefix
= ''):
806 """Write the metadata records to F, each with the given PREFIX."""
807 f
.write('\n## Metadata.\n')
808 for k
, v
in _iteritems(me
._meta
):
809 f
.write('%s%s=%s\n' %
(prefix
, _enc_metaname(k
), _enc_metaval(v
)))
811 def _get_meta(me
, name
, default
):
812 return me
._meta
.get(name
, default
)
813 def _put_meta(me
, name
, value
):
815 me
._meta
[name
] = value
816 def _del_meta(me
, name
):
820 return _iteritems(me
._meta
)
822 def _parse_passwd(me
, line
):
823 """Parse LINE as a password LABEL=PAYLOAD pair, and updates `_pw'."""
824 k
, v
= me
._eqsplit(line
)
825 me
._pw
[_unb64(k
)] = _unb64(v
)
827 def _write_passwd(me
, f
, prefix
= ''):
828 """Write the password records to F, each with the given PREFIX."""
829 f
.write('\n## Password data.\n')
830 for k
, v
in _iteritems(me
._pw
):
831 f
.write('%s%s=%s\n' %
(prefix
, _b64(k
), _b64(v
)))
833 def _get_passwd(me
, label
):
834 return me
._pw
[str(label
)]
835 def _put_passwd(me
, label
, payload
):
837 me
._pw
[str(label
)] = payload
838 def _del_passwd(me
, label
):
840 del me
._pw
[str(label
)]
841 def _iter_passwds(me
):
842 return _iteritems(me
._pw
)
844 class FlatFileStorageBackend (PlainTextBackend
):
846 I maintain a password database in a plain text file.
848 The text file consists of lines, as follows.
850 * Empty lines, and lines beginning with `#' (in the leftmost column only)
853 * Lines of the form `$LABEL=PAYLOAD' store password data. Both LABEL and
854 PAYLOAD are base64-encoded, without `=' padding.
856 * Lines of the form `NAME=VALUE' store metadata. If the NAME contains
857 characters other than alphanumerics, hyphens, underscores, and colons,
858 then it is form-urlencoded, and prefixed wth `!'. If the VALUE
859 contains such characters, then it is base64-encoded, without `='
860 padding, and prefixed with `?'.
862 * Other lines are erroneous.
864 The file is rewritten from scratch when it's changed: any existing
865 commentary is lost, and items may be reordered. There is no file locking,
866 but the file is updated atomically, by renaming.
868 It is expected that the FlatFileStorageBackend is used mostly for
869 diagnostics and transfer, rather than for a live system.
874 MAGIC
= '### pwsafe password database'
876 def _open(me
, file, writep
):
877 if not _OS
.path
.isfile(file): raise StorageBackendRefusal
878 me
._parse_file(file, magic
= me
.MAGIC
)
879 def _parse_line(me
, line
):
880 if line
.startswith('$'): me
._parse_passwd(line
[1:])
881 else: me
._parse_meta(line
)
883 def _create(me
, file):
884 with me
._create_file(file, freshp
= True) as f
: pass
888 def _close(me
, abruptp
):
889 if not abruptp
and me
._dirtyp
:
890 me
._write_file(me
._file
, me
._write_body
, magic
= me
.MAGIC
)
892 def _write_body(me
, f
):
894 me
._write_passwd(f
, '$')
896 class DirectoryStorageBackend (PlainTextBackend
):
898 I maintain a password database in a directory, with one file per password.
900 This makes password databases easy to maintain in a revision-control system
903 The directory is structured as follows.
905 dir/meta Contains metadata, similar to the `FlatFileBackend'.
907 dir/pw/LABEL Contains the (raw binary) payload for the given password
908 LABEL (base64-encoded, without the useless `=' padding, and
909 with `/' replaced by `.').
911 dir/tmp/ Contains temporary files used by the implementation.
915 METAMAGIC
= '### pwsafe password directory metadata'
917 def _open(me
, file, writep
):
918 if not _OS
.path
.isdir(file) or \
919 not _OS
.path
.isdir(_OS
.path
.join(file, 'pw')) or \
920 not _OS
.path
.isdir(_OS
.path
.join(file, 'tmp')) or \
921 not _OS
.path
.isfile(_OS
.path
.join(file, 'meta')):
922 raise StorageBackendRefusal
924 me
._parse_file(_OS
.path
.join(file, 'meta'), magic
= me
.METAMAGIC
)
925 def _parse_line(me
, line
):
928 def _create(me
, file):
929 _OS
.mkdir(file, 0700)
930 _OS
.mkdir(_OS
.path
.join(file, 'pw'), 0700)
931 _OS
.mkdir(_OS
.path
.join(file, 'tmp'), 0700)
935 def _close(me
, abruptp
):
936 if not abruptp
and me
._dirtyp
:
937 me
._write_file(_OS
.path
.join(me
._dir
, 'meta'),
938 me
._write_meta
, magic
= me
.METAMAGIC
)
940 def _pwfile(me
, label
, dir = 'pw'):
941 return _OS
.path
.join(me
._dir
, dir, _b64(label
).replace('/', '.'))
942 def _get_passwd(me
, label
):
944 f
= open(me
._pwfile(label
), 'rb')
945 except (OSError, IOError):
946 if _excval().errno
== _E
.ENOENT
: raise KeyError(label
)
948 with f
: return f
.read()
949 def _put_passwd(me
, label
, payload
):
950 new
= me
._pwfile(label
, 'tmp')
951 fd
= _OS
.open(new
, _OS
.O_WRONLY | _OS
.O_CREAT | _OS
.O_TRUNC
, 0600)
953 with
open(new
, 'wb') as f
: f
.write(payload
)
954 _OS
.rename(new
, me
._pwfile(label
))
955 def _del_passwd(me
, label
):
957 _OS
.remove(me
._pwfile(label
))
958 except (OSError, IOError):
959 if _excval().errno
== _E
.ENOENT
: raise KeyError(label
)
961 def _iter_passwds(me
):
962 pw
= _OS
.path
.join(me
._dir
, 'pw')
963 for i
in _OS
.listdir(pw
):
964 with
open(_OS
.path
.join(pw
, i
), 'rb') as f
: pld
= f
.read()
965 yield _unb64(i
.replace('.', '/')), pld
967 ###--------------------------------------------------------------------------
968 ### Password storage.
972 I represent a secure (ish) password store.
974 I can store short secrets, associated with textual names, in a way which
975 doesn't leak too much information about them.
977 I implement (some of) the Python mapping protocol.
979 I keep track of everything using a StorageBackend object. This contains
980 password entries, identified by cryptographic labels, and a number of
983 cipher Names the Catacomb cipher selected.
985 hash Names the Catacomb hash function selected.
987 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
988 length and concatenated, encrypted using the master
991 mac Names the Catacomb message authentication code selected.
993 magic A magic string for obscuring password tag names.
995 salt The salt for hashing the passphrase.
997 tag The master passphrase's tag, for the Pixie's benefit.
999 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
1000 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
1001 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
1002 encrypted using the stored keys.
1005 def __init__(me
, file, writep
= False):
1007 Initialize a PW object from the database in FILE.
1009 If WRITEP is false (the default) then the database is opened read-only;
1010 if true then it may be written. Requests the database password from the
1011 Pixie, which may cause interaction.
1014 ## Open the database.
1015 me
.db
= StorageBackend
.open(file, writep
)
1017 ## Find out what crypto to use.
1018 c
= _C
.gcciphers
[me
.db
.get_meta('cipher')]
1019 h
= _C
.gchashes
[me
.db
.get_meta('hash')]
1020 m
= _C
.gcmacs
[me
.db
.get_meta('mac')]
1022 ## Request the passphrase and extract the master keys.
1023 tag
= me
.db
.get_meta('tag')
1024 ppk
= PPK(_C
.ppread(tag
), c
, h
, m
, me
.db
.get_meta('salt'))
1026 b
= _C
.ReadBuffer(ppk
.decrypt(me
.db
.get_meta('key')))
1027 except DecryptError
:
1030 me
.ck
= b
.getblk16()
1031 me
.mk
= b
.getblk16()
1032 if not b
.endp
: raise ValueError('trailing junk')
1034 ## Set the key, and stash it and the tag-hashing secret.
1035 me
.k
= Crypto(c
, h
, m
, me
.ck
, me
.mk
)
1036 me
.magic
= me
.k
.decrypt(me
.db
.get_meta('magic'))
1039 def create(cls
, dbcls
, file, tag
, c
, h
, m
):
1041 Create and initialize a new database FILE using StorageBackend DBCLS.
1043 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
1044 and a Pixie passphrase TAG.
1046 This doesn't return a working object: it just creates the database file
1047 and gets out of the way.
1050 ## Set up the cryptography.
1051 pp
= _C
.ppread(tag
, _C
.PMODE_VERIFY
)
1052 ppk
= PPK(pp
, c
, h
, m
)
1053 ck
= _C
.rand
.block(c
.keysz
.default
)
1054 mk
= _C
.rand
.block(c
.keysz
.default
)
1055 k
= Crypto(c
, h
, m
, ck
, mk
)
1057 ## Set up and initialize the database.
1058 kct
= ppk
.encrypt(_C
.WriteBuffer().putblk16(ck
).putblk16(mk
))
1059 with dbcls
.create(file) as db
:
1060 db
.put_meta('tag', tag
)
1061 db
.put_meta('salt', ppk
.salt
)
1062 db
.put_meta('cipher', c
.name
)
1063 db
.put_meta('hash', h
.name
)
1064 db
.put_meta('mac', m
.name
)
1065 db
.put_meta('key', kct
)
1066 db
.put_meta('magic', k
.encrypt(_C
.rand
.block(h
.hashsz
)))
1068 def keyxform(me
, key
):
1069 """Transform the KEY (actually a password tag) into a password label."""
1070 return me
.k
.h().hash(me
.magic
).hash(key
).done()
1074 Change the database password.
1076 Requests the new password from the Pixie, which will probably cause
1079 tag
= me
.db
.get_meta('tag')
1081 ppk
= PPK(_C
.ppread(tag
, _C
.PMODE_VERIFY
),
1082 me
.k
.c
.__class__
, me
.k
.h
, me
.k
.m
.__class__
)
1083 kct
= ppk
.encrypt(_C
.WriteBuffer().putblk16(me
.ck
).putblk16(me
.mk
))
1084 me
.db
.put_meta('key', kct
)
1085 me
.db
.put_meta('salt', ppk
.salt
)
1087 def pack(me
, key
, value
):
1088 """Pack the KEY and VALUE into a ciphertext, and return it."""
1089 b
= _C
.WriteBuffer()
1090 b
.putblk16(key
).putblk16(value
)
1091 b
.zero(((b
.size
+ 255) & ~
255) - b
.size
)
1092 return me
.k
.encrypt(b
)
1096 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
1098 Might raise DecryptError, of course.
1100 b
= _C
.ReadBuffer(me
.k
.decrypt(ct
))
1102 value
= b
.getblk16()
1105 ## Mapping protocol.
1107 def __getitem__(me
, key
):
1108 """Return the password for the given KEY."""
1109 try: return me
.unpack(me
.db
.get_passwd(me
.keyxform(key
)))[1]
1110 except KeyError: raise KeyError(key
)
1112 def __setitem__(me
, key
, value
):
1113 """Associate the password VALUE with the KEY."""
1114 me
.db
.put_passwd(me
.keyxform(key
), me
.pack(key
, value
))
1116 def __delitem__(me
, key
):
1117 """Forget all about the KEY."""
1118 try: me
.db
.del_passwd(me
.keyxform(key
))
1119 except KeyError: raise KeyError(key
)
1122 """Iterate over the known password tags."""
1123 for _
, pld
in me
.db
.iter_passwds():
1124 yield me
.unpack(pld
)[0]
1126 ## Context protocol.
1130 def __exit__(me
, excty
, excval
, exctb
):
1131 me
.db
.close(excval
is not None)
1133 ###----- That's all, folks --------------------------------------------------