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