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