@@@ py_buffer/freebin wip
[catacomb-python] / catacomb / pwsafe.py
1 ### -*-python-*-
2 ###
3 ### Management of a secure password database
4 ###
5 ### (c) 2005 Straylight/Edgeware
6 ###
7
8 ###----- Licensing notice ---------------------------------------------------
9 ###
10 ### This file is part of the Python interface to Catacomb.
11 ###
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.
16 ###
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.
21 ###
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.
25
26 ###--------------------------------------------------------------------------
27 ### Imported modules.
28
29 from __future__ import with_statement
30
31 import binascii as _B
32 import errno as _E
33 import os as _OS
34 import sys as _SYS
35
36 if _SYS.version_info >= (3,): from io import StringIO as _StringIO
37 else: from cStringIO import StringIO as _StringIO
38
39 import catacomb as _C
40
41 ###--------------------------------------------------------------------------
42 ### Python version portability.
43
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")
50 else:
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
56
57 _NUL = _bin('\0')
58 _CIPHER = _bin('cipher:')
59 _MAC = _bin('mac:')
60
61 def _with_metaclass(meta, *supers):
62 return meta("#<anonymous base %s>" % meta.__name__,
63 supers or (object,), dict())
64
65 def _excval(): return _SYS.exc_info()[1]
66
67 _M600 = int("600", 8)
68 _M700 = int("700", 8)
69
70 ###--------------------------------------------------------------------------
71 ### Text encoding utilities.
72
73 def _literalp(s):
74 """
75 Answer whether S can be represented literally.
76
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.
79 """
80 return all(ch.isalnum() or ch in '-_:' for ch in s)
81
82 def _enc_metaname(name):
83 """Encode NAME as a metadata item name, returning the result."""
84 if _literalp(name):
85 return name
86 else:
87 sio = _StringIO()
88 sio.write('!')
89 for ch in name:
90 if _literalp(ch): sio.write(ch)
91 elif ch == ' ': sio.write('+')
92 else: sio.write('%%%02x' % ord(ch))
93 return sio.getvalue()
94
95 def _dec_metaname(name):
96 """Decode NAME as a metadata item name, returning the result."""
97 if not name.startswith('!'):
98 return name
99 else:
100 sio = _StringIO()
101 i, n = 1, len(name)
102 while i < n:
103 ch = name[i]
104 i += 1
105 if ch == '+':
106 sio.write(' ')
107 elif ch == '%':
108 sio.write(chr(int(name[i:i + 2], 16)))
109 i += 2
110 else:
111 sio.write(ch)
112 return sio.getvalue()
113
114 def _b64(s):
115 """Encode S as base64, without newlines, and trimming `=' padding."""
116 return _text(_B.b2a_base64(s)).replace('\n', '').rstrip('=')
117 def _unb64(s):
118 """Decode S as base64 with trimmed `=' padding."""
119 return _B.a2b_base64(s + '='*((4 - len(s))%4))
120
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)
125
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:])
130
131 ###--------------------------------------------------------------------------
132 ### Underlying cryptography.
133
134 class DecryptError (Exception):
135 """
136 I represent a failure to decrypt a message.
137
138 Usually this means that someone used the wrong key, though it can also
139 mean that a ciphertext has been modified.
140 """
141 pass
142
143 class Crypto (object):
144 """
145 I represent a symmetric crypto transform.
146
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:
150
151 * y = v || E(K0, v; m)
152 * t = M(K1; y)
153
154 The final ciphertext is t || y.
155 """
156
157 def __init__(me, c, h, m, ck, mk):
158 """
159 Initialize the Crypto object with a given algorithm selection and keys.
160
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.
163 """
164 me.c = c(ck)
165 me.m = m(mk)
166 me.h = h
167
168 def encrypt(me, pt):
169 """
170 Encrypt the message PT and return the resulting ciphertext.
171 """
172 blksz = me.c.__class__.blksz
173 b = _C.WriteBuffer()
174 if blksz:
175 iv = _C.rand.block(blksz)
176 me.c.setiv(iv)
177 b.put(iv)
178 b.put(me.c.encrypt(pt))
179 t = me.m().hash(b).done()
180 return t + str(buffer(b))
181
182 def decrypt(me, ct):
183 """
184 Decrypt the ciphertext CT, returning the plaintext.
185
186 Raises DecryptError if anything goes wrong.
187 """
188 blksz = me.c.__class__.blksz
189 tagsz = me.m.__class__.tagsz
190 b = _C.ReadBuffer(ct)
191 t = b.get(tagsz)
192 h = me.m()
193 if blksz:
194 iv = b.get(blksz)
195 me.c.setiv(iv)
196 h.hash(iv)
197 x = b.get(b.left)
198 h.hash(x)
199 if t != h.done(): raise DecryptError
200 return me.c.decrypt(x)
201
202 class PPK (Crypto):
203 """
204 I represent a crypto transform whose keys are derived from a passphrase.
205
206 The password is salted and hashed; the salt is available as the `salt'
207 attribute.
208 """
209
210 def __init__(me, pp, c, h, m, salt = None):
211 """
212 Initialize the PPK object with a passphrase and algorithm selection.
213
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.
217 """
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())
223 me.salt = salt
224
225 ###--------------------------------------------------------------------------
226 ### Backend storage.
227
228 class StorageBackendRefusal (Exception):
229 """
230 I signify that a StorageBackend subclass has refused to open a file.
231
232 This is used by the StorageBackend.open class method.
233 """
234 pass
235
236 class StorageBackendClass (type):
237 """
238 I am a metaclass for StorageBackend classes.
239
240 My main feature is that I register my concrete instances (with a `NAME'
241 which is not `None') with the StorageBackend class.
242 """
243 def __init__(me, name, supers, dict):
244 """
245 Register a new concrete StorageBackend subclass.
246 """
247 super(StorageBackendClass, me).__init__(name, supers, dict)
248 try: name = me.NAME
249 except AttributeError: pass
250 else: StorageBackend.register_concrete_subclass(me)
251
252 class StorageBackend (_with_metaclass(StorageBackendClass)):
253 """
254 I provide basic protocol for password storage backends.
255
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.
259
260 Backends are responsible for storing and retrieving stuff, but not for the
261 cryptographic details. Backends need to store two kinds of information:
262
263 * metadata, consisting of a number of property names and their values;
264 and
265
266 * password mappings, consisting of a number of binary labels and
267 payloads.
268
269 Backends need to implement the following ordinary methods. See the calling
270 methods for details of the subclass responsibilities.
271
272 BE._create(FILE) Create a new database in FILE; used by `create'.
273
274 BE._open(FILE, WRITEP)
275 Open the existing database FILE; used by `open'.
276
277 BE._close(ABRUPTP) Close the database, freeing up any resources. If
278 ABRUPTP then don't try to commit changes.
279
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
283 `get_meta'.
284
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'.
288
289 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
290 `KeyError' if there is no such item; used by
291 `del_meta'.
292
293 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
294 pairs; used by `iter_meta'.
295
296 BE._get_passwd(LABEL)
297 Return the password payload stored with the (binary)
298 LABEL; used by `get_passwd'.
299
300 BE._put_passwd(LABEL, PAYLOAD)
301 Associate the (binary) PAYLOAD with the LABEL,
302 forgetting any previous payload for that LABEL; used
303 by `put_passwd'.
304
305 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
306 by `_del_passwd'.
307
308 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
309 pairs; used by `iter_passwds'.
310
311 Also, concrete subclasses should define the following class attributes.
312
313 NAME The name of the backend, so that the user can select
314 it when creating a new database.
315
316 PRIO An integer priority: backends are tried in decreasing
317 priority order when opening an existing database.
318 """
319
320 PRIO = 10
321
322 ## The registry of subclasses.
323 CLASSES = {}
324
325 FAIL = ['FAIL']
326
327 @staticmethod
328 def register_concrete_subclass(sub):
329 """Register a concrete subclass, so that `open' can try it."""
330 StorageBackend.CLASSES[sub.NAME] = sub
331
332 @staticmethod
333 def byname(name):
334 """
335 Return the concrete subclass with the given NAME.
336
337 Raise `KeyError' if the name isn't found.
338 """
339 return StorageBackend.CLASSES[name]
340
341 @staticmethod
342 def classes():
343 """Return an iterator over the concrete subclasses."""
344 return _itervalues(StorageBackend.CLASSES)
345
346 @staticmethod
347 def open(file, writep = False):
348 """Open a database FILE, using some appropriate backend."""
349 _OS.stat(file)
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
355
356 @classmethod
357 def create(cls, file):
358 """
359 Create a new database in the named FILE, using this backend.
360
361 Subclasses must implement the `_create' instance method.
362 """
363 return cls(writep = True, _magic = lambda me: me._create(file))
364
365 def __init__(me, file = None, writep = False, _magic = None, *args, **kw):
366 """
367 Main constructor.
368
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.
373 """
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)
379 me._writep = writep
380 me._livep = True
381
382 def close(me, abruptp = False):
383 """
384 Close the database.
385
386 It is harmless to attempt to close a database which has been closed
387 already. Calls the subclass's `_close' method.
388 """
389 if me._livep:
390 me._livep = False
391 me._close(abruptp)
392
393 ## Utilities.
394
395 def _check_live(me):
396 """Raise an error if the receiver has been closed."""
397 if not me._livep: raise ValueError('database is closed')
398
399 def _check_write(me):
400 """Raise an error if the receiver is not open for writing."""
401 me._check_live()
402 if not me._writep: raise ValueError('database is read-only')
403
404 def _check_meta_name(me, name):
405 """
406 Raise an error unless NAME is a valid name for a metadata item.
407
408 Metadata names may not start with `$': such names are reserved for
409 password storage.
410 """
411 if name.startswith('$'):
412 raise ValueError("invalid metadata key `%s'" % name)
413
414 ## Context protocol.
415
416 def __enter__(me):
417 """Context protocol: make sure the database is closed on exit."""
418 return me
419 def __exit__(me, exctype, excvalue, exctb):
420 """Context protocol: see `__enter__'."""
421 me.close(excvalue is not None)
422
423 ## Metadata.
424
425 def get_meta(me, name, default = FAIL):
426 """
427 Fetch the value for the metadata item NAME.
428
429 If no such item exists, then return DEFAULT if that was set; otherwise
430 raise a `KeyError'.
431
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.
435 """
436 me._check_meta_name(name)
437 me._check_live()
438 value = me._get_meta(name, default)
439 if value is StorageBackend.FAIL: raise KeyError(name)
440 return value
441
442 def put_meta(me, name, value):
443 """
444 Store VALUE in the metadata item called NAME.
445
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.
448 """
449 me._check_meta_name(name)
450 me._check_write()
451 me._put_meta(name, value)
452
453 def del_meta(me, name):
454 """
455 Forget about the metadata item with the given NAME.
456
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.
459 """
460 me._check_meta_name(name)
461 me._check_write()
462 me._del_meta(name)
463
464 def iter_meta(me):
465 """
466 Return an iterator over the name/value metadata items.
467
468 This calls the subclass's `_iter_meta' method, which may assume that the
469 database is open.
470 """
471 me._check_live()
472 return me._iter_meta()
473
474 def get_passwd(me, label):
475 """
476 Fetch and return the payload stored with the (opaque, binary) LABEL.
477
478 If there is no such payload then raise `KeyError'.
479
480 This calls the subclass's `_get_passwd' method, which may assume that the
481 database is open.
482 """
483 me._check_live()
484 return me._get_passwd(label)
485
486 def put_passwd(me, label, payload):
487 """
488 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
489
490 Any previous payload for LABEL is forgotten.
491
492 This calls the subclass's `_put_passwd' method, which may assume that the
493 database is open for writing.
494 """
495 me._check_write()
496 me._put_passwd(label, payload)
497
498 def del_passwd(me, label):
499 """
500 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
501
502 If there is no such payload then raise `KeyError'.
503
504 This calls the subclass's `_del_passwd' method, which may assume that the
505 database is open for writing.
506 """
507 me._check_write()
508 me._del_passwd(label)
509
510 def iter_passwds(me):
511 """
512 Return an iterator over the stored password label/payload pairs.
513
514 This calls the subclass's `_iter_passwds' method, which may assume that
515 the database is open.
516 """
517 me._check_live()
518 return me._iter_passwds()
519
520 try: import gdbm as _G
521 except ImportError: pass
522 else:
523 class GDBMStorageBackend (StorageBackend):
524 """
525 My instances store password data in a GDBM database.
526
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 `$'.
531 """
532
533 NAME = 'gdbm'
534
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())
538
539 def _create(me, file):
540 me._db = _G.open(file, 'n', _M600)
541
542 def _close(me, abruptp):
543 me._db.close()
544 me._db = None
545
546 def _get_meta(me, name, default):
547 try: return me._db[name]
548 except KeyError: return default
549
550 def _put_meta(me, name, value):
551 me._db[name] = value
552
553 def _del_meta(me, name):
554 del me._db[name]
555
556 def _iter_meta(me):
557 k = me._db.firstkey()
558 while k is not None:
559 if not k.startswith('$'): yield k, me._db[k]
560 k = me._db.nextkey(k)
561
562 def _get_passwd(me, label):
563 return me._db['$' + label]
564
565 def _put_passwd(me, label, payload):
566 me._db['$' + label] = payload
567
568 def _del_passwd(me, label):
569 del me._db['$' + label]
570
571 def _iter_passwds(me):
572 k = me._db.firstkey()
573 while k is not None:
574 if k.startswith('$'): yield k[1:], me._db[k]
575 k = me._db.nextkey(k)
576
577 try: import sqlite3 as _Q
578 except ImportError: pass
579 else:
580 class SQLiteStorageBackend (StorageBackend):
581 """
582 I represent a password database stored in SQLite.
583
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:
587
588 $version The schema version of the table.
589 """
590
591 NAME = 'sqlite'
592 VERSION = 0
593
594 def _open(me, file, writep):
595 try:
596 me._db = _Q.connect(file)
597 ver = me._query_scalar(
598 "SELECT value FROM meta WHERE name = '$version'",
599 "version check")
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))
606
607 def _create(me, file):
608 fd = _OS.open(file, _OS.O_WRONLY | _OS.O_CREAT | _OS.O_EXCL, _M600)
609 _OS.close(fd)
610 try:
611 me._db = _Q.connect(file)
612 c = me._db.cursor()
613 c.execute("""
614 CREATE TABLE meta (
615 name TEXT PRIMARY KEY NOT NULL,
616 value BLOB NOT NULL);
617 """)
618 c.execute("""
619 CREATE TABLE passwd (
620 label BLOB PRIMARY KEY NOT NULL,
621 payload BLOB NOT NULL);
622 """)
623 c.execute("""
624 INSERT INTO meta (name, value) VALUES ('$version', ?);
625 """, [me.VERSION])
626 except:
627 try: _OS.unlink(file)
628 except OSError: pass
629 raise
630
631 def _upgrade(me, ver):
632 """Upgrade the database from schema version VER."""
633 assert False, 'how embarrassing'
634
635 def _close(me, abruptp):
636 if not abruptp: me._db.commit()
637 me._db.close()
638 me._db = None
639
640 def _fetch_scalar(me, c, what, default = None):
641 try: row = next(c)
642 except StopIteration: val = default
643 else: val, = row
644 try: row = next(c)
645 except StopIteration: pass
646 else: raise ValueError('multiple matching records for %s' % what)
647 return val
648
649 def _query_scalar(me, query, what, default = None, args = []):
650 c = me._db.cursor()
651 c.execute(query, args)
652 return me._fetch_scalar(c, what, default)
653
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
659 else: return str(v)
660
661 def _put_meta(me, name, value):
662 c = me._db.cursor()
663 c.execute("INSERT OR REPLACE INTO meta (name, value) VALUES (?, ?)",
664 [name, buffer(value)])
665
666 def _del_meta(me, name):
667 c = me._db.cursor()
668 c.execute("DELETE FROM meta WHERE name = ?", [name])
669 if not c.rowcount: raise KeyError(name)
670
671 def _iter_meta(me):
672 c = me._db.cursor()
673 c.execute("SELECT name, value FROM meta WHERE name NOT LIKE '$%'")
674 for k, v in c: yield k, str(v)
675
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)
681 return str(pld)
682
683 def _put_passwd(me, label, payload):
684 c = me._db.cursor()
685 c.execute("INSERT OR REPLACE INTO passwd (label, payload) "
686 "VALUES (?, ?)",
687 [buffer(label), buffer(payload)])
688
689 def _del_passwd(me, label):
690 c = me._db.cursor()
691 c.execute("DELETE FROM passwd WHERE label = ?", [label])
692 if not c.rowcount: raise KeyError(label)
693
694 def _iter_passwds(me):
695 c = me._db.cursor()
696 c.execute("SELECT label, payload FROM passwd")
697 for k, v in c: yield str(k), str(v)
698
699 class PlainTextBackend (StorageBackend):
700 """
701 I'm a utility base class for storage backends which use plain text files.
702
703 I provide subclasses with the following capabilities.
704
705 * Creating files, with given modes, optionally ensuring that the file
706 doesn't exist already.
707
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.
711
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.)
716
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 `?'
721 if necessary.
722
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
725 (without padding).
726
727 The following attributes are available for subclasses:
728
729 _meta Dictionary mapping metadata item names to their values.
730 Populated by `_parse_meta' and managed by `_get_meta' and
731 friends.
732
733 _pw Dictionary mapping password labels to encrypted payloads.
734 Populated by `_parse_passwd' and managed by `_get_passwd' and
735 friends.
736
737 _dirtyp Boolean: set if either of the dictionaries has been modified.
738 """
739
740 def __init__(me, *args, **kw):
741 """
742 Hook for initialization.
743
744 Sets up the published instance attributes.
745 """
746 me._meta = {}
747 me._pw = {}
748 me._dirtyp = False
749 super(PlainTextBackend, me).__init__(*args, **kw)
750
751 def _create_file(me, file, mode = _M600, freshp = False):
752 """
753 Make sure FILE exists, creating it with the given MODE if necessary.
754
755 If FRESHP is true, then make sure the file did not exist previously.
756 Return a file object for the newly created file.
757 """
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')
763
764 def _mark_dirty(me):
765 """
766 Set the `_dirtyp' flag.
767
768 Subclasses might find it useful to intercept this method.
769 """
770 me._dirtyp = True
771
772 def _eqsplit(me, line):
773 """
774 Extract the KEY, VALUE pair from a LINE of the form `KEY=VALUE'.
775
776 Raise `ValueError' if there is no `=' in the LINE.
777 """
778 eq = line.index('=')
779 return line[:eq], line[eq + 1:]
780
781 def _parse_file(me, file, magic = None):
782 """
783 Parse a FILE.
784
785 Specifically:
786
787 * Raise `StorageBackendRefusal' if that the first line doesn't match
788 MAGIC (if provided). MAGIC should not contain the terminating
789 newline.
790
791 * Ignore comments (beginning `#') and blank lines.
792
793 * Call `_parse_line' (provided by the subclass) for other lines.
794 """
795 with open(file, 'r') as f:
796 if magic is not None:
797 if f.readline().rstrip('\n') != magic: raise StorageBackendRefusal
798 for line in f:
799 line = line.rstrip('\n')
800 if not line or line.startswith('#'): continue
801 me._parse_line(line)
802
803 def _write_file(me, file, writebody, mode = _M600, magic = None):
804 """
805 Update FILE atomically.
806
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.
810 """
811 new = file + '.new'
812 with me._create_file(new, mode) as f:
813 if magic is not None: f.write(magic + '\n')
814 writebody(f)
815 _OS.rename(new, file)
816
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)
821
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)))
827
828 def _get_meta(me, name, default):
829 return me._meta.get(name, default)
830 def _put_meta(me, name, value):
831 me._mark_dirty()
832 me._meta[name] = value
833 def _del_meta(me, name):
834 me._mark_dirty()
835 del me._meta[name]
836 def _iter_meta(me):
837 return _iteritems(me._meta)
838
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)
843
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)))
849
850 def _get_passwd(me, label):
851 return me._pw[str(label)]
852 def _put_passwd(me, label, payload):
853 me._mark_dirty()
854 me._pw[str(label)] = payload
855 def _del_passwd(me, label):
856 me._mark_dirty()
857 del me._pw[str(label)]
858 def _iter_passwds(me):
859 return _iteritems(me._pw)
860
861 class FlatFileStorageBackend (PlainTextBackend):
862 """
863 I maintain a password database in a plain text file.
864
865 The text file consists of lines, as follows.
866
867 * Empty lines, and lines beginning with `#' (in the leftmost column only)
868 are ignored.
869
870 * Lines of the form `$LABEL=PAYLOAD' store password data. Both LABEL and
871 PAYLOAD are base64-encoded, without `=' padding.
872
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 `?'.
878
879 * Other lines are erroneous.
880
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.
884
885 It is expected that the FlatFileStorageBackend is used mostly for
886 diagnostics and transfer, rather than for a live system.
887 """
888
889 NAME = 'flat'
890 PRIO = 0
891 MAGIC = '### pwsafe password database'
892
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)
899
900 def _create(me, file):
901 with me._create_file(file, freshp = True) as f: pass
902 me._file = file
903 me._mark_dirty()
904
905 def _close(me, abruptp):
906 if not abruptp and me._dirtyp:
907 me._write_file(me._file, me._write_body, magic = me.MAGIC)
908
909 def _write_body(me, f):
910 me._write_meta(f)
911 me._write_passwd(f, '$')
912
913 class DirectoryStorageBackend (PlainTextBackend):
914 """
915 I maintain a password database in a directory, with one file per password.
916
917 This makes password databases easy to maintain in a revision-control system
918 such as Git.
919
920 The directory is structured as follows.
921
922 dir/meta Contains metadata, similar to the `FlatFileBackend'.
923
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 `.').
927
928 dir/tmp/ Contains temporary files used by the implementation.
929 """
930
931 NAME = 'dir'
932 METAMAGIC = '### pwsafe password directory metadata'
933
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
940 me._dir = file
941 me._parse_file(_OS.path.join(file, 'meta'), magic = me.METAMAGIC)
942 def _parse_line(me, line):
943 me._parse_meta(line)
944
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)
949 me._mark_dirty()
950 me._dir = file
951
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)
956
957 def _pwfile(me, label, dir = 'pw'):
958 return _OS.path.join(me._dir, dir, _b64(label).replace('/', '.'))
959 def _get_passwd(me, label):
960 try:
961 f = open(me._pwfile(label), 'rb')
962 except (OSError, IOError):
963 if _excval().errno == _E.ENOENT: raise KeyError(label)
964 else: raise
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)
969 _OS.close(fd)
970 with open(new, 'wb') as f: f.write(payload)
971 _OS.rename(new, me._pwfile(label))
972 def _del_passwd(me, label):
973 try:
974 _OS.remove(me._pwfile(label))
975 except (OSError, IOError):
976 if _excval().errno == _E.ENOENT: raise KeyError(label)
977 else: raise
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
983
984 ###--------------------------------------------------------------------------
985 ### Password storage.
986
987 class PW (object):
988 """
989 I represent a secure (ish) password store.
990
991 I can store short secrets, associated with textual names, in a way which
992 doesn't leak too much information about them.
993
994 I implement (some of) the Python mapping protocol.
995
996 I keep track of everything using a StorageBackend object. This contains
997 password entries, identified by cryptographic labels, and a number of
998 metadata items.
999
1000 cipher Names the Catacomb cipher selected.
1001
1002 hash Names the Catacomb hash function selected.
1003
1004 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
1005 length and concatenated, encrypted using the master
1006 passphrase.
1007
1008 mac Names the Catacomb message authentication code selected.
1009
1010 magic A magic string for obscuring password tag names.
1011
1012 salt The salt for hashing the passphrase.
1013
1014 tag The master passphrase's tag, for the Pixie's benefit.
1015
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.
1020 """
1021
1022 def __init__(me, file, writep = False):
1023 """
1024 Initialize a PW object from the database in FILE.
1025
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.
1029 """
1030
1031 ## Open the database.
1032 me.db = StorageBackend.open(file, writep)
1033
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')]
1038
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'))
1042 try:
1043 b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
1044 except DecryptError:
1045 _C.ppcancel(tag)
1046 raise
1047 me.ck = b.getblk16()
1048 me.mk = b.getblk16()
1049 if not b.endp: raise ValueError('trailing junk')
1050
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'))
1054
1055 @classmethod
1056 def create(cls, dbcls, file, tag, c, h, m):
1057 """
1058 Create and initialize a new database FILE using StorageBackend DBCLS.
1059
1060 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
1061 and a Pixie passphrase TAG.
1062
1063 This doesn't return a working object: it just creates the database file
1064 and gets out of the way.
1065 """
1066
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)
1073
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)))
1084
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()
1088
1089 def changepp(me):
1090 """
1091 Change the database password.
1092
1093 Requests the new password from the Pixie, which will probably cause
1094 interaction.
1095 """
1096 tag = me.db.get_meta('tag')
1097 _C.ppcancel(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)
1103
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)
1110
1111 def unpack(me, ct):
1112 """
1113 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
1114
1115 Might raise DecryptError, of course.
1116 """
1117 b = _C.ReadBuffer(me.k.decrypt(ct))
1118 key = b.getblk16()
1119 value = b.getblk16()
1120 return key, value
1121
1122 ## Mapping protocol.
1123
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)
1128
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))
1132
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)
1137
1138 def __iter__(me):
1139 """Iterate over the known password tags."""
1140 for _, pld in me.db.iter_passwds():
1141 yield me.unpack(pld)[0]
1142
1143 ## Context protocol.
1144
1145 def __enter__(me):
1146 return me
1147 def __exit__(me, excty, excval, exctb):
1148 me.db.close(excval is not None)
1149
1150 ###----- That's all, folks --------------------------------------------------