Commit | Line | Data |
---|---|---|
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 |
29 | from __future__ import with_statement |
30 | ||
6baae405 MW |
31 | import os as _OS |
32 | ||
43c09851 | 33 | import catacomb as _C |
34 | import gdbm as _G | |
d1c45f5c MW |
35 | |
36 | ###-------------------------------------------------------------------------- | |
37 | ### Underlying cryptography. | |
38 | ||
43c09851 | 39 | class 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 | ||
48 | class 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 | 107 | class 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 |
133 | class 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 | ||
141 | class 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 |
155 | class 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 | ||
425 | class 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 | 482 | class 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 -------------------------------------------------- |