catacomb/pwsafe.py: Abolish the `PWIter' class.
[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
43c09851 129class 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
43c09851 164 def __init__(me, file, mode = 'r'):
d1c45f5c
MW
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.
43c09851 174 me.db = _G.open(file, mode)
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
d1c45f5c 302###----- That's all, folks --------------------------------------------------