chiark / gitweb /
catacomb/pwsafe.py: New Git-friendly `DirectoryStorageBackend'.
[catacomb-python] / catacomb / pwsafe.py
CommitLineData
d1c45f5c
MW
1### -*-python-*-
2###
3### Management of a secure password database
4###
5### (c) 2005 Straylight/Edgeware
6###
7
8###----- Licensing notice ---------------------------------------------------
9###
10### This file is part of the Python interface to Catacomb.
11###
12### Catacomb/Python is free software; you can redistribute it and/or modify
13### it under the terms of the GNU General Public License as published by
14### the Free Software Foundation; either version 2 of the License, or
15### (at your option) any later version.
16###
17### Catacomb/Python is distributed in the hope that it will be useful,
18### but WITHOUT ANY WARRANTY; without even the implied warranty of
19### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20### GNU General Public License for more details.
21###
22### You should have received a copy of the GNU General Public License along
23### with Catacomb/Python; if not, write to the Free Software Foundation,
24### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25
26###--------------------------------------------------------------------------
27### Imported modules.
43c09851 28
494b719c
MW
29from __future__ import with_statement
30
af861fb7 31import errno as _E
6baae405 32import os as _OS
af861fb7 33from cStringIO import StringIO as _StringIO
6baae405 34
43c09851 35import catacomb as _C
36import gdbm as _G
d1c45f5c 37
af861fb7
MW
38###--------------------------------------------------------------------------
39### Text encoding utilities.
40
41def _literalp(s):
42 """
43 Answer whether S can be represented literally.
44
45 If True, then S can be stored literally, as a metadata item name or
46 value; if False, then S requires some kind of encoding.
47 """
48 return all(ch.isalnum() or ch in '-_:' for ch in s)
49
50def _enc_metaname(name):
51 """Encode NAME as a metadata item name, returning the result."""
52 if _literalp(name):
53 return name
54 else:
55 sio = _StringIO()
56 sio.write('!')
57 for ch in name:
58 if _literalp(ch): sio.write(ch)
59 elif ch == ' ': sio.write('+')
60 else: sio.write('%%%02x' % ord(ch))
61 return sio.getvalue()
62
63def _dec_metaname(name):
64 """Decode NAME as a metadata item name, returning the result."""
65 if not name.startswith('!'):
66 return name
67 else:
68 sio = _StringIO()
69 i, n = 1, len(name)
70 while i < n:
71 ch = name[i]
72 i += 1
73 if ch == '+':
74 sio.write(' ')
75 elif ch == '%':
76 sio.write(chr(int(name[i:i + 2], 16)))
77 i += 2
78 else:
79 sio.write(ch)
80 return sio.getvalue()
81
82def _b64(s):
83 """Encode S as base64, without newlines, and trimming `=' padding."""
84 return s.encode('base64').translate(None, '\n=')
85def _unb64(s):
86 """Decode S as base64 with trimmed `=' padding."""
87 return (s + '='*((4 - len(s))%4)).decode('base64')
88
89def _enc_metaval(val):
90 """Encode VAL as a metadata item value, returning the result."""
91 if _literalp(val): return val
92 else: return '?' + _b64(val)
93
94def _dec_metaval(val):
95 """Decode VAL as a metadata item value, returning the result."""
96 if not val.startswith('?'): return val
97 else: return _unb64(val[1:])
98
d1c45f5c
MW
99###--------------------------------------------------------------------------
100### Underlying cryptography.
101
43c09851 102class DecryptError (Exception):
d1c45f5c
MW
103 """
104 I represent a failure to decrypt a message.
105
106 Usually this means that someone used the wrong key, though it can also
107 mean that a ciphertext has been modified.
108 """
43c09851 109 pass
110
111class Crypto (object):
d1c45f5c
MW
112 """
113 I represent a symmetric crypto transform.
114
115 There's currently only one transform implemented, which is the obvious
116 generic-composition construction: given a message m, and keys K0 and K1, we
117 choose an IV v, and compute:
118
119 * y = v || E(K0, v; m)
120 * t = M(K1; y)
121
122 The final ciphertext is t || y.
123 """
124
43c09851 125 def __init__(me, c, h, m, ck, mk):
d1c45f5c
MW
126 """
127 Initialize the Crypto object with a given algorithm selection and keys.
128
129 We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and
130 keys CK and MK for C and M respectively.
131 """
43c09851 132 me.c = c(ck)
133 me.m = m(mk)
134 me.h = h
d1c45f5c 135
43c09851 136 def encrypt(me, pt):
d1c45f5c
MW
137 """
138 Encrypt the message PT and return the resulting ciphertext.
139 """
9a7b948f
MW
140 blksz = me.c.__class__.blksz
141 b = _C.WriteBuffer()
142 if blksz:
143 iv = _C.rand.block(blksz)
43c09851 144 me.c.setiv(iv)
9a7b948f
MW
145 b.put(iv)
146 b.put(me.c.encrypt(pt))
147 t = me.m().hash(b).done()
148 return t + str(buffer(b))
149
43c09851 150 def decrypt(me, ct):
d1c45f5c
MW
151 """
152 Decrypt the ciphertext CT, returning the plaintext.
153
154 Raises DecryptError if anything goes wrong.
155 """
9a7b948f
MW
156 blksz = me.c.__class__.blksz
157 tagsz = me.m.__class__.tagsz
158 b = _C.ReadBuffer(ct)
159 t = b.get(tagsz)
160 h = me.m()
161 if blksz:
162 iv = b.get(blksz)
163 me.c.setiv(iv)
164 h.hash(iv)
165 x = b.get(b.left)
166 h.hash(x)
167 if t != h.done(): raise DecryptError
168 return me.c.decrypt(x)
b2687a0a 169
43c09851 170class PPK (Crypto):
d1c45f5c
MW
171 """
172 I represent a crypto transform whose keys are derived from a passphrase.
173
174 The password is salted and hashed; the salt is available as the `salt'
175 attribute.
176 """
177
43c09851 178 def __init__(me, pp, c, h, m, salt = None):
d1c45f5c
MW
179 """
180 Initialize the PPK object with a passphrase and algorithm selection.
181
182 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
183 subclass M, and a SALT. The SALT may be None, if we're generating new
184 keys, indicating that a salt should be chosen randomly.
185 """
43c09851 186 if not salt: salt = _C.rand.block(h.hashsz)
187 tag = '%s\0%s' % (pp, salt)
188 Crypto.__init__(me, c, h, m,
d1c45f5c
MW
189 h().hash('cipher:' + tag).done(),
190 h().hash('mac:' + tag).done())
43c09851 191 me.salt = salt
192
494b719c
MW
193###--------------------------------------------------------------------------
194### Backend storage.
195
6baae405
MW
196class StorageBackendRefusal (Exception):
197 """
198 I signify that a StorageBackend subclass has refused to open a file.
199
200 This is used by the StorageBackend.open class method.
201 """
202 pass
203
204class StorageBackendClass (type):
205 """
206 I am a metaclass for StorageBackend classes.
207
208 My main feature is that I register my concrete instances (with a `NAME'
209 which is not `None') with the StorageBackend class.
210 """
211 def __init__(me, name, supers, dict):
212 """
213 Register a new concrete StorageBackend subclass.
214 """
215 super(StorageBackendClass, me).__init__(name, supers, dict)
216 if me.NAME is not None: StorageBackend.register_concrete_subclass(me)
217
494b719c
MW
218class StorageBackend (object):
219 """
1726ab40
MW
220 I provide basic protocol for password storage backends.
221
222 I'm an abstract class: you want one of my subclasses if you actually want
6baae405
MW
223 to do something useful. But I maintain a list of my subclasses and can
224 choose an appropriate one to open a database file you've found lying about.
494b719c
MW
225
226 Backends are responsible for storing and retrieving stuff, but not for the
227 cryptographic details. Backends need to store two kinds of information:
228
229 * metadata, consisting of a number of property names and their values;
230 and
231
232 * password mappings, consisting of a number of binary labels and
233 payloads.
1726ab40
MW
234
235 Backends need to implement the following ordinary methods. See the calling
236 methods for details of the subclass responsibilities.
237
238 BE._create(FILE) Create a new database in FILE; used by `create'.
239
240 BE._open(FILE, WRITEP)
241 Open the existing database FILE; used by `open'.
242
053c2659
MW
243 BE._close(ABRUPTP) Close the database, freeing up any resources. If
244 ABRUPTP then don't try to commit changes.
1726ab40
MW
245
246 BE._get_meta(NAME, DEFAULT)
247 Return the value of the metadata item with the given
248 NAME, or DEFAULT if it doesn't exist; used by
249 `get_meta'.
250
251 BE._put_meta(NAME, VALUE)
252 Set the VALUE of the metadata item with the given
253 NAME, creating one if necessary; used by `put_meta'.
254
255 BE._del_meta(NAME) Forget the metadata item with the given NAME; raise
256 `KeyError' if there is no such item; used by
257 `del_meta'.
258
259 BE._iter_meta() Return an iterator over the metadata (NAME, VALUE)
260 pairs; used by `iter_meta'.
261
262 BE._get_passwd(LABEL)
263 Return the password payload stored with the (binary)
264 LABEL; used by `get_passwd'.
265
266 BE._put_passwd(LABEL, PAYLOAD)
267 Associate the (binary) PAYLOAD with the LABEL,
268 forgetting any previous payload for that LABEL; used
269 by `put_passwd'.
270
271 BE._del_passwd(LABEL) Forget the password record with the given LABEL; used
272 by `_del_passwd'.
273
274 BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD)
275 pairs; used by `iter_passwds'.
6baae405
MW
276
277 Also, concrete subclasses should define the following class attributes.
278
279 NAME The name of the backend, so that the user can select
280 it when creating a new database.
281
282 PRIO An integer priority: backends are tried in decreasing
283 priority order when opening an existing database.
494b719c
MW
284 """
285
6baae405
MW
286 __metaclass__ = StorageBackendClass
287 NAME = None
288 PRIO = 10
289
290 ## The registry of subclasses.
291 CLASSES = {}
292
494b719c
MW
293 FAIL = ['FAIL']
294
6baae405
MW
295 @staticmethod
296 def register_concrete_subclass(sub):
297 """Register a concrete subclass, so that `open' can try it."""
298 StorageBackend.CLASSES[sub.NAME] = sub
299
300 @staticmethod
301 def byname(name):
302 """
303 Return the concrete subclass with the given NAME.
304
305 Raise `KeyError' if the name isn't found.
306 """
307 return StorageBackend.CLASSES[name]
308
309 @staticmethod
310 def classes():
311 """Return an iterator over the concrete subclasses."""
312 return StorageBackend.CLASSES.itervalues()
313
314 @staticmethod
315 def open(file, writep = False):
316 """Open a database FILE, using some appropriate backend."""
317 _OS.stat(file)
318 for cls in sorted(StorageBackend.CLASSES.values(), reverse = True,
319 key = lambda cls: cls.PRIO):
320 try: return cls(file, writep)
321 except StorageBackendRefusal: pass
322 raise StorageBackendRefusal
494b719c
MW
323
324 @classmethod
325 def create(cls, file):
1726ab40
MW
326 """
327 Create a new database in the named FILE, using this backend.
328
329 Subclasses must implement the `_create' instance method.
330 """
494b719c 331 return cls(writep = True, _magic = lambda me: me._create(file))
494b719c
MW
332
333 def __init__(me, file = None, writep = False, _magic = None, *args, **kw):
334 """
335 Main constructor.
1726ab40
MW
336
337 Subclasses are not, in general, expected to override this: there's a
338 somewhat hairy protocol between the constructor and some of the class
339 methods. Instead, the main hook for customization is the subclass's
340 `_open' method, which is invoked in the usual case.
494b719c
MW
341 """
342 super(StorageBackend, me).__init__(*args, **kw)
6baae405 343 if me.NAME is None: raise ValueError, 'abstract class'
494b719c
MW
344 if _magic is not None: _magic(me)
345 elif file is None: raise ValueError, 'missing file parameter'
1726ab40 346 else: me._open(file, writep)
494b719c
MW
347 me._writep = writep
348 me._livep = True
349
053c2659 350 def close(me, abruptp = False):
494b719c
MW
351 """
352 Close the database.
353
354 It is harmless to attempt to close a database which has been closed
1726ab40 355 already. Calls the subclass's `_close' method.
494b719c
MW
356 """
357 if me._livep:
358 me._livep = False
053c2659 359 me._close(abruptp)
494b719c
MW
360
361 ## Utilities.
362
363 def _check_live(me):
364 """Raise an error if the receiver has been closed."""
365 if not me._livep: raise ValueError, 'database is closed'
366
367 def _check_write(me):
368 """Raise an error if the receiver is not open for writing."""
369 me._check_live()
370 if not me._writep: raise ValueError, 'database is read-only'
371
372 def _check_meta_name(me, name):
373 """
374 Raise an error unless NAME is a valid name for a metadata item.
375
376 Metadata names may not start with `$': such names are reserved for
377 password storage.
378 """
379 if name.startswith('$'):
380 raise ValueError, "invalid metadata key `%s'" % name
381
382 ## Context protocol.
383
384 def __enter__(me):
385 """Context protocol: make sure the database is closed on exit."""
386 return me
387 def __exit__(me, exctype, excvalue, exctb):
388 """Context protocol: see `__enter__'."""
053c2659 389 me.close(excvalue is not None)
494b719c
MW
390
391 ## Metadata.
392
393 def get_meta(me, name, default = FAIL):
394 """
395 Fetch the value for the metadata item NAME.
396
397 If no such item exists, then return DEFAULT if that was set; otherwise
398 raise a `KeyError'.
1726ab40
MW
399
400 This calls the subclass's `_get_meta' method, which should return the
401 requested item or return the given DEFAULT value. It may assume that the
402 name is valid and the database is open.
494b719c
MW
403 """
404 me._check_meta_name(name)
405 me._check_live()
1726ab40 406 value = me._get_meta(name, default)
494b719c
MW
407 if value is StorageBackend.FAIL: raise KeyError, name
408 return value
409
410 def put_meta(me, name, value):
1726ab40
MW
411 """
412 Store VALUE in the metadata item called NAME.
413
414 This calls the subclass's `_put_meta' method, which may assume that the
415 name is valid and the database is open for writing.
416 """
494b719c
MW
417 me._check_meta_name(name)
418 me._check_write()
1726ab40 419 me._put_meta(name, value)
494b719c
MW
420
421 def del_meta(me, name):
1726ab40
MW
422 """
423 Forget about the metadata item with the given NAME.
424
425 This calls the subclass's `_del_meta' method, which may assume that the
426 name is valid and the database is open for writing.
427 """
494b719c
MW
428 me._check_meta_name(name)
429 me._check_write()
1726ab40 430 me._del_meta(name)
494b719c
MW
431
432 def iter_meta(me):
1726ab40
MW
433 """
434 Return an iterator over the name/value metadata items.
494b719c 435
1726ab40
MW
436 This calls the subclass's `_iter_meta' method, which may assume that the
437 database is open.
438 """
439 me._check_live()
440 return me._iter_meta()
494b719c
MW
441
442 def get_passwd(me, label):
443 """
444 Fetch and return the payload stored with the (opaque, binary) LABEL.
445
446 If there is no such payload then raise `KeyError'.
1726ab40
MW
447
448 This calls the subclass's `_get_passwd' method, which may assume that the
449 database is open.
494b719c
MW
450 """
451 me._check_live()
1726ab40 452 return me._get_passwd(label)
494b719c
MW
453
454 def put_passwd(me, label, payload):
455 """
456 Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
457
458 Any previous payload for LABEL is forgotten.
1726ab40
MW
459
460 This calls the subclass's `_put_passwd' method, which may assume that the
461 database is open for writing.
494b719c
MW
462 """
463 me._check_write()
1726ab40 464 me._put_passwd(label, payload)
494b719c
MW
465
466 def del_passwd(me, label):
467 """
468 Forget any PAYLOAD associated with the (opaque, binary) LABEL.
469
470 If there is no such payload then raise `KeyError'.
1726ab40
MW
471
472 This calls the subclass's `_del_passwd' method, which may assume that the
473 database is open for writing.
494b719c
MW
474 """
475 me._check_write()
1726ab40 476 me._del_passwd(label, payload)
494b719c
MW
477
478 def iter_passwds(me):
1726ab40
MW
479 """
480 Return an iterator over the stored password label/payload pairs.
481
482 This calls the subclass's `_iter_passwds' method, which may assume that
483 the database is open.
484 """
494b719c 485 me._check_live()
1726ab40
MW
486 return me._iter_passwds()
487
488class GDBMStorageBackend (StorageBackend):
489 """
490 My instances store password data in a GDBM database.
491
492 Metadata and password entries are mixed into the same database. The key
493 for a metadata item is simply its name; the key for a password entry is
494 the entry's label prefixed by `$', since we're guaranteed that no
495 metadata item name begins with `$'.
496 """
497
6baae405
MW
498 NAME = 'gdbm'
499
1726ab40
MW
500 def _open(me, file, writep):
501 try: me._db = _G.open(file, writep and 'w' or 'r')
502 except _G.error, e: raise StorageBackendRefusal, e
503
504 def _create(me, file):
505 me._db = _G.open(file, 'n', 0600)
506
053c2659 507 def _close(me, abruptp):
1726ab40
MW
508 me._db.close()
509 me._db = None
510
511 def _get_meta(me, name, default):
512 try: return me._db[name]
513 except KeyError: return default
514
515 def _put_meta(me, name, value):
516 me._db[name] = value
517
518 def _del_meta(me, name):
519 del me._db[name]
520
521 def _iter_meta(me):
522 k = me._db.firstkey()
523 while k is not None:
524 if not k.startswith('$'): yield k, me._db[k]
525 k = me._db.nextkey(k)
526
527 def _get_passwd(me, label):
528 return me._db['$' + label]
529
530 def _put_passwd(me, label, payload):
531 me._db['$' + label] = payload
532
533 def _del_passwd(me, label):
534 del me._db['$' + label]
535
536 def _iter_passwds(me):
494b719c
MW
537 k = me._db.firstkey()
538 while k is not None:
539 if k.startswith('$'): yield k[1:], me._db[k]
540 k = me._db.nextkey(k)
541
af861fb7
MW
542class PlainTextBackend (StorageBackend):
543 """
544 I'm a utility base class for storage backends which use plain text files.
545
546 I provide subclasses with the following capabilities.
547
548 * Creating files, with given modes, optionally ensuring that the file
549 doesn't exist already.
550
551 * Parsing flat text files, checking leading magic, skipping comments, and
552 providing standard encodings of troublesome characters and binary
553 strings in metadata and password records. See below.
554
555 * Maintenance of metadata and password records in in-memory dictionaries,
556 with ready implementations of the necessary StorageBackend subclass
557 responsibility methods. (Subclasses can override these if they want to
558 make different arrangements.)
559
560 Metadata records are written with an optional prefix string chosen by the
561 caller, followed by a `NAME=VALUE' pair. The NAME is form-urlencoded and
562 prefixed with `!' if it contains strange characters; the VALUE is base64-
563 encoded (without the pointless trailing `=' padding) and prefixed with `?'
564 if necessary.
565
566 Password records are written with an optional prefix string chosen by the
567 caller, followed by a LABEL=PAYLOAD pair, both of which are base64-encoded
568 (without padding).
569
570 The following attributes are available for subclasses:
571
572 _meta Dictionary mapping metadata item names to their values.
573 Populated by `_parse_meta' and managed by `_get_meta' and
574 friends.
575
576 _pw Dictionary mapping password labels to encrypted payloads.
577 Populated by `_parse_passwd' and managed by `_get_passwd' and
578 friends.
579
580 _dirtyp Boolean: set if either of the dictionaries has been modified.
581 """
582
583 def __init__(me, *args, **kw):
584 """
585 Hook for initialization.
586
587 Sets up the published instance attributes.
588 """
589 me._meta = {}
590 me._pw = {}
591 me._dirtyp = False
592 super(PlainTextBackend, me).__init__(*args, **kw)
593
594 def _create_file(me, file, mode = 0600, freshp = False):
595 """
596 Make sure FILE exists, creating it with the given MODE if necessary.
597
598 If FRESHP is true, then make sure the file did not exist previously.
599 Return a file object for the newly created file.
600 """
601 flags = _OS.O_CREAT | _OS.O_WRONLY
602 if freshp: flags |= _OS.O_EXCL
603 else: flags |= _OS.O_TRUNC
604 fd = _OS.open(file, flags, mode)
605 return _OS.fdopen(fd, 'w')
606
607 def _mark_dirty(me):
608 """
609 Set the `_dirtyp' flag.
610
611 Subclasses might find it useful to intercept this method.
612 """
613 me._dirtyp = True
614
615 def _eqsplit(me, line):
616 """
617 Extract the KEY, VALUE pair from a LINE of the form `KEY=VALUE'.
618
619 Raise `ValueError' if there is no `=' in the LINE.
620 """
621 eq = line.index('=')
622 return line[:eq], line[eq + 1:]
623
624 def _parse_file(me, file, magic = None):
625 """
626 Parse a FILE.
627
628 Specifically:
629
630 * Raise `StorageBackendRefusal' if that the first line doesn't match
631 MAGIC (if provided). MAGIC should not contain the terminating
632 newline.
633
634 * Ignore comments (beginning `#') and blank lines.
635
636 * Call `_parse_line' (provided by the subclass) for other lines.
637 """
638 with open(file, 'r') as f:
639 if magic is not None:
640 if f.readline().rstrip('\n') != magic: raise StorageBackendRefusal
641 for line in f:
642 line = line.rstrip('\n')
643 if not line or line.startswith('#'): continue
644 me._parse_line(line)
645
646 def _write_file(me, file, writebody, mode = 0600, magic = None):
647 """
648 Update FILE atomically.
649
650 The newly created file will have the given MODE. If MAGIC is given, then
651 write that as the first line. Calls WRITEBODY(F) to write the main body
652 of the file where F is a file object for the new file.
653 """
654 new = file + '.new'
655 with me._create_file(new, mode) as f:
656 if magic is not None: f.write(magic + '\n')
657 writebody(f)
658 _OS.rename(new, file)
659
660 def _parse_meta(me, line):
661 """Parse LINE as a metadata NAME=VALUE pair, and updates `_meta'."""
662 k, v = me._eqsplit(line)
663 me._meta[_dec_metaname(k)] = _dec_metaval(v)
664
665 def _write_meta(me, f, prefix = ''):
666 """Write the metadata records to F, each with the given PREFIX."""
667 f.write('\n## Metadata.\n')
668 for k, v in me._meta.iteritems():
669 f.write('%s%s=%s\n' % (prefix, _enc_metaname(k), _enc_metaval(v)))
670
671 def _get_meta(me, name, default):
672 return me._meta.get(name, default)
673 def _put_meta(me, name, value):
674 me._mark_dirty()
675 me._meta[name] = value
676 def _del_meta(me, name):
677 me._mark_dirty()
678 del me._meta[name]
679 def _iter_meta(me):
680 return me._meta.iteritems()
681
682 def _parse_passwd(me, line):
683 """Parse LINE as a password LABEL=PAYLOAD pair, and updates `_pw'."""
684 k, v = me._eqsplit(line)
685 me._pw[_unb64(k)] = _unb64(v)
686
687 def _write_passwd(me, f, prefix = ''):
688 """Write the password records to F, each with the given PREFIX."""
689 f.write('\n## Password data.\n')
690 for k, v in me._pw.iteritems():
691 f.write('%s%s=%s\n' % (prefix, _b64(k), _b64(v)))
692
693 def _get_passwd(me, label):
694 return me._pw[str(label)]
695 def _put_passwd(me, label, payload):
696 me._mark_dirty()
697 me._pw[str(label)] = payload
698 def _del_passwd(me, label):
699 me._mark_dirty()
700 del me._pw[str(label)]
701 def _iter_passwds(me):
702 return me._pw.iteritems()
703
704class FlatFileStorageBackend (PlainTextBackend):
705 """
706 I maintain a password database in a plain text file.
707
708 The text file consists of lines, as follows.
709
710 * Empty lines, and lines beginning with `#' (in the leftmost column only)
711 are ignored.
712
713 * Lines of the form `$LABEL=PAYLOAD' store password data. Both LABEL and
714 PAYLOAD are base64-encoded, without `=' padding.
715
716 * Lines of the form `NAME=VALUE' store metadata. If the NAME contains
717 characters other than alphanumerics, hyphens, underscores, and colons,
718 then it is form-urlencoded, and prefixed wth `!'. If the VALUE
719 contains such characters, then it is base64-encoded, without `='
720 padding, and prefixed with `?'.
721
722 * Other lines are erroneous.
723
724 The file is rewritten from scratch when it's changed: any existing
725 commentary is lost, and items may be reordered. There is no file locking,
726 but the file is updated atomically, by renaming.
727
728 It is expected that the FlatFileStorageBackend is used mostly for
729 diagnostics and transfer, rather than for a live system.
730 """
731
732 NAME = 'flat'
733 PRIO = 0
734 MAGIC = '### pwsafe password database'
735
736 def _open(me, file, writep):
737 if not _OS.path.isfile(file): raise StorageBackendRefusal
738 me._parse_file(file, magic = me.MAGIC)
739 def _parse_line(me, line):
740 if line.startswith('$'): me._parse_passwd(line[1:])
741 else: me._parse_meta(line)
742
743 def _create(me, file):
744 with me._create_file(file, freshp = True) as f: pass
745 me._file = file
746 me._mark_dirty()
747
748 def _close(me, abruptp):
749 if not abruptp and me._dirtyp:
750 me._write_file(me._file, me._write_body, magic = me.MAGIC)
751
752 def _write_body(me, f):
753 me._write_meta(f)
754 me._write_passwd(f, '$')
755
b61e9efe
MW
756class DirectoryStorageBackend (PlainTextBackend):
757 """
758 I maintain a password database in a directory, with one file per password.
759
760 This makes password databases easy to maintain in a revision-control system
761 such as Git.
762
763 The directory is structured as follows.
764
765 dir/meta Contains metadata, similar to the `FlatFileBackend'.
766
767 dir/pw/LABEL Contains the (raw binary) payload for the given password
768 LABEL (base64-encoded, without the useless `=' padding, and
769 with `/' replaced by `.').
770
771 dir/tmp/ Contains temporary files used by the implementation.
772 """
773
774 NAME = 'dir'
775 METAMAGIC = '### pwsafe password directory metadata'
776
777 def _open(me, file, writep):
778 if not _OS.path.isdir(file) or \
779 not _OS.path.isdir(_OS.path.join(file, 'pw')) or \
780 not _OS.path.isdir(_OS.path.join(file, 'tmp')) or \
781 not _OS.path.isfile(_OS.path.join(file, 'meta')):
782 raise StorageBackendRefusal
783 me._dir = file
784 me._parse_file(_OS.path.join(file, 'meta'), magic = me.METAMAGIC)
785 def _parse_line(me, line):
786 me._parse_meta(line)
787
788 def _create(me, file):
789 _OS.mkdir(file, 0700)
790 _OS.mkdir(_OS.path.join(file, 'pw'), 0700)
791 _OS.mkdir(_OS.path.join(file, 'tmp'), 0700)
792 me._mark_dirty()
793 me._dir = file
794
795 def _close(me, abruptp):
796 if not abruptp and me._dirtyp:
797 me._write_file(_OS.path.join(me._dir, 'meta'),
798 me._write_meta, magic = me.METAMAGIC)
799
800 def _pwfile(me, label, dir = 'pw'):
801 return _OS.path.join(me._dir, dir, _b64(label).replace('/', '.'))
802 def _get_passwd(me, label):
803 try:
804 f = open(me._pwfile(label), 'rb')
805 except (OSError, IOError), e:
806 if e.errno == _E.ENOENT: raise KeyError, label
807 else: raise
808 with f: return f.read()
809 def _put_passwd(me, label, payload):
810 new = me._pwfile(label, 'tmp')
811 fd = _OS.open(new, _OS.O_WRONLY | _OS.O_CREAT | _OS.O_TRUNC, 0600)
812 _OS.close(fd)
813 with open(new, 'wb') as f: f.write(payload)
814 _OS.rename(new, me._pwfile(label))
815 def _del_passwd(me, label):
816 try:
817 _OS.remove(me._pwfile(label))
818 except (OSError, IOError), e:
819 if e == _E.ENOENT: raise KeyError, label
820 else: raise
821 def _iter_passwds(me):
822 pw = _OS.path.join(me._dir, 'pw')
823 for i in _OS.listdir(pw):
824 with open(_OS.path.join(pw, i), 'rb') as f: pld = f.read()
825 yield _unb64(i.replace('.', '/')), pld
826
d1c45f5c
MW
827###--------------------------------------------------------------------------
828### Password storage.
43c09851 829
43c09851 830class PW (object):
d1c45f5c
MW
831 """
832 I represent a secure (ish) password store.
833
834 I can store short secrets, associated with textual names, in a way which
835 doesn't leak too much information about them.
836
2119e334 837 I implement (some of) the Python mapping protocol.
d1c45f5c 838
494b719c
MW
839 I keep track of everything using a StorageBackend object. This contains
840 password entries, identified by cryptographic labels, and a number of
841 metadata items.
d1c45f5c
MW
842
843 cipher Names the Catacomb cipher selected.
844
845 hash Names the Catacomb hash function selected.
846
847 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
848 length and concatenated, encrypted using the master
849 passphrase.
850
851 mac Names the Catacomb message authentication code selected.
852
853 magic A magic string for obscuring password tag names.
854
855 salt The salt for hashing the passphrase.
856
857 tag The master passphrase's tag, for the Pixie's benefit.
858
494b719c
MW
859 Password entries are assigned labels of the form `$' || H(MAGIC || TAG);
860 the corresponding value consists of a pair (TAG, PASSWD), prefixed with
861 16-bit lengths, concatenated, padded to a multiple of 256 octets, and
862 encrypted using the stored keys.
d1c45f5c
MW
863 """
864
4a35c9a7 865 def __init__(me, file, writep = False):
d1c45f5c 866 """
494b719c 867 Initialize a PW object from the database in FILE.
d1c45f5c 868
494b719c
MW
869 If WRITEP is false (the default) then the database is opened read-only;
870 if true then it may be written. Requests the database password from the
871 Pixie, which may cause interaction.
d1c45f5c
MW
872 """
873
874 ## Open the database.
6baae405 875 me.db = StorageBackend.open(file, writep)
d1c45f5c
MW
876
877 ## Find out what crypto to use.
494b719c
MW
878 c = _C.gcciphers[me.db.get_meta('cipher')]
879 h = _C.gchashes[me.db.get_meta('hash')]
880 m = _C.gcmacs[me.db.get_meta('mac')]
d1c45f5c
MW
881
882 ## Request the passphrase and extract the master keys.
494b719c
MW
883 tag = me.db.get_meta('tag')
884 ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt'))
43c09851 885 try:
494b719c 886 b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key')))
43c09851 887 except DecryptError:
888 _C.ppcancel(tag)
889 raise
9a7b948f
MW
890 me.ck = b.getblk16()
891 me.mk = b.getblk16()
892 if not b.endp: raise ValueError, 'trailing junk'
d1c45f5c
MW
893
894 ## Set the key, and stash it and the tag-hashing secret.
43c09851 895 me.k = Crypto(c, h, m, me.ck, me.mk)
494b719c 896 me.magic = me.k.decrypt(me.db.get_meta('magic'))
d1c45f5c 897
09b8041d 898 @classmethod
6baae405 899 def create(cls, dbcls, file, tag, c, h, m):
09b8041d 900 """
6baae405 901 Create and initialize a new database FILE using StorageBackend DBCLS.
09b8041d
MW
902
903 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
904 and a Pixie passphrase TAG.
905
906 This doesn't return a working object: it just creates the database file
907 and gets out of the way.
908 """
909
910 ## Set up the cryptography.
911 pp = _C.ppread(tag, _C.PMODE_VERIFY)
912 ppk = PPK(pp, c, h, m)
913 ck = _C.rand.block(c.keysz.default)
914 mk = _C.rand.block(c.keysz.default)
915 k = Crypto(c, h, m, ck, mk)
916
917 ## Set up and initialize the database.
494b719c 918 kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
6baae405 919 with dbcls.create(file) as db:
494b719c
MW
920 db.put_meta('tag', tag)
921 db.put_meta('salt', ppk.salt)
922 db.put_meta('cipher', c.name)
923 db.put_meta('hash', h.name)
924 db.put_meta('mac', m.name)
925 db.put_meta('key', kct)
926 db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz)))
09b8041d 927
43c09851 928 def keyxform(me, key):
494b719c
MW
929 """Transform the KEY (actually a password tag) into a password label."""
930 return me.k.h().hash(me.magic).hash(key).done()
d1c45f5c 931
43c09851 932 def changepp(me):
d1c45f5c
MW
933 """
934 Change the database password.
935
936 Requests the new password from the Pixie, which will probably cause
937 interaction.
938 """
494b719c 939 tag = me.db.get_meta('tag')
43c09851 940 _C.ppcancel(tag)
941 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
d1c45f5c 942 me.k.c.__class__, me.k.h, me.k.m.__class__)
494b719c
MW
943 kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
944 me.db.put_meta('key', kct)
945 me.db.put_meta('salt', ppk.salt)
d1c45f5c 946
43c09851 947 def pack(me, key, value):
494b719c 948 """Pack the KEY and VALUE into a ciphertext, and return it."""
9a7b948f
MW
949 b = _C.WriteBuffer()
950 b.putblk16(key).putblk16(value)
951 b.zero(((b.size + 255) & ~255) - b.size)
952 return me.k.encrypt(b)
d1c45f5c
MW
953
954 def unpack(me, ct):
955 """
956 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
957
958 Might raise DecryptError, of course.
959 """
9a7b948f
MW
960 b = _C.ReadBuffer(me.k.decrypt(ct))
961 key = b.getblk16()
962 value = b.getblk16()
43c09851 963 return key, value
d1c45f5c
MW
964
965 ## Mapping protocol.
966
43c09851 967 def __getitem__(me, key):
494b719c
MW
968 """Return the password for the given KEY."""
969 try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1]
970 except KeyError: raise KeyError, key
d1c45f5c 971
43c09851 972 def __setitem__(me, key, value):
494b719c
MW
973 """Associate the password VALUE with the KEY."""
974 me.db.put_passwd(me.keyxform(key), me.pack(key, value))
d1c45f5c 975
43c09851 976 def __delitem__(me, key):
494b719c
MW
977 """Forget all about the KEY."""
978 try: me.db.del_passwd(me.keyxform(key))
979 except KeyError: raise KeyError, key
d1c45f5c 980
43c09851 981 def __iter__(me):
494b719c
MW
982 """Iterate over the known password tags."""
983 for _, pld in me.db.iter_passwds():
984 yield me.unpack(pld)[0]
43c09851 985
5bf6e9f5
MW
986 ## Context protocol.
987
988 def __enter__(me):
989 return me
990 def __exit__(me, excty, excval, exctb):
053c2659 991 me.db.close(excval is not None)
5bf6e9f5 992
d1c45f5c 993###----- That's all, folks --------------------------------------------------