catacomb/pwsafe.py: Hack around the change in metaclass syntax.
[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 errno as _E
32 import os as _OS
33 from cStringIO import StringIO as _StringIO
34
35 import catacomb as _C
36
37 ###--------------------------------------------------------------------------
38 ### Python version portability.
39
40 def _iterkeys(dict): return dict.iterkeys()
41 def _itervalues(dict): return dict.itervalues()
42 def _iteritems(dict): return dict.iteritems()
43
44 def _bin(text): return text
45 def _text(bin): return bin
46
47 _NUL = _bin('\0')
48 _CIPHER = _bin('cipher:')
49 _MAC = _bin('mac:')
50
51 def _with_metaclass(meta, *supers):
52 return meta("#<anonymous base %s>" % meta.__name__,
53 supers or (object,), dict())
54
55 def _excval(): return SYS.exc_info()[1]
56
57 _M600 = int("600", 8)
58 _M700 = int("700", 8)
59
60 ###--------------------------------------------------------------------------
61 ### Text encoding utilities.
62
63 def _literalp(s):
64 """
65 Answer whether S can be represented literally.
66
67 If True, then S can be stored literally, as a metadata item name or
68 value; if False, then S requires some kind of encoding.
69 """
70 return all(ch.isalnum() or ch in '-_:' for ch in s)
71
72 def _enc_metaname(name):
73 """Encode NAME as a metadata item name, returning the result."""
74 if _literalp(name):
75 return name
76 else:
77 sio = _StringIO()
78 sio.write('!')
79 for ch in name:
80 if _literalp(ch): sio.write(ch)
81 elif ch == ' ': sio.write('+')
82 else: sio.write('%%%02x' % ord(ch))
83 return sio.getvalue()
84
85 def _dec_metaname(name):
86 """Decode NAME as a metadata item name, returning the result."""
87 if not name.startswith('!'):
88 return name
89 else:
90 sio = _StringIO()
91 i, n = 1, len(name)
92 while i < n:
93 ch = name[i]
94 i += 1
95 if ch == '+':
96 sio.write(' ')
97 elif ch == '%':
98 sio.write(chr(int(name[i:i + 2], 16)))
99 i += 2
100 else:
101 sio.write(ch)
102 return sio.getvalue()
103
104 def _b64(s):
105 """Encode S as base64, without newlines, and trimming `=' padding."""
106 return s.encode('base64').replace('\n', '').rstrip('=')
107 def _unb64(s):
108 """Decode S as base64 with trimmed `=' padding."""
109 return (s + '='*((4 - len(s))%4)).decode('base64')
110
111 def _enc_metaval(val):
112 """Encode VAL as a metadata item value, returning the result."""
113 if _literalp(val): return val
114 else: return '?' + _b64(val)
115
116 def _dec_metaval(val):
117 """Decode VAL as a metadata item value, returning the result."""
118 if not val.startswith('?'): return val
119 else: return _unb64(val[1:])
120
121 ###--------------------------------------------------------------------------
122 ### Underlying cryptography.
123
124 class DecryptError (Exception):
125 """
126 I represent a failure to decrypt a message.
127
128 Usually this means that someone used the wrong key, though it can also
129 mean that a ciphertext has been modified.
130 """
131 pass
132
133 class Crypto (object):
134 """
135 I represent a symmetric crypto transform.
136
137 There's currently only one transform implemented, which is the obvious
138 generic-composition construction: given a message m, and keys K0 and K1, we
139 choose an IV v, and compute:
140
141 * y = v || E(K0, v; m)
142 * t = M(K1; y)
143
144 The final ciphertext is t || y.
145 """
146
147 def __init__(me, c, h, m, ck, mk):
148 """
149 Initialize the Crypto object with a given algorithm selection and keys.
150
151 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
152 keys CK and MK for C and M respectively.
153 """
154 me.c = c(ck)
155 me.m = m(mk)
156 me.h = h
157
158 def encrypt(me, pt):
159 """
160 Encrypt the message PT and return the resulting ciphertext.
161 """
162 blksz = me.c.__class__.blksz
163 b = _C.WriteBuffer()
164 if blksz:
165 iv = _C.rand.block(blksz)
166 me.c.setiv(iv)
167 b.put(iv)
168 b.put(me.c.encrypt(pt))
169 t = me.m().hash(b).done()
170 return t + str(buffer(b))
171
172 def decrypt(me, ct):
173 """
174 Decrypt the ciphertext CT, returning the plaintext.
175
176 Raises DecryptError if anything goes wrong.
177 """
178 blksz = me.c.__class__.blksz
179 tagsz = me.m.__class__.tagsz
180 b = _C.ReadBuffer(ct)
181 t = b.get(tagsz)
182 h = me.m()
183 if blksz:
184 iv = b.get(blksz)
185 me.c.setiv(iv)
186 h.hash(iv)
187 x = b.get(b.left)
188 h.hash(x)
189 if t != h.done(): raise DecryptError
190 return me.c.decrypt(x)
191
192 class PPK (Crypto):
193 """
194 I represent a crypto transform whose keys are derived from a passphrase.
195
196 The password is salted and hashed; the salt is available as the `salt'
197 attribute.
198 """
199
200 def __init__(me, pp, c, h, m, salt = None):
201 """
202 Initialize the PPK object with a passphrase and algorithm selection.
203
204 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
205 subclass M, and a SALT. The SALT may be None, if we're generating new
206 keys, indicating that a salt should be chosen randomly.
207 """
208 if not salt: salt = _C.rand.block(h.hashsz)
209 tag = pp + _NUL + salt
210 Crypto.__init__(me, c, h, m,
211 h().hash(_CIPHER).hash(tag).done(),
212 h().hash(_MAC).hash(tag).done())
213 me.salt = salt
214
215 ###--------------------------------------------------------------------------
216 ### Backend storage.
217
218 class StorageBackendRefusal (Exception):
219 """
220 I signify that a StorageBackend subclass has refused to open a file.
221
222 This is used by the StorageBackend.open class method.
223 """
224 pass
225
226 class StorageBackendClass (type):
227 """
228 I am a metaclass for StorageBackend classes.
229
230 My main feature is that I register my concrete instances (with a `NAME'
231 which is not `None') with the StorageBackend class.
232 """
233 def __init__(me, name, supers, dict):
234 """
235 Register a new concrete StorageBackend subclass.
236 """
237 super(StorageBackendClass, me).__init__(name, supers, dict)
238 try: name = me.NAME
239 except AttributeError: pass
240 else: StorageBackend.register_concrete_subclass(me)
241
242 class StorageBackend (_with_metaclass(StorageBackendClass)):
243 """
244 I provide basic protocol for password storage backends.
245
246 I'm an abstract class: you want one of my subclasses if you actually want
247 to do something useful. But I maintain a list of my subclasses and can
248 choose an appropriate one to open a database file you've found lying about.
249
250 Backends are responsible for storing and retrieving stuff, but not for the
251 cryptographic details. Backends need to store two kinds of information:
252
253 * metadata, consisting of a number of property names and their values;
254 and
255
256 * password mappings, consisting of a number of binary labels and
257 payloads.
258
259 Backends need to implement the following ordinary methods. See the calling
260 methods for details of the subclass responsibilities.
261
262 BE._create(FILE) Create a new database in FILE; used by `create'.
263
264 BE._open(FILE, WRITEP)
265 Open the existing database FILE; used by `open'.
266
267 BE._close(ABRUPTP) Close the database, freeing up any resources. If
268 ABRUPTP then don't try to commit changes.
269
270 BE._get_meta(NAME, DEFAULT)
271 Return the value of the metadata item with the given
272 NAME, or DEFAULT if it doesn't exist; used by
273 `get_meta'.
274
275 BE._put_meta(NAME, VALUE)
276 Set the VALUE of the metadata item with the given
277 NAME, creating one if necessary; used by `put_meta'.
278
279 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
280 `KeyError' if there is no such item; used by
281 `del_meta'.
282
283 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
284 pairs; used by `iter_meta'.
285
286 BE._get_passwd(LABEL)
287 Return the password payload stored with the (binary)
288 LABEL; used by `get_passwd'.
289
290 BE._put_passwd(LABEL, PAYLOAD)
291 Associate the (binary) PAYLOAD with the LABEL,
292 forgetting any previous payload for that LABEL; used
293 by `put_passwd'.
294
295 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
296 by `_del_passwd'.
297
298 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
299 pairs; used by `iter_passwds'.
300
301 Also, concrete subclasses should define the following class attributes.
302
303 NAME The name of the backend, so that the user can select
304 it when creating a new database.
305
306 PRIO An integer priority: backends are tried in decreasing
307 priority order when opening an existing database.
308 """
309
310 PRIO = 10
311
312 ## The registry of subclasses.
313 CLASSES = {}
314
315 FAIL = ['FAIL']
316
317 @staticmethod
318 def register_concrete_subclass(sub):
319 """Register a concrete subclass, so that `open' can try it."""
320 StorageBackend.CLASSES[sub.NAME] = sub
321
322 @staticmethod
323 def byname(name):
324 """
325 Return the concrete subclass with the given NAME.
326
327 Raise `KeyError' if the name isn't found.
328 """
329 return StorageBackend.CLASSES[name]
330
331 @staticmethod
332 def classes():
333 """Return an iterator over the concrete subclasses."""
334 return _itervalues(StorageBackend.CLASSES)
335
336 @staticmethod
337 def open(file, writep = False):
338 """Open a database FILE, using some appropriate backend."""
339 _OS.stat(file)
340 for cls in sorted(StorageBackend.CLASSES.values(), reverse = True,
341 key = lambda cls: cls.PRIO):
342 try: return cls(file, writep)
343 except StorageBackendRefusal: pass
344 raise StorageBackendRefusal
345
346 @classmethod
347 def create(cls, file):
348 """
349 Create a new database in the named FILE, using this backend.
350
351 Subclasses must implement the `_create' instance method.
352 """
353 return cls(writep = True, _magic = lambda me: me._create(file))
354
355 def __init__(me, file = None, writep = False, _magic = None, *args, **kw):
356 """
357 Main constructor.
358
359 Subclasses are not, in general, expected to override this: there's a
360 somewhat hairy protocol between the constructor and some of the class
361 methods. Instead, the main hook for customization is the subclass's
362 `_open' method, which is invoked in the usual case.
363 """
364 super(StorageBackend, me).__init__(*args, **kw)
365 if me.NAME is None: raise ValueError('abstract class')
366 if _magic is not None: _magic(me)
367 elif file is None: raise ValueError('missing file parameter')
368 else: me._open(file, writep)
369 me._writep = writep
370 me._livep = True
371
372 def close(me, abruptp = False):
373 """
374 Close the database.
375
376 It is harmless to attempt to close a database which has been closed
377 already. Calls the subclass's `_close' method.
378 """
379 if me._livep:
380 me._livep = False
381 me._close(abruptp)
382
383 ## Utilities.
384
385 def _check_live(me):
386 """Raise an error if the receiver has been closed."""
387 if not me._livep: raise ValueError('database is closed')
388
389 def _check_write(me):
390 """Raise an error if the receiver is not open for writing."""
391 me._check_live()
392 if not me._writep: raise ValueError('database is read-only')
393
394 def _check_meta_name(me, name):
395 """
396 Raise an error unless NAME is a valid name for a metadata item.
397
398 Metadata names may not start with `$': such names are reserved for
399 password storage.
400 """
401 if name.startswith('$'):
402 raise ValueError("invalid metadata key `%s'" % name)
403
404 ## Context protocol.
405
406 def __enter__(me):
407 """Context protocol: make sure the database is closed on exit."""
408 return me
409 def __exit__(me, exctype, excvalue, exctb):
410 """Context protocol: see `__enter__'."""
411 me.close(excvalue is not None)
412
413 ## Metadata.
414
415 def get_meta(me, name, default = FAIL):
416 """
417 Fetch the value for the metadata item NAME.
418
419 If no such item exists, then return DEFAULT if that was set; otherwise
420 raise a `KeyError'.
421
422 This calls the subclass's `_get_meta' method, which should return the
423 requested item or return the given DEFAULT value. It may assume that the
424 name is valid and the database is open.
425 """
426 me._check_meta_name(name)
427 me._check_live()
428 value = me._get_meta(name, default)
429 if value is StorageBackend.FAIL: raise KeyError(name)
430 return value
431
432 def put_meta(me, name, value):
433 """
434 Store VALUE in the metadata item called NAME.
435
436 This calls the subclass's `_put_meta' method, which may assume that the
437 name is valid and the database is open for writing.
438 """
439 me._check_meta_name(name)
440 me._check_write()
441 me._put_meta(name, value)
442
443 def del_meta(me, name):
444 """
445 Forget about the metadata item with the given NAME.
446
447 This calls the subclass's `_del_meta' method, which may assume that the
448 name is valid and the database is open for writing.
449 """
450 me._check_meta_name(name)
451 me._check_write()
452 me._del_meta(name)
453
454 def iter_meta(me):
455 """
456 Return an iterator over the name/value metadata items.
457
458 This calls the subclass's `_iter_meta' method, which may assume that the
459 database is open.
460 """
461 me._check_live()
462 return me._iter_meta()
463
464 def get_passwd(me, label):
465 """
466 Fetch and return the payload stored with the (opaque, binary) LABEL.
467
468 If there is no such payload then raise `KeyError'.
469
470 This calls the subclass's `_get_passwd' method, which may assume that the
471 database is open.
472 """
473 me._check_live()
474 return me._get_passwd(label)
475
476 def put_passwd(me, label, payload):
477 """
478 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
479
480 Any previous payload for LABEL is forgotten.
481
482 This calls the subclass's `_put_passwd' method, which may assume that the
483 database is open for writing.
484 """
485 me._check_write()
486 me._put_passwd(label, payload)
487
488 def del_passwd(me, label):
489 """
490 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
491
492 If there is no such payload then raise `KeyError'.
493
494 This calls the subclass's `_del_passwd' method, which may assume that the
495 database is open for writing.
496 """
497 me._check_write()
498 me._del_passwd(label)
499
500 def iter_passwds(me):
501 """
502 Return an iterator over the stored password label/payload pairs.
503
504 This calls the subclass's `_iter_passwds' method, which may assume that
505 the database is open.
506 """
507 me._check_live()
508 return me._iter_passwds()
509
510 try: import gdbm as _G
511 except ImportError: pass
512 else:
513 class GDBMStorageBackend (StorageBackend):
514 """
515 My instances store password data in a GDBM database.
516
517 Metadata and password entries are mixed into the same database. The key
518 for a metadata item is simply its name; the key for a password entry is
519 the entry's label prefixed by `$', since we're guaranteed that no
520 metadata item name begins with `$'.
521 """
522
523 NAME = 'gdbm'
524
525 def _open(me, file, writep):
526 try: me._db = _G.open(file, writep and 'w' or 'r')
527 except _G.error: raise StorageBackendRefusal(_excval())
528
529 def _create(me, file):
530 me._db = _G.open(file, 'n', _M600)
531
532 def _close(me, abruptp):
533 me._db.close()
534 me._db = None
535
536 def _get_meta(me, name, default):
537 try: return me._db[name]
538 except KeyError: return default
539
540 def _put_meta(me, name, value):
541 me._db[name] = value
542
543 def _del_meta(me, name):
544 del me._db[name]
545
546 def _iter_meta(me):
547 k = me._db.firstkey()
548 while k is not None:
549 if not k.startswith('$'): yield k, me._db[k]
550 k = me._db.nextkey(k)
551
552 def _get_passwd(me, label):
553 return me._db['$' + label]
554
555 def _put_passwd(me, label, payload):
556 me._db['$' + label] = payload
557
558 def _del_passwd(me, label):
559 del me._db['$' + label]
560
561 def _iter_passwds(me):
562 k = me._db.firstkey()
563 while k is not None:
564 if k.startswith('$'): yield k[1:], me._db[k]
565 k = me._db.nextkey(k)
566
567 try: import sqlite3 as _Q
568 except ImportError: pass
569 else:
570 class SQLiteStorageBackend (StorageBackend):
571 """
572 I represent a password database stored in SQLite.
573
574 Metadata and password items are stored in separate tables, so there's no
575 conflict. Some additional metadata is stored in the `meta' table, with
576 names beginning with `$' so as not to conflict with clients:
577
578 $version The schema version of the table.
579 """
580
581 NAME = 'sqlite'
582 VERSION = 0
583
584 def _open(me, file, writep):
585 try:
586 me._db = _Q.connect(file)
587 ver = me._query_scalar(
588 "SELECT value FROM meta WHERE name = '$version'",
589 "version check")
590 except (_Q.DatabaseError, _Q.OperationalError):
591 raise StorageBackendRefusal(_excval())
592 if ver is None: raise ValueError('database broken (missing $version)')
593 elif ver < me.VERSION: me._upgrade(ver)
594 elif ver > me.VERSION: raise ValueError \
595 ('unknown database schema version (%d > %d)' % (ver, me.VERSION))
596
597 def _create(me, file):
598 fd = _OS.open(file, _OS.O_WRONLY | _OS.O_CREAT | _OS.O_EXCL, _M600)
599 _OS.close(fd)
600 try:
601 me._db = _Q.connect(file)
602 c = me._db.cursor()
603 c.execute("""
604 CREATE TABLE meta (
605 name TEXT PRIMARY KEY NOT NULL,
606 value BLOB NOT NULL);
607 """)
608 c.execute("""
609 CREATE TABLE passwd (
610 label BLOB PRIMARY KEY NOT NULL,
611 payload BLOB NOT NULL);
612 """)
613 c.execute("""
614 INSERT INTO meta (name, value) VALUES ('$version', ?);
615 """, [me.VERSION])
616 except:
617 try: _OS.unlink(file)
618 except OSError: pass
619 raise
620
621 def _upgrade(me, ver):
622 """Upgrade the database from schema version VER."""
623 assert False, 'how embarrassing'
624
625 def _close(me, abruptp):
626 if not abruptp: me._db.commit()
627 me._db.close()
628 me._db = None
629
630 def _fetch_scalar(me, c, what, default = None):
631 try: row = next(c)
632 except StopIteration: val = default
633 else: val, = row
634 try: row = next(c)
635 except StopIteration: pass
636 else: raise ValueError('multiple matching records for %s' % what)
637 return val
638
639 def _query_scalar(me, query, what, default = None, args = []):
640 c = me._db.cursor()
641 c.execute(query, args)
642 return me._fetch_scalar(c, what, default)
643
644 def _get_meta(me, name, default):
645 v = me._query_scalar("SELECT value FROM meta WHERE name = ?",
646 "metadata item `%s'" % name,
647 default = default, args = [name])
648 if v is default: return v
649 else: return str(v)
650
651 def _put_meta(me, name, value):
652 c = me._db.cursor()
653 c.execute("INSERT OR REPLACE INTO meta (name, value) VALUES (?, ?)",
654 [name, buffer(value)])
655
656 def _del_meta(me, name):
657 c = me._db.cursor()
658 c.execute("DELETE FROM meta WHERE name = ?", [name])
659 if not c.rowcount: raise KeyError(name)
660
661 def _iter_meta(me):
662 c = me._db.cursor()
663 c.execute("SELECT name, value FROM meta WHERE name NOT LIKE '$%'")
664 for k, v in c: yield k, str(v)
665
666 def _get_passwd(me, label):
667 pld = me._query_scalar("SELECT payload FROM passwd WHERE label = ?",
668 "password", default = None,
669 args = [buffer(label)])
670 if pld is None: raise KeyError(label)
671 return str(pld)
672
673 def _put_passwd(me, label, payload):
674 c = me._db.cursor()
675 c.execute("INSERT OR REPLACE INTO passwd (label, payload) "
676 "VALUES (?, ?)",
677 [buffer(label), buffer(payload)])
678
679 def _del_passwd(me, label):
680 c = me._db.cursor()
681 c.execute("DELETE FROM passwd WHERE label = ?", [label])
682 if not c.rowcount: raise KeyError(label)
683
684 def _iter_passwds(me):
685 c = me._db.cursor()
686 c.execute("SELECT label, payload FROM passwd")
687 for k, v in c: yield str(k), str(v)
688
689 class PlainTextBackend (StorageBackend):
690 """
691 I'm a utility base class for storage backends which use plain text files.
692
693 I provide subclasses with the following capabilities.
694
695 * Creating files, with given modes, optionally ensuring that the file
696 doesn't exist already.
697
698 * Parsing flat text files, checking leading magic, skipping comments, and
699 providing standard encodings of troublesome characters and binary
700 strings in metadata and password records. See below.
701
702 * Maintenance of metadata and password records in in-memory dictionaries,
703 with ready implementations of the necessary StorageBackend subclass
704 responsibility methods. (Subclasses can override these if they want to
705 make different arrangements.)
706
707 Metadata records are written with an optional prefix string chosen by the
708 caller, followed by a `NAME=VALUE' pair. The NAME is form-urlencoded and
709 prefixed with `!' if it contains strange characters; the VALUE is base64-
710 encoded (without the pointless trailing `=' padding) and prefixed with `?'
711 if necessary.
712
713 Password records are written with an optional prefix string chosen by the
714 caller, followed by a LABEL=PAYLOAD pair, both of which are base64-encoded
715 (without padding).
716
717 The following attributes are available for subclasses:
718
719 _meta Dictionary mapping metadata item names to their values.
720 Populated by `_parse_meta' and managed by `_get_meta' and
721 friends.
722
723 _pw Dictionary mapping password labels to encrypted payloads.
724 Populated by `_parse_passwd' and managed by `_get_passwd' and
725 friends.
726
727 _dirtyp Boolean: set if either of the dictionaries has been modified.
728 """
729
730 def __init__(me, *args, **kw):
731 """
732 Hook for initialization.
733
734 Sets up the published instance attributes.
735 """
736 me._meta = {}
737 me._pw = {}
738 me._dirtyp = False
739 super(PlainTextBackend, me).__init__(*args, **kw)
740
741 def _create_file(me, file, mode = _M600, freshp = False):
742 """
743 Make sure FILE exists, creating it with the given MODE if necessary.
744
745 If FRESHP is true, then make sure the file did not exist previously.
746 Return a file object for the newly created file.
747 """
748 flags = _OS.O_CREAT | _OS.O_WRONLY
749 if freshp: flags |= _OS.O_EXCL
750 else: flags |= _OS.O_TRUNC
751 fd = _OS.open(file, flags, mode)
752 return _OS.fdopen(fd, 'w')
753
754 def _mark_dirty(me):
755 """
756 Set the `_dirtyp' flag.
757
758 Subclasses might find it useful to intercept this method.
759 """
760 me._dirtyp = True
761
762 def _eqsplit(me, line):
763 """
764 Extract the KEY, VALUE pair from a LINE of the form `KEY=VALUE'.
765
766 Raise `ValueError' if there is no `=' in the LINE.
767 """
768 eq = line.index('=')
769 return line[:eq], line[eq + 1:]
770
771 def _parse_file(me, file, magic = None):
772 """
773 Parse a FILE.
774
775 Specifically:
776
777 * Raise `StorageBackendRefusal' if that the first line doesn't match
778 MAGIC (if provided). MAGIC should not contain the terminating
779 newline.
780
781 * Ignore comments (beginning `#') and blank lines.
782
783 * Call `_parse_line' (provided by the subclass) for other lines.
784 """
785 with open(file, 'r') as f:
786 if magic is not None:
787 if f.readline().rstrip('\n') != magic: raise StorageBackendRefusal
788 for line in f:
789 line = line.rstrip('\n')
790 if not line or line.startswith('#'): continue
791 me._parse_line(line)
792
793 def _write_file(me, file, writebody, mode = _M600, magic = None):
794 """
795 Update FILE atomically.
796
797 The newly created file will have the given MODE. If MAGIC is given, then
798 write that as the first line. Calls WRITEBODY(F) to write the main body
799 of the file where F is a file object for the new file.
800 """
801 new = file + '.new'
802 with me._create_file(new, mode) as f:
803 if magic is not None: f.write(magic + '\n')
804 writebody(f)
805 _OS.rename(new, file)
806
807 def _parse_meta(me, line):
808 """Parse LINE as a metadata NAME=VALUE pair, and updates `_meta'."""
809 k, v = me._eqsplit(line)
810 me._meta[_dec_metaname(k)] = _dec_metaval(v)
811
812 def _write_meta(me, f, prefix = ''):
813 """Write the metadata records to F, each with the given PREFIX."""
814 f.write('\n## Metadata.\n')
815 for k, v in _iteritems(me._meta):
816 f.write('%s%s=%s\n' % (prefix, _enc_metaname(k), _enc_metaval(v)))
817
818 def _get_meta(me, name, default):
819 return me._meta.get(name, default)
820 def _put_meta(me, name, value):
821 me._mark_dirty()
822 me._meta[name] = value
823 def _del_meta(me, name):
824 me._mark_dirty()
825 del me._meta[name]
826 def _iter_meta(me):
827 return _iteritems(me._meta)
828
829 def _parse_passwd(me, line):
830 """Parse LINE as a password LABEL=PAYLOAD pair, and updates `_pw'."""
831 k, v = me._eqsplit(line)
832 me._pw[_unb64(k)] = _unb64(v)
833
834 def _write_passwd(me, f, prefix = ''):
835 """Write the password records to F, each with the given PREFIX."""
836 f.write('\n## Password data.\n')
837 for k, v in _iteritems(me._pw):
838 f.write('%s%s=%s\n' % (prefix, _b64(k), _b64(v)))
839
840 def _get_passwd(me, label):
841 return me._pw[str(label)]
842 def _put_passwd(me, label, payload):
843 me._mark_dirty()
844 me._pw[str(label)] = payload
845 def _del_passwd(me, label):
846 me._mark_dirty()
847 del me._pw[str(label)]
848 def _iter_passwds(me):
849 return _iteritems(me._pw)
850
851 class FlatFileStorageBackend (PlainTextBackend):
852 """
853 I maintain a password database in a plain text file.
854
855 The text file consists of lines, as follows.
856
857 * Empty lines, and lines beginning with `#' (in the leftmost column only)
858 are ignored.
859
860 * Lines of the form `$LABEL=PAYLOAD' store password data. Both LABEL and
861 PAYLOAD are base64-encoded, without `=' padding.
862
863 * Lines of the form `NAME=VALUE' store metadata. If the NAME contains
864 characters other than alphanumerics, hyphens, underscores, and colons,
865 then it is form-urlencoded, and prefixed wth `!'. If the VALUE
866 contains such characters, then it is base64-encoded, without `='
867 padding, and prefixed with `?'.
868
869 * Other lines are erroneous.
870
871 The file is rewritten from scratch when it's changed: any existing
872 commentary is lost, and items may be reordered. There is no file locking,
873 but the file is updated atomically, by renaming.
874
875 It is expected that the FlatFileStorageBackend is used mostly for
876 diagnostics and transfer, rather than for a live system.
877 """
878
879 NAME = 'flat'
880 PRIO = 0
881 MAGIC = '### pwsafe password database'
882
883 def _open(me, file, writep):
884 if not _OS.path.isfile(file): raise StorageBackendRefusal
885 me._parse_file(file, magic = me.MAGIC)
886 def _parse_line(me, line):
887 if line.startswith('$'): me._parse_passwd(line[1:])
888 else: me._parse_meta(line)
889
890 def _create(me, file):
891 with me._create_file(file, freshp = True) as f: pass
892 me._file = file
893 me._mark_dirty()
894
895 def _close(me, abruptp):
896 if not abruptp and me._dirtyp:
897 me._write_file(me._file, me._write_body, magic = me.MAGIC)
898
899 def _write_body(me, f):
900 me._write_meta(f)
901 me._write_passwd(f, '$')
902
903 class DirectoryStorageBackend (PlainTextBackend):
904 """
905 I maintain a password database in a directory, with one file per password.
906
907 This makes password databases easy to maintain in a revision-control system
908 such as Git.
909
910 The directory is structured as follows.
911
912 dir/meta Contains metadata, similar to the `FlatFileBackend'.
913
914 dir/pw/LABEL Contains the (raw binary) payload for the given password
915 LABEL (base64-encoded, without the useless `=' padding, and
916 with `/' replaced by `.').
917
918 dir/tmp/ Contains temporary files used by the implementation.
919 """
920
921 NAME = 'dir'
922 METAMAGIC = '### pwsafe password directory metadata'
923
924 def _open(me, file, writep):
925 if not _OS.path.isdir(file) or \
926 not _OS.path.isdir(_OS.path.join(file, 'pw')) or \
927 not _OS.path.isdir(_OS.path.join(file, 'tmp')) or \
928 not _OS.path.isfile(_OS.path.join(file, 'meta')):
929 raise StorageBackendRefusal
930 me._dir = file
931 me._parse_file(_OS.path.join(file, 'meta'), magic = me.METAMAGIC)
932 def _parse_line(me, line):
933 me._parse_meta(line)
934
935 def _create(me, file):
936 _OS.mkdir(file, _M700)
937 _OS.mkdir(_OS.path.join(file, 'pw'), _M700)
938 _OS.mkdir(_OS.path.join(file, 'tmp'), _M700)
939 me._mark_dirty()
940 me._dir = file
941
942 def _close(me, abruptp):
943 if not abruptp and me._dirtyp:
944 me._write_file(_OS.path.join(me._dir, 'meta'),
945 me._write_meta, magic = me.METAMAGIC)
946
947 def _pwfile(me, label, dir = 'pw'):
948 return _OS.path.join(me._dir, dir, _b64(label).replace('/', '.'))
949 def _get_passwd(me, label):
950 try:
951 f = open(me._pwfile(label), 'rb')
952 except (OSError, IOError):
953 if _excval().errno == _E.ENOENT: raise KeyError(label)
954 else: raise
955 with f: return f.read()
956 def _put_passwd(me, label, payload):
957 new = me._pwfile(label, 'tmp')
958 fd = _OS.open(new, _OS.O_WRONLY | _OS.O_CREAT | _OS.O_TRUNC, _M600)
959 _OS.close(fd)
960 with open(new, 'wb') as f: f.write(payload)
961 _OS.rename(new, me._pwfile(label))
962 def _del_passwd(me, label):
963 try:
964 _OS.remove(me._pwfile(label))
965 except (OSError, IOError):
966 if _excval().errno == _E.ENOENT: raise KeyError(label)
967 else: raise
968 def _iter_passwds(me):
969 pw = _OS.path.join(me._dir, 'pw')
970 for i in _OS.listdir(pw):
971 with open(_OS.path.join(pw, i), 'rb') as f: pld = f.read()
972 yield _unb64(i.replace('.', '/')), pld
973
974 ###--------------------------------------------------------------------------
975 ### Password storage.
976
977 class PW (object):
978 """
979 I represent a secure (ish) password store.
980
981 I can store short secrets, associated with textual names, in a way which
982 doesn't leak too much information about them.
983
984 I implement (some of) the Python mapping protocol.
985
986 I keep track of everything using a StorageBackend object. This contains
987 password entries, identified by cryptographic labels, and a number of
988 metadata items.
989
990 cipher Names the Catacomb cipher selected.
991
992 hash Names the Catacomb hash function selected.
993
994 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
995 length and concatenated, encrypted using the master
996 passphrase.
997
998 mac Names the Catacomb message authentication code selected.
999
1000 magic A magic string for obscuring password tag names.
1001
1002 salt The salt for hashing the passphrase.
1003
1004 tag The master passphrase's tag, for the Pixie's benefit.
1005
1006 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
1007 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
1008 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
1009 encrypted using the stored keys.
1010 """
1011
1012 def __init__(me, file, writep = False):
1013 """
1014 Initialize a PW object from the database in FILE.
1015
1016 If WRITEP is false (the default) then the database is opened read-only;
1017 if true then it may be written. Requests the database password from the
1018 Pixie, which may cause interaction.
1019 """
1020
1021 ## Open the database.
1022 me.db = StorageBackend.open(file, writep)
1023
1024 ## Find out what crypto to use.
1025 c = _C.gcciphers[me.db.get_meta('cipher')]
1026 h = _C.gchashes[me.db.get_meta('hash')]
1027 m = _C.gcmacs[me.db.get_meta('mac')]
1028
1029 ## Request the passphrase and extract the master keys.
1030 tag = me.db.get_meta('tag')
1031 ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt'))
1032 try:
1033 b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
1034 except DecryptError:
1035 _C.ppcancel(tag)
1036 raise
1037 me.ck = b.getblk16()
1038 me.mk = b.getblk16()
1039 if not b.endp: raise ValueError('trailing junk')
1040
1041 ## Set the key, and stash it and the tag-hashing secret.
1042 me.k = Crypto(c, h, m, me.ck, me.mk)
1043 me.magic = me.k.decrypt(me.db.get_meta('magic'))
1044
1045 @classmethod
1046 def create(cls, dbcls, file, tag, c, h, m):
1047 """
1048 Create and initialize a new database FILE using StorageBackend DBCLS.
1049
1050 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
1051 and a Pixie passphrase TAG.
1052
1053 This doesn't return a working object: it just creates the database file
1054 and gets out of the way.
1055 """
1056
1057 ## Set up the cryptography.
1058 pp = _C.ppread(tag, _C.PMODE_VERIFY)
1059 ppk = PPK(pp, c, h, m)
1060 ck = _C.rand.block(c.keysz.default)
1061 mk = _C.rand.block(c.keysz.default)
1062 k = Crypto(c, h, m, ck, mk)
1063
1064 ## Set up and initialize the database.
1065 kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
1066 with dbcls.create(file) as db:
1067 db.put_meta('tag', tag)
1068 db.put_meta('salt', ppk.salt)
1069 db.put_meta('cipher', c.name)
1070 db.put_meta('hash', h.name)
1071 db.put_meta('mac', m.name)
1072 db.put_meta('key', kct)
1073 db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz)))
1074
1075 def keyxform(me, key):
1076 """Transform the KEY (actually a password tag) into a password label."""
1077 return me.k.h().hash(me.magic).hash(key).done()
1078
1079 def changepp(me):
1080 """
1081 Change the database password.
1082
1083 Requests the new password from the Pixie, which will probably cause
1084 interaction.
1085 """
1086 tag = me.db.get_meta('tag')
1087 _C.ppcancel(tag)
1088 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
1089 me.k.c.__class__, me.k.h, me.k.m.__class__)
1090 kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
1091 me.db.put_meta('key', kct)
1092 me.db.put_meta('salt', ppk.salt)
1093
1094 def pack(me, key, value):
1095 """Pack the KEY and VALUE into a ciphertext, and return it."""
1096 b = _C.WriteBuffer()
1097 b.putblk16(key).putblk16(value)
1098 b.zero(((b.size + 255) & ~255) - b.size)
1099 return me.k.encrypt(b)
1100
1101 def unpack(me, ct):
1102 """
1103 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
1104
1105 Might raise DecryptError, of course.
1106 """
1107 b = _C.ReadBuffer(me.k.decrypt(ct))
1108 key = b.getblk16()
1109 value = b.getblk16()
1110 return key, value
1111
1112 ## Mapping protocol.
1113
1114 def __getitem__(me, key):
1115 """Return the password for the given KEY."""
1116 try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1]
1117 except KeyError: raise KeyError(key)
1118
1119 def __setitem__(me, key, value):
1120 """Associate the password VALUE with the KEY."""
1121 me.db.put_passwd(me.keyxform(key), me.pack(key, value))
1122
1123 def __delitem__(me, key):
1124 """Forget all about the KEY."""
1125 try: me.db.del_passwd(me.keyxform(key))
1126 except KeyError: raise KeyError(key)
1127
1128 def __iter__(me):
1129 """Iterate over the known password tags."""
1130 for _, pld in me.db.iter_passwds():
1131 yield me.unpack(pld)[0]
1132
1133 ## Context protocol.
1134
1135 def __enter__(me):
1136 return me
1137 def __exit__(me, excty, excval, exctb):
1138 me.db.close(excval is not None)
1139
1140 ###----- That's all, folks --------------------------------------------------