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