chiark / gitweb /
Use users.db. trackdb* moves to lib/, as it's now used by client.c to
[disorder] / lib / cookies.c
1 /*
2  * This file is part of DisOrder
3  * Copyright (C) 2007 Richard Kettlewell
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18  * USA
19  */
20 /** @file lib/cookies.c
21  * @brief Cookie support
22  */
23
24 #include <config.h>
25 #include "types.h"
26
27 #include <stdlib.h>
28 #include <string.h>
29 #include <stdio.h>
30 #include <errno.h>
31 #include <time.h>
32 #include <gcrypt.h>
33 #include <pcre.h>
34
35 #include "cookies.h"
36 #include "hash.h"
37 #include "mem.h"
38 #include "log.h"
39 #include "printf.h"
40 #include "mime.h"
41 #include "configuration.h"
42 #include "kvp.h"
43 #include "trackdb.h"
44
45 /** @brief Hash function used in signing HMAC */
46 #define ALGO GCRY_MD_SHA1
47
48 /** @brief Size of key to use */
49 #define HASHSIZE 20
50
51 /** @brief Signing key */
52 static uint8_t signing_key[HASHSIZE];
53
54 /** @brief Previous signing key */
55 static uint8_t old_signing_key[HASHSIZE];
56
57 /** @brief Signing key validity limit or 0 if none */
58 static time_t signing_key_validity_limit;
59
60 /** @brief Hash of revoked cookies */
61 static hash *revoked;
62
63 /** @brief Callback to expire revocation list */
64 static int revoked_cleanup_callback(const char *key, void *value,
65                                     void *u) {
66   if(*(time_t *)value < *(time_t *)u)
67     hash_remove(revoked, key);
68   return 0;
69 }
70
71 /** @brief Generate a new key */
72 static void newkey(void) {
73   time_t now;
74
75   time(&now);
76   memcpy(old_signing_key, signing_key, HASHSIZE);
77   gcry_randomize(signing_key, HASHSIZE, GCRY_STRONG_RANDOM);
78   signing_key_validity_limit = now + config->cookie_key_lifetime;
79   /* Now is a good time to clean up the revocation list... */
80   if(revoked)
81     hash_foreach(revoked, revoked_cleanup_callback, &now);
82 }
83
84 /** @brief Sign @p subject with @p key and return the base64 of the result
85  * @param key Key to sign with (@ref HASHSIZE bytes)
86  * @param subject Subject string
87  * @return Base64-encoded signature or NULL
88  */
89 static char *sign(const uint8_t *key,
90                   const char *subject) {
91   gcry_error_t e;
92   gcry_md_hd_t h;
93   uint8_t *sig;
94   char *sig64;
95
96   if((e = gcry_md_open(&h, ALGO, GCRY_MD_FLAG_HMAC))) {
97     error(0, "gcry_md_open: %s", gcry_strerror(e));
98     return 0;
99   }
100   if((e = gcry_md_setkey(h, key, HASHSIZE))) {
101     error(0, "gcry_md_setkey: %s", gcry_strerror(e));
102     gcry_md_close(h);
103     return 0;
104   }
105   gcry_md_write(h, subject, strlen(subject));
106   sig = gcry_md_read(h, ALGO);
107   sig64 = mime_to_base64(sig, HASHSIZE);
108   gcry_md_close(h);
109   return sig64;
110 }
111
112 /** @brief Create a login cookie
113  * @param user Username
114  * @return Cookie or NULL
115  */
116 char *make_cookie(const char *user) {
117   const char *password;
118   time_t now;
119   char *b, *bp, *c, *g;
120
121   /* semicolons aren't allowed in usernames */
122   if(strchr(user, ';')) {
123     error(0, "make_cookie for username with semicolon");
124     return 0;
125   }
126   /* look up the password */
127   password = trackdb_get_password(user);
128   if(!password) {
129     error(0, "make_cookie for nonexistent user");
130     return 0;
131   }
132   /* make sure we have a valid signing key */
133   time(&now);
134   if(now >= signing_key_validity_limit)
135     newkey();
136   /* construct the subject */
137   byte_xasprintf(&b, "%jx;%s;", (intmax_t)now + config->cookie_login_lifetime,
138                  urlencodestring(user));
139   byte_xasprintf(&bp, "%s%s", b, password);
140   /* sign it */
141   if(!(g = sign(signing_key, bp)))
142     return 0;
143   /* put together the final cookie */
144   byte_xasprintf(&c, "%s%s", b, g);
145   return c;
146 }
147
148 /** @brief Verify a cookie
149  * @param cookie Cookie to verify
150  * @return Verified user or NULL
151  */
152 char *verify_cookie(const char *cookie) {
153   char *c1, *c2;
154   intmax_t t;
155   time_t now;
156   char *user, *bp, *sig;
157   const char *password;
158
159   /* check the revocation list */
160   if(revoked && hash_find(revoked, cookie)) {
161     error(0, "attempt to log in with revoked cookie");
162     return 0;
163   }
164   /* parse the cookie */
165   errno = 0;
166   t = strtoimax(cookie, &c1, 16);
167   if(errno) {
168     error(errno, "error parsing cookie timestamp");
169     return 0;
170   }
171   if(*c1 != ';') {
172     error(0, "invalid cookie timestamp");
173     return 0;
174   }
175   /* There'd better be two semicolons */
176   c2 = strchr(c1 + 1, ';');
177   if(c2 == 0) {
178     error(0, "invalid cookie syntax");
179     return 0;
180   }
181   /* Extract the username */
182   user = xstrndup(c1 + 1, c2 - (c1 + 1));
183   /* check expiry */
184   time(&now);
185   if(now >= t) {
186     error(0, "cookie has expired");
187     return 0;
188   }
189   /* look up the password */
190   password = trackdb_get_password(user);
191   if(!password) {
192     error(0, "verify_cookie for nonexistent user");
193     return 0;
194   }
195   /* construct the expected subject.  We re-encode the timestamp and the
196    * password. */
197   byte_xasprintf(&bp, "%jx;%s;%s", t, urlencodestring(user), password);
198   /* Compute the expected signature.  NB we base64 the expected signature and
199    * compare that rather than exposing our base64 parser to the cookie. */
200   if(!(sig = sign(signing_key, bp)))
201     return 0;
202   if(!strcmp(sig, c2 + 1))
203     return user;
204   /* that didn't match, try the old key */
205   if(!(sig = sign(old_signing_key, bp)))
206     return 0;
207   if(!strcmp(sig, c2 + 1))
208     return user;
209   /* that didn't match either */
210   error(0, "cookie signature does not match");
211   return 0;
212 }
213
214 /** @brief Revoke a cookie
215  * @param cookie Cookie to revoke
216  *
217  * Further attempts to log in with @p cookie will fail.
218  */
219 void revoke_cookie(const char *cookie) {
220   time_t when;
221   char *ptr;
222
223   /* find the cookie's expiry time */
224   errno = 0;
225   when = (time_t)strtoimax(cookie, &ptr, 16);
226   /* reject bogus cookies */
227   if(errno)
228     return;
229   if(*ptr != ';')
230     return;
231   /* make sure the revocation list exists */
232   if(!revoked)
233     revoked = hash_new(sizeof(time_t));
234   /* add the cookie to it; its value is the expiry time */
235   hash_add(revoked, cookie, &when, HASH_INSERT);
236 }
237
238 /*
239 Local Variables:
240 c-basic-offset:2
241 comment-column:40
242 fill-column:79
243 indent-tabs-mode:nil
244 End:
245 */