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 | |
d1c45f5c MW |
31 | |
32 | ###-------------------------------------------------------------------------- | |
33 | ### Underlying cryptography. | |
34 | ||
43c09851 | 35 | class 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 | ||
44 | class 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 | 103 | class 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 | |
129 | class 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 | 159 | class 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 -------------------------------------------------- |