| 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. |
| 28 | |
| 29 | from __future__ import with_statement |
| 30 | |
| 31 | import catacomb as _C |
| 32 | import gdbm as _G |
| 33 | |
| 34 | ###-------------------------------------------------------------------------- |
| 35 | ### Underlying cryptography. |
| 36 | |
| 37 | class DecryptError (Exception): |
| 38 | """ |
| 39 | I represent a failure to decrypt a message. |
| 40 | |
| 41 | Usually this means that someone used the wrong key, though it can also |
| 42 | mean that a ciphertext has been modified. |
| 43 | """ |
| 44 | pass |
| 45 | |
| 46 | class Crypto (object): |
| 47 | """ |
| 48 | I represent a symmetric crypto transform. |
| 49 | |
| 50 | There's currently only one transform implemented, which is the obvious |
| 51 | generic-composition construction: given a message m, and keys K0 and K1, we |
| 52 | choose an IV v, and compute: |
| 53 | |
| 54 | * y = v || E(K0, v; m) |
| 55 | * t = M(K1; y) |
| 56 | |
| 57 | The final ciphertext is t || y. |
| 58 | """ |
| 59 | |
| 60 | def __init__(me, c, h, m, ck, mk): |
| 61 | """ |
| 62 | Initialize the Crypto object with a given algorithm selection and keys. |
| 63 | |
| 64 | We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and |
| 65 | keys CK and MK for C and M respectively. |
| 66 | """ |
| 67 | me.c = c(ck) |
| 68 | me.m = m(mk) |
| 69 | me.h = h |
| 70 | |
| 71 | def encrypt(me, pt): |
| 72 | """ |
| 73 | Encrypt the message PT and return the resulting ciphertext. |
| 74 | """ |
| 75 | blksz = me.c.__class__.blksz |
| 76 | b = _C.WriteBuffer() |
| 77 | if blksz: |
| 78 | iv = _C.rand.block(blksz) |
| 79 | me.c.setiv(iv) |
| 80 | b.put(iv) |
| 81 | b.put(me.c.encrypt(pt)) |
| 82 | t = me.m().hash(b).done() |
| 83 | return t + str(buffer(b)) |
| 84 | |
| 85 | def decrypt(me, ct): |
| 86 | """ |
| 87 | Decrypt the ciphertext CT, returning the plaintext. |
| 88 | |
| 89 | Raises DecryptError if anything goes wrong. |
| 90 | """ |
| 91 | blksz = me.c.__class__.blksz |
| 92 | tagsz = me.m.__class__.tagsz |
| 93 | b = _C.ReadBuffer(ct) |
| 94 | t = b.get(tagsz) |
| 95 | h = me.m() |
| 96 | if blksz: |
| 97 | iv = b.get(blksz) |
| 98 | me.c.setiv(iv) |
| 99 | h.hash(iv) |
| 100 | x = b.get(b.left) |
| 101 | h.hash(x) |
| 102 | if t != h.done(): raise DecryptError |
| 103 | return me.c.decrypt(x) |
| 104 | |
| 105 | class PPK (Crypto): |
| 106 | """ |
| 107 | I represent a crypto transform whose keys are derived from a passphrase. |
| 108 | |
| 109 | The password is salted and hashed; the salt is available as the `salt' |
| 110 | attribute. |
| 111 | """ |
| 112 | |
| 113 | def __init__(me, pp, c, h, m, salt = None): |
| 114 | """ |
| 115 | Initialize the PPK object with a passphrase and algorithm selection. |
| 116 | |
| 117 | We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC |
| 118 | subclass M, and a SALT. The SALT may be None, if we're generating new |
| 119 | keys, indicating that a salt should be chosen randomly. |
| 120 | """ |
| 121 | if not salt: salt = _C.rand.block(h.hashsz) |
| 122 | tag = '%s\0%s' % (pp, salt) |
| 123 | Crypto.__init__(me, c, h, m, |
| 124 | h().hash('cipher:' + tag).done(), |
| 125 | h().hash('mac:' + tag).done()) |
| 126 | me.salt = salt |
| 127 | |
| 128 | ###-------------------------------------------------------------------------- |
| 129 | ### Backend storage. |
| 130 | |
| 131 | class StorageBackend (object): |
| 132 | """ |
| 133 | I provide basic protocol for password storage backends. |
| 134 | |
| 135 | I'm an abstract class: you want one of my subclasses if you actually want |
| 136 | to do something useful. |
| 137 | |
| 138 | Backends are responsible for storing and retrieving stuff, but not for the |
| 139 | cryptographic details. Backends need to store two kinds of information: |
| 140 | |
| 141 | * metadata, consisting of a number of property names and their values; |
| 142 | and |
| 143 | |
| 144 | * password mappings, consisting of a number of binary labels and |
| 145 | payloads. |
| 146 | |
| 147 | Backends need to implement the following ordinary methods. See the calling |
| 148 | methods for details of the subclass responsibilities. |
| 149 | |
| 150 | BE._create(FILE) Create a new database in FILE; used by `create'. |
| 151 | |
| 152 | BE._open(FILE, WRITEP) |
| 153 | Open the existing database FILE; used by `open'. |
| 154 | |
| 155 | BE._close() Close the database, freeing up any resources. |
| 156 | |
| 157 | BE._get_meta(NAME, DEFAULT) |
| 158 | Return the value of the metadata item with the given |
| 159 | NAME, or DEFAULT if it doesn't exist; used by |
| 160 | `get_meta'. |
| 161 | |
| 162 | BE._put_meta(NAME, VALUE) |
| 163 | Set the VALUE of the metadata item with the given |
| 164 | NAME, creating one if necessary; used by `put_meta'. |
| 165 | |
| 166 | BE._del_meta(NAME) Forget the metadata item with the given NAME; raise |
| 167 | `KeyError' if there is no such item; used by |
| 168 | `del_meta'. |
| 169 | |
| 170 | BE._iter_meta() Return an iterator over the metadata (NAME, VALUE) |
| 171 | pairs; used by `iter_meta'. |
| 172 | |
| 173 | BE._get_passwd(LABEL) |
| 174 | Return the password payload stored with the (binary) |
| 175 | LABEL; used by `get_passwd'. |
| 176 | |
| 177 | BE._put_passwd(LABEL, PAYLOAD) |
| 178 | Associate the (binary) PAYLOAD with the LABEL, |
| 179 | forgetting any previous payload for that LABEL; used |
| 180 | by `put_passwd'. |
| 181 | |
| 182 | BE._del_passwd(LABEL) Forget the password record with the given LABEL; used |
| 183 | by `_del_passwd'. |
| 184 | |
| 185 | BE._iter_passwds() Return an iterator over the password (LABEL, PAYLOAD) |
| 186 | pairs; used by `iter_passwds'. |
| 187 | """ |
| 188 | |
| 189 | FAIL = ['FAIL'] |
| 190 | |
| 191 | ## Life cycle methods. |
| 192 | |
| 193 | @classmethod |
| 194 | def create(cls, file): |
| 195 | """ |
| 196 | Create a new database in the named FILE, using this backend. |
| 197 | |
| 198 | Subclasses must implement the `_create' instance method. |
| 199 | """ |
| 200 | return cls(writep = True, _magic = lambda me: me._create(file)) |
| 201 | |
| 202 | def __init__(me, file = None, writep = False, _magic = None, *args, **kw): |
| 203 | """ |
| 204 | Main constructor. |
| 205 | |
| 206 | Subclasses are not, in general, expected to override this: there's a |
| 207 | somewhat hairy protocol between the constructor and some of the class |
| 208 | methods. Instead, the main hook for customization is the subclass's |
| 209 | `_open' method, which is invoked in the usual case. |
| 210 | """ |
| 211 | super(StorageBackend, me).__init__(*args, **kw) |
| 212 | if _magic is not None: _magic(me) |
| 213 | elif file is None: raise ValueError, 'missing file parameter' |
| 214 | else: me._open(file, writep) |
| 215 | me._writep = writep |
| 216 | me._livep = True |
| 217 | |
| 218 | def close(me): |
| 219 | """ |
| 220 | Close the database. |
| 221 | |
| 222 | It is harmless to attempt to close a database which has been closed |
| 223 | already. Calls the subclass's `_close' method. |
| 224 | """ |
| 225 | if me._livep: |
| 226 | me._livep = False |
| 227 | me._close() |
| 228 | |
| 229 | ## Utilities. |
| 230 | |
| 231 | def _check_live(me): |
| 232 | """Raise an error if the receiver has been closed.""" |
| 233 | if not me._livep: raise ValueError, 'database is closed' |
| 234 | |
| 235 | def _check_write(me): |
| 236 | """Raise an error if the receiver is not open for writing.""" |
| 237 | me._check_live() |
| 238 | if not me._writep: raise ValueError, 'database is read-only' |
| 239 | |
| 240 | def _check_meta_name(me, name): |
| 241 | """ |
| 242 | Raise an error unless NAME is a valid name for a metadata item. |
| 243 | |
| 244 | Metadata names may not start with `$': such names are reserved for |
| 245 | password storage. |
| 246 | """ |
| 247 | if name.startswith('$'): |
| 248 | raise ValueError, "invalid metadata key `%s'" % name |
| 249 | |
| 250 | ## Context protocol. |
| 251 | |
| 252 | def __enter__(me): |
| 253 | """Context protocol: make sure the database is closed on exit.""" |
| 254 | return me |
| 255 | def __exit__(me, exctype, excvalue, exctb): |
| 256 | """Context protocol: see `__enter__'.""" |
| 257 | me.close() |
| 258 | |
| 259 | ## Metadata. |
| 260 | |
| 261 | def get_meta(me, name, default = FAIL): |
| 262 | """ |
| 263 | Fetch the value for the metadata item NAME. |
| 264 | |
| 265 | If no such item exists, then return DEFAULT if that was set; otherwise |
| 266 | raise a `KeyError'. |
| 267 | |
| 268 | This calls the subclass's `_get_meta' method, which should return the |
| 269 | requested item or return the given DEFAULT value. It may assume that the |
| 270 | name is valid and the database is open. |
| 271 | """ |
| 272 | me._check_meta_name(name) |
| 273 | me._check_live() |
| 274 | value = me._get_meta(name, default) |
| 275 | if value is StorageBackend.FAIL: raise KeyError, name |
| 276 | return value |
| 277 | |
| 278 | def put_meta(me, name, value): |
| 279 | """ |
| 280 | Store VALUE in the metadata item called NAME. |
| 281 | |
| 282 | This calls the subclass's `_put_meta' method, which may assume that the |
| 283 | name is valid and the database is open for writing. |
| 284 | """ |
| 285 | me._check_meta_name(name) |
| 286 | me._check_write() |
| 287 | me._put_meta(name, value) |
| 288 | |
| 289 | def del_meta(me, name): |
| 290 | """ |
| 291 | Forget about the metadata item with the given NAME. |
| 292 | |
| 293 | This calls the subclass's `_del_meta' method, which may assume that the |
| 294 | name is valid and the database is open for writing. |
| 295 | """ |
| 296 | me._check_meta_name(name) |
| 297 | me._check_write() |
| 298 | me._del_meta(name) |
| 299 | |
| 300 | def iter_meta(me): |
| 301 | """ |
| 302 | Return an iterator over the name/value metadata items. |
| 303 | |
| 304 | This calls the subclass's `_iter_meta' method, which may assume that the |
| 305 | database is open. |
| 306 | """ |
| 307 | me._check_live() |
| 308 | return me._iter_meta() |
| 309 | |
| 310 | def get_passwd(me, label): |
| 311 | """ |
| 312 | Fetch and return the payload stored with the (opaque, binary) LABEL. |
| 313 | |
| 314 | If there is no such payload then raise `KeyError'. |
| 315 | |
| 316 | This calls the subclass's `_get_passwd' method, which may assume that the |
| 317 | database is open. |
| 318 | """ |
| 319 | me._check_live() |
| 320 | return me._get_passwd(label) |
| 321 | |
| 322 | def put_passwd(me, label, payload): |
| 323 | """ |
| 324 | Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL. |
| 325 | |
| 326 | Any previous payload for LABEL is forgotten. |
| 327 | |
| 328 | This calls the subclass's `_put_passwd' method, which may assume that the |
| 329 | database is open for writing. |
| 330 | """ |
| 331 | me._check_write() |
| 332 | me._put_passwd(label, payload) |
| 333 | |
| 334 | def del_passwd(me, label): |
| 335 | """ |
| 336 | Forget any PAYLOAD associated with the (opaque, binary) LABEL. |
| 337 | |
| 338 | If there is no such payload then raise `KeyError'. |
| 339 | |
| 340 | This calls the subclass's `_del_passwd' method, which may assume that the |
| 341 | database is open for writing. |
| 342 | """ |
| 343 | me._check_write() |
| 344 | me._del_passwd(label, payload) |
| 345 | |
| 346 | def iter_passwds(me): |
| 347 | """ |
| 348 | Return an iterator over the stored password label/payload pairs. |
| 349 | |
| 350 | This calls the subclass's `_iter_passwds' method, which may assume that |
| 351 | the database is open. |
| 352 | """ |
| 353 | me._check_live() |
| 354 | return me._iter_passwds() |
| 355 | |
| 356 | class GDBMStorageBackend (StorageBackend): |
| 357 | """ |
| 358 | My instances store password data in a GDBM database. |
| 359 | |
| 360 | Metadata and password entries are mixed into the same database. The key |
| 361 | for a metadata item is simply its name; the key for a password entry is |
| 362 | the entry's label prefixed by `$', since we're guaranteed that no |
| 363 | metadata item name begins with `$'. |
| 364 | """ |
| 365 | |
| 366 | def _open(me, file, writep): |
| 367 | try: me._db = _G.open(file, writep and 'w' or 'r') |
| 368 | except _G.error, e: raise StorageBackendRefusal, e |
| 369 | |
| 370 | def _create(me, file): |
| 371 | me._db = _G.open(file, 'n', 0600) |
| 372 | |
| 373 | def _close(me): |
| 374 | me._db.close() |
| 375 | me._db = None |
| 376 | |
| 377 | def _get_meta(me, name, default): |
| 378 | try: return me._db[name] |
| 379 | except KeyError: return default |
| 380 | |
| 381 | def _put_meta(me, name, value): |
| 382 | me._db[name] = value |
| 383 | |
| 384 | def _del_meta(me, name): |
| 385 | del me._db[name] |
| 386 | |
| 387 | def _iter_meta(me): |
| 388 | k = me._db.firstkey() |
| 389 | while k is not None: |
| 390 | if not k.startswith('$'): yield k, me._db[k] |
| 391 | k = me._db.nextkey(k) |
| 392 | |
| 393 | def _get_passwd(me, label): |
| 394 | return me._db['$' + label] |
| 395 | |
| 396 | def _put_passwd(me, label, payload): |
| 397 | me._db['$' + label] = payload |
| 398 | |
| 399 | def _del_passwd(me, label): |
| 400 | del me._db['$' + label] |
| 401 | |
| 402 | def _iter_passwds(me): |
| 403 | k = me._db.firstkey() |
| 404 | while k is not None: |
| 405 | if k.startswith('$'): yield k[1:], me._db[k] |
| 406 | k = me._db.nextkey(k) |
| 407 | |
| 408 | ###-------------------------------------------------------------------------- |
| 409 | ### Password storage. |
| 410 | |
| 411 | class PW (object): |
| 412 | """ |
| 413 | I represent a secure (ish) password store. |
| 414 | |
| 415 | I can store short secrets, associated with textual names, in a way which |
| 416 | doesn't leak too much information about them. |
| 417 | |
| 418 | I implement (some of) the Python mapping protocol. |
| 419 | |
| 420 | I keep track of everything using a StorageBackend object. This contains |
| 421 | password entries, identified by cryptographic labels, and a number of |
| 422 | metadata items. |
| 423 | |
| 424 | cipher Names the Catacomb cipher selected. |
| 425 | |
| 426 | hash Names the Catacomb hash function selected. |
| 427 | |
| 428 | key Cipher and MAC keys, each prefixed by a 16-bit big-endian |
| 429 | length and concatenated, encrypted using the master |
| 430 | passphrase. |
| 431 | |
| 432 | mac Names the Catacomb message authentication code selected. |
| 433 | |
| 434 | magic A magic string for obscuring password tag names. |
| 435 | |
| 436 | salt The salt for hashing the passphrase. |
| 437 | |
| 438 | tag The master passphrase's tag, for the Pixie's benefit. |
| 439 | |
| 440 | Password entries are assigned labels of the form `$' || H(MAGIC || TAG); |
| 441 | the corresponding value consists of a pair (TAG, PASSWD), prefixed with |
| 442 | 16-bit lengths, concatenated, padded to a multiple of 256 octets, and |
| 443 | encrypted using the stored keys. |
| 444 | """ |
| 445 | |
| 446 | def __init__(me, file, writep = False): |
| 447 | """ |
| 448 | Initialize a PW object from the database in FILE. |
| 449 | |
| 450 | If WRITEP is false (the default) then the database is opened read-only; |
| 451 | if true then it may be written. Requests the database password from the |
| 452 | Pixie, which may cause interaction. |
| 453 | """ |
| 454 | |
| 455 | ## Open the database. |
| 456 | me.db = GDBMStorageBackend(file, writep) |
| 457 | |
| 458 | ## Find out what crypto to use. |
| 459 | c = _C.gcciphers[me.db.get_meta('cipher')] |
| 460 | h = _C.gchashes[me.db.get_meta('hash')] |
| 461 | m = _C.gcmacs[me.db.get_meta('mac')] |
| 462 | |
| 463 | ## Request the passphrase and extract the master keys. |
| 464 | tag = me.db.get_meta('tag') |
| 465 | ppk = PPK(_C.ppread(tag), c, h, m, me.db.get_meta('salt')) |
| 466 | try: |
| 467 | b = _C.ReadBuffer(ppk.decrypt(me.db.get_meta('key'))) |
| 468 | except DecryptError: |
| 469 | _C.ppcancel(tag) |
| 470 | raise |
| 471 | me.ck = b.getblk16() |
| 472 | me.mk = b.getblk16() |
| 473 | if not b.endp: raise ValueError, 'trailing junk' |
| 474 | |
| 475 | ## Set the key, and stash it and the tag-hashing secret. |
| 476 | me.k = Crypto(c, h, m, me.ck, me.mk) |
| 477 | me.magic = me.k.decrypt(me.db.get_meta('magic')) |
| 478 | |
| 479 | @classmethod |
| 480 | def create(cls, file, tag, c, h, m): |
| 481 | """ |
| 482 | Create and initialize a new database FILE. |
| 483 | |
| 484 | We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M; |
| 485 | and a Pixie passphrase TAG. |
| 486 | |
| 487 | This doesn't return a working object: it just creates the database file |
| 488 | and gets out of the way. |
| 489 | """ |
| 490 | |
| 491 | ## Set up the cryptography. |
| 492 | pp = _C.ppread(tag, _C.PMODE_VERIFY) |
| 493 | ppk = PPK(pp, c, h, m) |
| 494 | ck = _C.rand.block(c.keysz.default) |
| 495 | mk = _C.rand.block(c.keysz.default) |
| 496 | k = Crypto(c, h, m, ck, mk) |
| 497 | |
| 498 | ## Set up and initialize the database. |
| 499 | kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk)) |
| 500 | with GDBM.StorageBackend.create(file) as db: |
| 501 | db.put_meta('tag', tag) |
| 502 | db.put_meta('salt', ppk.salt) |
| 503 | db.put_meta('cipher', c.name) |
| 504 | db.put_meta('hash', h.name) |
| 505 | db.put_meta('mac', m.name) |
| 506 | db.put_meta('key', kct) |
| 507 | db.put_meta('magic', k.encrypt(_C.rand.block(h.hashsz))) |
| 508 | |
| 509 | def keyxform(me, key): |
| 510 | """Transform the KEY (actually a password tag) into a password label.""" |
| 511 | return me.k.h().hash(me.magic).hash(key).done() |
| 512 | |
| 513 | def changepp(me): |
| 514 | """ |
| 515 | Change the database password. |
| 516 | |
| 517 | Requests the new password from the Pixie, which will probably cause |
| 518 | interaction. |
| 519 | """ |
| 520 | tag = me.db.get_meta('tag') |
| 521 | _C.ppcancel(tag) |
| 522 | ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY), |
| 523 | me.k.c.__class__, me.k.h, me.k.m.__class__) |
| 524 | kct = ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk)) |
| 525 | me.db.put_meta('key', kct) |
| 526 | me.db.put_meta('salt', ppk.salt) |
| 527 | |
| 528 | def pack(me, key, value): |
| 529 | """Pack the KEY and VALUE into a ciphertext, and return it.""" |
| 530 | b = _C.WriteBuffer() |
| 531 | b.putblk16(key).putblk16(value) |
| 532 | b.zero(((b.size + 255) & ~255) - b.size) |
| 533 | return me.k.encrypt(b) |
| 534 | |
| 535 | def unpack(me, ct): |
| 536 | """ |
| 537 | Unpack a ciphertext CT and return a (KEY, VALUE) pair. |
| 538 | |
| 539 | Might raise DecryptError, of course. |
| 540 | """ |
| 541 | b = _C.ReadBuffer(me.k.decrypt(ct)) |
| 542 | key = b.getblk16() |
| 543 | value = b.getblk16() |
| 544 | return key, value |
| 545 | |
| 546 | ## Mapping protocol. |
| 547 | |
| 548 | def __getitem__(me, key): |
| 549 | """Return the password for the given KEY.""" |
| 550 | try: return me.unpack(me.db.get_passwd(me.keyxform(key)))[1] |
| 551 | except KeyError: raise KeyError, key |
| 552 | |
| 553 | def __setitem__(me, key, value): |
| 554 | """Associate the password VALUE with the KEY.""" |
| 555 | me.db.put_passwd(me.keyxform(key), me.pack(key, value)) |
| 556 | |
| 557 | def __delitem__(me, key): |
| 558 | """Forget all about the KEY.""" |
| 559 | try: me.db.del_passwd(me.keyxform(key)) |
| 560 | except KeyError: raise KeyError, key |
| 561 | |
| 562 | def __iter__(me): |
| 563 | """Iterate over the known password tags.""" |
| 564 | for _, pld in me.db.iter_passwds(): |
| 565 | yield me.unpack(pld)[0] |
| 566 | |
| 567 | ## Context protocol. |
| 568 | |
| 569 | def __enter__(me): |
| 570 | return me |
| 571 | def __exit__(me, excty, excval, exctb): |
| 572 | me.db.close() |
| 573 | |
| 574 | ###----- That's all, folks -------------------------------------------------- |