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