| 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 | import catacomb as _C |
| 30 | import gdbm as _G |
| 31 | |
| 32 | ###-------------------------------------------------------------------------- |
| 33 | ### Underlying cryptography. |
| 34 | |
| 35 | class DecryptError (Exception): |
| 36 | """ |
| 37 | I represent a failure to decrypt a message. |
| 38 | |
| 39 | Usually this means that someone used the wrong key, though it can also |
| 40 | mean that a ciphertext has been modified. |
| 41 | """ |
| 42 | pass |
| 43 | |
| 44 | class Crypto (object): |
| 45 | """ |
| 46 | I represent a symmetric crypto transform. |
| 47 | |
| 48 | There's currently only one transform implemented, which is the obvious |
| 49 | generic-composition construction: given a message m, and keys K0 and K1, we |
| 50 | choose an IV v, and compute: |
| 51 | |
| 52 | * y = v || E(K0, v; m) |
| 53 | * t = M(K1; y) |
| 54 | |
| 55 | The final ciphertext is t || y. |
| 56 | """ |
| 57 | |
| 58 | def __init__(me, c, h, m, ck, mk): |
| 59 | """ |
| 60 | Initialize the Crypto object with a given algorithm selection and keys. |
| 61 | |
| 62 | We need a GCipher subclass C, a GHash subclass H, a GMAC subclass M, and |
| 63 | keys CK and MK for C and M respectively. |
| 64 | """ |
| 65 | me.c = c(ck) |
| 66 | me.m = m(mk) |
| 67 | me.h = h |
| 68 | |
| 69 | def encrypt(me, pt): |
| 70 | """ |
| 71 | Encrypt the message PT and return the resulting ciphertext. |
| 72 | """ |
| 73 | blksz = me.c.__class__.blksz |
| 74 | b = _C.WriteBuffer() |
| 75 | if blksz: |
| 76 | iv = _C.rand.block(blksz) |
| 77 | me.c.setiv(iv) |
| 78 | b.put(iv) |
| 79 | b.put(me.c.encrypt(pt)) |
| 80 | t = me.m().hash(b).done() |
| 81 | return t + str(buffer(b)) |
| 82 | |
| 83 | def decrypt(me, ct): |
| 84 | """ |
| 85 | Decrypt the ciphertext CT, returning the plaintext. |
| 86 | |
| 87 | Raises DecryptError if anything goes wrong. |
| 88 | """ |
| 89 | blksz = me.c.__class__.blksz |
| 90 | tagsz = me.m.__class__.tagsz |
| 91 | b = _C.ReadBuffer(ct) |
| 92 | t = b.get(tagsz) |
| 93 | h = me.m() |
| 94 | if blksz: |
| 95 | iv = b.get(blksz) |
| 96 | me.c.setiv(iv) |
| 97 | h.hash(iv) |
| 98 | x = b.get(b.left) |
| 99 | h.hash(x) |
| 100 | if t != h.done(): raise DecryptError |
| 101 | return me.c.decrypt(x) |
| 102 | |
| 103 | class PPK (Crypto): |
| 104 | """ |
| 105 | I represent a crypto transform whose keys are derived from a passphrase. |
| 106 | |
| 107 | The password is salted and hashed; the salt is available as the `salt' |
| 108 | attribute. |
| 109 | """ |
| 110 | |
| 111 | def __init__(me, pp, c, h, m, salt = None): |
| 112 | """ |
| 113 | Initialize the PPK object with a passphrase and algorithm selection. |
| 114 | |
| 115 | We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC |
| 116 | subclass M, and a SALT. The SALT may be None, if we're generating new |
| 117 | keys, indicating that a salt should be chosen randomly. |
| 118 | """ |
| 119 | if not salt: salt = _C.rand.block(h.hashsz) |
| 120 | tag = '%s\0%s' % (pp, salt) |
| 121 | Crypto.__init__(me, c, h, m, |
| 122 | h().hash('cipher:' + tag).done(), |
| 123 | h().hash('mac:' + tag).done()) |
| 124 | me.salt = salt |
| 125 | |
| 126 | ###-------------------------------------------------------------------------- |
| 127 | ### Password storage. |
| 128 | |
| 129 | class PW (object): |
| 130 | """ |
| 131 | I represent a secure (ish) password store. |
| 132 | |
| 133 | I can store short secrets, associated with textual names, in a way which |
| 134 | doesn't leak too much information about them. |
| 135 | |
| 136 | I implement (some of the) Python mapping protocol. |
| 137 | |
| 138 | Here's how we use the underlying GDBM key/value storage to keep track of |
| 139 | the necessary things. Password entries have keys whose name begins with |
| 140 | `$'; other keys have specific meanings, as follows. |
| 141 | |
| 142 | cipher Names the Catacomb cipher selected. |
| 143 | |
| 144 | hash Names the Catacomb hash function selected. |
| 145 | |
| 146 | key Cipher and MAC keys, each prefixed by a 16-bit big-endian |
| 147 | length and concatenated, encrypted using the master |
| 148 | passphrase. |
| 149 | |
| 150 | mac Names the Catacomb message authentication code selected. |
| 151 | |
| 152 | magic A magic string for obscuring password tag names. |
| 153 | |
| 154 | salt The salt for hashing the passphrase. |
| 155 | |
| 156 | tag The master passphrase's tag, for the Pixie's benefit. |
| 157 | |
| 158 | Password entries are assigned keys of the form `$' || H(MAGIC || TAG); the |
| 159 | corresponding value consists of a pair (TAG, PASSWD), prefixed with 16-bit |
| 160 | lengths, concatenated, padded to a multiple of 256 octets, and encrypted |
| 161 | using the stored keys. |
| 162 | """ |
| 163 | |
| 164 | def __init__(me, file, writep = False): |
| 165 | """ |
| 166 | Initialize a PW object from the GDBM database in FILE. |
| 167 | |
| 168 | If WRITEP is true, then allow write-access to the database; otherwise |
| 169 | allow read access only. Requests the database password from the Pixie, |
| 170 | which may cause interaction. |
| 171 | """ |
| 172 | |
| 173 | ## Open the database. |
| 174 | me.db = _G.open(file, writep and 'w' or 'r') |
| 175 | |
| 176 | ## Find out what crypto to use. |
| 177 | c = _C.gcciphers[me.db['cipher']] |
| 178 | h = _C.gchashes[me.db['hash']] |
| 179 | m = _C.gcmacs[me.db['mac']] |
| 180 | |
| 181 | ## Request the passphrase and extract the master keys. |
| 182 | tag = me.db['tag'] |
| 183 | ppk = PPK(_C.ppread(tag), c, h, m, me.db['salt']) |
| 184 | try: |
| 185 | b = _C.ReadBuffer(ppk.decrypt(me.db['key'])) |
| 186 | except DecryptError: |
| 187 | _C.ppcancel(tag) |
| 188 | raise |
| 189 | me.ck = b.getblk16() |
| 190 | me.mk = b.getblk16() |
| 191 | if not b.endp: raise ValueError, 'trailing junk' |
| 192 | |
| 193 | ## Set the key, and stash it and the tag-hashing secret. |
| 194 | me.k = Crypto(c, h, m, me.ck, me.mk) |
| 195 | me.magic = me.k.decrypt(me.db['magic']) |
| 196 | |
| 197 | @classmethod |
| 198 | def create(cls, file, c, h, m, tag): |
| 199 | """ |
| 200 | Create and initialize a new, empty, database FILE. |
| 201 | |
| 202 | We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M; |
| 203 | and a Pixie passphrase TAG. |
| 204 | |
| 205 | This doesn't return a working object: it just creates the database file |
| 206 | and gets out of the way. |
| 207 | """ |
| 208 | |
| 209 | ## Set up the cryptography. |
| 210 | pp = _C.ppread(tag, _C.PMODE_VERIFY) |
| 211 | ppk = PPK(pp, c, h, m) |
| 212 | ck = _C.rand.block(c.keysz.default) |
| 213 | mk = _C.rand.block(c.keysz.default) |
| 214 | k = Crypto(c, h, m, ck, mk) |
| 215 | |
| 216 | ## Set up and initialize the database. |
| 217 | db = _G.open(file, 'n', 0600) |
| 218 | db['tag'] = tag |
| 219 | db['salt'] = ppk.salt |
| 220 | db['cipher'] = c.name |
| 221 | db['hash'] = h.name |
| 222 | db['mac'] = m.name |
| 223 | db['key'] = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk)) |
| 224 | db['magic'] = k.encrypt(_C.rand.block(h.hashsz)) |
| 225 | |
| 226 | def keyxform(me, key): |
| 227 | """ |
| 228 | Transform the KEY (actually a password tag) into a GDBM record key. |
| 229 | """ |
| 230 | return '$' + me.k.h().hash(me.magic).hash(key).done() |
| 231 | |
| 232 | def changepp(me): |
| 233 | """ |
| 234 | Change the database password. |
| 235 | |
| 236 | Requests the new password from the Pixie, which will probably cause |
| 237 | interaction. |
| 238 | """ |
| 239 | tag = me.db['tag'] |
| 240 | _C.ppcancel(tag) |
| 241 | ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY), |
| 242 | me.k.c.__class__, me.k.h, me.k.m.__class__) |
| 243 | me.db['key'] = \ |
| 244 | ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk)) |
| 245 | me.db['salt'] = ppk.salt |
| 246 | |
| 247 | def pack(me, key, value): |
| 248 | """ |
| 249 | Pack the KEY and VALUE into a ciphertext, and return it. |
| 250 | """ |
| 251 | b = _C.WriteBuffer() |
| 252 | b.putblk16(key).putblk16(value) |
| 253 | b.zero(((b.size + 255) & ~255) - b.size) |
| 254 | return me.k.encrypt(b) |
| 255 | |
| 256 | def unpack(me, ct): |
| 257 | """ |
| 258 | Unpack a ciphertext CT and return a (KEY, VALUE) pair. |
| 259 | |
| 260 | Might raise DecryptError, of course. |
| 261 | """ |
| 262 | b = _C.ReadBuffer(me.k.decrypt(ct)) |
| 263 | key = b.getblk16() |
| 264 | value = b.getblk16() |
| 265 | return key, value |
| 266 | |
| 267 | ## Mapping protocol. |
| 268 | |
| 269 | def __getitem__(me, key): |
| 270 | """ |
| 271 | Return the password for the given KEY. |
| 272 | """ |
| 273 | try: |
| 274 | return me.unpack(me.db[me.keyxform(key)])[1] |
| 275 | except KeyError: |
| 276 | raise KeyError, key |
| 277 | |
| 278 | def __setitem__(me, key, value): |
| 279 | """ |
| 280 | Associate the password VALUE with the KEY. |
| 281 | """ |
| 282 | me.db[me.keyxform(key)] = me.pack(key, value) |
| 283 | |
| 284 | def __delitem__(me, key): |
| 285 | """ |
| 286 | Forget all about the KEY. |
| 287 | """ |
| 288 | try: |
| 289 | del me.db[me.keyxform(key)] |
| 290 | except KeyError: |
| 291 | raise KeyError, key |
| 292 | |
| 293 | def __iter__(me): |
| 294 | """ |
| 295 | Iterate over the known password tags. |
| 296 | """ |
| 297 | k = me.db.firstkey() |
| 298 | while k is not None: |
| 299 | if k[0] == '$': yield me.unpack(me.db[k])[0] |
| 300 | k = me.db.nextkey(k) |
| 301 | |
| 302 | ###----- That's all, folks -------------------------------------------------- |