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