Commit | Line | Data |
---|---|---|
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 | |
29 | import catacomb as _C | |
30 | import gdbm as _G | |
31 | import struct as _S | |
32 | ||
d1c45f5c MW |
33 | ###-------------------------------------------------------------------------- |
34 | ### Utilities. | |
35 | ||
36 | class 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 | ||
90 | def _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 | 102 | class 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 | ||
111 | class 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 | 162 | class 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 | |
188 | class 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 | 218 | class 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 | |
43c09851 | 286 | def keyxform(me, key): |
d1c45f5c MW |
287 | """ |
288 | Transform the KEY (actually a password tag) into a GDBM record key. | |
289 | """ | |
43c09851 | 290 | return '$' + me.k.h().hash(me.magic).hash(key).done() |
d1c45f5c | 291 | |
43c09851 | 292 | def changepp(me): |
d1c45f5c MW |
293 | """ |
294 | Change the database password. | |
295 | ||
296 | Requests the new password from the Pixie, which will probably cause | |
297 | interaction. | |
298 | """ | |
43c09851 | 299 | tag = me.db['tag'] |
300 | _C.ppcancel(tag) | |
301 | ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY), | |
d1c45f5c | 302 | me.k.c.__class__, me.k.h, me.k.m.__class__) |
43c09851 | 303 | me.db['key'] = ppk.encrypt(_wrapstr(me.ck) + _wrapstr(me.mk)) |
304 | me.db['salt'] = ppk.salt | |
d1c45f5c | 305 | |
43c09851 | 306 | def pack(me, key, value): |
d1c45f5c MW |
307 | """ |
308 | Pack the KEY and VALUE into a ciphertext, and return it. | |
309 | """ | |
43c09851 | 310 | w = _wrapstr(key) + _wrapstr(value) |
311 | pl = (len(w) + 255) & ~255 | |
312 | w += '\0' * (pl - len(w)) | |
313 | return me.k.encrypt(w) | |
d1c45f5c MW |
314 | |
315 | def unpack(me, ct): | |
316 | """ | |
317 | Unpack a ciphertext CT and return a (KEY, VALUE) pair. | |
318 | ||
319 | Might raise DecryptError, of course. | |
320 | """ | |
321 | buf = Buffer(me.k.decrypt(ct)) | |
43c09851 | 322 | key = buf.getstring() |
323 | value = buf.getstring() | |
324 | return key, value | |
d1c45f5c MW |
325 | |
326 | ## Mapping protocol. | |
327 | ||
43c09851 | 328 | def __getitem__(me, key): |
d1c45f5c MW |
329 | """ |
330 | Return the password for the given KEY. | |
331 | """ | |
2e6a3fda | 332 | try: |
333 | return me.unpack(me.db[me.keyxform(key)])[1] | |
334 | except KeyError: | |
335 | raise KeyError, key | |
d1c45f5c | 336 | |
43c09851 | 337 | def __setitem__(me, key, value): |
d1c45f5c MW |
338 | """ |
339 | Associate the password VALUE with the KEY. | |
340 | """ | |
43c09851 | 341 | me.db[me.keyxform(key)] = me.pack(key, value) |
d1c45f5c | 342 | |
43c09851 | 343 | def __delitem__(me, key): |
d1c45f5c MW |
344 | """ |
345 | Forget all about the KEY. | |
346 | """ | |
2e6a3fda | 347 | try: |
348 | del me.db[me.keyxform(key)] | |
349 | except KeyError: | |
350 | raise KeyError, key | |
d1c45f5c | 351 | |
43c09851 | 352 | def __iter__(me): |
d1c45f5c MW |
353 | """ |
354 | Iterate over the known password tags. | |
355 | """ | |
43c09851 | 356 | return PWIter(me) |
357 | ||
d1c45f5c | 358 | ###----- That's all, folks -------------------------------------------------- |