chiark / gitweb /
catacomb/pwsafe.py: Add a new ABRUPTP argument to `close' methods.
[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
6baae405
MW
31import os as _OS
32
43c09851 33import catacomb as _C
34import gdbm as _G
d1c45f5c
MW
35
36###--------------------------------------------------------------------------
37### Underlying cryptography.
38
43c09851 39class DecryptError (Exception):
d1c45f5c
MW
40 """
41 I represent a failure to decrypt a message.
42
43 Usually this means that someone used the wrong key, though it can also
44 mean that a ciphertext has been modified.
45 """
43c09851 46 pass
47
48class Crypto (object):
d1c45f5c
MW
49 """
50 I represent a symmetric crypto transform.
51
52 There's currently only one transform implemented, which is the obvious
53 generic-composition construction: given a message m, and keys K0 and K1, we
54 choose an IV v, and compute:
55
56 * y = v || E(K0, v; m)
57 * t = M(K1; y)
58
59 The final ciphertext is t || y.
60 """
61
43c09851 62 def __init__(me, c, h, m, ck, mk):
d1c45f5c
MW
63 """
64 Initialize the Crypto object with a given algorithm selection and keys.
65
66 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
67 keys CK and MK for C and M respectively.
68 """
43c09851 69 me.c = c(ck)
70 me.m = m(mk)
71 me.h = h
d1c45f5c 72
43c09851 73 def encrypt(me, pt):
d1c45f5c
MW
74 """
75 Encrypt the message PT and return the resulting ciphertext.
76 """
9a7b948f
MW
77 blksz = me.c.__class__.blksz
78 b = _C.WriteBuffer()
79 if blksz:
80 iv = _C.rand.block(blksz)
43c09851 81 me.c.setiv(iv)
9a7b948f
MW
82 b.put(iv)
83 b.put(me.c.encrypt(pt))
84 t = me.m().hash(b).done()
85 return t + str(buffer(b))
86
43c09851 87 def decrypt(me, ct):
d1c45f5c
MW
88 """
89 Decrypt the ciphertext CT, returning the plaintext.
90
91 Raises DecryptError if anything goes wrong.
92 """
9a7b948f
MW
93 blksz = me.c.__class__.blksz
94 tagsz = me.m.__class__.tagsz
95 b = _C.ReadBuffer(ct)
96 t = b.get(tagsz)
97 h = me.m()
98 if blksz:
99 iv = b.get(blksz)
100 me.c.setiv(iv)
101 h.hash(iv)
102 x = b.get(b.left)
103 h.hash(x)
104 if t != h.done(): raise DecryptError
105 return me.c.decrypt(x)
b2687a0a 106
43c09851 107class PPK (Crypto):
d1c45f5c
MW
108 """
109 I represent a crypto transform whose keys are derived from a passphrase.
110
111 The password is salted and hashed; the salt is available as the `salt'
112 attribute.
113 """
114
43c09851 115 def __init__(me, pp, c, h, m, salt = None):
d1c45f5c
MW
116 """
117 Initialize the PPK object with a passphrase and algorithm selection.
118
119 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
120 subclass M, and a SALT. The SALT may be None, if we're generating new
121 keys, indicating that a salt should be chosen randomly.
122 """
43c09851 123 if not salt: salt = _C.rand.block(h.hashsz)
124 tag = '%s\0%s' % (pp, salt)
125 Crypto.__init__(me, c, h, m,
d1c45f5c
MW
126 h().hash('cipher:' + tag).done(),
127 h().hash('mac:' + tag).done())
43c09851 128 me.salt = salt
129
494b719c
MW
130###--------------------------------------------------------------------------
131### Backend storage.
132
6baae405
MW
133class StorageBackendRefusal (Exception):
134 """
135 I signify that a StorageBackend subclass has refused to open a file.
136
137 This is used by the StorageBackend.open class method.
138 """
139 pass
140
141class StorageBackendClass (type):
142 """
143 I am a metaclass for StorageBackend classes.
144
145 My main feature is that I register my concrete instances (with a `NAME'
146 which is not `None') with the StorageBackend class.
147 """
148 def __init__(me, name, supers, dict):
149 """
150 Register a new concrete StorageBackend subclass.
151 """
152 super(StorageBackendClass, me).__init__(name, supers, dict)
153 if me.NAME is not None: StorageBackend.register_concrete_subclass(me)
154
494b719c
MW
155class StorageBackend (object):
156 """
1726ab40
MW
157 I provide basic protocol for password storage backends.
158
159 I'm an abstract class: you want one of my subclasses if you actually want
6baae405
MW
160 to do something useful. But I maintain a list of my subclasses and can
161 choose an appropriate one to open a database file you've found lying about.
494b719c
MW
162
163 Backends are responsible for storing and retrieving stuff, but not for the
164 cryptographic details. Backends need to store two kinds of information:
165
166 * metadata, consisting of a number of property names and their values;
167 and
168
169 * password mappings, consisting of a number of binary labels and
170 payloads.
1726ab40
MW
171
172 Backends need to implement the following ordinary methods. See the calling
173 methods for details of the subclass responsibilities.
174
175 BE._create(FILE) Create a new database in FILE; used by `create'.
176
177 BE._open(FILE, WRITEP)
178 Open the existing database FILE; used by `open'.
179
053c2659
MW
180 BE._close(ABRUPTP) Close the database, freeing up any resources. If
181 ABRUPTP then don't try to commit changes.
1726ab40
MW
182
183 BE._get_meta(NAME, DEFAULT)
184 Return the value of the metadata item with the given
185 NAME, or DEFAULT if it doesn't exist; used by
186 `get_meta'.
187
188 BE._put_meta(NAME, VALUE)
189 Set the VALUE of the metadata item with the given
190 NAME, creating one if necessary; used by `put_meta'.
191
192 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
193 `KeyError' if there is no such item; used by
194 `del_meta'.
195
196 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
197 pairs; used by `iter_meta'.
198
199 BE._get_passwd(LABEL)
200 Return the password payload stored with the (binary)
201 LABEL; used by `get_passwd'.
202
203 BE._put_passwd(LABEL, PAYLOAD)
204 Associate the (binary) PAYLOAD with the LABEL,
205 forgetting any previous payload for that LABEL; used
206 by `put_passwd'.
207
208 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
209 by `_del_passwd'.
210
211 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
212 pairs; used by `iter_passwds'.
6baae405
MW
213
214 Also, concrete subclasses should define the following class attributes.
215
216 NAME The name of the backend, so that the user can select
217 it when creating a new database.
218
219 PRIO An integer priority: backends are tried in decreasing
220 priority order when opening an existing database.
494b719c
MW
221 """
222
6baae405
MW
223 __metaclass__ = StorageBackendClass
224 NAME = None
225 PRIO = 10
226
227 ## The registry of subclasses.
228 CLASSES = {}
229
494b719c
MW
230 FAIL = ['FAIL']
231
6baae405
MW
232 @staticmethod
233 def register_concrete_subclass(sub):
234 """Register a concrete subclass, so that `open' can try it."""
235 StorageBackend.CLASSES[sub.NAME] = sub
236
237 @staticmethod
238 def byname(name):
239 """
240 Return the concrete subclass with the given NAME.
241
242 Raise `KeyError' if the name isn't found.
243 """
244 return StorageBackend.CLASSES[name]
245
246 @staticmethod
247 def classes():
248 """Return an iterator over the concrete subclasses."""
249 return StorageBackend.CLASSES.itervalues()
250
251 @staticmethod
252 def open(file, writep = False):
253 """Open a database FILE, using some appropriate backend."""
254 _OS.stat(file)
255 for cls in sorted(StorageBackend.CLASSES.values(), reverse = True,
256 key = lambda cls: cls.PRIO):
257 try: return cls(file, writep)
258 except StorageBackendRefusal: pass
259 raise StorageBackendRefusal
494b719c
MW
260
261 @classmethod
262 def create(cls, file):
1726ab40
MW
263 """
264 Create a new database in the named FILE, using this backend.
265
266 Subclasses must implement the `_create' instance method.
267 """
494b719c 268 return cls(writep = True, _magic = lambda me: me._create(file))
494b719c
MW
269
270 def __init__(me, file = None, writep = False, _magic = None, *args, **kw):
271 """
272 Main constructor.
1726ab40
MW
273
274 Subclasses are not, in general, expected to override this: there's a
275 somewhat hairy protocol between the constructor and some of the class
276 methods. Instead, the main hook for customization is the subclass's
277 `_open' method, which is invoked in the usual case.
494b719c
MW
278 """
279 super(StorageBackend, me).__init__(*args, **kw)
6baae405 280 if me.NAME is None: raise ValueError, 'abstract class'
494b719c
MW
281 if _magic is not None: _magic(me)
282 elif file is None: raise ValueError, 'missing file parameter'
1726ab40 283 else: me._open(file, writep)
494b719c
MW
284 me._writep = writep
285 me._livep = True
286
053c2659 287 def close(me, abruptp = False):
494b719c
MW
288 """
289 Close the database.
290
291 It is harmless to attempt to close a database which has been closed
1726ab40 292 already. Calls the subclass's `_close' method.
494b719c
MW
293 """
294 if me._livep:
295 me._livep = False
053c2659 296 me._close(abruptp)
494b719c
MW
297
298 ## Utilities.
299
300 def _check_live(me):
301 """Raise an error if the receiver has been closed."""
302 if not me._livep: raise ValueError, 'database is closed'
303
304 def _check_write(me):
305 """Raise an error if the receiver is not open for writing."""
306 me._check_live()
307 if not me._writep: raise ValueError, 'database is read-only'
308
309 def _check_meta_name(me, name):
310 """
311 Raise an error unless NAME is a valid name for a metadata item.
312
313 Metadata names may not start with `$': such names are reserved for
314 password storage.
315 """
316 if name.startswith('$'):
317 raise ValueError, "invalid metadata key `%s'" % name
318
319 ## Context protocol.
320
321 def __enter__(me):
322 """Context protocol: make sure the database is closed on exit."""
323 return me
324 def __exit__(me, exctype, excvalue, exctb):
325 """Context protocol: see `__enter__'."""
053c2659 326 me.close(excvalue is not None)
494b719c
MW
327
328 ## Metadata.
329
330 def get_meta(me, name, default = FAIL):
331 """
332 Fetch the value for the metadata item NAME.
333
334 If no such item exists, then return DEFAULT if that was set; otherwise
335 raise a `KeyError'.
1726ab40
MW
336
337 This calls the subclass's `_get_meta' method, which should return the
338 requested item or return the given DEFAULT value. It may assume that the
339 name is valid and the database is open.
494b719c
MW
340 """
341 me._check_meta_name(name)
342 me._check_live()
1726ab40 343 value = me._get_meta(name, default)
494b719c
MW
344 if value is StorageBackend.FAIL: raise KeyError, name
345 return value
346
347 def put_meta(me, name, value):
1726ab40
MW
348 """
349 Store VALUE in the metadata item called NAME.
350
351 This calls the subclass's `_put_meta' method, which may assume that the
352 name is valid and the database is open for writing.
353 """
494b719c
MW
354 me._check_meta_name(name)
355 me._check_write()
1726ab40 356 me._put_meta(name, value)
494b719c
MW
357
358 def del_meta(me, name):
1726ab40
MW
359 """
360 Forget about the metadata item with the given NAME.
361
362 This calls the subclass's `_del_meta' method, which may assume that the
363 name is valid and the database is open for writing.
364 """
494b719c
MW
365 me._check_meta_name(name)
366 me._check_write()
1726ab40 367 me._del_meta(name)
494b719c
MW
368
369 def iter_meta(me):
1726ab40
MW
370 """
371 Return an iterator over the name/value metadata items.
494b719c 372
1726ab40
MW
373 This calls the subclass's `_iter_meta' method, which may assume that the
374 database is open.
375 """
376 me._check_live()
377 return me._iter_meta()
494b719c
MW
378
379 def get_passwd(me, label):
380 """
381 Fetch and return the payload stored with the (opaque, binary) LABEL.
382
383 If there is no such payload then raise `KeyError'.
1726ab40
MW
384
385 This calls the subclass's `_get_passwd' method, which may assume that the
386 database is open.
494b719c
MW
387 """
388 me._check_live()
1726ab40 389 return me._get_passwd(label)
494b719c
MW
390
391 def put_passwd(me, label, payload):
392 """
393 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
394
395 Any previous payload for LABEL is forgotten.
1726ab40
MW
396
397 This calls the subclass's `_put_passwd' method, which may assume that the
398 database is open for writing.
494b719c
MW
399 """
400 me._check_write()
1726ab40 401 me._put_passwd(label, payload)
494b719c
MW
402
403 def del_passwd(me, label):
404 """
405 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
406
407 If there is no such payload then raise `KeyError'.
1726ab40
MW
408
409 This calls the subclass's `_del_passwd' method, which may assume that the
410 database is open for writing.
494b719c
MW
411 """
412 me._check_write()
1726ab40 413 me._del_passwd(label, payload)
494b719c
MW
414
415 def iter_passwds(me):
1726ab40
MW
416 """
417 Return an iterator over the stored password label/payload pairs.
418
419 This calls the subclass's `_iter_passwds' method, which may assume that
420 the database is open.
421 """
494b719c 422 me._check_live()
1726ab40
MW
423 return me._iter_passwds()
424
425class GDBMStorageBackend (StorageBackend):
426 """
427 My instances store password data in a GDBM database.
428
429 Metadata and password entries are mixed into the same database. The key
430 for a metadata item is simply its name; the key for a password entry is
431 the entry's label prefixed by `$', since we're guaranteed that no
432 metadata item name begins with `$'.
433 """
434
6baae405
MW
435 NAME = 'gdbm'
436
1726ab40
MW
437 def _open(me, file, writep):
438 try: me._db = _G.open(file, writep and 'w' or 'r')
439 except _G.error, e: raise StorageBackendRefusal, e
440
441 def _create(me, file):
442 me._db = _G.open(file, 'n', 0600)
443
053c2659 444 def _close(me, abruptp):
1726ab40
MW
445 me._db.close()
446 me._db = None
447
448 def _get_meta(me, name, default):
449 try: return me._db[name]
450 except KeyError: return default
451
452 def _put_meta(me, name, value):
453 me._db[name] = value
454
455 def _del_meta(me, name):
456 del me._db[name]
457
458 def _iter_meta(me):
459 k = me._db.firstkey()
460 while k is not None:
461 if not k.startswith('$'): yield k, me._db[k]
462 k = me._db.nextkey(k)
463
464 def _get_passwd(me, label):
465 return me._db['$' + label]
466
467 def _put_passwd(me, label, payload):
468 me._db['$' + label] = payload
469
470 def _del_passwd(me, label):
471 del me._db['$' + label]
472
473 def _iter_passwds(me):
494b719c
MW
474 k = me._db.firstkey()
475 while k is not None:
476 if k.startswith('$'): yield k[1:], me._db[k]
477 k = me._db.nextkey(k)
478
d1c45f5c
MW
479###--------------------------------------------------------------------------
480### Password storage.
43c09851 481
43c09851 482class PW (object):
d1c45f5c
MW
483 """
484 I represent a secure (ish) password store.
485
486 I can store short secrets, associated with textual names, in a way which
487 doesn't leak too much information about them.
488
2119e334 489 I implement (some of) the Python mapping protocol.
d1c45f5c 490
494b719c
MW
491 I keep track of everything using a StorageBackend object. This contains
492 password entries, identified by cryptographic labels, and a number of
493 metadata items.
d1c45f5c
MW
494
495 cipher Names the Catacomb cipher selected.
496
497 hash Names the Catacomb hash function selected.
498
499 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
500 length and concatenated, encrypted using the master
501 passphrase.
502
503 mac Names the Catacomb message authentication code selected.
504
505 magic A magic string for obscuring password tag names.
506
507 salt The salt for hashing the passphrase.
508
509 tag The master passphrase's tag, for the Pixie's benefit.
510
494b719c
MW
511 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
512 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
513 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
514 encrypted using the stored keys.
d1c45f5c
MW
515 """
516
4a35c9a7 517 def __init__(me, file, writep = False):
d1c45f5c 518 """
494b719c 519 Initialize a PW object from the database in FILE.
d1c45f5c 520
494b719c
MW
521 If WRITEP is false (the default) then the database is opened read-only;
522 if true then it may be written. Requests the database password from the
523 Pixie, which may cause interaction.
d1c45f5c
MW
524 """
525
526 ## Open the database.
6baae405 527 me.db = StorageBackend.open(file, writep)
d1c45f5c
MW
528
529 ## Find out what crypto to use.
494b719c
MW
530 c = _C.gcciphers[me.db.get_meta('cipher')]
531 h = _C.gchashes[me.db.get_meta('hash')]
532 m = _C.gcmacs[me.db.get_meta('mac')]
d1c45f5c
MW
533
534 ## Request the passphrase and extract the master keys.
494b719c
MW
535 tag = me.db.get_meta('tag')
536 ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt'))
43c09851 537 try:
494b719c 538 b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
43c09851 539 except DecryptError:
540 _C.ppcancel(tag)
541 raise
9a7b948f
MW
542 me.ck = b.getblk16()
543 me.mk = b.getblk16()
544 if not b.endp: raise ValueError, 'trailing junk'
d1c45f5c
MW
545
546 ## Set the key, and stash it and the tag-hashing secret.
43c09851 547 me.k = Crypto(c, h, m, me.ck, me.mk)
494b719c 548 me.magic = me.k.decrypt(me.db.get_meta('magic'))
d1c45f5c 549
09b8041d 550 @classmethod
6baae405 551 def create(cls, dbcls, file, tag, c, h, m):
09b8041d 552 """
6baae405 553 Create and initialize a new database FILE using StorageBackend DBCLS.
09b8041d
MW
554
555 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
556 and a Pixie passphrase TAG.
557
558 This doesn't return a working object: it just creates the database file
559 and gets out of the way.
560 """
561
562 ## Set up the cryptography.
563 pp = _C.ppread(tag, _C.PMODE_VERIFY)
564 ppk = PPK(pp, c, h, m)
565 ck = _C.rand.block(c.keysz.default)
566 mk = _C.rand.block(c.keysz.default)
567 k = Crypto(c, h, m, ck, mk)
568
569 ## Set up and initialize the database.
494b719c 570 kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
6baae405 571 with dbcls.create(file) as db:
494b719c
MW
572 db.put_meta('tag', tag)
573 db.put_meta('salt', ppk.salt)
574 db.put_meta('cipher', c.name)
575 db.put_meta('hash', h.name)
576 db.put_meta('mac', m.name)
577 db.put_meta('key', kct)
578 db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz)))
09b8041d 579
43c09851 580 def keyxform(me, key):
494b719c
MW
581 """Transform the KEY (actually a password tag) into a password label."""
582 return me.k.h().hash(me.magic).hash(key).done()
d1c45f5c 583
43c09851 584 def changepp(me):
d1c45f5c
MW
585 """
586 Change the database password.
587
588 Requests the new password from the Pixie, which will probably cause
589 interaction.
590 """
494b719c 591 tag = me.db.get_meta('tag')
43c09851 592 _C.ppcancel(tag)
593 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
d1c45f5c 594 me.k.c.__class__, me.k.h, me.k.m.__class__)
494b719c
MW
595 kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
596 me.db.put_meta('key', kct)
597 me.db.put_meta('salt', ppk.salt)
d1c45f5c 598
43c09851 599 def pack(me, key, value):
494b719c 600 """Pack the KEY and VALUE into a ciphertext, and return it."""
9a7b948f
MW
601 b = _C.WriteBuffer()
602 b.putblk16(key).putblk16(value)
603 b.zero(((b.size + 255) & ~255) - b.size)
604 return me.k.encrypt(b)
d1c45f5c
MW
605
606 def unpack(me, ct):
607 """
608 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
609
610 Might raise DecryptError, of course.
611 """
9a7b948f
MW
612 b = _C.ReadBuffer(me.k.decrypt(ct))
613 key = b.getblk16()
614 value = b.getblk16()
43c09851 615 return key, value
d1c45f5c
MW
616
617 ## Mapping protocol.
618
43c09851 619 def __getitem__(me, key):
494b719c
MW
620 """Return the password for the given KEY."""
621 try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1]
622 except KeyError: raise KeyError, key
d1c45f5c 623
43c09851 624 def __setitem__(me, key, value):
494b719c
MW
625 """Associate the password VALUE with the KEY."""
626 me.db.put_passwd(me.keyxform(key), me.pack(key, value))
d1c45f5c 627
43c09851 628 def __delitem__(me, key):
494b719c
MW
629 """Forget all about the KEY."""
630 try: me.db.del_passwd(me.keyxform(key))
631 except KeyError: raise KeyError, key
d1c45f5c 632
43c09851 633 def __iter__(me):
494b719c
MW
634 """Iterate over the known password tags."""
635 for _, pld in me.db.iter_passwds():
636 yield me.unpack(pld)[0]
43c09851 637
5bf6e9f5
MW
638 ## Context protocol.
639
640 def __enter__(me):
641 return me
642 def __exit__(me, excty, excval, exctb):
053c2659 643 me.db.close(excval is not None)
5bf6e9f5 644
d1c45f5c 645###----- That's all, folks --------------------------------------------------