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