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