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