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