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 | ||
43c09851 | 31 | import catacomb as _C |
32 | import gdbm as _G | |
d1c45f5c MW |
33 | |
34 | ###-------------------------------------------------------------------------- | |
35 | ### Underlying cryptography. | |
36 | ||
43c09851 | 37 | class 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 | ||
46 | class 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 | 105 | class 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 | ||
131 | class 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 | ||
356 | class 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 | 411 | class 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 -------------------------------------------------- |