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
36 if _SYS
.version_info
>= (3,): from io
import StringIO
as _StringIO
37 else: from cStringIO
import StringIO
as _StringIO
41 ###--------------------------------------------------------------------------
42 ### Python version portability.
44 if _SYS
.version_info
>= (3,):
45 def _iterkeys(dict): return dict.keys()
46 def _itervalues(dict): return dict.values()
47 def _iteritems(dict): return dict.items()
48 def _bin(text
): return text
.encode(errors
= "surrogateescape")
49 def _text(bin
): return bin
.decode(errors
= "surrogateescape")
51 def _iterkeys(dict): return dict.iterkeys()
52 def _itervalues(dict): return dict.itervalues()
53 def _iteritems(dict): return dict.iteritems()
54 def _bin(text
): return text
55 def _text(bin
): return bin
58 _CIPHER
= _bin('cipher:')
61 def _with_metaclass(meta
, *supers
):
62 return meta("#<anonymous base %s>" % meta
.__name__
,
63 supers
or (object,), dict())
65 def _excval(): return _SYS
.exc_info()[1]
70 ###--------------------------------------------------------------------------
71 ### Text encoding utilities.
75 Answer whether S can be represented literally.
77 If True, then S can be stored literally, as a metadata item name or
78 value; if False, then S requires some kind of encoding.
80 return all(ch
.isalnum() or ch
in '-_:' for ch
in s
)
82 def _enc_metaname(name
):
83 """Encode NAME as a metadata item name, returning the result."""
90 if _literalp(ch
): sio
.write(ch
)
91 elif ch
== ' ': sio
.write('+')
92 else: sio
.write('%%%02x' % ord
(ch
))
95 def _dec_metaname(name
):
96 """Decode NAME as a metadata item name, returning the result."""
97 if not name
.startswith('!'):
108 sio
.write(chr(int(name
[i
:i
+ 2], 16)))
112 return sio
.getvalue()
115 """Encode S as base64, without newlines, and trimming `=' padding."""
116 return _text(_B
.b2a_base64(s
)).replace('\n', '').rstrip('=')
118 """Decode S as base64 with trimmed `=' padding."""
119 return _B
.a2b_base64(s
+ '='*((4 - len(s
))%4))
121 def _enc_metaval(val
):
122 """Encode VAL as a metadata item value, returning the result."""
123 if _literalp(val
): return val
124 else: return '?' + _b64(val
)
126 def _dec_metaval(val
):
127 """Decode VAL as a metadata item value, returning the result."""
128 if not val
.startswith('?'): return val
129 else: return _unb64(val
[1:])
131 ###--------------------------------------------------------------------------
132 ### Underlying cryptography.
134 class DecryptError (Exception):
136 I represent a failure to decrypt a message.
138 Usually this means that someone used the wrong key, though it can also
139 mean that a ciphertext has been modified.
143 class Crypto (object):
145 I represent a symmetric crypto transform.
147 There's currently only one transform implemented, which is the obvious
148 generic-composition construction: given a message m, and keys K0 and K1, we
149 choose an IV v, and compute:
151 * y = v || E(K0, v; m)
154 The final ciphertext is t || y.
157 def __init__(me
, c
, h
, m
, ck
, mk
):
159 Initialize the Crypto object with a given algorithm selection and keys.
161 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
162 keys CK and MK for C and M respectively.
170 Encrypt the message PT and return the resulting ciphertext.
172 blksz
= me
.c
.__class__
.blksz
175 iv
= _C
.rand
.block(blksz
)
178 b
.put(me
.c
.encrypt(pt
))
179 t
= me
.m().hash(b
).done()
180 return t
+ str(buffer(b
))
184 Decrypt the ciphertext CT, returning the plaintext.
186 Raises DecryptError if anything goes wrong.
188 blksz
= me
.c
.__class__
.blksz
189 tagsz
= me
.m
.__class__
.tagsz
190 b
= _C
.ReadBuffer(ct
)
199 if t
!= h
.done(): raise DecryptError
200 return me
.c
.decrypt(x
)
204 I represent a crypto transform whose keys are derived from a passphrase.
206 The password is salted and hashed; the salt is available as the `salt'
210 def __init__(me
, pp
, c
, h
, m
, salt
= None):
212 Initialize the PPK object with a passphrase and algorithm selection.
214 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
215 subclass M, and a SALT. The SALT may be None, if we're generating new
216 keys, indicating that a salt should be chosen randomly.
218 if not salt
: salt
= _C
.rand
.block(h
.hashsz
)
219 tag
= pp
+ _NUL
+ salt
220 Crypto
.__init__(me
, c
, h
, m
,
221 h().hash(_CIPHER
).hash(tag
).done(),
222 h().hash(_MAC
).hash(tag
).done())
225 ###--------------------------------------------------------------------------
228 class StorageBackendRefusal (Exception):
230 I signify that a StorageBackend subclass has refused to open a file.
232 This is used by the StorageBackend.open class method.
236 class StorageBackendClass (type):
238 I am a metaclass for StorageBackend classes.
240 My main feature is that I register my concrete instances (with a `NAME'
241 which is not `None') with the StorageBackend class.
243 def __init__(me
, name
, supers
, dict):
245 Register a new concrete StorageBackend subclass.
247 super(StorageBackendClass
, me
).__init__(name
, supers
, dict)
249 except AttributeError: pass
250 else: StorageBackend
.register_concrete_subclass(me
)
252 class StorageBackend (_with_metaclass(StorageBackendClass
)):
254 I provide basic protocol for password storage backends.
256 I'm an abstract class: you want one of my subclasses if you actually want
257 to do something useful. But I maintain a list of my subclasses and can
258 choose an appropriate one to open a database file you've found lying about.
260 Backends are responsible for storing and retrieving stuff, but not for the
261 cryptographic details. Backends need to store two kinds of information:
263 * metadata, consisting of a number of property names and their values;
266 * password mappings, consisting of a number of binary labels and
269 Backends need to implement the following ordinary methods. See the calling
270 methods for details of the subclass responsibilities.
272 BE._create(FILE) Create a new database in FILE; used by `create'.
274 BE._open(FILE, WRITEP)
275 Open the existing database FILE; used by `open'.
277 BE._close(ABRUPTP) Close the database, freeing up any resources. If
278 ABRUPTP then don't try to commit changes.
280 BE._get_meta(NAME, DEFAULT)
281 Return the value of the metadata item with the given
282 NAME, or DEFAULT if it doesn't exist; used by
285 BE._put_meta(NAME, VALUE)
286 Set the VALUE of the metadata item with the given
287 NAME, creating one if necessary; used by `put_meta'.
289 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
290 `KeyError' if there is no such item; used by
293 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
294 pairs; used by `iter_meta'.
296 BE._get_passwd(LABEL)
297 Return the password payload stored with the (binary)
298 LABEL; used by `get_passwd'.
300 BE._put_passwd(LABEL, PAYLOAD)
301 Associate the (binary) PAYLOAD with the LABEL,
302 forgetting any previous payload for that LABEL; used
305 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
308 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
309 pairs; used by `iter_passwds'.
311 Also, concrete subclasses should define the following class attributes.
313 NAME The name of the backend, so that the user can select
314 it when creating a new database.
316 PRIO An integer priority: backends are tried in decreasing
317 priority order when opening an existing database.
322 ## The registry of subclasses.
328 def register_concrete_subclass(sub
):
329 """Register a concrete subclass, so that `open' can try it."""
330 StorageBackend
.CLASSES
[sub
.NAME
] = sub
335 Return the concrete subclass with the given NAME.
337 Raise `KeyError' if the name isn't found.
339 return StorageBackend
.CLASSES
[name
]
343 """Return an iterator over the concrete subclasses."""
344 return _itervalues(StorageBackend
.CLASSES
)
347 def open(file, writep
= False):
348 """Open a database FILE, using some appropriate backend."""
350 for cls
in sorted(StorageBackend
.CLASSES
.values(), reverse
= True,
351 key
= lambda cls
: cls
.PRIO
):
352 try: return cls(file, writep
)
353 except StorageBackendRefusal
: pass
354 raise StorageBackendRefusal
357 def create(cls
, file):
359 Create a new database in the named FILE, using this backend.
361 Subclasses must implement the `_create' instance method.
363 return cls(writep
= True, _magic
= lambda me
: me
._create(file))
365 def __init__(me
, file = None, writep
= False, _magic
= None, *args
, **kw
):
369 Subclasses are not, in general, expected to override this: there's a
370 somewhat hairy protocol between the constructor and some of the class
371 methods. Instead, the main hook for customization is the subclass's
372 `_open' method, which is invoked in the usual case.
374 super(StorageBackend
, me
).__init__(*args
, **kw
)
375 if me
.NAME
is None: raise ValueError('abstract class')
376 if _magic
is not None: _magic(me
)
377 elif file is None: raise ValueError('missing file parameter')
378 else: me
._open(file, writep
)
382 def close(me
, abruptp
= False):
386 It is harmless to attempt to close a database which has been closed
387 already. Calls the subclass's `_close' method.
396 """Raise an error if the receiver has been closed."""
397 if not me
._livep
: raise ValueError('database is closed')
399 def _check_write(me
):
400 """Raise an error if the receiver is not open for writing."""
402 if not me
._writep
: raise ValueError('database is read-only')
404 def _check_meta_name(me
, name
):
406 Raise an error unless NAME is a valid name for a metadata item.
408 Metadata names may not start with `$': such names are reserved for
411 if name
.startswith('$'):
412 raise ValueError("invalid metadata key `%s'" % name
)
417 """Context protocol: make sure the database is closed on exit."""
419 def __exit__(me
, exctype
, excvalue
, exctb
):
420 """Context protocol: see `__enter__'."""
421 me
.close(excvalue
is not None)
425 def get_meta(me
, name
, default
= FAIL
):
427 Fetch the value for the metadata item NAME.
429 If no such item exists, then return DEFAULT if that was set; otherwise
432 This calls the subclass's `_get_meta' method, which should return the
433 requested item or return the given DEFAULT value. It may assume that the
434 name is valid and the database is open.
436 me
._check_meta_name(name
)
438 value
= me
._get_meta(name
, default
)
439 if value
is StorageBackend
.FAIL
: raise KeyError(name
)
442 def put_meta(me
, name
, value
):
444 Store VALUE in the metadata item called NAME.
446 This calls the subclass's `_put_meta' method, which may assume that the
447 name is valid and the database is open for writing.
449 me
._check_meta_name(name
)
451 me
._put_meta(name
, value
)
453 def del_meta(me
, name
):
455 Forget about the metadata item with the given NAME.
457 This calls the subclass's `_del_meta' method, which may assume that the
458 name is valid and the database is open for writing.
460 me
._check_meta_name(name
)
466 Return an iterator over the name/value metadata items.
468 This calls the subclass's `_iter_meta' method, which may assume that the
472 return me
._iter_meta()
474 def get_passwd(me
, label
):
476 Fetch and return the payload stored with the (opaque, binary) LABEL.
478 If there is no such payload then raise `KeyError'.
480 This calls the subclass's `_get_passwd' method, which may assume that the
484 return me
._get_passwd(label
)
486 def put_passwd(me
, label
, payload
):
488 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
490 Any previous payload for LABEL is forgotten.
492 This calls the subclass's `_put_passwd' method, which may assume that the
493 database is open for writing.
496 me
._put_passwd(label
, payload
)
498 def del_passwd(me
, label
):
500 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
502 If there is no such payload then raise `KeyError'.
504 This calls the subclass's `_del_passwd' method, which may assume that the
505 database is open for writing.
508 me
._del_passwd(label
)
510 def iter_passwds(me
):
512 Return an iterator over the stored password label/payload pairs.
514 This calls the subclass's `_iter_passwds' method, which may assume that
515 the database is open.
518 return me
._iter_passwds()
520 try: import gdbm
as _G
521 except ImportError: pass
523 class GDBMStorageBackend (StorageBackend
):
525 My instances store password data in a GDBM database.
527 Metadata and password entries are mixed into the same database. The key
528 for a metadata item is simply its name; the key for a password entry is
529 the entry's label prefixed by `$', since we're guaranteed that no
530 metadata item name begins with `$'.
535 def _open(me
, file, writep
):
536 try: me
._db
= _G
.open(file, writep
and 'w' or 'r')
537 except _G
.error
: raise StorageBackendRefusal(_excval())
539 def _create(me
, file):
540 me
._db
= _G
.open(file, 'n', _M600
)
542 def _close(me
, abruptp
):
546 def _get_meta(me
, name
, default
):
547 try: return me
._db
[name
]
548 except KeyError: return default
550 def _put_meta(me
, name
, value
):
553 def _del_meta(me
, name
):
557 k
= me
._db
.firstkey()
559 if not k
.startswith('$'): yield k
, me
._db
[k
]
560 k
= me
._db
.nextkey(k
)
562 def _get_passwd(me
, label
):
563 return me
._db
['$' + label
]
565 def _put_passwd(me
, label
, payload
):
566 me
._db
['$' + label
] = payload
568 def _del_passwd(me
, label
):
569 del me
._db
['$' + label
]
571 def _iter_passwds(me
):
572 k
= me
._db
.firstkey()
574 if k
.startswith('$'): yield k
[1:], me
._db
[k
]
575 k
= me
._db
.nextkey(k
)
577 try: import sqlite3
as _Q
578 except ImportError: pass
580 class SQLiteStorageBackend (StorageBackend
):
582 I represent a password database stored in SQLite.
584 Metadata and password items are stored in separate tables, so there's no
585 conflict. Some additional metadata is stored in the `meta' table, with
586 names beginning with `$' so as not to conflict with clients:
588 $version The schema version of the table.
594 def _open(me
, file, writep
):
596 me
._db
= _Q
.connect(file)
597 ver
= me
._query_scalar(
598 "SELECT value FROM meta WHERE name = '$version'",
600 except (_Q
.DatabaseError
, _Q
.OperationalError
):
601 raise StorageBackendRefusal(_excval())
602 if ver
is None: raise ValueError('database broken (missing $version)')
603 elif ver
< me
.VERSION
: me
._upgrade(ver
)
604 elif ver
> me
.VERSION
: raise ValueError \
605 ('unknown database schema version (%d > %d)' %
(ver
, me
.VERSION
))
607 def _create(me
, file):
608 fd
= _OS
.open(file, _OS
.O_WRONLY | _OS
.O_CREAT | _OS
.O_EXCL
, _M600
)
611 me
._db
= _Q
.connect(file)
615 name TEXT PRIMARY KEY NOT NULL,
616 value BLOB NOT NULL);
619 CREATE TABLE passwd (
620 label BLOB PRIMARY KEY NOT NULL,
621 payload BLOB NOT NULL);
624 INSERT INTO meta (name, value) VALUES ('$version', ?);
627 try: _OS
.unlink(file)
631 def _upgrade(me
, ver
):
632 """Upgrade the database from schema version VER."""
633 assert False, 'how embarrassing'
635 def _close(me
, abruptp
):
636 if not abruptp
: me
._db
.commit()
640 def _fetch_scalar(me
, c
, what
, default
= None):
642 except StopIteration: val
= default
645 except StopIteration: pass
646 else: raise ValueError('multiple matching records for %s' % what
)
649 def _query_scalar(me
, query
, what
, default
= None, args
= []):
651 c
.execute(query
, args
)
652 return me
._fetch_scalar(c
, what
, default
)
654 def _get_meta(me
, name
, default
):
655 v
= me
._query_scalar("SELECT value FROM meta WHERE name = ?",
656 "metadata item `%s'" % name
,
657 default
= default
, args
= [name
])
658 if v
is default
: return v
661 def _put_meta(me
, name
, value
):
663 c
.execute("INSERT OR REPLACE INTO meta (name, value) VALUES (?, ?)",
664 [name
, buffer(value
)])
666 def _del_meta(me
, name
):
668 c
.execute("DELETE FROM meta WHERE name = ?", [name
])
669 if not c
.rowcount
: raise KeyError(name
)
673 c
.execute("SELECT name, value FROM meta WHERE name NOT LIKE '$%'")
674 for k
, v
in c
: yield k
, str(v
)
676 def _get_passwd(me
, label
):
677 pld
= me
._query_scalar("SELECT payload FROM passwd WHERE label = ?",
678 "password", default
= None,
679 args
= [buffer(label
)])
680 if pld
is None: raise KeyError(label
)
683 def _put_passwd(me
, label
, payload
):
685 c
.execute("INSERT OR REPLACE INTO passwd (label, payload) "
687 [buffer(label
), buffer(payload
)])
689 def _del_passwd(me
, label
):
691 c
.execute("DELETE FROM passwd WHERE label = ?", [label
])
692 if not c
.rowcount
: raise KeyError(label
)
694 def _iter_passwds(me
):
696 c
.execute("SELECT label, payload FROM passwd")
697 for k
, v
in c
: yield str(k
), str(v
)
699 class PlainTextBackend (StorageBackend
):
701 I'm a utility base class for storage backends which use plain text files.
703 I provide subclasses with the following capabilities.
705 * Creating files, with given modes, optionally ensuring that the file
706 doesn't exist already.
708 * Parsing flat text files, checking leading magic, skipping comments, and
709 providing standard encodings of troublesome characters and binary
710 strings in metadata and password records. See below.
712 * Maintenance of metadata and password records in in-memory dictionaries,
713 with ready implementations of the necessary StorageBackend subclass
714 responsibility methods. (Subclasses can override these if they want to
715 make different arrangements.)
717 Metadata records are written with an optional prefix string chosen by the
718 caller, followed by a `NAME=VALUE' pair. The NAME is form-urlencoded and
719 prefixed with `!' if it contains strange characters; the VALUE is base64-
720 encoded (without the pointless trailing `=' padding) and prefixed with `?'
723 Password records are written with an optional prefix string chosen by the
724 caller, followed by a LABEL=PAYLOAD pair, both of which are base64-encoded
727 The following attributes are available for subclasses:
729 _meta Dictionary mapping metadata item names to their values.
730 Populated by `_parse_meta' and managed by `_get_meta' and
733 _pw Dictionary mapping password labels to encrypted payloads.
734 Populated by `_parse_passwd' and managed by `_get_passwd' and
737 _dirtyp Boolean: set if either of the dictionaries has been modified.
740 def __init__(me
, *args
, **kw
):
742 Hook for initialization.
744 Sets up the published instance attributes.
749 super(PlainTextBackend
, me
).__init__(*args
, **kw
)
751 def _create_file(me
, file, mode
= _M600
, freshp
= False):
753 Make sure FILE exists, creating it with the given MODE if necessary.
755 If FRESHP is true, then make sure the file did not exist previously.
756 Return a file object for the newly created file.
758 flags
= _OS
.O_CREAT | _OS
.O_WRONLY
759 if freshp
: flags |
= _OS
.O_EXCL
760 else: flags |
= _OS
.O_TRUNC
761 fd
= _OS
.open(file, flags
, mode
)
762 return _OS
.fdopen(fd
, 'w')
766 Set the `_dirtyp' flag.
768 Subclasses might find it useful to intercept this method.
772 def _eqsplit(me
, line
):
774 Extract the KEY, VALUE pair from a LINE of the form `KEY=VALUE'.
776 Raise `ValueError' if there is no `=' in the LINE.
779 return line
[:eq
], line
[eq
+ 1:]
781 def _parse_file(me
, file, magic
= None):
787 * Raise `StorageBackendRefusal' if that the first line doesn't match
788 MAGIC (if provided). MAGIC should not contain the terminating
791 * Ignore comments (beginning `#') and blank lines.
793 * Call `_parse_line' (provided by the subclass) for other lines.
795 with
open(file, 'r') as f
:
796 if magic
is not None:
797 if f
.readline().rstrip('\n') != magic
: raise StorageBackendRefusal
799 line
= line
.rstrip('\n')
800 if not line
or line
.startswith('#'): continue
803 def _write_file(me
, file, writebody
, mode
= _M600
, magic
= None):
805 Update FILE atomically.
807 The newly created file will have the given MODE. If MAGIC is given, then
808 write that as the first line. Calls WRITEBODY(F) to write the main body
809 of the file where F is a file object for the new file.
812 with me
._create_file(new
, mode
) as f
:
813 if magic
is not None: f
.write(magic
+ '\n')
815 _OS
.rename(new
, file)
817 def _parse_meta(me
, line
):
818 """Parse LINE as a metadata NAME=VALUE pair, and updates `_meta'."""
819 k
, v
= me
._eqsplit(line
)
820 me
._meta
[_dec_metaname(k
)] = _dec_metaval(v
)
822 def _write_meta(me
, f
, prefix
= ''):
823 """Write the metadata records to F, each with the given PREFIX."""
824 f
.write('\n## Metadata.\n')
825 for k
, v
in _iteritems(me
._meta
):
826 f
.write('%s%s=%s\n' %
(prefix
, _enc_metaname(k
), _enc_metaval(v
)))
828 def _get_meta(me
, name
, default
):
829 return me
._meta
.get(name
, default
)
830 def _put_meta(me
, name
, value
):
832 me
._meta
[name
] = value
833 def _del_meta(me
, name
):
837 return _iteritems(me
._meta
)
839 def _parse_passwd(me
, line
):
840 """Parse LINE as a password LABEL=PAYLOAD pair, and updates `_pw'."""
841 k
, v
= me
._eqsplit(line
)
842 me
._pw
[_unb64(k
)] = _unb64(v
)
844 def _write_passwd(me
, f
, prefix
= ''):
845 """Write the password records to F, each with the given PREFIX."""
846 f
.write('\n## Password data.\n')
847 for k
, v
in _iteritems(me
._pw
):
848 f
.write('%s%s=%s\n' %
(prefix
, _b64(k
), _b64(v
)))
850 def _get_passwd(me
, label
):
851 return me
._pw
[str(label
)]
852 def _put_passwd(me
, label
, payload
):
854 me
._pw
[str(label
)] = payload
855 def _del_passwd(me
, label
):
857 del me
._pw
[str(label
)]
858 def _iter_passwds(me
):
859 return _iteritems(me
._pw
)
861 class FlatFileStorageBackend (PlainTextBackend
):
863 I maintain a password database in a plain text file.
865 The text file consists of lines, as follows.
867 * Empty lines, and lines beginning with `#' (in the leftmost column only)
870 * Lines of the form `$LABEL=PAYLOAD' store password data. Both LABEL and
871 PAYLOAD are base64-encoded, without `=' padding.
873 * Lines of the form `NAME=VALUE' store metadata. If the NAME contains
874 characters other than alphanumerics, hyphens, underscores, and colons,
875 then it is form-urlencoded, and prefixed wth `!'. If the VALUE
876 contains such characters, then it is base64-encoded, without `='
877 padding, and prefixed with `?'.
879 * Other lines are erroneous.
881 The file is rewritten from scratch when it's changed: any existing
882 commentary is lost, and items may be reordered. There is no file locking,
883 but the file is updated atomically, by renaming.
885 It is expected that the FlatFileStorageBackend is used mostly for
886 diagnostics and transfer, rather than for a live system.
891 MAGIC
= '### pwsafe password database'
893 def _open(me
, file, writep
):
894 if not _OS
.path
.isfile(file): raise StorageBackendRefusal
895 me
._parse_file(file, magic
= me
.MAGIC
)
896 def _parse_line(me
, line
):
897 if line
.startswith('$'): me
._parse_passwd(line
[1:])
898 else: me
._parse_meta(line
)
900 def _create(me
, file):
901 with me
._create_file(file, freshp
= True) as f
: pass
905 def _close(me
, abruptp
):
906 if not abruptp
and me
._dirtyp
:
907 me
._write_file(me
._file
, me
._write_body
, magic
= me
.MAGIC
)
909 def _write_body(me
, f
):
911 me
._write_passwd(f
, '$')
913 class DirectoryStorageBackend (PlainTextBackend
):
915 I maintain a password database in a directory, with one file per password.
917 This makes password databases easy to maintain in a revision-control system
920 The directory is structured as follows.
922 dir/meta Contains metadata, similar to the `FlatFileBackend'.
924 dir/pw/LABEL Contains the (raw binary) payload for the given password
925 LABEL (base64-encoded, without the useless `=' padding, and
926 with `/' replaced by `.').
928 dir/tmp/ Contains temporary files used by the implementation.
932 METAMAGIC
= '### pwsafe password directory metadata'
934 def _open(me
, file, writep
):
935 if not _OS
.path
.isdir(file) or \
936 not _OS
.path
.isdir(_OS
.path
.join(file, 'pw')) or \
937 not _OS
.path
.isdir(_OS
.path
.join(file, 'tmp')) or \
938 not _OS
.path
.isfile(_OS
.path
.join(file, 'meta')):
939 raise StorageBackendRefusal
941 me
._parse_file(_OS
.path
.join(file, 'meta'), magic
= me
.METAMAGIC
)
942 def _parse_line(me
, line
):
945 def _create(me
, file):
946 _OS
.mkdir(file, _M700
)
947 _OS
.mkdir(_OS
.path
.join(file, 'pw'), _M700
)
948 _OS
.mkdir(_OS
.path
.join(file, 'tmp'), _M700
)
952 def _close(me
, abruptp
):
953 if not abruptp
and me
._dirtyp
:
954 me
._write_file(_OS
.path
.join(me
._dir
, 'meta'),
955 me
._write_meta
, magic
= me
.METAMAGIC
)
957 def _pwfile(me
, label
, dir = 'pw'):
958 return _OS
.path
.join(me
._dir
, dir, _b64(label
).replace('/', '.'))
959 def _get_passwd(me
, label
):
961 f
= open(me
._pwfile(label
), 'rb')
962 except (OSError, IOError):
963 if _excval().errno
== _E
.ENOENT
: raise KeyError(label
)
965 with f
: return f
.read()
966 def _put_passwd(me
, label
, payload
):
967 new
= me
._pwfile(label
, 'tmp')
968 fd
= _OS
.open(new
, _OS
.O_WRONLY | _OS
.O_CREAT | _OS
.O_TRUNC
, _M600
)
970 with
open(new
, 'wb') as f
: f
.write(payload
)
971 _OS
.rename(new
, me
._pwfile(label
))
972 def _del_passwd(me
, label
):
974 _OS
.remove(me
._pwfile(label
))
975 except (OSError, IOError):
976 if _excval().errno
== _E
.ENOENT
: raise KeyError(label
)
978 def _iter_passwds(me
):
979 pw
= _OS
.path
.join(me
._dir
, 'pw')
980 for i
in _OS
.listdir(pw
):
981 with
open(_OS
.path
.join(pw
, i
), 'rb') as f
: pld
= f
.read()
982 yield _unb64(i
.replace('.', '/')), pld
984 ###--------------------------------------------------------------------------
985 ### Password storage.
989 I represent a secure (ish) password store.
991 I can store short secrets, associated with textual names, in a way which
992 doesn't leak too much information about them.
994 I implement (some of) the Python mapping protocol.
996 I keep track of everything using a StorageBackend object. This contains
997 password entries, identified by cryptographic labels, and a number of
1000 cipher Names the Catacomb cipher selected.
1002 hash Names the Catacomb hash function selected.
1004 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
1005 length and concatenated, encrypted using the master
1008 mac Names the Catacomb message authentication code selected.
1010 magic A magic string for obscuring password tag names.
1012 salt The salt for hashing the passphrase.
1014 tag The master passphrase's tag, for the Pixie's benefit.
1016 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
1017 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
1018 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
1019 encrypted using the stored keys.
1022 def __init__(me
, file, writep
= False):
1024 Initialize a PW object from the database in FILE.
1026 If WRITEP is false (the default) then the database is opened read-only;
1027 if true then it may be written. Requests the database password from the
1028 Pixie, which may cause interaction.
1031 ## Open the database.
1032 me
.db
= StorageBackend
.open(file, writep
)
1034 ## Find out what crypto to use.
1035 c
= _C
.gcciphers
[me
.db
.get_meta('cipher')]
1036 h
= _C
.gchashes
[me
.db
.get_meta('hash')]
1037 m
= _C
.gcmacs
[me
.db
.get_meta('mac')]
1039 ## Request the passphrase and extract the master keys.
1040 tag
= me
.db
.get_meta('tag')
1041 ppk
= PPK(_C
.ppread(tag
), c
, h
, m
, me
.db
.get_meta('salt'))
1043 b
= _C
.ReadBuffer(ppk
.decrypt(me
.db
.get_meta('key')))
1044 except DecryptError
:
1047 me
.ck
= b
.getblk16()
1048 me
.mk
= b
.getblk16()
1049 if not b
.endp
: raise ValueError('trailing junk')
1051 ## Set the key, and stash it and the tag-hashing secret.
1052 me
.k
= Crypto(c
, h
, m
, me
.ck
, me
.mk
)
1053 me
.magic
= me
.k
.decrypt(me
.db
.get_meta('magic'))
1056 def create(cls
, dbcls
, file, tag
, c
, h
, m
):
1058 Create and initialize a new database FILE using StorageBackend DBCLS.
1060 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
1061 and a Pixie passphrase TAG.
1063 This doesn't return a working object: it just creates the database file
1064 and gets out of the way.
1067 ## Set up the cryptography.
1068 pp
= _C
.ppread(tag
, _C
.PMODE_VERIFY
)
1069 ppk
= PPK(pp
, c
, h
, m
)
1070 ck
= _C
.rand
.block(c
.keysz
.default
)
1071 mk
= _C
.rand
.block(c
.keysz
.default
)
1072 k
= Crypto(c
, h
, m
, ck
, mk
)
1074 ## Set up and initialize the database.
1075 kct
= ppk
.encrypt(_C
.WriteBuffer().putblk16(ck
).putblk16(mk
))
1076 with dbcls
.create(file) as db
:
1077 db
.put_meta('tag', tag
)
1078 db
.put_meta('salt', ppk
.salt
)
1079 db
.put_meta('cipher', c
.name
)
1080 db
.put_meta('hash', h
.name
)
1081 db
.put_meta('mac', m
.name
)
1082 db
.put_meta('key', kct
)
1083 db
.put_meta('magic', k
.encrypt(_C
.rand
.block(h
.hashsz
)))
1085 def keyxform(me
, key
):
1086 """Transform the KEY (actually a password tag) into a password label."""
1087 return me
.k
.h().hash(me
.magic
).hash(key
).done()
1091 Change the database password.
1093 Requests the new password from the Pixie, which will probably cause
1096 tag
= me
.db
.get_meta('tag')
1098 ppk
= PPK(_C
.ppread(tag
, _C
.PMODE_VERIFY
),
1099 me
.k
.c
.__class__
, me
.k
.h
, me
.k
.m
.__class__
)
1100 kct
= ppk
.encrypt(_C
.WriteBuffer().putblk16(me
.ck
).putblk16(me
.mk
))
1101 me
.db
.put_meta('key', kct
)
1102 me
.db
.put_meta('salt', ppk
.salt
)
1104 def pack(me
, key
, value
):
1105 """Pack the KEY and VALUE into a ciphertext, and return it."""
1106 b
= _C
.WriteBuffer()
1107 b
.putblk16(key
).putblk16(value
)
1108 b
.zero(((b
.size
+ 255) & ~
255) - b
.size
)
1109 return me
.k
.encrypt(b
)
1113 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
1115 Might raise DecryptError, of course.
1117 b
= _C
.ReadBuffer(me
.k
.decrypt(ct
))
1119 value
= b
.getblk16()
1122 ## Mapping protocol.
1124 def __getitem__(me
, key
):
1125 """Return the password for the given KEY."""
1126 try: return me
.unpack(me
.db
.get_passwd(me
.keyxform(key
)))[1]
1127 except KeyError: raise KeyError(key
)
1129 def __setitem__(me
, key
, value
):
1130 """Associate the password VALUE with the KEY."""
1131 me
.db
.put_passwd(me
.keyxform(key
), me
.pack(key
, value
))
1133 def __delitem__(me
, key
):
1134 """Forget all about the KEY."""
1135 try: me
.db
.del_passwd(me
.keyxform(key
))
1136 except KeyError: raise KeyError(key
)
1139 """Iterate over the known password tags."""
1140 for _
, pld
in me
.db
.iter_passwds():
1141 yield me
.unpack(pld
)[0]
1143 ## Context protocol.
1147 def __exit__(me
, excty
, excval
, exctb
):
1148 me
.db
.close(excval
is not None)
1150 ###----- That's all, folks --------------------------------------------------