catacomb/pwsafe.py: Abolish the `PWIter' class.
[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 PW (object):
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
164 def __init__(me, file, mode = 'r'):
165 """
166 Initialize a PW object from the GDBM database in FILE.
167
168 MODE can be `r' for read-only access to the underlying database, or `w'
169 for read-write access. Requests the database password from the Pixie,
170 which may cause interaction.
171 """
172
173 ## Open the database.
174 me.db = _G.open(file, mode)
175
176 ## Find out what crypto to use.
177 c = _C.gcciphers[me.db['cipher']]
178 h = _C.gchashes[me.db['hash']]
179 m = _C.gcmacs[me.db['mac']]
180
181 ## Request the passphrase and extract the master keys.
182 tag = me.db['tag']
183 ppk = PPK(_C.ppread(tag), c, h, m, me.db['salt'])
184 try:
185 b = _C.ReadBuffer(ppk.decrypt(me.db['key']))
186 except DecryptError:
187 _C.ppcancel(tag)
188 raise
189 me.ck = b.getblk16()
190 me.mk = b.getblk16()
191 if not b.endp: raise ValueError, 'trailing junk'
192
193 ## Set the key, and stash it and the tag-hashing secret.
194 me.k = Crypto(c, h, m, me.ck, me.mk)
195 me.magic = me.k.decrypt(me.db['magic'])
196
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
223 db['key'] = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
224 db['magic'] = k.encrypt(_C.rand.block(h.hashsz))
225
226 def keyxform(me, key):
227 """
228 Transform the KEY (actually a password tag) into a GDBM record key.
229 """
230 return '$' + me.k.h().hash(me.magic).hash(key).done()
231
232 def changepp(me):
233 """
234 Change the database password.
235
236 Requests the new password from the Pixie, which will probably cause
237 interaction.
238 """
239 tag = me.db['tag']
240 _C.ppcancel(tag)
241 ppk = PPK(_C.ppread(tag, _C.PMODE_VERIFY),
242 me.k.c.__class__, me.k.h, me.k.m.__class__)
243 me.db['key'] = \
244 ppk.encrypt(_C.WriteBuffer().putblk16(me.ck).putblk16(me.mk))
245 me.db['salt'] = ppk.salt
246
247 def pack(me, key, value):
248 """
249 Pack the KEY and VALUE into a ciphertext, and return it.
250 """
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)
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 """
262 b = _C.ReadBuffer(me.k.decrypt(ct))
263 key = b.getblk16()
264 value = b.getblk16()
265 return key, value
266
267 ## Mapping protocol.
268
269 def __getitem__(me, key):
270 """
271 Return the password for the given KEY.
272 """
273 try:
274 return me.unpack(me.db[me.keyxform(key)])[1]
275 except KeyError:
276 raise KeyError, key
277
278 def __setitem__(me, key, value):
279 """
280 Associate the password VALUE with the KEY.
281 """
282 me.db[me.keyxform(key)] = me.pack(key, value)
283
284 def __delitem__(me, key):
285 """
286 Forget all about the KEY.
287 """
288 try:
289 del me.db[me.keyxform(key)]
290 except KeyError:
291 raise KeyError, key
292
293 def __iter__(me):
294 """
295 Iterate over the known password tags.
296 """
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)
301
302 ###----- That's all, folks --------------------------------------------------