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