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