chiark / gitweb /
a9e06052efa592bbe46cc22fd7d57b85d2b2e9d5
[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, writep = False):
165     """
166     Initialize a PW object from the GDBM database in FILE.
167
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,
170     which may cause interaction.
171     """
172
173     ## Open the database.
174     me.db = _G.open(file, writep and 'w' or 'r')
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   ## Context protocol.
303
304   def __enter__(me):
305     return me
306   def __exit__(me, excty, excval, exctb):
307     me.db.close()
308
309 ###----- That's all, folks --------------------------------------------------