3 ### Management of a secure password database
5 ### (c) 2005 Straylight/Edgeware
8 ###----- Licensing notice ---------------------------------------------------
10 ### This file is part of the Python interface to Catacomb.
12 ### Catacomb/Python is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU General Public License as published by
14 ### the Free Software Foundation; either version 2 of the License, or
15 ### (at your option) any later version.
17 ### Catacomb/Python is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ### GNU General Public License for more details.
22 ### You should have received a copy of the GNU General Public License along
23 ### with Catacomb/Python; if not, write to the Free Software Foundation,
24 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26 ###--------------------------------------------------------------------------
29 from __future__
import with_statement
33 from cStringIO
import StringIO
as _StringIO
37 ###--------------------------------------------------------------------------
38 ### Python version portability.
40 def _iterkeys(dict): return dict.iterkeys()
41 def _itervalues(dict): return dict.itervalues()
42 def _iteritems(dict): return dict.iteritems()
44 def _bin(text
): return text
45 def _text(bin
): return bin
48 _CIPHER
= _bin('cipher:')
51 def _excval(): return SYS
.exc_info()[1]
56 ###--------------------------------------------------------------------------
57 ### Text encoding utilities.
61 Answer whether S can be represented literally.
63 If True, then S can be stored literally, as a metadata item name or
64 value; if False, then S requires some kind of encoding.
66 return all(ch
.isalnum() or ch
in '-_:' for ch
in s
)
68 def _enc_metaname(name
):
69 """Encode NAME as a metadata item name, returning the result."""
76 if _literalp(ch
): sio
.write(ch
)
77 elif ch
== ' ': sio
.write('+')
78 else: sio
.write('%%%02x' % ord
(ch
))
81 def _dec_metaname(name
):
82 """Decode NAME as a metadata item name, returning the result."""
83 if not name
.startswith('!'):
94 sio
.write(chr(int(name
[i
:i
+ 2], 16)))
101 """Encode S as base64, without newlines, and trimming `=' padding."""
102 return s
.encode('base64').replace('\n', '').rstrip('=')
104 """Decode S as base64 with trimmed `=' padding."""
105 return (s
+ '='*((4 - len(s
))%4)).decode('base64')
107 def _enc_metaval(val
):
108 """Encode VAL as a metadata item value, returning the result."""
109 if _literalp(val
): return val
110 else: return '?' + _b64(val
)
112 def _dec_metaval(val
):
113 """Decode VAL as a metadata item value, returning the result."""
114 if not val
.startswith('?'): return val
115 else: return _unb64(val
[1:])
117 ###--------------------------------------------------------------------------
118 ### Underlying cryptography.
120 class DecryptError (Exception):
122 I represent a failure to decrypt a message.
124 Usually this means that someone used the wrong key, though it can also
125 mean that a ciphertext has been modified.
129 class Crypto (object):
131 I represent a symmetric crypto transform.
133 There's currently only one transform implemented, which is the obvious
134 generic-composition construction: given a message m, and keys K0 and K1, we
135 choose an IV v, and compute:
137 * y = v || E(K0, v; m)
140 The final ciphertext is t || y.
143 def __init__(me
, c
, h
, m
, ck
, mk
):
145 Initialize the Crypto object with a given algorithm selection and keys.
147 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
148 keys CK and MK for C and M respectively.
156 Encrypt the message PT and return the resulting ciphertext.
158 blksz
= me
.c
.__class__
.blksz
161 iv
= _C
.rand
.block(blksz
)
164 b
.put(me
.c
.encrypt(pt
))
165 t
= me
.m().hash(b
).done()
166 return t
+ str(buffer(b
))
170 Decrypt the ciphertext CT, returning the plaintext.
172 Raises DecryptError if anything goes wrong.
174 blksz
= me
.c
.__class__
.blksz
175 tagsz
= me
.m
.__class__
.tagsz
176 b
= _C
.ReadBuffer(ct
)
185 if t
!= h
.done(): raise DecryptError
186 return me
.c
.decrypt(x
)
190 I represent a crypto transform whose keys are derived from a passphrase.
192 The password is salted and hashed; the salt is available as the `salt'
196 def __init__(me
, pp
, c
, h
, m
, salt
= None):
198 Initialize the PPK object with a passphrase and algorithm selection.
200 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
201 subclass M, and a SALT. The SALT may be None, if we're generating new
202 keys, indicating that a salt should be chosen randomly.
204 if not salt
: salt
= _C
.rand
.block(h
.hashsz
)
205 tag
= pp
+ _NUL
+ salt
206 Crypto
.__init__(me
, c
, h
, m
,
207 h().hash(_CIPHER
).hash(tag
).done(),
208 h().hash(_MAC
).hash(tag
).done())
211 ###--------------------------------------------------------------------------
214 class StorageBackendRefusal (Exception):
216 I signify that a StorageBackend subclass has refused to open a file.
218 This is used by the StorageBackend.open class method.
222 class StorageBackendClass (type):
224 I am a metaclass for StorageBackend classes.
226 My main feature is that I register my concrete instances (with a `NAME'
227 which is not `None') with the StorageBackend class.
229 def __init__(me
, name
, supers
, dict):
231 Register a new concrete StorageBackend subclass.
233 super(StorageBackendClass
, me
).__init__(name
, supers
, dict)
235 except AttributeError: pass
236 else: StorageBackend
.register_concrete_subclass(me
)
238 class StorageBackend (object):
240 I provide basic protocol for password storage backends.
242 I'm an abstract class: you want one of my subclasses if you actually want
243 to do something useful. But I maintain a list of my subclasses and can
244 choose an appropriate one to open a database file you've found lying about.
246 Backends are responsible for storing and retrieving stuff, but not for the
247 cryptographic details. Backends need to store two kinds of information:
249 * metadata, consisting of a number of property names and their values;
252 * password mappings, consisting of a number of binary labels and
255 Backends need to implement the following ordinary methods. See the calling
256 methods for details of the subclass responsibilities.
258 BE._create(FILE) Create a new database in FILE; used by `create'.
260 BE._open(FILE, WRITEP)
261 Open the existing database FILE; used by `open'.
263 BE._close(ABRUPTP) Close the database, freeing up any resources. If
264 ABRUPTP then don't try to commit changes.
266 BE._get_meta(NAME, DEFAULT)
267 Return the value of the metadata item with the given
268 NAME, or DEFAULT if it doesn't exist; used by
271 BE._put_meta(NAME, VALUE)
272 Set the VALUE of the metadata item with the given
273 NAME, creating one if necessary; used by `put_meta'.
275 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
276 `KeyError' if there is no such item; used by
279 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
280 pairs; used by `iter_meta'.
282 BE._get_passwd(LABEL)
283 Return the password payload stored with the (binary)
284 LABEL; used by `get_passwd'.
286 BE._put_passwd(LABEL, PAYLOAD)
287 Associate the (binary) PAYLOAD with the LABEL,
288 forgetting any previous payload for that LABEL; used
291 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
294 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
295 pairs; used by `iter_passwds'.
297 Also, concrete subclasses should define the following class attributes.
299 NAME The name of the backend, so that the user can select
300 it when creating a new database.
302 PRIO An integer priority: backends are tried in decreasing
303 priority order when opening an existing database.
306 __metaclass__
= StorageBackendClass
309 ## The registry of subclasses.
315 def register_concrete_subclass(sub
):
316 """Register a concrete subclass, so that `open' can try it."""
317 StorageBackend
.CLASSES
[sub
.NAME
] = sub
322 Return the concrete subclass with the given NAME.
324 Raise `KeyError' if the name isn't found.
326 return StorageBackend
.CLASSES
[name
]
330 """Return an iterator over the concrete subclasses."""
331 return _itervalues(StorageBackend
.CLASSES
)
334 def open(file, writep
= False):
335 """Open a database FILE, using some appropriate backend."""
337 for cls
in sorted(StorageBackend
.CLASSES
.values(), reverse
= True,
338 key
= lambda cls
: cls
.PRIO
):
339 try: return cls(file, writep
)
340 except StorageBackendRefusal
: pass
341 raise StorageBackendRefusal
344 def create(cls
, file):
346 Create a new database in the named FILE, using this backend.
348 Subclasses must implement the `_create' instance method.
350 return cls(writep
= True, _magic
= lambda me
: me
._create(file))
352 def __init__(me
, file = None, writep
= False, _magic
= None, *args
, **kw
):
356 Subclasses are not, in general, expected to override this: there's a
357 somewhat hairy protocol between the constructor and some of the class
358 methods. Instead, the main hook for customization is the subclass's
359 `_open' method, which is invoked in the usual case.
361 super(StorageBackend
, me
).__init__(*args
, **kw
)
362 if me
.NAME
is None: raise ValueError('abstract class')
363 if _magic
is not None: _magic(me
)
364 elif file is None: raise ValueError('missing file parameter')
365 else: me
._open(file, writep
)
369 def close(me
, abruptp
= False):
373 It is harmless to attempt to close a database which has been closed
374 already. Calls the subclass's `_close' method.
383 """Raise an error if the receiver has been closed."""
384 if not me
._livep
: raise ValueError('database is closed')
386 def _check_write(me
):
387 """Raise an error if the receiver is not open for writing."""
389 if not me
._writep
: raise ValueError('database is read-only')
391 def _check_meta_name(me
, name
):
393 Raise an error unless NAME is a valid name for a metadata item.
395 Metadata names may not start with `$': such names are reserved for
398 if name
.startswith('$'):
399 raise ValueError("invalid metadata key `%s'" % name
)
404 """Context protocol: make sure the database is closed on exit."""
406 def __exit__(me
, exctype
, excvalue
, exctb
):
407 """Context protocol: see `__enter__'."""
408 me
.close(excvalue
is not None)
412 def get_meta(me
, name
, default
= FAIL
):
414 Fetch the value for the metadata item NAME.
416 If no such item exists, then return DEFAULT if that was set; otherwise
419 This calls the subclass's `_get_meta' method, which should return the
420 requested item or return the given DEFAULT value. It may assume that the
421 name is valid and the database is open.
423 me
._check_meta_name(name
)
425 value
= me
._get_meta(name
, default
)
426 if value
is StorageBackend
.FAIL
: raise KeyError(name
)
429 def put_meta(me
, name
, value
):
431 Store VALUE in the metadata item called NAME.
433 This calls the subclass's `_put_meta' method, which may assume that the
434 name is valid and the database is open for writing.
436 me
._check_meta_name(name
)
438 me
._put_meta(name
, value
)
440 def del_meta(me
, name
):
442 Forget about the metadata item with the given NAME.
444 This calls the subclass's `_del_meta' method, which may assume that the
445 name is valid and the database is open for writing.
447 me
._check_meta_name(name
)
453 Return an iterator over the name/value metadata items.
455 This calls the subclass's `_iter_meta' method, which may assume that the
459 return me
._iter_meta()
461 def get_passwd(me
, label
):
463 Fetch and return the payload stored with the (opaque, binary) LABEL.
465 If there is no such payload then raise `KeyError'.
467 This calls the subclass's `_get_passwd' method, which may assume that the
471 return me
._get_passwd(label
)
473 def put_passwd(me
, label
, payload
):
475 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
477 Any previous payload for LABEL is forgotten.
479 This calls the subclass's `_put_passwd' method, which may assume that the
480 database is open for writing.
483 me
._put_passwd(label
, payload
)
485 def del_passwd(me
, label
):
487 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
489 If there is no such payload then raise `KeyError'.
491 This calls the subclass's `_del_passwd' method, which may assume that the
492 database is open for writing.
495 me
._del_passwd(label
)
497 def iter_passwds(me
):
499 Return an iterator over the stored password label/payload pairs.
501 This calls the subclass's `_iter_passwds' method, which may assume that
502 the database is open.
505 return me
._iter_passwds()
507 try: import gdbm
as _G
508 except ImportError: pass
510 class GDBMStorageBackend (StorageBackend
):
512 My instances store password data in a GDBM database.
514 Metadata and password entries are mixed into the same database. The key
515 for a metadata item is simply its name; the key for a password entry is
516 the entry's label prefixed by `$', since we're guaranteed that no
517 metadata item name begins with `$'.
522 def _open(me
, file, writep
):
523 try: me
._db
= _G
.open(file, writep
and 'w' or 'r')
524 except _G
.error
: raise StorageBackendRefusal(_excval())
526 def _create(me
, file):
527 me
._db
= _G
.open(file, 'n', _M600
)
529 def _close(me
, abruptp
):
533 def _get_meta(me
, name
, default
):
534 try: return me
._db
[name
]
535 except KeyError: return default
537 def _put_meta(me
, name
, value
):
540 def _del_meta(me
, name
):
544 k
= me
._db
.firstkey()
546 if not k
.startswith('$'): yield k
, me
._db
[k
]
547 k
= me
._db
.nextkey(k
)
549 def _get_passwd(me
, label
):
550 return me
._db
['$' + label
]
552 def _put_passwd(me
, label
, payload
):
553 me
._db
['$' + label
] = payload
555 def _del_passwd(me
, label
):
556 del me
._db
['$' + label
]
558 def _iter_passwds(me
):
559 k
= me
._db
.firstkey()
561 if k
.startswith('$'): yield k
[1:], me
._db
[k
]
562 k
= me
._db
.nextkey(k
)
564 try: import sqlite3
as _Q
565 except ImportError: pass
567 class SQLiteStorageBackend (StorageBackend
):
569 I represent a password database stored in SQLite.
571 Metadata and password items are stored in separate tables, so there's no
572 conflict. Some additional metadata is stored in the `meta' table, with
573 names beginning with `$' so as not to conflict with clients:
575 $version The schema version of the table.
581 def _open(me
, file, writep
):
583 me
._db
= _Q
.connect(file)
584 ver
= me
._query_scalar(
585 "SELECT value FROM meta WHERE name = '$version'",
587 except (_Q
.DatabaseError
, _Q
.OperationalError
):
588 raise StorageBackendRefusal(_excval())
589 if ver
is None: raise ValueError('database broken (missing $version)')
590 elif ver
< me
.VERSION
: me
._upgrade(ver
)
591 elif ver
> me
.VERSION
: raise ValueError \
592 ('unknown database schema version (%d > %d)' %
(ver
, me
.VERSION
))
594 def _create(me
, file):
595 fd
= _OS
.open(file, _OS
.O_WRONLY | _OS
.O_CREAT | _OS
.O_EXCL
, _M600
)
598 me
._db
= _Q
.connect(file)
602 name TEXT PRIMARY KEY NOT NULL,
603 value BLOB NOT NULL);
606 CREATE TABLE passwd (
607 label BLOB PRIMARY KEY NOT NULL,
608 payload BLOB NOT NULL);
611 INSERT INTO meta (name, value) VALUES ('$version', ?);
614 try: _OS
.unlink(file)
618 def _upgrade(me
, ver
):
619 """Upgrade the database from schema version VER."""
620 assert False, 'how embarrassing'
622 def _close(me
, abruptp
):
623 if not abruptp
: me
._db
.commit()
627 def _fetch_scalar(me
, c
, what
, default
= None):
629 except StopIteration: val
= default
632 except StopIteration: pass
633 else: raise ValueError('multiple matching records for %s' % what
)
636 def _query_scalar(me
, query
, what
, default
= None, args
= []):
638 c
.execute(query
, args
)
639 return me
._fetch_scalar(c
, what
, default
)
641 def _get_meta(me
, name
, default
):
642 v
= me
._query_scalar("SELECT value FROM meta WHERE name = ?",
643 "metadata item `%s'" % name
,
644 default
= default
, args
= [name
])
645 if v
is default
: return v
648 def _put_meta(me
, name
, value
):
650 c
.execute("INSERT OR REPLACE INTO meta (name, value) VALUES (?, ?)",
651 [name
, buffer(value
)])
653 def _del_meta(me
, name
):
655 c
.execute("DELETE FROM meta WHERE name = ?", [name
])
656 if not c
.rowcount
: raise KeyError(name
)
660 c
.execute("SELECT name, value FROM meta WHERE name NOT LIKE '$%'")
661 for k
, v
in c
: yield k
, str(v
)
663 def _get_passwd(me
, label
):
664 pld
= me
._query_scalar("SELECT payload FROM passwd WHERE label = ?",
665 "password", default
= None,
666 args
= [buffer(label
)])
667 if pld
is None: raise KeyError(label
)
670 def _put_passwd(me
, label
, payload
):
672 c
.execute("INSERT OR REPLACE INTO passwd (label, payload) "
674 [buffer(label
), buffer(payload
)])
676 def _del_passwd(me
, label
):
678 c
.execute("DELETE FROM passwd WHERE label = ?", [label
])
679 if not c
.rowcount
: raise KeyError(label
)
681 def _iter_passwds(me
):
683 c
.execute("SELECT label, payload FROM passwd")
684 for k
, v
in c
: yield str(k
), str(v
)
686 class PlainTextBackend (StorageBackend
):
688 I'm a utility base class for storage backends which use plain text files.
690 I provide subclasses with the following capabilities.
692 * Creating files, with given modes, optionally ensuring that the file
693 doesn't exist already.
695 * Parsing flat text files, checking leading magic, skipping comments, and
696 providing standard encodings of troublesome characters and binary
697 strings in metadata and password records. See below.
699 * Maintenance of metadata and password records in in-memory dictionaries,
700 with ready implementations of the necessary StorageBackend subclass
701 responsibility methods. (Subclasses can override these if they want to
702 make different arrangements.)
704 Metadata records are written with an optional prefix string chosen by the
705 caller, followed by a `NAME=VALUE' pair. The NAME is form-urlencoded and
706 prefixed with `!' if it contains strange characters; the VALUE is base64-
707 encoded (without the pointless trailing `=' padding) and prefixed with `?'
710 Password records are written with an optional prefix string chosen by the
711 caller, followed by a LABEL=PAYLOAD pair, both of which are base64-encoded
714 The following attributes are available for subclasses:
716 _meta Dictionary mapping metadata item names to their values.
717 Populated by `_parse_meta' and managed by `_get_meta' and
720 _pw Dictionary mapping password labels to encrypted payloads.
721 Populated by `_parse_passwd' and managed by `_get_passwd' and
724 _dirtyp Boolean: set if either of the dictionaries has been modified.
727 def __init__(me
, *args
, **kw
):
729 Hook for initialization.
731 Sets up the published instance attributes.
736 super(PlainTextBackend
, me
).__init__(*args
, **kw
)
738 def _create_file(me
, file, mode
= _M600
, freshp
= False):
740 Make sure FILE exists, creating it with the given MODE if necessary.
742 If FRESHP is true, then make sure the file did not exist previously.
743 Return a file object for the newly created file.
745 flags
= _OS
.O_CREAT | _OS
.O_WRONLY
746 if freshp
: flags |
= _OS
.O_EXCL
747 else: flags |
= _OS
.O_TRUNC
748 fd
= _OS
.open(file, flags
, mode
)
749 return _OS
.fdopen(fd
, 'w')
753 Set the `_dirtyp' flag.
755 Subclasses might find it useful to intercept this method.
759 def _eqsplit(me
, line
):
761 Extract the KEY, VALUE pair from a LINE of the form `KEY=VALUE'.
763 Raise `ValueError' if there is no `=' in the LINE.
766 return line
[:eq
], line
[eq
+ 1:]
768 def _parse_file(me
, file, magic
= None):
774 * Raise `StorageBackendRefusal' if that the first line doesn't match
775 MAGIC (if provided). MAGIC should not contain the terminating
778 * Ignore comments (beginning `#') and blank lines.
780 * Call `_parse_line' (provided by the subclass) for other lines.
782 with
open(file, 'r') as f
:
783 if magic
is not None:
784 if f
.readline().rstrip('\n') != magic
: raise StorageBackendRefusal
786 line
= line
.rstrip('\n')
787 if not line
or line
.startswith('#'): continue
790 def _write_file(me
, file, writebody
, mode
= _M600
, magic
= None):
792 Update FILE atomically.
794 The newly created file will have the given MODE. If MAGIC is given, then
795 write that as the first line. Calls WRITEBODY(F) to write the main body
796 of the file where F is a file object for the new file.
799 with me
._create_file(new
, mode
) as f
:
800 if magic
is not None: f
.write(magic
+ '\n')
802 _OS
.rename(new
, file)
804 def _parse_meta(me
, line
):
805 """Parse LINE as a metadata NAME=VALUE pair, and updates `_meta'."""
806 k
, v
= me
._eqsplit(line
)
807 me
._meta
[_dec_metaname(k
)] = _dec_metaval(v
)
809 def _write_meta(me
, f
, prefix
= ''):
810 """Write the metadata records to F, each with the given PREFIX."""
811 f
.write('\n## Metadata.\n')
812 for k
, v
in _iteritems(me
._meta
):
813 f
.write('%s%s=%s\n' %
(prefix
, _enc_metaname(k
), _enc_metaval(v
)))
815 def _get_meta(me
, name
, default
):
816 return me
._meta
.get(name
, default
)
817 def _put_meta(me
, name
, value
):
819 me
._meta
[name
] = value
820 def _del_meta(me
, name
):
824 return _iteritems(me
._meta
)
826 def _parse_passwd(me
, line
):
827 """Parse LINE as a password LABEL=PAYLOAD pair, and updates `_pw'."""
828 k
, v
= me
._eqsplit(line
)
829 me
._pw
[_unb64(k
)] = _unb64(v
)
831 def _write_passwd(me
, f
, prefix
= ''):
832 """Write the password records to F, each with the given PREFIX."""
833 f
.write('\n## Password data.\n')
834 for k
, v
in _iteritems(me
._pw
):
835 f
.write('%s%s=%s\n' %
(prefix
, _b64(k
), _b64(v
)))
837 def _get_passwd(me
, label
):
838 return me
._pw
[str(label
)]
839 def _put_passwd(me
, label
, payload
):
841 me
._pw
[str(label
)] = payload
842 def _del_passwd(me
, label
):
844 del me
._pw
[str(label
)]
845 def _iter_passwds(me
):
846 return _iteritems(me
._pw
)
848 class FlatFileStorageBackend (PlainTextBackend
):
850 I maintain a password database in a plain text file.
852 The text file consists of lines, as follows.
854 * Empty lines, and lines beginning with `#' (in the leftmost column only)
857 * Lines of the form `$LABEL=PAYLOAD' store password data. Both LABEL and
858 PAYLOAD are base64-encoded, without `=' padding.
860 * Lines of the form `NAME=VALUE' store metadata. If the NAME contains
861 characters other than alphanumerics, hyphens, underscores, and colons,
862 then it is form-urlencoded, and prefixed wth `!'. If the VALUE
863 contains such characters, then it is base64-encoded, without `='
864 padding, and prefixed with `?'.
866 * Other lines are erroneous.
868 The file is rewritten from scratch when it's changed: any existing
869 commentary is lost, and items may be reordered. There is no file locking,
870 but the file is updated atomically, by renaming.
872 It is expected that the FlatFileStorageBackend is used mostly for
873 diagnostics and transfer, rather than for a live system.
878 MAGIC
= '### pwsafe password database'
880 def _open(me
, file, writep
):
881 if not _OS
.path
.isfile(file): raise StorageBackendRefusal
882 me
._parse_file(file, magic
= me
.MAGIC
)
883 def _parse_line(me
, line
):
884 if line
.startswith('$'): me
._parse_passwd(line
[1:])
885 else: me
._parse_meta(line
)
887 def _create(me
, file):
888 with me
._create_file(file, freshp
= True) as f
: pass
892 def _close(me
, abruptp
):
893 if not abruptp
and me
._dirtyp
:
894 me
._write_file(me
._file
, me
._write_body
, magic
= me
.MAGIC
)
896 def _write_body(me
, f
):
898 me
._write_passwd(f
, '$')
900 class DirectoryStorageBackend (PlainTextBackend
):
902 I maintain a password database in a directory, with one file per password.
904 This makes password databases easy to maintain in a revision-control system
907 The directory is structured as follows.
909 dir/meta Contains metadata, similar to the `FlatFileBackend'.
911 dir/pw/LABEL Contains the (raw binary) payload for the given password
912 LABEL (base64-encoded, without the useless `=' padding, and
913 with `/' replaced by `.').
915 dir/tmp/ Contains temporary files used by the implementation.
919 METAMAGIC
= '### pwsafe password directory metadata'
921 def _open(me
, file, writep
):
922 if not _OS
.path
.isdir(file) or \
923 not _OS
.path
.isdir(_OS
.path
.join(file, 'pw')) or \
924 not _OS
.path
.isdir(_OS
.path
.join(file, 'tmp')) or \
925 not _OS
.path
.isfile(_OS
.path
.join(file, 'meta')):
926 raise StorageBackendRefusal
928 me
._parse_file(_OS
.path
.join(file, 'meta'), magic
= me
.METAMAGIC
)
929 def _parse_line(me
, line
):
932 def _create(me
, file):
933 _OS
.mkdir(file, _M700
)
934 _OS
.mkdir(_OS
.path
.join(file, 'pw'), _M700
)
935 _OS
.mkdir(_OS
.path
.join(file, 'tmp'), _M700
)
939 def _close(me
, abruptp
):
940 if not abruptp
and me
._dirtyp
:
941 me
._write_file(_OS
.path
.join(me
._dir
, 'meta'),
942 me
._write_meta
, magic
= me
.METAMAGIC
)
944 def _pwfile(me
, label
, dir = 'pw'):
945 return _OS
.path
.join(me
._dir
, dir, _b64(label
).replace('/', '.'))
946 def _get_passwd(me
, label
):
948 f
= open(me
._pwfile(label
), 'rb')
949 except (OSError, IOError):
950 if _excval().errno
== _E
.ENOENT
: raise KeyError(label
)
952 with f
: return f
.read()
953 def _put_passwd(me
, label
, payload
):
954 new
= me
._pwfile(label
, 'tmp')
955 fd
= _OS
.open(new
, _OS
.O_WRONLY | _OS
.O_CREAT | _OS
.O_TRUNC
, _M600
)
957 with
open(new
, 'wb') as f
: f
.write(payload
)
958 _OS
.rename(new
, me
._pwfile(label
))
959 def _del_passwd(me
, label
):
961 _OS
.remove(me
._pwfile(label
))
962 except (OSError, IOError):
963 if _excval().errno
== _E
.ENOENT
: raise KeyError(label
)
965 def _iter_passwds(me
):
966 pw
= _OS
.path
.join(me
._dir
, 'pw')
967 for i
in _OS
.listdir(pw
):
968 with
open(_OS
.path
.join(pw
, i
), 'rb') as f
: pld
= f
.read()
969 yield _unb64(i
.replace('.', '/')), pld
971 ###--------------------------------------------------------------------------
972 ### Password storage.
976 I represent a secure (ish) password store.
978 I can store short secrets, associated with textual names, in a way which
979 doesn't leak too much information about them.
981 I implement (some of) the Python mapping protocol.
983 I keep track of everything using a StorageBackend object. This contains
984 password entries, identified by cryptographic labels, and a number of
987 cipher Names the Catacomb cipher selected.
989 hash Names the Catacomb hash function selected.
991 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
992 length and concatenated, encrypted using the master
995 mac Names the Catacomb message authentication code selected.
997 magic A magic string for obscuring password tag names.
999 salt The salt for hashing the passphrase.
1001 tag The master passphrase's tag, for the Pixie's benefit.
1003 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
1004 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
1005 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
1006 encrypted using the stored keys.
1009 def __init__(me
, file, writep
= False):
1011 Initialize a PW object from the database in FILE.
1013 If WRITEP is false (the default) then the database is opened read-only;
1014 if true then it may be written. Requests the database password from the
1015 Pixie, which may cause interaction.
1018 ## Open the database.
1019 me
.db
= StorageBackend
.open(file, writep
)
1021 ## Find out what crypto to use.
1022 c
= _C
.gcciphers
[me
.db
.get_meta('cipher')]
1023 h
= _C
.gchashes
[me
.db
.get_meta('hash')]
1024 m
= _C
.gcmacs
[me
.db
.get_meta('mac')]
1026 ## Request the passphrase and extract the master keys.
1027 tag
= me
.db
.get_meta('tag')
1028 ppk
= PPK(_C
.ppread(tag
), c
, h
, m
, me
.db
.get_meta('salt'))
1030 b
= _C
.ReadBuffer(ppk
.decrypt(me
.db
.get_meta('key')))
1031 except DecryptError
:
1034 me
.ck
= b
.getblk16()
1035 me
.mk
= b
.getblk16()
1036 if not b
.endp
: raise ValueError('trailing junk')
1038 ## Set the key, and stash it and the tag-hashing secret.
1039 me
.k
= Crypto(c
, h
, m
, me
.ck
, me
.mk
)
1040 me
.magic
= me
.k
.decrypt(me
.db
.get_meta('magic'))
1043 def create(cls
, dbcls
, file, tag
, c
, h
, m
):
1045 Create and initialize a new database FILE using StorageBackend DBCLS.
1047 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
1048 and a Pixie passphrase TAG.
1050 This doesn't return a working object: it just creates the database file
1051 and gets out of the way.
1054 ## Set up the cryptography.
1055 pp
= _C
.ppread(tag
, _C
.PMODE_VERIFY
)
1056 ppk
= PPK(pp
, c
, h
, m
)
1057 ck
= _C
.rand
.block(c
.keysz
.default
)
1058 mk
= _C
.rand
.block(c
.keysz
.default
)
1059 k
= Crypto(c
, h
, m
, ck
, mk
)
1061 ## Set up and initialize the database.
1062 kct
= ppk
.encrypt(_C
.WriteBuffer().putblk16(ck
).putblk16(mk
))
1063 with dbcls
.create(file) as db
:
1064 db
.put_meta('tag', tag
)
1065 db
.put_meta('salt', ppk
.salt
)
1066 db
.put_meta('cipher', c
.name
)
1067 db
.put_meta('hash', h
.name
)
1068 db
.put_meta('mac', m
.name
)
1069 db
.put_meta('key', kct
)
1070 db
.put_meta('magic', k
.encrypt(_C
.rand
.block(h
.hashsz
)))
1072 def keyxform(me
, key
):
1073 """Transform the KEY (actually a password tag) into a password label."""
1074 return me
.k
.h().hash(me
.magic
).hash(key
).done()
1078 Change the database password.
1080 Requests the new password from the Pixie, which will probably cause
1083 tag
= me
.db
.get_meta('tag')
1085 ppk
= PPK(_C
.ppread(tag
, _C
.PMODE_VERIFY
),
1086 me
.k
.c
.__class__
, me
.k
.h
, me
.k
.m
.__class__
)
1087 kct
= ppk
.encrypt(_C
.WriteBuffer().putblk16(me
.ck
).putblk16(me
.mk
))
1088 me
.db
.put_meta('key', kct
)
1089 me
.db
.put_meta('salt', ppk
.salt
)
1091 def pack(me
, key
, value
):
1092 """Pack the KEY and VALUE into a ciphertext, and return it."""
1093 b
= _C
.WriteBuffer()
1094 b
.putblk16(key
).putblk16(value
)
1095 b
.zero(((b
.size
+ 255) & ~
255) - b
.size
)
1096 return me
.k
.encrypt(b
)
1100 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
1102 Might raise DecryptError, of course.
1104 b
= _C
.ReadBuffer(me
.k
.decrypt(ct
))
1106 value
= b
.getblk16()
1109 ## Mapping protocol.
1111 def __getitem__(me
, key
):
1112 """Return the password for the given KEY."""
1113 try: return me
.unpack(me
.db
.get_passwd(me
.keyxform(key
)))[1]
1114 except KeyError: raise KeyError(key
)
1116 def __setitem__(me
, key
, value
):
1117 """Associate the password VALUE with the KEY."""
1118 me
.db
.put_passwd(me
.keyxform(key
), me
.pack(key
, value
))
1120 def __delitem__(me
, key
):
1121 """Forget all about the KEY."""
1122 try: me
.db
.del_passwd(me
.keyxform(key
))
1123 except KeyError: raise KeyError(key
)
1126 """Iterate over the known password tags."""
1127 for _
, pld
in me
.db
.iter_passwds():
1128 yield me
.unpack(pld
)[0]
1130 ## Context protocol.
1134 def __exit__(me
, excty
, excval
, exctb
):
1135 me
.db
.close(excval
is not None)
1137 ###----- That's all, folks --------------------------------------------------