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