]> mj.ucw.cz Git - subauth.git/blob - server/auth.c
Release target including pre-building HTML man pages
[subauth.git] / server / auth.c
1 /*
2  *      Sub-authentication Daemon: Authentication database
3  *
4  *      (c) 2017 Martin Mares <mj@ucw.cz>
5  */
6
7 #include <ucw/lib.h>
8 #include <ucw/base64.h>
9 #include <ucw/gary.h>
10 #include <ucw/stkstring.h>
11 #include <ucw/string.h>
12 #include <ucw/unaligned.h>
13
14 #include <errno.h>
15 #include <fcntl.h>
16 #include <gcrypt.h>
17 #include <time.h>
18 #include <unistd.h>
19
20 #include "subauthd.h"
21
22 struct auth_token *auth_fake_token;
23
24 #define HASH_NODE struct auth_user
25 #define HASH_PREFIX(x) user_hash_##x
26 #define HASH_KEY_ENDSTRING login
27 #define HASH_WANT_FIND
28 #define HASH_WANT_LOOKUP
29 #define HASH_WANT_REMOVE
30 #define HASH_GIVE_INIT_DATA
31
32 static void user_hash_init_data(struct auth_user *au)
33 {
34   clist_init(&au->accounts);
35 }
36
37 #include <ucw/hashtable.h>
38
39 struct auth_zone *auth_find_zone(const char *name)
40 {
41   CLIST_FOR_EACH(struct auth_zone *, z, zone_list)
42     if (!strcmp(z->name, name))
43       return z;
44   return NULL;
45 }
46
47 struct auth_user *auth_find_user(const char *login, bool create)
48 {
49   if (create)
50     return user_hash_lookup((char *) login);
51   else
52     return user_hash_find((char *) login);
53 }
54
55 struct auth_acct *auth_find_acct(struct auth_user *au, struct auth_zone *az, bool create)
56 {
57   CLIST_FOR_EACH(struct auth_acct *, aa, au->accounts)
58     if (aa->zone == az)
59       return aa;
60
61   if (!create)
62     return NULL;
63
64   struct auth_acct *aa = xmalloc_zero(sizeof(*aa));
65   clist_add_tail(&au->accounts, &aa->n);
66   aa->user = au;
67   aa->zone = az;
68   clist_init(&aa->tokens);
69   return aa;
70 }
71
72 struct auth_token *auth_find_token_passwd(struct auth_acct *aa)
73 {
74   CLIST_FOR_EACH(struct auth_token *, at, aa->tokens)
75     if (at->type == TOKEN_PASSWORD)
76       return at;
77   return NULL;
78 }
79
80 struct auth_token *auth_find_token_generated(struct auth_acct *aa, const char *ident)
81 {
82   CLIST_FOR_EACH(struct auth_token *, at, aa->tokens)
83     if (at->type == TOKEN_GENERATED && !strcmp(at->ident, ident))
84       return at;
85   return NULL;
86 }
87
88 void auth_delete_user(struct auth_user *au)
89 {
90   struct auth_acct *aa;
91   while (aa = clist_head(&au->accounts))
92     auth_delete_acct(aa);
93
94   user_hash_remove(au);
95 }
96
97 void auth_delete_acct(struct auth_acct *ac)
98 {
99   struct auth_token *at;
100   while (at = clist_head(&ac->tokens))
101     auth_delete_token(at);
102   clist_remove(&ac->n);
103 }
104
105 void auth_delete_token(struct auth_token *at)
106 {
107   clist_remove(&at->n);
108   xfree(at->salt);
109   xfree(at->hash);
110   xfree(at->comment);
111 }
112
113 struct auth_token *auth_create_token(struct auth_acct *aa)
114 {
115   struct auth_token *at = xmalloc_zero(sizeof(*at));
116   clist_add_tail(&aa->tokens, &at->n);
117   at->acct = aa;
118   at->last_modified = time(NULL);
119   return at;
120 }
121
122 static void auth_hash(char *hash_text, struct auth_token *at, const char *passwd)
123 {
124   // This is PBKDF2 based on SHA-256-HMAC
125
126   gcry_md_hd_t md;
127   if (gcry_md_open(&md, GCRY_MD_SHA256, GCRY_MD_FLAG_HMAC) != GPG_ERR_NO_ERROR)
128     die("gcry_md_open(CGRY_MD_SHA256) failed");
129   if (gcry_md_setkey(md, passwd, strlen(passwd)) != GPG_ERR_NO_ERROR)
130     die("gcry_md_setkey() failed");
131
132   byte current_hash[HASH_BYTES];
133   gcry_md_write(md, at->salt, strlen(at->salt));
134   byte zeroes[4] = { 0, 0, 0, 0 };
135   gcry_md_write(md, zeroes, 4);
136   memcpy(current_hash, gcry_md_read(md, 0), HASH_BYTES);
137
138   byte accumulator[HASH_BYTES];
139   memcpy(accumulator, current_hash, HASH_BYTES);
140
141   for (uint i=1; i < at->iterations; i++)
142     {
143       gcry_md_reset(md);
144       gcry_md_write(md, current_hash, HASH_BYTES);
145       memcpy(current_hash, gcry_md_read(md, 0), HASH_BYTES);
146       for (uint j=0; j<HASH_BYTES; j++)
147         accumulator[j] ^= current_hash[j];
148     }
149
150   gcry_md_close(md);
151
152   uint out_len = base64_encode(hash_text, accumulator, HASH_BYTES);
153   hash_text[out_len] = 0;
154 }
155
156 static void set_salt(struct auth_token *at)
157 {
158   byte salt_bytes[DEFAULT_SALT_BYTES];
159   gcry_randomize(salt_bytes, DEFAULT_SALT_BYTES, GCRY_STRONG_RANDOM);
160
161   char salt_text[2*DEFAULT_SALT_BYTES + 1];
162   uint salt_len = base64_encode(salt_text, salt_bytes, DEFAULT_SALT_BYTES);
163   salt_text[salt_len] = 0;
164
165   at->salt = xstrdup(salt_text);
166   at->iterations = DEFAULT_HASH_ITERATIONS;
167 }
168
169 void auth_set_token_passwd(struct auth_token *at, const char *passwd)
170 {
171   at->type = TOKEN_PASSWORD;
172   set_salt(at);
173
174   char hash[MAX_TEXT_HASH_SIZE];
175   auth_hash(hash, at, passwd);
176   at->hash = xstrdup(hash);
177 }
178
179 char *auth_set_token_generated(struct auth_token *at, const char *comment, struct mempool *pool)
180 {
181   char ident_text[2*DEFAULT_IDENT_BYTES + 1];
182   do
183     {
184       byte ident_bytes[DEFAULT_IDENT_BYTES];
185       gcry_randomize(ident_bytes, DEFAULT_IDENT_BYTES, GCRY_STRONG_RANDOM);
186       mem_to_hex(ident_text, ident_bytes, DEFAULT_IDENT_BYTES, 0);
187     }
188   while (auth_find_token_generated(at->acct, ident_text));
189
190   at->type = TOKEN_GENERATED;
191   at->ident = xstrdup(ident_text);
192   at->comment = xstrdup(comment);
193   set_salt(at);
194
195   byte token_bytes[DEFAULT_GENERATED_BYTES];
196   gcry_randomize(token_bytes, DEFAULT_GENERATED_BYTES, GCRY_STRONG_RANDOM);
197
198   uint token_len = 2*DEFAULT_IDENT_BYTES + 1 + 2*DEFAULT_GENERATED_BYTES + 1;
199   char *token_text = mp_alloc(pool, token_len);
200   memcpy(token_text, ident_text, 2*DEFAULT_IDENT_BYTES);
201   char *token_rhs = token_text + 2*DEFAULT_IDENT_BYTES;
202   *token_rhs++ = '-';
203   uint out_len = base64_encode(token_rhs, token_bytes, DEFAULT_GENERATED_BYTES);
204   if (out_len > 0 && token_rhs[out_len-1] == '=')
205     out_len--;
206   token_rhs[out_len] = 0;
207
208   char hash[MAX_TEXT_HASH_SIZE];
209   auth_hash(hash, at, token_rhs);
210   at->hash = xstrdup(hash);
211
212   return token_text;
213 }
214
215 bool auth_check_token(struct auth_token *at, const char *passwd)
216 {
217   char hash[MAX_TEXT_HASH_SIZE];
218   auth_hash(hash, at, passwd);
219   return !strcmp(at->hash, hash);
220 }
221
222 static void auth_create_fake_token(void)
223 {
224   struct auth_token *at = xmalloc_zero(sizeof(*at));
225   auth_set_token_passwd(at, "TheKeyIsUnderTheFlyingCarpet");
226   auth_fake_token = at;
227 }
228
229 static void NONRET db_corrupted(const char *reason)
230 {
231   die("Database file corrupted: %s", reason);
232 }
233
234 static void db_parse_user(struct json_node *ju)
235 {
236   if (ju->type != JSON_OBJECT)
237     db_corrupted("Expected object");
238
239   const char *login = get_string(ju, "l");
240   if (!login)
241     db_corrupted("User has no login");
242
243   struct json_node **jas = get_array(ju, "a");
244   if (!jas)
245     db_corrupted("User has no accounts");
246
247   struct auth_user *au = auth_find_user(login, 1);
248   if (clist_head(&au->accounts))
249     db_corrupted("Multiple users with the same login");
250
251   for (uint i=0; i < GARY_SIZE(jas); i++)
252     {
253       struct json_node *ja = jas[i];
254
255       const char *zone_name = get_string(ja, "z");
256       if (!zone_name)
257         db_corrupted("Account has no zone");
258
259       struct auth_acct *aa = xmalloc_zero(sizeof(*aa));
260       clist_add_tail(&au->accounts, &aa->n);
261       aa->user = au;
262       aa->zone = auth_find_zone(zone_name);
263       if (!aa->zone)
264         die("Database defines accounts in zone %s, which is not configured", zone_name);
265       clist_init(&aa->tokens);
266
267       struct json_node **jts = get_array(ja, "t");
268       if (jts)
269         {
270           for (uint j=0; j < GARY_SIZE(jts); j++)
271             {
272               struct json_node *jt = jts[j];
273               if (jt->type != JSON_OBJECT)
274                 db_corrupted("Token is not an object");
275
276               uint type;
277               if (!get_uint(jt, "t", &type) || type >= TOKEN_NUM_TYPES)
278                 db_corrupted("Token has no valid type");
279
280               const char *salt = get_string(jt, "s");
281               const char *hash = get_string(jt, "h");
282               const char *cmt = get_string(jt, "c");
283               const char *ident = get_string(jt, "i");
284               if (!salt || !hash)
285                 db_corrupted("Token must have salt and hash");
286
287               struct auth_token *at = auth_create_token(aa);
288               at->type = type;
289               at->salt = xstrdup(salt);
290               at->hash = xstrdup(hash);
291               at->comment = xstrdup(cmt);
292               at->ident = xstrdup(ident);
293
294               uint lastmod;
295               if (!get_uint(jt, "m", &lastmod))
296                 db_corrupted("Token has invalid last modified time");
297               at->last_modified = lastmod;
298
299               if (!get_uint(jt, "n", &at->iterations))
300                 db_corrupted("Token has invalid number of hash iterations");
301             }
302         }
303     }
304 }
305
306 void auth_change_token_comment(struct auth_token *at, const char *comment)
307 {
308   xfree(at->comment);
309   at->comment = xstrdup(comment);
310 }
311
312 static void db_read(void)
313 {
314   struct fastbuf *fb = bopen_try(database_name, O_RDONLY, 65536);
315   if (!fb)
316     {
317       msg(L_WARN, "No database file found, starting with no users");
318       return;
319     }
320
321   struct json_context *js = json_new();
322   json_set_input(js, fb);
323
324   struct json_node *sep = json_next_token(js);
325   if (sep->type != JSON_BEGIN_ARRAY)
326     db_corrupted("Does not start with '['");
327
328   for (;;)
329     {
330       json_push(js);
331
332       sep = json_peek_token(js);
333       if (sep->type == JSON_END_ARRAY)
334         {
335           json_next_token(js);
336           json_pop(js);
337           break;
338         }
339       else if (sep->type == JSON_VALUE_SEP)
340         json_next_token(js);
341
342       struct json_node *ju = json_next_value(js);
343       db_parse_user(ju);
344       json_pop(js);
345     }
346
347   sep = json_next_token(js);
348   if (sep->type != JSON_EOF)
349     db_corrupted("Garbage at the end");
350
351   json_delete(js);
352   bclose(fb);
353 }
354
355 void db_write(void)
356 {
357   char *temp_name = stk_strcat(database_name, ".new");
358   struct fastbuf *fb = bopen(temp_name, O_WRONLY | O_CREAT | O_TRUNC, 65536);
359   struct json_context *js = json_new();
360   json_set_output(js, fb);
361   bputs(fb, "[\n");
362   uint cnt = 0;
363
364   HASH_FOR_ALL(user_hash, au)
365     {
366       json_push(js);
367       if (cnt)
368         bputs(fb, ",\n");
369       struct json_node *ju = json_new_object(js);
370       json_object_set(ju, "l", json_new_string_ref(js, au->login));
371       struct json_node *jas = json_new_array(js);
372       json_object_set(ju, "a", jas);
373       CLIST_FOR_EACH(struct auth_acct *, aa, au->accounts)
374         {
375           struct json_node *ja = json_new_object(js);
376           json_array_append(jas, ja);
377           json_object_set(ja, "z", json_new_string_ref(js, aa->zone->name));
378           struct json_node *jts = json_new_array(js);
379           CLIST_FOR_EACH(struct auth_token *, at, aa->tokens)
380             {
381               struct json_node *jt = json_new_object(js);
382               json_array_append(jts, jt);
383               json_object_set(jt, "t", json_new_number(js, at->type));
384               if (at->salt)
385                 json_object_set(jt, "s", json_new_string_ref(js, at->salt));
386               if (at->hash)
387                 json_object_set(jt, "h", json_new_string_ref(js, at->hash));
388               if (at->comment)
389                 json_object_set(jt, "c", json_new_string_ref(js, at->comment));
390               if (at->ident)
391                 json_object_set(jt, "i", json_new_string_ref(js, at->ident));
392               json_object_set(jt, "n", json_new_number(js, at->iterations));
393               json_object_set(jt, "m", json_new_number(js, at->last_modified));
394             }
395           if (GARY_SIZE(jts->elements))
396             json_object_set(ja, "t", jts);
397         }
398       json_write_value(js, ju);
399       json_pop(js);
400       cnt++;
401     }
402   HASH_END_FOR;
403
404   bputs(fb, "\n]\n");
405   json_delete(js);
406   bclose(fb);
407
408   char *old_name = stk_strcat(database_name, ".old");
409   if (rename(database_name, old_name) < 0 && errno != ENOENT)
410     die("Cannot rename %s to %s: %m", database_name, old_name);
411
412   if (rename(temp_name, database_name) < 0)
413     die("Cannot rename %s to %s: %m", temp_name, database_name);
414 }
415
416 void auth_init(void)
417 {
418   if (!gcry_check_version(GCRYPT_VERSION))
419     die("libgcrypt version mismatch");
420   gcry_control(GCRYCTL_DISABLE_SECMEM, 0);
421   gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
422
423   user_hash_init();
424   db_read();
425   auth_create_fake_token();
426 }