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 | ||
3c24c1aa | 31 | import binascii as _B |
af861fb7 | 32 | import errno as _E |
6baae405 | 33 | import os as _OS |
f7623015 MW |
34 | import sys as _SYS |
35 | ||
36 | if _SYS.version_info >= (3,): from io import StringIO as _StringIO | |
37 | else: from cStringIO import StringIO as _StringIO | |
6baae405 | 38 | |
43c09851 | 39 | import catacomb as _C |
d1c45f5c MW |
40 | |
41 | ###-------------------------------------------------------------------------- | |
f6d012db MW |
42 | ### Python version portability. |
43 | ||
f7623015 MW |
44 | if _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") | |
50 | else: | |
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 |
61 | def _with_metaclass(meta, *supers): |
62 | return meta("#<anonymous base %s>" % meta.__name__, | |
63 | supers or (object,), dict()) | |
64 | ||
f6d012db MW |
65 | def _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 | ||
73 | def _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 | ||
82 | def _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 | ||
95 | def _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 | ||
114 | def _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 |
117 | def _unb64(s): |
118 | """Decode S as base64 with trimmed `=' padding.""" | |
3c24c1aa | 119 | return _B.a2b_base64(s + '='*((4 - len(s))%4)) |
af861fb7 MW |
120 | |
121 | def _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 | ||
126 | def _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 | 134 | class 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 | ||
143 | class 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 | 202 | class 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 |
228 | class 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 | ||
236 | class 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 | 252 | class 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 |
520 | try: import gdbm as _G |
521 | except ImportError: pass | |
522 | else: | |
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 |
577 | try: import sqlite3 as _Q |
578 | except ImportError: pass | |
579 | else: | |
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 |
699 | class 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 | |
861 | class 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 |
913 | class 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 | 987 | class 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 -------------------------------------------------- |