chiark / gitweb /
Grey out edit playlists menu item if server does not appear to support
[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 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 */
48 static uint8_t signing_key[HASHSIZE];
49
50 /** @brief Previous signing key */
51 static uint8_t old_signing_key[HASHSIZE];
52
53 /** @brief Signing key validity limit or 0 if none */
54 static time_t signing_key_validity_limit;
55
56 /** @brief Hash of revoked cookies */
57 static hash *revoked;
58
59 /** @brief Callback to expire revocation list */
60 static 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 */
68 static 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  */
87 static 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  */
95 static 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  */
122 char *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  */
159 char *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  */
231 void 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 /*
251 Local Variables:
252 c-basic-offset:2
253 comment-column:40
254 fill-column:79
255 indent-tabs-mode:nil
256 End:
257 */