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