chiark / gitweb /
Fiddle with CSS+HTML in effort to get more consistent buttons
[disorder] / lib / cookies.c
... / ...
CommitLineData
1/*
2 * This file is part of DisOrder
3 * Copyright (C) 2007, 2008 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 "common.h"
25
26#include <errno.h>
27#include <time.h>
28#include <gcrypt.h>
29#include <pcre.h>
30
31#include "cookies.h"
32#include "hash.h"
33#include "mem.h"
34#include "log.h"
35#include "printf.h"
36#include "base64.h"
37#include "configuration.h"
38#include "kvp.h"
39#include "trackdb.h"
40
41/** @brief Hash function used in signing HMAC */
42#define ALGO GCRY_MD_SHA1
43
44/** @brief Size of key to use */
45#define HASHSIZE 20
46
47/** @brief Signing key */
48static uint8_t signing_key[HASHSIZE];
49
50/** @brief Previous signing key */
51static uint8_t old_signing_key[HASHSIZE];
52
53/** @brief Signing key validity limit or 0 if none */
54static time_t signing_key_validity_limit;
55
56/** @brief Hash of revoked cookies */
57static hash *revoked;
58
59/** @brief Callback to expire revocation list */
60static int revoked_cleanup_callback(const char *key, void *value,
61 void *u) {
62 if(*(time_t *)value < *(time_t *)u)
63 hash_remove(revoked, key);
64 return 0;
65}
66
67/** @brief Generate a new key */
68static void newkey(void) {
69 time_t now;
70
71 time(&now);
72 memcpy(old_signing_key, signing_key, HASHSIZE);
73 gcry_randomize(signing_key, HASHSIZE, GCRY_STRONG_RANDOM);
74 signing_key_validity_limit = now + config->cookie_key_lifetime;
75 /* Now is a good time to clean up the revocation list... */
76 if(revoked)
77 hash_foreach(revoked, revoked_cleanup_callback, &now);
78}
79
80/** @brief Base64 mapping table for cookies
81 *
82 * Stupid Safari cannot cope with quoted cookies, so cookies had better not
83 * need quoting. We use $ to separate the parts of the cookie and +%# to where
84 * MIME uses +/=; see @ref base64.c. See http_separator() for the characters
85 * to avoid.
86 */
87static const char cookie_base64_table[] =
88 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+%#";
89
90/** @brief Sign @p subject with @p key and return the base64 of the result
91 * @param key Key to sign with (@ref HASHSIZE bytes)
92 * @param subject Subject string
93 * @return Base64-encoded signature or NULL
94 */
95static char *sign(const uint8_t *key,
96 const char *subject) {
97 gcry_error_t e;
98 gcry_md_hd_t h;
99 uint8_t *sig;
100 char *sig64;
101
102 if((e = gcry_md_open(&h, ALGO, GCRY_MD_FLAG_HMAC))) {
103 error(0, "gcry_md_open: %s", gcry_strerror(e));
104 return 0;
105 }
106 if((e = gcry_md_setkey(h, key, HASHSIZE))) {
107 error(0, "gcry_md_setkey: %s", gcry_strerror(e));
108 gcry_md_close(h);
109 return 0;
110 }
111 gcry_md_write(h, subject, strlen(subject));
112 sig = gcry_md_read(h, ALGO);
113 sig64 = generic_to_base64(sig, HASHSIZE, cookie_base64_table);
114 gcry_md_close(h);
115 return sig64;
116}
117
118/** @brief Create a login cookie
119 * @param user Username
120 * @return Cookie or NULL
121 */
122char *make_cookie(const char *user) {
123 const char *password;
124 time_t now;
125 char *b, *bp, *c, *g;
126
127 /* dollar signs aren't allowed in usernames */
128 if(strchr(user, '$')) {
129 error(0, "make_cookie for username with dollar sign");
130 return 0;
131 }
132 /* look up the password */
133 password = trackdb_get_password(user);
134 if(!password) {
135 error(0, "make_cookie for nonexistent user");
136 return 0;
137 }
138 /* make sure we have a valid signing key */
139 time(&now);
140 if(now >= signing_key_validity_limit)
141 newkey();
142 /* construct the subject */
143 byte_xasprintf(&b, "%jx$%s$", (intmax_t)now + config->cookie_login_lifetime,
144 urlencodestring(user));
145 byte_xasprintf(&bp, "%s%s", b, password);
146 /* sign it */
147 if(!(g = sign(signing_key, bp)))
148 return 0;
149 /* put together the final cookie */
150 byte_xasprintf(&c, "%s%s", b, g);
151 return c;
152}
153
154/** @brief Verify a cookie
155 * @param cookie Cookie to verify
156 * @param rights Where to store rights value
157 * @return Verified user or NULL
158 */
159char *verify_cookie(const char *cookie, rights_type *rights) {
160 char *c1, *c2;
161 intmax_t t;
162 time_t now;
163 char *user, *bp, *sig;
164 const char *password;
165 struct kvp *k;
166
167 /* check the revocation list */
168 if(revoked && hash_find(revoked, cookie)) {
169 error(0, "attempt to log in with revoked cookie");
170 return 0;
171 }
172 /* parse the cookie */
173 errno = 0;
174 t = strtoimax(cookie, &c1, 16);
175 if(errno) {
176 error(errno, "error parsing cookie timestamp");
177 return 0;
178 }
179 if(*c1 != '$') {
180 error(0, "invalid cookie timestamp");
181 return 0;
182 }
183 /* There'd better be two dollar signs */
184 c2 = strchr(c1 + 1, '$');
185 if(c2 == 0) {
186 error(0, "invalid cookie syntax");
187 return 0;
188 }
189 /* Extract the username */
190 user = xstrndup(c1 + 1, c2 - (c1 + 1));
191 /* check expiry */
192 time(&now);
193 if(now >= t) {
194 error(0, "cookie has expired");
195 return 0;
196 }
197 /* look up the password */
198 k = trackdb_getuserinfo(user);
199 if(!k) {
200 error(0, "verify_cookie for nonexistent user");
201 return 0;
202 }
203 password = kvp_get(k, "password");
204 if(!password) password = "";
205 if(parse_rights(kvp_get(k, "rights"), rights, 1))
206 return 0;
207 /* construct the expected subject. We re-encode the timestamp and the
208 * password. */
209 byte_xasprintf(&bp, "%jx$%s$%s", t, urlencodestring(user), password);
210 /* Compute the expected signature. NB we base64 the expected signature and
211 * compare that rather than exposing our base64 parser to the cookie. */
212 if(!(sig = sign(signing_key, bp)))
213 return 0;
214 if(!strcmp(sig, c2 + 1))
215 return user;
216 /* that didn't match, try the old key */
217 if(!(sig = sign(old_signing_key, bp)))
218 return 0;
219 if(!strcmp(sig, c2 + 1))
220 return user;
221 /* that didn't match either */
222 error(0, "cookie signature does not match");
223 return 0;
224}
225
226/** @brief Revoke a cookie
227 * @param cookie Cookie to revoke
228 *
229 * Further attempts to log in with @p cookie will fail.
230 */
231void revoke_cookie(const char *cookie) {
232 time_t when;
233 char *ptr;
234
235 /* find the cookie's expiry time */
236 errno = 0;
237 when = (time_t)strtoimax(cookie, &ptr, 16);
238 /* reject bogus cookies */
239 if(errno)
240 return;
241 if(*ptr != '$')
242 return;
243 /* make sure the revocation list exists */
244 if(!revoked)
245 revoked = hash_new(sizeof(time_t));
246 /* add the cookie to it; its value is the expiry time */
247 hash_add(revoked, cookie, &when, HASH_INSERT);
248}
249
250/*
251Local Variables:
252c-basic-offset:2
253comment-column:40
254fill-column:79
255indent-tabs-mode:nil
256End:
257*/