catacomb/pwsafe.py: Eliminate the Buffer class and struct 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
d1c45f5c
MW
31
32###--------------------------------------------------------------------------
33### Underlying cryptography.
34
43c09851 35class DecryptError (Exception):
d1c45f5c
MW
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 """
43c09851 42 pass
43
44class Crypto (object):
d1c45f5c
MW
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
43c09851 58 def __init__(me, c, h, m, ck, mk):
d1c45f5c
MW
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 """
43c09851 65 me.c = c(ck)
66 me.m = m(mk)
67 me.h = h
d1c45f5c 68
43c09851 69 def encrypt(me, pt):
d1c45f5c
MW
70 """
71 Encrypt the message PT and return the resulting ciphertext.
72 """
9a7b948f
MW
73 blksz = me.c.__class__.blksz
74 b = _C.WriteBuffer()
75 if blksz:
76 iv = _C.rand.block(blksz)
43c09851 77 me.c.setiv(iv)
9a7b948f
MW
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
43c09851 83 def decrypt(me, ct):
d1c45f5c
MW
84 """
85 Decrypt the ciphertext CT, returning the plaintext.
86
87 Raises DecryptError if anything goes wrong.
88 """
9a7b948f
MW
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)
b2687a0a 102
43c09851 103class PPK (Crypto):
d1c45f5c
MW
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
43c09851 111 def __init__(me, pp, c, h, m, salt = None):
d1c45f5c
MW
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 """
43c09851 119 if not salt: salt = _C.rand.block(h.hashsz)
120 tag = '%s\0%s' % (pp, salt)
121 Crypto.__init__(me, c, h, m,
d1c45f5c
MW
122 h().hash('cipher:' + tag).done(),
123 h().hash('mac:' + tag).done())
43c09851 124 me.salt = salt
125
d1c45f5c
MW
126###--------------------------------------------------------------------------
127### Password storage.
43c09851 128
129class PWIter (object):
d1c45f5c
MW
130 """
131 I am an iterator over items in a password database.
132
133 I implement the usual Python iteration protocol.
134 """
135
43c09851 136 def __init__(me, pw):
d1c45f5c
MW
137 """
138 Initialize a PWIter object, to fetch items from PW.
139 """
43c09851 140 me.pw = pw
141 me.k = me.pw.db.firstkey()
d1c45f5c 142
43c09851 143 def next(me):
d1c45f5c
MW
144 """
145 Return the next tag from the database.
146
147 Raises StopIteration if there are no more tags.
148 """
43c09851 149 k = me.k
150 while True:
151 if k is None:
d1c45f5c 152 raise StopIteration
43c09851 153 if k[0] == '$':
d1c45f5c 154 break
43c09851 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]
d1c45f5c 158
43c09851 159class PW (object):
d1c45f5c
MW
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
43c09851 194 def __init__(me, file, mode = 'r'):
d1c45f5c
MW
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.
43c09851 204 me.db = _G.open(file, mode)
d1c45f5c
MW
205
206 ## Find out what crypto to use.
43c09851 207 c = _C.gcciphers[me.db['cipher']]
208 h = _C.gchashes[me.db['hash']]
209 m = _C.gcmacs[me.db['mac']]
d1c45f5c
MW
210
211 ## Request the passphrase and extract the master keys.
43c09851 212 tag = me.db['tag']
213 ppk = PPK(_C.ppread(tag), c, h, m, me.db['salt'])
214 try:
9a7b948f 215 b = _C.ReadBuffer(ppk.decrypt(me.db['key']))
43c09851 216 except DecryptError:
217 _C.ppcancel(tag)
218 raise
9a7b948f
MW
219 me.ck = b.getblk16()
220 me.mk = b.getblk16()
221 if not b.endp: raise ValueError, 'trailing junk'
d1c45f5c
MW
222
223 ## Set the key, and stash it and the tag-hashing secret.
43c09851 224 me.k = Crypto(c, h, m, me.ck, me.mk)
225 me.magic = me.k.decrypt(me.db['magic'])
d1c45f5c 226
09b8041d
MW
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
9a7b948f 253 db['key'] = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
09b8041d
MW
254 db['magic'] = k.encrypt(_C.rand.block(h.hashsz))
255
43c09851 256 def keyxform(me, key):
d1c45f5c
MW
257 """
258 Transform the KEY (actually a password tag) into a GDBM record key.
259 """
43c09851 260 return '$' + me.k.h().hash(me.magic).hash(key).done()
d1c45f5c 261
43c09851 262 def changepp(me):
d1c45f5c
MW
263 """
264 Change the database password.
265
266 Requests the new password from the Pixie, which will probably cause
267 interaction.
268 """
43c09851 269 tag = me.db['tag']
270 _C.ppcancel(tag)
271 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
d1c45f5c 272 me.k.c.__class__, me.k.h, me.k.m.__class__)
9a7b948f
MW
273 me.db['key'] = \
274 ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
43c09851 275 me.db['salt'] = ppk.salt
d1c45f5c 276
43c09851 277 def pack(me, key, value):
d1c45f5c
MW
278 """
279 Pack the KEY and VALUE into a ciphertext, and return it.
280 """
9a7b948f
MW
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)
d1c45f5c
MW
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 """
9a7b948f
MW
292 b = _C.ReadBuffer(me.k.decrypt(ct))
293 key = b.getblk16()
294 value = b.getblk16()
43c09851 295 return key, value
d1c45f5c
MW
296
297 ## Mapping protocol.
298
43c09851 299 def __getitem__(me, key):
d1c45f5c
MW
300 """
301 Return the password for the given KEY.
302 """
2e6a3fda 303 try:
304 return me.unpack(me.db[me.keyxform(key)])[1]
305 except KeyError:
306 raise KeyError, key
d1c45f5c 307
43c09851 308 def __setitem__(me, key, value):
d1c45f5c
MW
309 """
310 Associate the password VALUE with the KEY.
311 """
43c09851 312 me.db[me.keyxform(key)] = me.pack(key, value)
d1c45f5c 313
43c09851 314 def __delitem__(me, key):
d1c45f5c
MW
315 """
316 Forget all about the KEY.
317 """
2e6a3fda 318 try:
319 del me.db[me.keyxform(key)]
320 except KeyError:
321 raise KeyError, key
d1c45f5c 322
43c09851 323 def __iter__(me):
d1c45f5c
MW
324 """
325 Iterate over the known password tags.
326 """
43c09851 327 return PWIter(me)
328
d1c45f5c 329###----- That's all, folks --------------------------------------------------