pwsafe, catacomb/pwsafe.py: Push database creation into module.
[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
29import catacomb as _C
30import gdbm as _G
31import struct as _S
32
d1c45f5c
MW
33###--------------------------------------------------------------------------
34### Utilities.
35
36class Buffer (object):
37 """
38 I am a simple gadget for parsing binary strings.
39
40 You should use Catacomb's ReadBuffer instead.
41 """
42
43 def __init__(me, s):
44 """
45 Initialize the buffer with a string S.
46 """
47 me.str = s
48 me.i = 0
49
50 def get(me, n):
51 """
52 Fetch and return the next N bytes from the buffer.
53 """
54 i = me.i
55 if n + i > len(me.str):
56 raise IndexError, 'buffer underflow'
57 me.i += n
58 return me.str[i:i + n]
59
60 def getbyte(me):
61 """
62 Fetch and return (as a small integer) the next byte from the buffer.
63 """
64 return ord(me.get(1))
65
66 def unpack(me, fmt):
67 """
68 Unpack a structure described by FMT from the next bytes of the buffer.
69
70 Return a tuple containing the unpacked items.
71 """
72 return _S.unpack(fmt, me.get(_S.calcsize(fmt)))
73
74 def getstring(me):
75 """
76 Fetch and return a counted string from the buffer.
77
78 The string is expected to be preceded by its 16-bit length, in network
79 byte order.
80 """
81 return me.get(me.unpack('>H')[0])
82
83 def checkend(me):
84 """
85 Raise an error if the buffer has not been completely consumed.
86 """
87 if me.i != len(me.str):
88 raise ValueError, 'junk at end of buffer'
89
90def _wrapstr(s):
91 """
92 Prefix the string S with its 16-bit length.
93
94 It can be read using Buffer.getstring. You should use Catacomb's
95 WriteBuffer.putblk16() function instead.
96 """
97 return _S.pack('>H', len(s)) + s
98
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 """
43c09851 140 if me.c.__class__.blksz:
141 iv = _C.rand.block(me.c.__class__.blksz)
142 me.c.setiv(iv)
143 else:
144 iv = ''
145 y = iv + me.c.encrypt(pt)
146 t = me.m().hash(y).done()
147 return t + y
148 def decrypt(me, ct):
d1c45f5c
MW
149 """
150 Decrypt the ciphertext CT, returning the plaintext.
151
152 Raises DecryptError if anything goes wrong.
153 """
43c09851 154 t = ct[:me.m.__class__.tagsz]
155 y = ct[me.m.__class__.tagsz:]
156 if t != me.m().hash(y).done():
157 raise DecryptError
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:])
b2687a0a 161
43c09851 162class PPK (Crypto):
d1c45f5c
MW
163 """
164 I represent a crypto transform whose keys are derived from a passphrase.
165
166 The password is salted and hashed; the salt is available as the `salt'
167 attribute.
168 """
169
43c09851 170 def __init__(me, pp, c, h, m, salt = None):
d1c45f5c
MW
171 """
172 Initialize the PPK object with a passphrase and algorithm selection.
173
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.
177 """
43c09851 178 if not salt: salt = _C.rand.block(h.hashsz)
179 tag = '%s\0%s' % (pp, salt)
180 Crypto.__init__(me, c, h, m,
d1c45f5c
MW
181 h().hash('cipher:' + tag).done(),
182 h().hash('mac:' + tag).done())
43c09851 183 me.salt = salt
184
d1c45f5c
MW
185###--------------------------------------------------------------------------
186### Password storage.
43c09851 187
188class PWIter (object):
d1c45f5c
MW
189 """
190 I am an iterator over items in a password database.
191
192 I implement the usual Python iteration protocol.
193 """
194
43c09851 195 def __init__(me, pw):
d1c45f5c
MW
196 """
197 Initialize a PWIter object, to fetch items from PW.
198 """
43c09851 199 me.pw = pw
200 me.k = me.pw.db.firstkey()
d1c45f5c 201
43c09851 202 def next(me):
d1c45f5c
MW
203 """
204 Return the next tag from the database.
205
206 Raises StopIteration if there are no more tags.
207 """
43c09851 208 k = me.k
209 while True:
210 if k is None:
d1c45f5c 211 raise StopIteration
43c09851 212 if k[0] == '$':
d1c45f5c 213 break
43c09851 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]
d1c45f5c 217
43c09851 218class PW (object):
d1c45f5c
MW
219 """
220 I represent a secure (ish) password store.
221
222 I can store short secrets, associated with textual names, in a way which
223 doesn't leak too much information about them.
224
225 I implement (some of the) Python mapping protocol.
226
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.
230
231 cipher Names the Catacomb cipher selected.
232
233 hash Names the Catacomb hash function selected.
234
235 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
236 length and concatenated, encrypted using the master
237 passphrase.
238
239 mac Names the Catacomb message authentication code selected.
240
241 magic A magic string for obscuring password tag names.
242
243 salt The salt for hashing the passphrase.
244
245 tag The master passphrase's tag, for the Pixie's benefit.
246
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.
251 """
252
43c09851 253 def __init__(me, file, mode = 'r'):
d1c45f5c
MW
254 """
255 Initialize a PW object from the GDBM database in FILE.
256
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.
260 """
261
262 ## Open the database.
43c09851 263 me.db = _G.open(file, mode)
d1c45f5c
MW
264
265 ## Find out what crypto to use.
43c09851 266 c = _C.gcciphers[me.db['cipher']]
267 h = _C.gchashes[me.db['hash']]
268 m = _C.gcmacs[me.db['mac']]
d1c45f5c
MW
269
270 ## Request the passphrase and extract the master keys.
43c09851 271 tag = me.db['tag']
272 ppk = PPK(_C.ppread(tag), c, h, m, me.db['salt'])
273 try:
274 buf = Buffer(ppk.decrypt(me.db['key']))
275 except DecryptError:
276 _C.ppcancel(tag)
277 raise
278 me.ck = buf.getstring()
279 me.mk = buf.getstring()
280 buf.checkend()
d1c45f5c
MW
281
282 ## Set the key, and stash it and the tag-hashing secret.
43c09851 283 me.k = Crypto(c, h, m, me.ck, me.mk)
284 me.magic = me.k.decrypt(me.db['magic'])
d1c45f5c 285
09b8041d
MW
286 @classmethod
287 def create(cls, file, c, h, m, tag):
288 """
289 Create and initialize a new, empty, database FILE.
290
291 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
292 and a Pixie passphrase TAG.
293
294 This doesn't return a working object: it just creates the database file
295 and gets out of the way.
296 """
297
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)
304
305 ## Set up and initialize the database.
306 db = _G.open(file, 'n', 0600)
307 db['tag'] = tag
308 db['salt'] = ppk.salt
309 db['cipher'] = c.name
310 db['hash'] = h.name
311 db['mac'] = m.name
312 db['key'] = ppk.encrypt(_wrapstr(ck) + _wrapstr(mk))
313 db['magic'] = k.encrypt(_C.rand.block(h.hashsz))
314
43c09851 315 def keyxform(me, key):
d1c45f5c
MW
316 """
317 Transform the KEY (actually a password tag) into a GDBM record key.
318 """
43c09851 319 return '$' + me.k.h().hash(me.magic).hash(key).done()
d1c45f5c 320
43c09851 321 def changepp(me):
d1c45f5c
MW
322 """
323 Change the database password.
324
325 Requests the new password from the Pixie, which will probably cause
326 interaction.
327 """
43c09851 328 tag = me.db['tag']
329 _C.ppcancel(tag)
330 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
d1c45f5c 331 me.k.c.__class__, me.k.h, me.k.m.__class__)
43c09851 332 me.db['key'] = ppk.encrypt(_wrapstr(me.ck) + _wrapstr(me.mk))
333 me.db['salt'] = ppk.salt
d1c45f5c 334
43c09851 335 def pack(me, key, value):
d1c45f5c
MW
336 """
337 Pack the KEY and VALUE into a ciphertext, and return it.
338 """
43c09851 339 w = _wrapstr(key) + _wrapstr(value)
340 pl = (len(w) + 255) & ~255
341 w += '\0' * (pl - len(w))
342 return me.k.encrypt(w)
d1c45f5c
MW
343
344 def unpack(me, ct):
345 """
346 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
347
348 Might raise DecryptError, of course.
349 """
350 buf = Buffer(me.k.decrypt(ct))
43c09851 351 key = buf.getstring()
352 value = buf.getstring()
353 return key, value
d1c45f5c
MW
354
355 ## Mapping protocol.
356
43c09851 357 def __getitem__(me, key):
d1c45f5c
MW
358 """
359 Return the password for the given KEY.
360 """
2e6a3fda 361 try:
362 return me.unpack(me.db[me.keyxform(key)])[1]
363 except KeyError:
364 raise KeyError, key
d1c45f5c 365
43c09851 366 def __setitem__(me, key, value):
d1c45f5c
MW
367 """
368 Associate the password VALUE with the KEY.
369 """
43c09851 370 me.db[me.keyxform(key)] = me.pack(key, value)
d1c45f5c 371
43c09851 372 def __delitem__(me, key):
d1c45f5c
MW
373 """
374 Forget all about the KEY.
375 """
2e6a3fda 376 try:
377 del me.db[me.keyxform(key)]
378 except KeyError:
379 raise KeyError, key
d1c45f5c 380
43c09851 381 def __iter__(me):
d1c45f5c
MW
382 """
383 Iterate over the known password tags.
384 """
43c09851 385 return PWIter(me)
386
d1c45f5c 387###----- That's all, folks --------------------------------------------------