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