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