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