chiark / gitweb /
Use users.db. trackdb* moves to lib/, as it's now used by client.c to
[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
RK
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"
f0feb22e 43#include "trackdb.h"
b12be54a
RK
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 */
52static uint8_t signing_key[HASHSIZE];
53
54/** @brief Previous signing key */
55static uint8_t old_signing_key[HASHSIZE];
56
57/** @brief Signing key validity limit or 0 if none */
58static time_t signing_key_validity_limit;
59
60/** @brief Hash of revoked cookies */
61static hash *revoked;
62
63/** @brief Callback to expire revocation list */
64static 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 */
72static 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 */
89static 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 */
116char *make_cookie(const char *user) {
f0feb22e 117 const char *password;
b12be54a
RK
118 time_t now;
119 char *b, *bp, *c, *g;
b12be54a
RK
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 */
f0feb22e
RK
127 password = trackdb_get_password(user);
128 if(!password) {
b12be54a
RK
129 error(0, "make_cookie for nonexistent user");
130 return 0;
131 }
b12be54a
RK
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 */
152char *verify_cookie(const char *cookie) {
153 char *c1, *c2;
154 intmax_t t;
155 time_t now;
f0feb22e
RK
156 char *user, *bp, *sig;
157 const char *password;
b12be54a
RK
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 */
f0feb22e
RK
190 password = trackdb_get_password(user);
191 if(!password) {
b12be54a
RK
192 error(0, "verify_cookie for nonexistent user");
193 return 0;
194 }
b12be54a
RK
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 */
219void 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/*
239Local Variables:
240c-basic-offset:2
241comment-column:40
242fill-column:79
243indent-tabs-mode:nil
244End:
245*/