]> mj.ucw.cz Git - subauth.git/blob - server/auth.c
Debian: Re-packaged for Bookworm
[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       uint allow_passwd_auth;
268       if (!get_uint(ja, "p", &allow_passwd_auth))
269         allow_passwd_auth = 0;
270       aa->allow_passwd_auth = allow_passwd_auth;
271
272       struct json_node **jts = get_array(ja, "t");
273       if (jts)
274         {
275           for (uint j=0; j < GARY_SIZE(jts); j++)
276             {
277               struct json_node *jt = jts[j];
278               if (jt->type != JSON_OBJECT)
279                 db_corrupted("Token is not an object");
280
281               uint type;
282               if (!get_uint(jt, "t", &type) || type >= TOKEN_NUM_TYPES)
283                 db_corrupted("Token has no valid type");
284
285               const char *salt = get_string(jt, "s");
286               const char *hash = get_string(jt, "h");
287               const char *cmt = get_string(jt, "c");
288               const char *ident = get_string(jt, "i");
289               if (!salt || !hash)
290                 db_corrupted("Token must have salt and hash");
291
292               struct auth_token *at = auth_create_token(aa);
293               at->type = type;
294               at->salt = xstrdup(salt);
295               at->hash = xstrdup(hash);
296               at->comment = xstrdup(cmt);
297               at->ident = xstrdup(ident);
298
299               uint lastmod;
300               if (!get_uint(jt, "m", &lastmod))
301                 db_corrupted("Token has invalid last modified time");
302               at->last_modified = lastmod;
303
304               if (!get_uint(jt, "n", &at->iterations))
305                 db_corrupted("Token has invalid number of hash iterations");
306             }
307         }
308     }
309 }
310
311 void auth_change_token_comment(struct auth_token *at, const char *comment)
312 {
313   xfree(at->comment);
314   at->comment = xstrdup(comment);
315 }
316
317 static void db_read(void)
318 {
319   struct fastbuf *fb = bopen_try(database_name, O_RDONLY, 65536);
320   if (!fb)
321     {
322       msg(L_WARN, "No database file found, starting with no users");
323       return;
324     }
325
326   struct json_context *js = json_new();
327   json_set_input(js, fb);
328
329   struct json_node *sep = json_next_token(js);
330   if (sep->type != JSON_BEGIN_ARRAY)
331     db_corrupted("Does not start with '['");
332
333   for (;;)
334     {
335       json_push(js);
336
337       sep = json_peek_token(js);
338       if (sep->type == JSON_END_ARRAY)
339         {
340           json_next_token(js);
341           json_pop(js);
342           break;
343         }
344       else if (sep->type == JSON_VALUE_SEP)
345         json_next_token(js);
346
347       struct json_node *ju = json_next_value(js);
348       db_parse_user(ju);
349       json_pop(js);
350     }
351
352   sep = json_next_token(js);
353   if (sep->type != JSON_EOF)
354     db_corrupted("Garbage at the end");
355
356   json_delete(js);
357   bclose(fb);
358 }
359
360 void db_write(void)
361 {
362   char *temp_name = stk_strcat(database_name, ".new");
363   struct fastbuf *fb = bopen(temp_name, O_WRONLY | O_CREAT | O_TRUNC, 65536);
364   struct json_context *js = json_new();
365   json_set_output(js, fb);
366   bputs(fb, "[\n");
367   uint cnt = 0;
368
369   HASH_FOR_ALL(user_hash, au)
370     {
371       json_push(js);
372       if (cnt)
373         bputs(fb, ",\n");
374       struct json_node *ju = json_new_object(js);
375       json_object_set(ju, "l", json_new_string_ref(js, au->login));
376       struct json_node *jas = json_new_array(js);
377       json_object_set(ju, "a", jas);
378       CLIST_FOR_EACH(struct auth_acct *, aa, au->accounts)
379         {
380           struct json_node *ja = json_new_object(js);
381           json_array_append(jas, ja);
382           json_object_set(ja, "z", json_new_string_ref(js, aa->zone->name));
383           json_object_set(ja, "p", json_new_number(js, aa->allow_passwd_auth));
384           struct json_node *jts = json_new_array(js);
385           CLIST_FOR_EACH(struct auth_token *, at, aa->tokens)
386             {
387               struct json_node *jt = json_new_object(js);
388               json_array_append(jts, jt);
389               json_object_set(jt, "t", json_new_number(js, at->type));
390               if (at->salt)
391                 json_object_set(jt, "s", json_new_string_ref(js, at->salt));
392               if (at->hash)
393                 json_object_set(jt, "h", json_new_string_ref(js, at->hash));
394               if (at->comment)
395                 json_object_set(jt, "c", json_new_string_ref(js, at->comment));
396               if (at->ident)
397                 json_object_set(jt, "i", json_new_string_ref(js, at->ident));
398               json_object_set(jt, "n", json_new_number(js, at->iterations));
399               json_object_set(jt, "m", json_new_number(js, at->last_modified));
400             }
401           if (GARY_SIZE(jts->elements))
402             json_object_set(ja, "t", jts);
403         }
404       json_write_value(js, ju);
405       json_pop(js);
406       cnt++;
407     }
408   HASH_END_FOR;
409
410   bputs(fb, "\n]\n");
411   json_delete(js);
412   bclose(fb);
413
414   char *old_name = stk_strcat(database_name, ".old");
415   if (rename(database_name, old_name) < 0 && errno != ENOENT)
416     die("Cannot rename %s to %s: %m", database_name, old_name);
417
418   if (rename(temp_name, database_name) < 0)
419     die("Cannot rename %s to %s: %m", temp_name, database_name);
420 }
421
422 void auth_init(void)
423 {
424   if (!gcry_check_version(GCRYPT_VERSION))
425     die("libgcrypt version mismatch");
426   gcry_control(GCRYCTL_DISABLE_SECMEM, 0);
427   gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
428
429   user_hash_init();
430   db_read();
431   auth_create_fake_token();
432 }