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