3 ### Management of a secure password database
5 ### (c) 2005 Straylight/Edgeware
8 ###----- Licensing notice ---------------------------------------------------
10 ### This file is part of the Python interface to Catacomb.
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.
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.
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.
26 ###--------------------------------------------------------------------------
33 ###--------------------------------------------------------------------------
36 class Buffer (object):
38 I am a simple gadget for parsing binary strings.
40 You should use Catacomb's ReadBuffer instead.
45 Initialize the buffer with a string S.
52 Fetch and return the next N bytes from the buffer.
55 if n + i > len(me.str):
56 raise IndexError, 'buffer underflow'
58 return me.str[i:i + n]
62 Fetch and return (as a small integer) the next byte from the buffer.
68 Unpack a structure described by FMT from the next bytes of the buffer.
70 Return a tuple containing the unpacked items.
72 return _S.unpack(fmt, me.get(_S.calcsize(fmt)))
76 Fetch and return a counted string from the buffer.
78 The string is expected to be preceded by its 16-bit length, in network
81 return me.get(me.unpack('>H')[0])
85 Raise an error if the buffer has not been completely consumed.
87 if me.i != len(me.str):
88 raise ValueError, 'junk at end of buffer'
92 Prefix the string S with its 16-bit length.
94 It can be read using Buffer.getstring. You should use Catacomb's
95 WriteBuffer.putblk16() function instead.
97 return _S.pack('>H', len(s)) + s
99 ###--------------------------------------------------------------------------
100 ### Underlying cryptography.
102 class DecryptError (Exception):
104 I represent a failure to decrypt a message.
106 Usually this means that someone used the wrong key, though it can also
107 mean that a ciphertext has been modified.
111 class Crypto (object):
113 I represent a symmetric crypto transform.
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:
119 * y = v || E(K0, v; m)
122 The final ciphertext is t || y.
125 def __init__(me, c, h, m, ck, mk):
127 Initialize the Crypto object with a given algorithm selection and keys.
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.
138 Encrypt the message PT and return the resulting ciphertext.
140 if me.c.__class__.blksz:
141 iv = _C.rand.block(me.c.__class__.blksz)
145 y = iv + me.c.encrypt(pt)
146 t = me.m().hash(y).done()
150 Decrypt the ciphertext CT, returning the plaintext.
152 Raises DecryptError if anything goes wrong.
154 t = ct[:me.m.__class__.tagsz]
155 y = ct[me.m.__class__.tagsz:]
156 if t != me.m().hash(y).done():
158 iv = y[:me.c.__class__.blksz]
159 if me.c.__class__.blksz: me.c.setiv(iv)
160 return me.c.decrypt(y[me.c.__class__.blksz:])
164 I represent a crypto transform whose keys are derived from a passphrase.
166 The password is salted and hashed; the salt is available as the `salt'
170 def __init__(me, pp, c, h, m, salt = None):
172 Initialize the PPK object with a passphrase and algorithm selection.
174 We want a passphrase PP, a GCipher subclass C, a GHash subclass H, a GMAC
175 subclass M, and a SALT. The SALT may be None, if we're generating new
176 keys, indicating that a salt should be chosen randomly.
178 if not salt: salt = _C.rand.block(h.hashsz)
179 tag = '%s\0%s' % (pp, salt)
180 Crypto.__init__(me, c, h, m,
181 h().hash('cipher:' + tag).done(),
182 h().hash('mac:' + tag).done())
185 ###--------------------------------------------------------------------------
186 ### Password storage.
188 class PWIter (object):
190 I am an iterator over items in a password database.
192 I implement the usual Python iteration protocol.
195 def __init__(me, pw):
197 Initialize a PWIter object, to fetch items from PW.
200 me.k = me.pw.db.firstkey()
204 Return the next tag from the database.
206 Raises StopIteration if there are no more tags.
214 k = me.pw.db.nextkey(k)
215 me.k = me.pw.db.nextkey(k)
216 return me.pw.unpack(me.pw.db[k])[0]
220 I represent a secure (ish) password store.
222 I can store short secrets, associated with textual names, in a way which
223 doesn't leak too much information about them.
225 I implement (some of the) Python mapping protocol.
227 Here's how we use the underlying GDBM key/value storage to keep track of
228 the necessary things. Password entries have keys whose name begins with
229 `$'; other keys have specific meanings, as follows.
231 cipher Names the Catacomb cipher selected.
233 hash Names the Catacomb hash function selected.
235 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
236 length and concatenated, encrypted using the master
239 mac Names the Catacomb message authentication code selected.
241 magic A magic string for obscuring password tag names.
243 salt The salt for hashing the passphrase.
245 tag The master passphrase's tag, for the Pixie's benefit.
247 Password entries are assigned keys of the form `$' || H(MAGIC || TAG); the
248 corresponding value consists of a pair (TAG, PASSWD), prefixed with 16-bit
249 lengths, concatenated, padded to a multiple of 256 octets, and encrypted
250 using the stored keys.
253 def __init__(me, file, mode = 'r'):
255 Initialize a PW object from the GDBM database in FILE.
257 MODE can be `r' for read-only access to the underlying database, or `w'
258 for read-write access. Requests the database password from the Pixie,
259 which may cause interaction.
262 ## Open the database.
263 me.db = _G.open(file, mode)
265 ## Find out what crypto to use.
266 c = _C.gcciphers[me.db['cipher']]
267 h = _C.gchashes[me.db['hash']]
268 m = _C.gcmacs[me.db['mac']]
270 ## Request the passphrase and extract the master keys.
272 ppk = PPK(_C.ppread(tag), c, h, m, me.db['salt'])
274 buf = Buffer(ppk.decrypt(me.db['key']))
278 me.ck = buf.getstring()
279 me.mk = buf.getstring()
282 ## Set the key, and stash it and the tag-hashing secret.
283 me.k = Crypto(c, h, m, me.ck, me.mk)
284 me.magic = me.k.decrypt(me.db['magic'])
287 def create(cls, file, c, h, m, tag):
289 Create and initialize a new, empty, database FILE.
291 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
292 and a Pixie passphrase TAG.
294 This doesn't return a working object: it just creates the database file
295 and gets out of the way.
298 ## Set up the cryptography.
299 pp = _C.ppread(tag, _C.PMODE_VERIFY)
300 ppk = PPK(pp, c, h, m)
301 ck = _C.rand.block(c.keysz.default)
302 mk = _C.rand.block(c.keysz.default)
303 k = Crypto(c, h, m, ck, mk)
305 ## Set up and initialize the database.
306 db = _G.open(file, 'n', 0600)
308 db['salt'] = ppk.salt
309 db['cipher'] = c.name
312 db['key'] = ppk.encrypt(_wrapstr(ck) + _wrapstr(mk))
313 db['magic'] = k.encrypt(_C.rand.block(h.hashsz))
315 def keyxform(me, key):
317 Transform the KEY (actually a password tag) into a GDBM record key.
319 return '$' + me.k.h().hash(me.magic).hash(key).done()
323 Change the database password.
325 Requests the new password from the Pixie, which will probably cause
330 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
331 me.k.c.__class__, me.k.h, me.k.m.__class__)
332 me.db['key'] = ppk.encrypt(_wrapstr(me.ck) + _wrapstr(me.mk))
333 me.db['salt'] = ppk.salt
335 def pack(me, key, value):
337 Pack the KEY and VALUE into a ciphertext, and return it.
339 w = _wrapstr(key) + _wrapstr(value)
340 pl = (len(w) + 255) & ~255
341 w += '\0' * (pl - len(w))
342 return me.k.encrypt(w)
346 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
348 Might raise DecryptError, of course.
350 buf = Buffer(me.k.decrypt(ct))
351 key = buf.getstring()
352 value = buf.getstring()
357 def __getitem__(me, key):
359 Return the password for the given KEY.
362 return me.unpack(me.db[me.keyxform(key)])[1]
366 def __setitem__(me, key, value):
368 Associate the password VALUE with the KEY.
370 me.db[me.keyxform(key)] = me.pack(key, value)
372 def __delitem__(me, key):
374 Forget all about the KEY.
377 del me.db[me.keyxform(key)]
383 Iterate over the known password tags.
387 ###----- That's all, folks --------------------------------------------------