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 _excval(): return _SYS
.exc_info()[1]
42 ###--------------------------------------------------------------------------
43 ### Text encoding utilities.
47 Answer whether S can be represented literally.
49 If True, then S can be stored literally, as a metadata item name or
50 value; if False, then S requires some kind of encoding.
52 return all(ch
.isalnum() or ch
in '-_:' for ch
in s
)
54 def _enc_metaname(name
):
55 """Encode NAME as a metadata item name, returning the result."""
62 if _literalp(ch
): sio
.write(ch
)
63 elif ch
== ' ': sio
.write('+')
64 else: sio
.write('%%%02x' % ord
(ch
))
67 def _dec_metaname(name
):
68 """Decode NAME as a metadata item name, returning the result."""
69 if not name
.startswith('!'):
80 sio
.write(chr(int(name
[i
:i
+ 2], 16)))
87 """Encode S as base64, without newlines, and trimming `=' padding."""
88 return s
.encode('base64').replace('\n', '').rstrip('=')
90 """Decode S as base64 with trimmed `=' padding."""
91 return (s
+ '='*((4 - len(s
))%4)).decode('base64')
93 def _enc_metaval(val
):
94 """Encode VAL as a metadata item value, returning the result."""
95 if _literalp(val
): return val
96 else: return '?' + _b64(val
)
98 def _dec_metaval(val
):
99 """Decode VAL as a metadata item value, returning the result."""
100 if not val
.startswith('?'): return val
101 else: return _unb64(val
[1:])
103 ###--------------------------------------------------------------------------
104 ### Underlying cryptography.
106 class DecryptError (Exception):
108 I represent a failure to decrypt a message.
110 Usually this means that someone used the wrong key, though it can also
111 mean that a ciphertext has been modified.
115 class Crypto (object):
117 I represent a symmetric crypto transform.
119 There's currently only one transform implemented, which is the obvious
120 generic-composition construction: given a message m, and keys K0 and K1, we
121 choose an IV v, and compute:
123 * y = v || E(K0, v; m)
126 The final ciphertext is t || y.
129 def __init__(me
, c
, h
, m
, ck
, mk
):
131 Initialize the Crypto object with a given algorithm selection and keys.
133 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
134 keys CK and MK for C and M respectively.
142 Encrypt the message PT and return the resulting ciphertext.
144 blksz
= me
.c
.__class__
.blksz
147 iv
= _C
.rand
.block(blksz
)
150 b
.put(me
.c
.encrypt(pt
))
151 t
= me
.m().hash(b
).done()
152 return t
+ str(buffer(b
))
156 Decrypt the ciphertext CT, returning the plaintext.
158 Raises DecryptError if anything goes wrong.
160 blksz
= me
.c
.__class__
.blksz
161 tagsz
= me
.m
.__class__
.tagsz
162 b
= _C
.ReadBuffer(ct
)
171 if t
!= h
.done(): raise DecryptError
172 return me
.c
.decrypt(x
)
176 I represent a crypto transform whose keys are derived from a passphrase.
178 The password is salted and hashed; the salt is available as the `salt'
182 def __init__(me
, pp
, c
, h
, m
, salt
= None):
184 Initialize the PPK object with a passphrase and algorithm selection.
186 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
187 subclass M, and a SALT. The SALT may be None, if we're generating new
188 keys, indicating that a salt should be chosen randomly.
190 if not salt
: salt
= _C
.rand
.block(h
.hashsz
)
191 tag
= '%s\0%s' %
(pp
, salt
)
192 Crypto
.__init__(me
, c
, h
, m
,
193 h().hash('cipher:' + tag
).done(),
194 h().hash('mac:' + tag
).done())
197 ###--------------------------------------------------------------------------
200 class StorageBackendRefusal (Exception):
202 I signify that a StorageBackend subclass has refused to open a file.
204 This is used by the StorageBackend.open class method.
208 class StorageBackendClass (type):
210 I am a metaclass for StorageBackend classes.
212 My main feature is that I register my concrete instances (with a `NAME'
213 which is not `None') with the StorageBackend class.
215 def __init__(me
, name
, supers
, dict):
217 Register a new concrete StorageBackend subclass.
219 super(StorageBackendClass
, me
).__init__(name
, supers
, dict)
220 if me
.NAME
is not None: StorageBackend
.register_concrete_subclass(me
)
222 class StorageBackend (object):
224 I provide basic protocol for password storage backends.
226 I'm an abstract class: you want one of my subclasses if you actually want
227 to do something useful. But I maintain a list of my subclasses and can
228 choose an appropriate one to open a database file you've found lying about.
230 Backends are responsible for storing and retrieving stuff, but not for the
231 cryptographic details. Backends need to store two kinds of information:
233 * metadata, consisting of a number of property names and their values;
236 * password mappings, consisting of a number of binary labels and
239 Backends need to implement the following ordinary methods. See the calling
240 methods for details of the subclass responsibilities.
242 BE._create(FILE) Create a new database in FILE; used by `create'.
244 BE._open(FILE, WRITEP)
245 Open the existing database FILE; used by `open'.
247 BE._close(ABRUPTP) Close the database, freeing up any resources. If
248 ABRUPTP then don't try to commit changes.
250 BE._get_meta(NAME, DEFAULT)
251 Return the value of the metadata item with the given
252 NAME, or DEFAULT if it doesn't exist; used by
255 BE._put_meta(NAME, VALUE)
256 Set the VALUE of the metadata item with the given
257 NAME, creating one if necessary; used by `put_meta'.
259 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
260 `KeyError' if there is no such item; used by
263 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
264 pairs; used by `iter_meta'.
266 BE._get_passwd(LABEL)
267 Return the password payload stored with the (binary)
268 LABEL; used by `get_passwd'.
270 BE._put_passwd(LABEL, PAYLOAD)
271 Associate the (binary) PAYLOAD with the LABEL,
272 forgetting any previous payload for that LABEL; used
275 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
278 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
279 pairs; used by `iter_passwds'.
281 Also, concrete subclasses should define the following class attributes.
283 NAME The name of the backend, so that the user can select
284 it when creating a new database.
286 PRIO An integer priority: backends are tried in decreasing
287 priority order when opening an existing database.
290 __metaclass__
= StorageBackendClass
294 ## The registry of subclasses.
300 def register_concrete_subclass(sub
):
301 """Register a concrete subclass, so that `open' can try it."""
302 StorageBackend
.CLASSES
[sub
.NAME
] = sub
307 Return the concrete subclass with the given NAME.
309 Raise `KeyError' if the name isn't found.
311 return StorageBackend
.CLASSES
[name
]
315 """Return an iterator over the concrete subclasses."""
316 return StorageBackend
.CLASSES
.itervalues()
319 def open(file, writep
= False):
320 """Open a database FILE, using some appropriate backend."""
322 for cls
in sorted(StorageBackend
.CLASSES
.values(), reverse
= True,
323 key
= lambda cls
: cls
.PRIO
):
324 try: return cls(file, writep
)
325 except StorageBackendRefusal
: pass
326 raise StorageBackendRefusal
329 def create(cls
, file):
331 Create a new database in the named FILE, using this backend.
333 Subclasses must implement the `_create' instance method.
335 return cls(writep
= True, _magic
= lambda me
: me
._create(file))
337 def __init__(me
, file = None, writep
= False, _magic
= None, *args
, **kw
):
341 Subclasses are not, in general, expected to override this: there's a
342 somewhat hairy protocol between the constructor and some of the class
343 methods. Instead, the main hook for customization is the subclass's
344 `_open' method, which is invoked in the usual case.
346 super(StorageBackend
, me
).__init__(*args
, **kw
)
347 if me
.NAME
is None: raise ValueError('abstract class')
348 if _magic
is not None: _magic(me
)
349 elif file is None: raise ValueError('missing file parameter')
350 else: me
._open(file, writep
)
354 def close(me
, abruptp
= False):
358 It is harmless to attempt to close a database which has been closed
359 already. Calls the subclass's `_close' method.
368 """Raise an error if the receiver has been closed."""
369 if not me
._livep
: raise ValueError('database is closed')
371 def _check_write(me
):
372 """Raise an error if the receiver is not open for writing."""
374 if not me
._writep
: raise ValueError('database is read-only')
376 def _check_meta_name(me
, name
):
378 Raise an error unless NAME is a valid name for a metadata item.
380 Metadata names may not start with `$': such names are reserved for
383 if name
.startswith('$'):
384 raise ValueError("invalid metadata key `%s'" % name
)
389 """Context protocol: make sure the database is closed on exit."""
391 def __exit__(me
, exctype
, excvalue
, exctb
):
392 """Context protocol: see `__enter__'."""
393 me
.close(excvalue
is not None)
397 def get_meta(me
, name
, default
= FAIL
):
399 Fetch the value for the metadata item NAME.
401 If no such item exists, then return DEFAULT if that was set; otherwise
404 This calls the subclass's `_get_meta' method, which should return the
405 requested item or return the given DEFAULT value. It may assume that the
406 name is valid and the database is open.
408 me
._check_meta_name(name
)
410 value
= me
._get_meta(name
, default
)
411 if value
is StorageBackend
.FAIL
: raise KeyError(name
)
414 def put_meta(me
, name
, value
):
416 Store VALUE in the metadata item called NAME.
418 This calls the subclass's `_put_meta' method, which may assume that the
419 name is valid and the database is open for writing.
421 me
._check_meta_name(name
)
423 me
._put_meta(name
, value
)
425 def del_meta(me
, name
):
427 Forget about the metadata item with the given NAME.
429 This calls the subclass's `_del_meta' method, which may assume that the
430 name is valid and the database is open for writing.
432 me
._check_meta_name(name
)
438 Return an iterator over the name/value metadata items.
440 This calls the subclass's `_iter_meta' method, which may assume that the
444 return me
._iter_meta()
446 def get_passwd(me
, label
):
448 Fetch and return the payload stored with the (opaque, binary) LABEL.
450 If there is no such payload then raise `KeyError'.
452 This calls the subclass's `_get_passwd' method, which may assume that the
456 return me
._get_passwd(label
)
458 def put_passwd(me
, label
, payload
):
460 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
462 Any previous payload for LABEL is forgotten.
464 This calls the subclass's `_put_passwd' method, which may assume that the
465 database is open for writing.
468 me
._put_passwd(label
, payload
)
470 def del_passwd(me
, label
):
472 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
474 If there is no such payload then raise `KeyError'.
476 This calls the subclass's `_del_passwd' method, which may assume that the
477 database is open for writing.
480 me
._del_passwd(label
)
482 def iter_passwds(me
):
484 Return an iterator over the stored password label/payload pairs.
486 This calls the subclass's `_iter_passwds' method, which may assume that
487 the database is open.
490 return me
._iter_passwds()
492 try: import gdbm
as _G
493 except ImportError: pass
495 class GDBMStorageBackend (StorageBackend
):
497 My instances store password data in a GDBM database.
499 Metadata and password entries are mixed into the same database. The key
500 for a metadata item is simply its name; the key for a password entry is
501 the entry's label prefixed by `$', since we're guaranteed that no
502 metadata item name begins with `$'.
507 def _open(me
, file, writep
):
508 try: me
._db
= _G
.open(file, writep
and 'w' or 'r')
509 except _G
.error
: raise StorageBackendRefusal(_excval())
511 def _create(me
, file):
512 me
._db
= _G
.open(file, 'n', 0600)
514 def _close(me
, abruptp
):
518 def _get_meta(me
, name
, default
):
519 try: return me
._db
[name
]
520 except KeyError: return default
522 def _put_meta(me
, name
, value
):
525 def _del_meta(me
, name
):
529 k
= me
._db
.firstkey()
531 if not k
.startswith('$'): yield k
, me
._db
[k
]
532 k
= me
._db
.nextkey(k
)
534 def _get_passwd(me
, label
):
535 return me
._db
['$' + label
]
537 def _put_passwd(me
, label
, payload
):
538 me
._db
['$' + label
] = payload
540 def _del_passwd(me
, label
):
541 del me
._db
['$' + label
]
543 def _iter_passwds(me
):
544 k
= me
._db
.firstkey()
546 if k
.startswith('$'): yield k
[1:], me
._db
[k
]
547 k
= me
._db
.nextkey(k
)
549 try: import sqlite3
as _Q
550 except ImportError: pass
552 class SQLiteStorageBackend (StorageBackend
):
554 I represent a password database stored in SQLite.
556 Metadata and password items are stored in separate tables, so there's no
557 conflict. Some additional metadata is stored in the `meta' table, with
558 names beginning with `$' so as not to conflict with clients:
560 $version The schema version of the table.
566 def _open(me
, file, writep
):
568 me
._db
= _Q
.connect(file)
569 ver
= me
._query_scalar(
570 "SELECT value FROM meta WHERE name = '$version'",
572 except (_Q
.DatabaseError
, _Q
.OperationalError
):
573 raise StorageBackendRefusal(_excval())
574 if ver
is None: raise ValueError('database broken (missing $version)')
575 elif ver
< me
.VERSION
: me
._upgrade(ver
)
576 elif ver
> me
.VERSION
: raise ValueError \
577 ('unknown database schema version (%d > %d)' %
(ver
, me
.VERSION
))
579 def _create(me
, file):
580 fd
= _OS
.open(file, _OS
.O_WRONLY | _OS
.O_CREAT | _OS
.O_EXCL
, 0600)
583 me
._db
= _Q
.connect(file)
587 name TEXT PRIMARY KEY NOT NULL,
588 value BLOB NOT NULL);
591 CREATE TABLE passwd (
592 label BLOB PRIMARY KEY NOT NULL,
593 payload BLOB NOT NULL);
596 INSERT INTO meta (name, value) VALUES ('$version', ?);
599 try: _OS
.unlink(file)
603 def _upgrade(me
, ver
):
604 """Upgrade the database from schema version VER."""
605 assert False, 'how embarrassing'
607 def _close(me
, abruptp
):
608 if not abruptp
: me
._db
.commit()
612 def _fetch_scalar(me
, c
, what
, default
= None):
614 except StopIteration: val
= default
617 except StopIteration: pass
618 else: raise ValueError('multiple matching records for %s' % what
)
621 def _query_scalar(me
, query
, what
, default
= None, args
= []):
623 c
.execute(query
, args
)
624 return me
._fetch_scalar(c
, what
, default
)
626 def _get_meta(me
, name
, default
):
627 v
= me
._query_scalar("SELECT value FROM meta WHERE name = ?",
628 "metadata item `%s'" % name
,
629 default
= default
, args
= [name
])
630 if v
is default
: return v
633 def _put_meta(me
, name
, value
):
635 c
.execute("INSERT OR REPLACE INTO meta (name, value) VALUES (?, ?)",
636 [name
, buffer(value
)])
638 def _del_meta(me
, name
):
640 c
.execute("DELETE FROM meta WHERE name = ?", [name
])
641 if not c
.rowcount
: raise KeyError(name
)
645 c
.execute("SELECT name, value FROM meta WHERE name NOT LIKE '$%'")
646 for k
, v
in c
: yield k
, str(v
)
648 def _get_passwd(me
, label
):
649 pld
= me
._query_scalar("SELECT payload FROM passwd WHERE label = ?",
650 "password", default
= None,
651 args
= [buffer(label
)])
652 if pld
is None: raise KeyError(label
)
655 def _put_passwd(me
, label
, payload
):
657 c
.execute("INSERT OR REPLACE INTO passwd (label, payload) "
659 [buffer(label
), buffer(payload
)])
661 def _del_passwd(me
, label
):
663 c
.execute("DELETE FROM passwd WHERE label = ?", [label
])
664 if not c
.rowcount
: raise KeyError(label
)
666 def _iter_passwds(me
):
668 c
.execute("SELECT label, payload FROM passwd")
669 for k
, v
in c
: yield str(k
), str(v
)
671 class PlainTextBackend (StorageBackend
):
673 I'm a utility base class for storage backends which use plain text files.
675 I provide subclasses with the following capabilities.
677 * Creating files, with given modes, optionally ensuring that the file
678 doesn't exist already.
680 * Parsing flat text files, checking leading magic, skipping comments, and
681 providing standard encodings of troublesome characters and binary
682 strings in metadata and password records. See below.
684 * Maintenance of metadata and password records in in-memory dictionaries,
685 with ready implementations of the necessary StorageBackend subclass
686 responsibility methods. (Subclasses can override these if they want to
687 make different arrangements.)
689 Metadata records are written with an optional prefix string chosen by the
690 caller, followed by a `NAME=VALUE' pair. The NAME is form-urlencoded and
691 prefixed with `!' if it contains strange characters; the VALUE is base64-
692 encoded (without the pointless trailing `=' padding) and prefixed with `?'
695 Password records are written with an optional prefix string chosen by the
696 caller, followed by a LABEL=PAYLOAD pair, both of which are base64-encoded
699 The following attributes are available for subclasses:
701 _meta Dictionary mapping metadata item names to their values.
702 Populated by `_parse_meta' and managed by `_get_meta' and
705 _pw Dictionary mapping password labels to encrypted payloads.
706 Populated by `_parse_passwd' and managed by `_get_passwd' and
709 _dirtyp Boolean: set if either of the dictionaries has been modified.
712 def __init__(me
, *args
, **kw
):
714 Hook for initialization.
716 Sets up the published instance attributes.
721 super(PlainTextBackend
, me
).__init__(*args
, **kw
)
723 def _create_file(me
, file, mode
= 0600, freshp
= False):
725 Make sure FILE exists, creating it with the given MODE if necessary.
727 If FRESHP is true, then make sure the file did not exist previously.
728 Return a file object for the newly created file.
730 flags
= _OS
.O_CREAT | _OS
.O_WRONLY
731 if freshp
: flags |
= _OS
.O_EXCL
732 else: flags |
= _OS
.O_TRUNC
733 fd
= _OS
.open(file, flags
, mode
)
734 return _OS
.fdopen(fd
, 'w')
738 Set the `_dirtyp' flag.
740 Subclasses might find it useful to intercept this method.
744 def _eqsplit(me
, line
):
746 Extract the KEY, VALUE pair from a LINE of the form `KEY=VALUE'.
748 Raise `ValueError' if there is no `=' in the LINE.
751 return line
[:eq
], line
[eq
+ 1:]
753 def _parse_file(me
, file, magic
= None):
759 * Raise `StorageBackendRefusal' if that the first line doesn't match
760 MAGIC (if provided). MAGIC should not contain the terminating
763 * Ignore comments (beginning `#') and blank lines.
765 * Call `_parse_line' (provided by the subclass) for other lines.
767 with
open(file, 'r') as f
:
768 if magic
is not None:
769 if f
.readline().rstrip('\n') != magic
: raise StorageBackendRefusal
771 line
= line
.rstrip('\n')
772 if not line
or line
.startswith('#'): continue
775 def _write_file(me
, file, writebody
, mode
= 0600, magic
= None):
777 Update FILE atomically.
779 The newly created file will have the given MODE. If MAGIC is given, then
780 write that as the first line. Calls WRITEBODY(F) to write the main body
781 of the file where F is a file object for the new file.
784 with me
._create_file(new
, mode
) as f
:
785 if magic
is not None: f
.write(magic
+ '\n')
787 _OS
.rename(new
, file)
789 def _parse_meta(me
, line
):
790 """Parse LINE as a metadata NAME=VALUE pair, and updates `_meta'."""
791 k
, v
= me
._eqsplit(line
)
792 me
._meta
[_dec_metaname(k
)] = _dec_metaval(v
)
794 def _write_meta(me
, f
, prefix
= ''):
795 """Write the metadata records to F, each with the given PREFIX."""
796 f
.write('\n## Metadata.\n')
797 for k
, v
in me
._meta
.iteritems():
798 f
.write('%s%s=%s\n' %
(prefix
, _enc_metaname(k
), _enc_metaval(v
)))
800 def _get_meta(me
, name
, default
):
801 return me
._meta
.get(name
, default
)
802 def _put_meta(me
, name
, value
):
804 me
._meta
[name
] = value
805 def _del_meta(me
, name
):
809 return me
._meta
.iteritems()
811 def _parse_passwd(me
, line
):
812 """Parse LINE as a password LABEL=PAYLOAD pair, and updates `_pw'."""
813 k
, v
= me
._eqsplit(line
)
814 me
._pw
[_unb64(k
)] = _unb64(v
)
816 def _write_passwd(me
, f
, prefix
= ''):
817 """Write the password records to F, each with the given PREFIX."""
818 f
.write('\n## Password data.\n')
819 for k
, v
in me
._pw
.iteritems():
820 f
.write('%s%s=%s\n' %
(prefix
, _b64(k
), _b64(v
)))
822 def _get_passwd(me
, label
):
823 return me
._pw
[str(label
)]
824 def _put_passwd(me
, label
, payload
):
826 me
._pw
[str(label
)] = payload
827 def _del_passwd(me
, label
):
829 del me
._pw
[str(label
)]
830 def _iter_passwds(me
):
831 return me
._pw
.iteritems()
833 class FlatFileStorageBackend (PlainTextBackend
):
835 I maintain a password database in a plain text file.
837 The text file consists of lines, as follows.
839 * Empty lines, and lines beginning with `#' (in the leftmost column only)
842 * Lines of the form `$LABEL=PAYLOAD' store password data. Both LABEL and
843 PAYLOAD are base64-encoded, without `=' padding.
845 * Lines of the form `NAME=VALUE' store metadata. If the NAME contains
846 characters other than alphanumerics, hyphens, underscores, and colons,
847 then it is form-urlencoded, and prefixed wth `!'. If the VALUE
848 contains such characters, then it is base64-encoded, without `='
849 padding, and prefixed with `?'.
851 * Other lines are erroneous.
853 The file is rewritten from scratch when it's changed: any existing
854 commentary is lost, and items may be reordered. There is no file locking,
855 but the file is updated atomically, by renaming.
857 It is expected that the FlatFileStorageBackend is used mostly for
858 diagnostics and transfer, rather than for a live system.
863 MAGIC
= '### pwsafe password database'
865 def _open(me
, file, writep
):
866 if not _OS
.path
.isfile(file): raise StorageBackendRefusal
867 me
._parse_file(file, magic
= me
.MAGIC
)
868 def _parse_line(me
, line
):
869 if line
.startswith('$'): me
._parse_passwd(line
[1:])
870 else: me
._parse_meta(line
)
872 def _create(me
, file):
873 with me
._create_file(file, freshp
= True) as f
: pass
877 def _close(me
, abruptp
):
878 if not abruptp
and me
._dirtyp
:
879 me
._write_file(me
._file
, me
._write_body
, magic
= me
.MAGIC
)
881 def _write_body(me
, f
):
883 me
._write_passwd(f
, '$')
885 class DirectoryStorageBackend (PlainTextBackend
):
887 I maintain a password database in a directory, with one file per password.
889 This makes password databases easy to maintain in a revision-control system
892 The directory is structured as follows.
894 dir/meta Contains metadata, similar to the `FlatFileBackend'.
896 dir/pw/LABEL Contains the (raw binary) payload for the given password
897 LABEL (base64-encoded, without the useless `=' padding, and
898 with `/' replaced by `.').
900 dir/tmp/ Contains temporary files used by the implementation.
904 METAMAGIC
= '### pwsafe password directory metadata'
906 def _open(me
, file, writep
):
907 if not _OS
.path
.isdir(file) or \
908 not _OS
.path
.isdir(_OS
.path
.join(file, 'pw')) or \
909 not _OS
.path
.isdir(_OS
.path
.join(file, 'tmp')) or \
910 not _OS
.path
.isfile(_OS
.path
.join(file, 'meta')):
911 raise StorageBackendRefusal
913 me
._parse_file(_OS
.path
.join(file, 'meta'), magic
= me
.METAMAGIC
)
914 def _parse_line(me
, line
):
917 def _create(me
, file):
918 _OS
.mkdir(file, 0700)
919 _OS
.mkdir(_OS
.path
.join(file, 'pw'), 0700)
920 _OS
.mkdir(_OS
.path
.join(file, 'tmp'), 0700)
924 def _close(me
, abruptp
):
925 if not abruptp
and me
._dirtyp
:
926 me
._write_file(_OS
.path
.join(me
._dir
, 'meta'),
927 me
._write_meta
, magic
= me
.METAMAGIC
)
929 def _pwfile(me
, label
, dir = 'pw'):
930 return _OS
.path
.join(me
._dir
, dir, _b64(label
).replace('/', '.'))
931 def _get_passwd(me
, label
):
933 f
= open(me
._pwfile(label
), 'rb')
934 except (OSError, IOError):
935 if _excval().errno
== _E
.ENOENT
: raise KeyError(label
)
937 with f
: return f
.read()
938 def _put_passwd(me
, label
, payload
):
939 new
= me
._pwfile(label
, 'tmp')
940 fd
= _OS
.open(new
, _OS
.O_WRONLY | _OS
.O_CREAT | _OS
.O_TRUNC
, 0600)
942 with
open(new
, 'wb') as f
: f
.write(payload
)
943 _OS
.rename(new
, me
._pwfile(label
))
944 def _del_passwd(me
, label
):
946 _OS
.remove(me
._pwfile(label
))
947 except (OSError, IOError):
948 if _excval().errno
== _E
.ENOENT
: raise KeyError(label
)
950 def _iter_passwds(me
):
951 pw
= _OS
.path
.join(me
._dir
, 'pw')
952 for i
in _OS
.listdir(pw
):
953 with
open(_OS
.path
.join(pw
, i
), 'rb') as f
: pld
= f
.read()
954 yield _unb64(i
.replace('.', '/')), pld
956 ###--------------------------------------------------------------------------
957 ### Password storage.
961 I represent a secure (ish) password store.
963 I can store short secrets, associated with textual names, in a way which
964 doesn't leak too much information about them.
966 I implement (some of) the Python mapping protocol.
968 I keep track of everything using a StorageBackend object. This contains
969 password entries, identified by cryptographic labels, and a number of
972 cipher Names the Catacomb cipher selected.
974 hash Names the Catacomb hash function selected.
976 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
977 length and concatenated, encrypted using the master
980 mac Names the Catacomb message authentication code selected.
982 magic A magic string for obscuring password tag names.
984 salt The salt for hashing the passphrase.
986 tag The master passphrase's tag, for the Pixie's benefit.
988 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
989 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
990 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
991 encrypted using the stored keys.
994 def __init__(me
, file, writep
= False):
996 Initialize a PW object from the database in FILE.
998 If WRITEP is false (the default) then the database is opened read-only;
999 if true then it may be written. Requests the database password from the
1000 Pixie, which may cause interaction.
1003 ## Open the database.
1004 me
.db
= StorageBackend
.open(file, writep
)
1006 ## Find out what crypto to use.
1007 c
= _C
.gcciphers
[me
.db
.get_meta('cipher')]
1008 h
= _C
.gchashes
[me
.db
.get_meta('hash')]
1009 m
= _C
.gcmacs
[me
.db
.get_meta('mac')]
1011 ## Request the passphrase and extract the master keys.
1012 tag
= me
.db
.get_meta('tag')
1013 ppk
= PPK(_C
.ppread(tag
), c
, h
, m
, me
.db
.get_meta('salt'))
1015 b
= _C
.ReadBuffer(ppk
.decrypt(me
.db
.get_meta('key')))
1016 except DecryptError
:
1019 me
.ck
= b
.getblk16()
1020 me
.mk
= b
.getblk16()
1021 if not b
.endp
: raise ValueError('trailing junk')
1023 ## Set the key, and stash it and the tag-hashing secret.
1024 me
.k
= Crypto(c
, h
, m
, me
.ck
, me
.mk
)
1025 me
.magic
= me
.k
.decrypt(me
.db
.get_meta('magic'))
1028 def create(cls
, dbcls
, file, tag
, c
, h
, m
):
1030 Create and initialize a new database FILE using StorageBackend DBCLS.
1032 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
1033 and a Pixie passphrase TAG.
1035 This doesn't return a working object: it just creates the database file
1036 and gets out of the way.
1039 ## Set up the cryptography.
1040 pp
= _C
.ppread(tag
, _C
.PMODE_VERIFY
)
1041 ppk
= PPK(pp
, c
, h
, m
)
1042 ck
= _C
.rand
.block(c
.keysz
.default
)
1043 mk
= _C
.rand
.block(c
.keysz
.default
)
1044 k
= Crypto(c
, h
, m
, ck
, mk
)
1046 ## Set up and initialize the database.
1047 kct
= ppk
.encrypt(_C
.WriteBuffer().putblk16(ck
).putblk16(mk
))
1048 with dbcls
.create(file) as db
:
1049 db
.put_meta('tag', tag
)
1050 db
.put_meta('salt', ppk
.salt
)
1051 db
.put_meta('cipher', c
.name
)
1052 db
.put_meta('hash', h
.name
)
1053 db
.put_meta('mac', m
.name
)
1054 db
.put_meta('key', kct
)
1055 db
.put_meta('magic', k
.encrypt(_C
.rand
.block(h
.hashsz
)))
1057 def keyxform(me
, key
):
1058 """Transform the KEY (actually a password tag) into a password label."""
1059 return me
.k
.h().hash(me
.magic
).hash(key
).done()
1063 Change the database password.
1065 Requests the new password from the Pixie, which will probably cause
1068 tag
= me
.db
.get_meta('tag')
1070 ppk
= PPK(_C
.ppread(tag
, _C
.PMODE_VERIFY
),
1071 me
.k
.c
.__class__
, me
.k
.h
, me
.k
.m
.__class__
)
1072 kct
= ppk
.encrypt(_C
.WriteBuffer().putblk16(me
.ck
).putblk16(me
.mk
))
1073 me
.db
.put_meta('key', kct
)
1074 me
.db
.put_meta('salt', ppk
.salt
)
1076 def pack(me
, key
, value
):
1077 """Pack the KEY and VALUE into a ciphertext, and return it."""
1078 b
= _C
.WriteBuffer()
1079 b
.putblk16(key
).putblk16(value
)
1080 b
.zero(((b
.size
+ 255) & ~
255) - b
.size
)
1081 return me
.k
.encrypt(b
)
1085 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
1087 Might raise DecryptError, of course.
1089 b
= _C
.ReadBuffer(me
.k
.decrypt(ct
))
1091 value
= b
.getblk16()
1094 ## Mapping protocol.
1096 def __getitem__(me
, key
):
1097 """Return the password for the given KEY."""
1098 try: return me
.unpack(me
.db
.get_passwd(me
.keyxform(key
)))[1]
1099 except KeyError: raise KeyError(key
)
1101 def __setitem__(me
, key
, value
):
1102 """Associate the password VALUE with the KEY."""
1103 me
.db
.put_passwd(me
.keyxform(key
), me
.pack(key
, value
))
1105 def __delitem__(me
, key
):
1106 """Forget all about the KEY."""
1107 try: me
.db
.del_passwd(me
.keyxform(key
))
1108 except KeyError: raise KeyError(key
)
1111 """Iterate over the known password tags."""
1112 for _
, pld
in me
.db
.iter_passwds():
1113 yield me
.unpack(pld
)[0]
1115 ## Context protocol.
1119 def __exit__(me
, excty
, excval
, exctb
):
1120 me
.db
.close(excval
is not None)
1122 ###----- That's all, folks --------------------------------------------------