catacomb/pwsafe.py: Eliminate the Buffer class and struct module.
[catacomb-python] / catacomb / pwsafe.py
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 PWIter (object):
130 """
131 I am an iterator over items in a password database.
132
133 I implement the usual Python iteration protocol.
134 """
135
136 def __init__(me, pw):
137 """
138 Initialize a PWIter object, to fetch items from PW.
139 """
140 me.pw = pw
141 me.k = me.pw.db.firstkey()
142
143 def next(me):
144 """
145 Return the next tag from the database.
146
147 Raises StopIteration if there are no more tags.
148 """
149 k = me.k
150 while True:
151 if k is None:
152 raise StopIteration
153 if k[0] == '$':
154 break
155 k = me.pw.db.nextkey(k)
156 me.k = me.pw.db.nextkey(k)
157 return me.pw.unpack(me.pw.db[k])[0]
158
159 class PW (object):
160 """
161 I represent a secure (ish) password store.
162
163 I can store short secrets, associated with textual names, in a way which
164 doesn't leak too much information about them.
165
166 I implement (some of the) Python mapping protocol.
167
168 Here's how we use the underlying GDBM key/value storage to keep track of
169 the necessary things. Password entries have keys whose name begins with
170 `$'; other keys have specific meanings, as follows.
171
172 cipher Names the Catacomb cipher selected.
173
174 hash Names the Catacomb hash function selected.
175
176 key Cipher and MAC keys, each prefixed by a 16-bit big-endian
177 length and concatenated, encrypted using the master
178 passphrase.
179
180 mac Names the Catacomb message authentication code selected.
181
182 magic A magic string for obscuring password tag names.
183
184 salt The salt for hashing the passphrase.
185
186 tag The master passphrase's tag, for the Pixie's benefit.
187
188 Password entries are assigned keys of the form `$' || H(MAGIC || TAG); the
189 corresponding value consists of a pair (TAG, PASSWD), prefixed with 16-bit
190 lengths, concatenated, padded to a multiple of 256 octets, and encrypted
191 using the stored keys.
192 """
193
194 def __init__(me, file, mode = 'r'):
195 """
196 Initialize a PW object from the GDBM database in FILE.
197
198 MODE can be `r' for read-only access to the underlying database, or `w'
199 for read-write access. Requests the database password from the Pixie,
200 which may cause interaction.
201 """
202
203 ## Open the database.
204 me.db = _G.open(file, mode)
205
206 ## Find out what crypto to use.
207 c = _C.gcciphers[me.db['cipher']]
208 h = _C.gchashes[me.db['hash']]
209 m = _C.gcmacs[me.db['mac']]
210
211 ## Request the passphrase and extract the master keys.
212 tag = me.db['tag']
213 ppk = PPK(_C.ppread(tag), c, h, m, me.db['salt'])
214 try:
215 b = _C.ReadBuffer(ppk.decrypt(me.db['key']))
216 except DecryptError:
217 _C.ppcancel(tag)
218 raise
219 me.ck = b.getblk16()
220 me.mk = b.getblk16()
221 if not b.endp: raise ValueError, 'trailing junk'
222
223 ## Set the key, and stash it and the tag-hashing secret.
224 me.k = Crypto(c, h, m, me.ck, me.mk)
225 me.magic = me.k.decrypt(me.db['magic'])
226
227 @classmethod
228 def create(cls, file, c, h, m, tag):
229 """
230 Create and initialize a new, empty, database FILE.
231
232 We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
233 and a Pixie passphrase TAG.
234
235 This doesn't return a working object: it just creates the database file
236 and gets out of the way.
237 """
238
239 ## Set up the cryptography.
240 pp = _C.ppread(tag, _C.PMODE_VERIFY)
241 ppk = PPK(pp, c, h, m)
242 ck = _C.rand.block(c.keysz.default)
243 mk = _C.rand.block(c.keysz.default)
244 k = Crypto(c, h, m, ck, mk)
245
246 ## Set up and initialize the database.
247 db = _G.open(file, 'n', 0600)
248 db['tag'] = tag
249 db['salt'] = ppk.salt
250 db['cipher'] = c.name
251 db['hash'] = h.name
252 db['mac'] = m.name
253 db['key'] = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
254 db['magic'] = k.encrypt(_C.rand.block(h.hashsz))
255
256 def keyxform(me, key):
257 """
258 Transform the KEY (actually a password tag) into a GDBM record key.
259 """
260 return '$' + me.k.h().hash(me.magic).hash(key).done()
261
262 def changepp(me):
263 """
264 Change the database password.
265
266 Requests the new password from the Pixie, which will probably cause
267 interaction.
268 """
269 tag = me.db['tag']
270 _C.ppcancel(tag)
271 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
272 me.k.c.__class__, me.k.h, me.k.m.__class__)
273 me.db['key'] = \
274 ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
275 me.db['salt'] = ppk.salt
276
277 def pack(me, key, value):
278 """
279 Pack the KEY and VALUE into a ciphertext, and return it.
280 """
281 b = _C.WriteBuffer()
282 b.putblk16(key).putblk16(value)
283 b.zero(((b.size + 255) & ~255) - b.size)
284 return me.k.encrypt(b)
285
286 def unpack(me, ct):
287 """
288 Unpack a ciphertext CT and return a (KEY, VALUE) pair.
289
290 Might raise DecryptError, of course.
291 """
292 b = _C.ReadBuffer(me.k.decrypt(ct))
293 key = b.getblk16()
294 value = b.getblk16()
295 return key, value
296
297 ## Mapping protocol.
298
299 def __getitem__(me, key):
300 """
301 Return the password for the given KEY.
302 """
303 try:
304 return me.unpack(me.db[me.keyxform(key)])[1]
305 except KeyError:
306 raise KeyError, key
307
308 def __setitem__(me, key, value):
309 """
310 Associate the password VALUE with the KEY.
311 """
312 me.db[me.keyxform(key)] = me.pack(key, value)
313
314 def __delitem__(me, key):
315 """
316 Forget all about the KEY.
317 """
318 try:
319 del me.db[me.keyxform(key)]
320 except KeyError:
321 raise KeyError, key
322
323 def __iter__(me):
324 """
325 Iterate over the known password tags.
326 """
327 return PWIter(me)
328
329 ###----- That's all, folks --------------------------------------------------