comparison auth/auth_crypto.c @ 264:04fee26ecce0

add authenticated JRPG conversation platform Add reusable auth/session storage, owned conversation recovery, guest quotas, admin workflows, URL-routed conversation UI, mobile frame support, and parallel browser acceptance. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 07:34:12 -0700
parents
children
comparison
equal deleted inserted replaced
263:ee04e4e69fed 264:04fee26ecce0
1 #include "auth/auth_crypto.h"
2
3 #include <stdio.h>
4 #include <string.h>
5
6 #include <openssl/crypto.h>
7 #include <openssl/evp.h>
8 #include <openssl/hmac.h>
9 #include <openssl/rand.h>
10
11 #define AUTH_CRYPTO_SCRYPT_N 32768
12 #define AUTH_CRYPTO_SCRYPT_R 8
13 #define AUTH_CRYPTO_SCRYPT_P 1
14 #define AUTH_CRYPTO_SCRYPT_MAXMEM (64ULL * 1024ULL * 1024ULL)
15
16 #define AUTH_CRYPTO_PASSWORD_PREFIX "zenbu-scrypt$v=1$N=32768$r=8$p=1$"
17 #define AUTH_CRYPTO_COOKIE_VERSION "v1"
18 #define AUTH_CRYPTO_COOKIE_HMAC_DOMAIN "zenbu-auth-guest-cookie-v1:"
19 #define AUTH_CRYPTO_IP_HMAC_DOMAIN "zenbu-auth-ip-binding-v1:"
20 #define AUTH_CRYPTO_HMAC_INPUT_SIZE 384
21
22 static boolean auth__bounded_length(
23 const char *text,
24 size_t maximum,
25 size_t *p_length)
26 {
27 if (!text || !p_length)
28 {
29 return FALSE;
30 }
31
32 for (size_t i = 0; i <= maximum; ++i)
33 {
34 if (text[i] == '\0')
35 {
36 *p_length = i;
37 return TRUE;
38 }
39 }
40
41 return FALSE;
42 }
43
44 static void auth__hex_encode(
45 const uint8 *bytes,
46 size_t byte_count,
47 char *hex)
48 {
49 static const char alphabet[] = "0123456789abcdef";
50
51 for (size_t i = 0; i < byte_count; ++i)
52 {
53 hex[i * 2] = alphabet[bytes[i] >> 4];
54 hex[i * 2 + 1] = alphabet[bytes[i] & 0x0f];
55 }
56 hex[byte_count * 2] = '\0';
57 }
58
59 static int auth__hex_value(char value)
60 {
61 if (value >= '0' && value <= '9')
62 {
63 return value - '0';
64 }
65 if (value >= 'a' && value <= 'f')
66 {
67 return value - 'a' + 10;
68 }
69 return -1;
70 }
71
72 static boolean auth__hex_decode(
73 const char *hex,
74 size_t hex_length,
75 uint8 *bytes,
76 size_t byte_count)
77 {
78 if (hex_length != byte_count * 2)
79 {
80 return FALSE;
81 }
82
83 for (size_t i = 0; i < byte_count; ++i)
84 {
85 int high = auth__hex_value(hex[i * 2]);
86 int low = auth__hex_value(hex[i * 2 + 1]);
87 if (high < 0 || low < 0)
88 {
89 return FALSE;
90 }
91 bytes[i] = (uint8)((high << 4) | low);
92 }
93
94 return TRUE;
95 }
96
97 static boolean auth__secret_is_valid(
98 const uint8 *cookie_secret,
99 size_t cookie_secret_length)
100 {
101 return cookie_secret &&
102 cookie_secret_length >= AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES &&
103 cookie_secret_length <= AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES;
104 }
105
106 static boolean auth__uuid_is_valid(const char *uuid, size_t uuid_length)
107 {
108 if (uuid_length != AUTH_CRYPTO_GUEST_UUID_SIZE - 1)
109 {
110 return FALSE;
111 }
112
113 for (size_t i = 0; i < uuid_length; ++i)
114 {
115 if (i == 8 || i == 13 || i == 18 || i == 23)
116 {
117 if (uuid[i] != '-')
118 {
119 return FALSE;
120 }
121 }
122 else if (auth__hex_value(uuid[i]) < 0)
123 {
124 return FALSE;
125 }
126 }
127
128 return TRUE;
129 }
130
131 static boolean auth__parse_uint64(
132 const char *digits,
133 size_t digit_count,
134 uint64 *p_value)
135 {
136 if (!digits || !p_value || digit_count == 0 || digit_count > 20 ||
137 (digit_count > 1 && digits[0] == '0'))
138 {
139 return FALSE;
140 }
141
142 uint64 value = 0;
143 uint64 maximum = (uint64)-1;
144 for (size_t i = 0; i < digit_count; ++i)
145 {
146 if (digits[i] < '0' || digits[i] > '9')
147 {
148 return FALSE;
149 }
150
151 uint8 digit = (uint8)(digits[i] - '0');
152 if (value > (maximum - digit) / 10)
153 {
154 return FALSE;
155 }
156 value = value * 10 + digit;
157 }
158
159 *p_value = value;
160 return TRUE;
161 }
162
163 static Auth_Crypto_Result auth__hmac_sha256(
164 const uint8 *secret,
165 size_t secret_length,
166 const char *domain,
167 const char *value,
168 size_t value_length,
169 uint8 digest[32])
170 {
171 size_t domain_length = strlen(domain);
172 if (domain_length + value_length > AUTH_CRYPTO_HMAC_INPUT_SIZE)
173 {
174 return AUTH_CRYPTO_INVALID_ARGUMENT;
175 }
176
177 uint8 input[AUTH_CRYPTO_HMAC_INPUT_SIZE];
178 memcpy(input, domain, domain_length);
179 memcpy(input + domain_length, value, value_length);
180
181 unsigned int digest_length = 0;
182 uint8 *result = HMAC(
183 EVP_sha256(),
184 secret,
185 (int)secret_length,
186 input,
187 domain_length + value_length,
188 digest,
189 &digest_length);
190 OPENSSL_cleanse(input, sizeof(input));
191
192 if (!result || digest_length != 32)
193 {
194 OPENSSL_cleanse(digest, 32);
195 return AUTH_CRYPTO_OPERATION_FAILED;
196 }
197
198 return AUTH_CRYPTO_OK;
199 }
200
201 static boolean auth__split_cookie(
202 const char *cookie,
203 size_t cookie_length,
204 const char *segments[5],
205 size_t segment_lengths[5])
206 {
207 size_t segment_start = 0;
208 size_t segment_count = 0;
209
210 for (size_t i = 0; i <= cookie_length; ++i)
211 {
212 if (i == cookie_length || cookie[i] == '.')
213 {
214 if (segment_count >= 5 || i == segment_start)
215 {
216 return FALSE;
217 }
218 segments[segment_count] = cookie + segment_start;
219 segment_lengths[segment_count] = i - segment_start;
220 ++segment_count;
221 segment_start = i + 1;
222 }
223 }
224
225 return segment_count == 5;
226 }
227
228 static void auth__base64url_32(
229 const uint8 bytes[AUTH_CRYPTO_TOKEN_BYTES],
230 char token[AUTH_CRYPTO_TOKEN_SIZE])
231 {
232 static const char alphabet[] =
233 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
234 size_t input_index = 0;
235 size_t output_index = 0;
236
237 while (input_index + 3 <= AUTH_CRYPTO_TOKEN_BYTES)
238 {
239 uint32 value = ((uint32)bytes[input_index] << 16) |
240 ((uint32)bytes[input_index + 1] << 8) |
241 bytes[input_index + 2];
242 token[output_index++] = alphabet[(value >> 18) & 63];
243 token[output_index++] = alphabet[(value >> 12) & 63];
244 token[output_index++] = alphabet[(value >> 6) & 63];
245 token[output_index++] = alphabet[value & 63];
246 input_index += 3;
247 }
248
249 uint32 remainder = ((uint32)bytes[input_index] << 8) |
250 bytes[input_index + 1];
251 token[output_index++] = alphabet[(remainder >> 10) & 63];
252 token[output_index++] = alphabet[(remainder >> 4) & 63];
253 token[output_index++] = alphabet[(remainder << 2) & 63];
254 token[output_index] = '\0';
255 }
256
257 Auth_Crypto_Result Auth_Crypto_Password_Hash(
258 const char *password,
259 char *encoded_hash,
260 size_t encoded_hash_capacity)
261 {
262 if (!password || !encoded_hash)
263 {
264 return AUTH_CRYPTO_INVALID_ARGUMENT;
265 }
266 if (encoded_hash_capacity < AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE)
267 {
268 return AUTH_CRYPTO_BUFFER_TOO_SMALL;
269 }
270 encoded_hash[0] = '\0';
271
272 size_t password_length = 0;
273 if (!auth__bounded_length(
274 password, AUTH_CRYPTO_PASSWORD_MAX_BYTES, &password_length))
275 {
276 return AUTH_CRYPTO_PASSWORD_TOO_LONG;
277 }
278
279 uint8 salt[AUTH_CRYPTO_PASSWORD_SALT_BYTES];
280 uint8 hash[AUTH_CRYPTO_PASSWORD_HASH_BYTES];
281 if (RAND_bytes(salt, sizeof(salt)) != 1)
282 {
283 OPENSSL_cleanse(salt, sizeof(salt));
284 return AUTH_CRYPTO_RANDOM_FAILED;
285 }
286
287 if (EVP_PBE_scrypt(
288 password,
289 password_length,
290 salt,
291 sizeof(salt),
292 AUTH_CRYPTO_SCRYPT_N,
293 AUTH_CRYPTO_SCRYPT_R,
294 AUTH_CRYPTO_SCRYPT_P,
295 AUTH_CRYPTO_SCRYPT_MAXMEM,
296 hash,
297 sizeof(hash)) != 1)
298 {
299 OPENSSL_cleanse(salt, sizeof(salt));
300 OPENSSL_cleanse(hash, sizeof(hash));
301 return AUTH_CRYPTO_OPERATION_FAILED;
302 }
303
304 char salt_hex[AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2 + 1];
305 char hash_hex[AUTH_CRYPTO_PASSWORD_HASH_BYTES * 2 + 1];
306 auth__hex_encode(salt, sizeof(salt), salt_hex);
307 auth__hex_encode(hash, sizeof(hash), hash_hex);
308
309 int written = snprintf(
310 encoded_hash,
311 encoded_hash_capacity,
312 "%s%s$%s",
313 AUTH_CRYPTO_PASSWORD_PREFIX,
314 salt_hex,
315 hash_hex);
316
317 OPENSSL_cleanse(salt, sizeof(salt));
318 OPENSSL_cleanse(hash, sizeof(hash));
319 OPENSSL_cleanse(hash_hex, sizeof(hash_hex));
320
321 if (written < 0 || (size_t)written >= encoded_hash_capacity)
322 {
323 encoded_hash[0] = '\0';
324 return AUTH_CRYPTO_BUFFER_TOO_SMALL;
325 }
326
327 return AUTH_CRYPTO_OK;
328 }
329
330 Auth_Crypto_Result Auth_Crypto_Password_Verify(
331 const char *password,
332 const char *encoded_hash)
333 {
334 if (!password || !encoded_hash)
335 {
336 return AUTH_CRYPTO_INVALID_ARGUMENT;
337 }
338
339 size_t password_length = 0;
340 if (!auth__bounded_length(
341 password, AUTH_CRYPTO_PASSWORD_MAX_BYTES, &password_length))
342 {
343 return AUTH_CRYPTO_PASSWORD_TOO_LONG;
344 }
345
346 size_t encoded_length = 0;
347 if (!auth__bounded_length(
348 encoded_hash,
349 AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE - 1,
350 &encoded_length))
351 {
352 return AUTH_CRYPTO_MALFORMED;
353 }
354
355 size_t prefix_length = sizeof(AUTH_CRYPTO_PASSWORD_PREFIX) - 1;
356 size_t expected_length = prefix_length +
357 AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2 + 1 +
358 AUTH_CRYPTO_PASSWORD_HASH_BYTES * 2;
359 if (encoded_length != expected_length ||
360 memcmp(encoded_hash, AUTH_CRYPTO_PASSWORD_PREFIX, prefix_length) != 0 ||
361 encoded_hash[prefix_length + AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2] != '$')
362 {
363 return AUTH_CRYPTO_MALFORMED;
364 }
365
366 uint8 salt[AUTH_CRYPTO_PASSWORD_SALT_BYTES];
367 uint8 expected_hash[AUTH_CRYPTO_PASSWORD_HASH_BYTES];
368 uint8 calculated_hash[AUTH_CRYPTO_PASSWORD_HASH_BYTES];
369 const char *salt_hex = encoded_hash + prefix_length;
370 const char *hash_hex =
371 salt_hex + AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2 + 1;
372
373 if (!auth__hex_decode(
374 salt_hex,
375 AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2,
376 salt,
377 sizeof(salt)) ||
378 !auth__hex_decode(
379 hash_hex,
380 AUTH_CRYPTO_PASSWORD_HASH_BYTES * 2,
381 expected_hash,
382 sizeof(expected_hash)))
383 {
384 return AUTH_CRYPTO_MALFORMED;
385 }
386
387 if (EVP_PBE_scrypt(
388 password,
389 password_length,
390 salt,
391 sizeof(salt),
392 AUTH_CRYPTO_SCRYPT_N,
393 AUTH_CRYPTO_SCRYPT_R,
394 AUTH_CRYPTO_SCRYPT_P,
395 AUTH_CRYPTO_SCRYPT_MAXMEM,
396 calculated_hash,
397 sizeof(calculated_hash)) != 1)
398 {
399 OPENSSL_cleanse(salt, sizeof(salt));
400 OPENSSL_cleanse(expected_hash, sizeof(expected_hash));
401 OPENSSL_cleanse(calculated_hash, sizeof(calculated_hash));
402 return AUTH_CRYPTO_OPERATION_FAILED;
403 }
404
405 int comparison = CRYPTO_memcmp(
406 calculated_hash, expected_hash, sizeof(calculated_hash));
407 OPENSSL_cleanse(salt, sizeof(salt));
408 OPENSSL_cleanse(expected_hash, sizeof(expected_hash));
409 OPENSSL_cleanse(calculated_hash, sizeof(calculated_hash));
410
411 return comparison == 0
412 ? AUTH_CRYPTO_OK
413 : AUTH_CRYPTO_AUTHENTICATION_FAILED;
414 }
415
416 Auth_Crypto_Result Auth_Crypto_Password_Hash_Validate(const char *encoded_hash)
417 {
418 if (!encoded_hash)
419 return AUTH_CRYPTO_INVALID_ARGUMENT;
420
421 size_t encoded_length = 0;
422 if (!auth__bounded_length(
423 encoded_hash,
424 AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE - 1,
425 &encoded_length))
426 return AUTH_CRYPTO_MALFORMED;
427
428 size_t prefix_length = sizeof(AUTH_CRYPTO_PASSWORD_PREFIX) - 1;
429 size_t expected_length = prefix_length +
430 AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2 + 1 +
431 AUTH_CRYPTO_PASSWORD_HASH_BYTES * 2;
432 if (encoded_length != expected_length ||
433 memcmp(encoded_hash, AUTH_CRYPTO_PASSWORD_PREFIX, prefix_length) != 0 ||
434 encoded_hash[prefix_length + AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2] != '$')
435 return AUTH_CRYPTO_MALFORMED;
436
437 /* Validate hex characters in salt and hash fields. */
438 const char *salt_hex = encoded_hash + prefix_length;
439 const char *hash_hex = salt_hex + AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2 + 1;
440 for (size_t i = 0; i < AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2; i++)
441 if (auth__hex_value(salt_hex[i]) < 0)
442 return AUTH_CRYPTO_MALFORMED;
443 for (size_t i = 0; i < AUTH_CRYPTO_PASSWORD_HASH_BYTES * 2; i++)
444 if (auth__hex_value(hash_hex[i]) < 0)
445 return AUTH_CRYPTO_MALFORMED;
446
447 return AUTH_CRYPTO_OK;
448 }
449
450 Auth_Crypto_Result Auth_Crypto_Token_Generate(
451 char *token,
452 size_t token_capacity)
453 {
454 if (!token)
455 {
456 return AUTH_CRYPTO_INVALID_ARGUMENT;
457 }
458 if (token_capacity < AUTH_CRYPTO_TOKEN_SIZE)
459 {
460 return AUTH_CRYPTO_BUFFER_TOO_SMALL;
461 }
462 token[0] = '\0';
463
464 uint8 random_bytes[AUTH_CRYPTO_TOKEN_BYTES];
465 if (RAND_bytes(random_bytes, sizeof(random_bytes)) != 1)
466 {
467 OPENSSL_cleanse(random_bytes, sizeof(random_bytes));
468 return AUTH_CRYPTO_RANDOM_FAILED;
469 }
470
471 auth__base64url_32(random_bytes, token);
472 OPENSSL_cleanse(random_bytes, sizeof(random_bytes));
473 return AUTH_CRYPTO_OK;
474 }
475
476 Auth_Crypto_Result Auth_Crypto_Token_Digest(
477 const char *token,
478 char *digest_hex,
479 size_t digest_hex_capacity)
480 {
481 if (!token || !digest_hex)
482 {
483 return AUTH_CRYPTO_INVALID_ARGUMENT;
484 }
485 if (digest_hex_capacity < AUTH_CRYPTO_TOKEN_DIGEST_SIZE)
486 {
487 return AUTH_CRYPTO_BUFFER_TOO_SMALL;
488 }
489 digest_hex[0] = '\0';
490
491 size_t token_length = 0;
492 if (!auth__bounded_length(token, AUTH_CRYPTO_TOKEN_SIZE - 1, &token_length) ||
493 token_length != AUTH_CRYPTO_TOKEN_SIZE - 1)
494 {
495 return AUTH_CRYPTO_MALFORMED;
496 }
497 for (size_t i = 0; i < token_length; ++i)
498 {
499 char value = token[i];
500 if (!((value >= 'A' && value <= 'Z') ||
501 (value >= 'a' && value <= 'z') ||
502 (value >= '0' && value <= '9') ||
503 value == '-' || value == '_'))
504 {
505 return AUTH_CRYPTO_MALFORMED;
506 }
507 }
508
509 uint8 digest[AUTH_CRYPTO_TOKEN_DIGEST_BYTES];
510 unsigned int digest_length = 0;
511 if (EVP_Digest(
512 token,
513 token_length,
514 digest,
515 &digest_length,
516 EVP_sha256(),
517 NULL) != 1 ||
518 digest_length != AUTH_CRYPTO_TOKEN_DIGEST_BYTES)
519 {
520 OPENSSL_cleanse(digest, sizeof(digest));
521 return AUTH_CRYPTO_OPERATION_FAILED;
522 }
523
524 auth__hex_encode(digest, sizeof(digest), digest_hex);
525 OPENSSL_cleanse(digest, sizeof(digest));
526 return AUTH_CRYPTO_OK;
527 }
528
529 Auth_Crypto_Result Auth_Crypto_IP_Binding_Digest(
530 const uint8 *cookie_secret,
531 size_t cookie_secret_length,
532 const char *canonical_peer_ip,
533 char *digest_hex,
534 size_t digest_hex_capacity)
535 {
536 if (!auth__secret_is_valid(cookie_secret, cookie_secret_length) ||
537 !canonical_peer_ip || !digest_hex)
538 {
539 return AUTH_CRYPTO_INVALID_ARGUMENT;
540 }
541 if (digest_hex_capacity < AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE)
542 {
543 return AUTH_CRYPTO_BUFFER_TOO_SMALL;
544 }
545 digest_hex[0] = '\0';
546
547 size_t ip_length = 0;
548 if (!auth__bounded_length(
549 canonical_peer_ip, AUTH_CRYPTO_IP_MAX_BYTES, &ip_length) ||
550 ip_length == 0)
551 {
552 return AUTH_CRYPTO_INVALID_ARGUMENT;
553 }
554
555 uint8 digest[32];
556 Auth_Crypto_Result result = auth__hmac_sha256(
557 cookie_secret,
558 cookie_secret_length,
559 AUTH_CRYPTO_IP_HMAC_DOMAIN,
560 canonical_peer_ip,
561 ip_length,
562 digest);
563 if (result != AUTH_CRYPTO_OK)
564 {
565 return result;
566 }
567
568 auth__hex_encode(digest, sizeof(digest), digest_hex);
569 OPENSSL_cleanse(digest, sizeof(digest));
570 return AUTH_CRYPTO_OK;
571 }
572
573 Auth_Crypto_Result Auth_Crypto_Guest_Cookie_Create(
574 const uint8 *cookie_secret,
575 size_t cookie_secret_length,
576 const char *guest_uuid,
577 uint64 expiration_unix,
578 const char *ip_binding_digest,
579 char *cookie,
580 size_t cookie_capacity)
581 {
582 if (!auth__secret_is_valid(cookie_secret, cookie_secret_length) ||
583 !guest_uuid || !ip_binding_digest || !cookie ||
584 expiration_unix == 0)
585 {
586 return AUTH_CRYPTO_INVALID_ARGUMENT;
587 }
588 if (cookie_capacity < AUTH_CRYPTO_GUEST_COOKIE_SIZE)
589 {
590 return AUTH_CRYPTO_BUFFER_TOO_SMALL;
591 }
592 cookie[0] = '\0';
593
594 size_t uuid_length = 0;
595 if (!auth__bounded_length(
596 guest_uuid, AUTH_CRYPTO_GUEST_UUID_SIZE - 1, &uuid_length) ||
597 !auth__uuid_is_valid(guest_uuid, uuid_length))
598 {
599 return AUTH_CRYPTO_INVALID_ARGUMENT;
600 }
601
602 size_t ip_digest_length = 0;
603 uint8 ip_digest_bytes[32];
604 if (!auth__bounded_length(
605 ip_binding_digest,
606 AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE - 1,
607 &ip_digest_length) ||
608 !auth__hex_decode(
609 ip_binding_digest,
610 ip_digest_length,
611 ip_digest_bytes,
612 sizeof(ip_digest_bytes)))
613 {
614 return AUTH_CRYPTO_INVALID_ARGUMENT;
615 }
616 OPENSSL_cleanse(ip_digest_bytes, sizeof(ip_digest_bytes));
617
618 char payload[AUTH_CRYPTO_GUEST_COOKIE_SIZE];
619 int payload_written = snprintf(
620 payload,
621 sizeof(payload),
622 "%s.%s.%llu.%s",
623 AUTH_CRYPTO_COOKIE_VERSION,
624 guest_uuid,
625 (unsigned long long)expiration_unix,
626 ip_binding_digest);
627 if (payload_written < 0 || (size_t)payload_written >= sizeof(payload))
628 {
629 return AUTH_CRYPTO_OPERATION_FAILED;
630 }
631
632 uint8 signature[32];
633 Auth_Crypto_Result result = auth__hmac_sha256(
634 cookie_secret,
635 cookie_secret_length,
636 AUTH_CRYPTO_COOKIE_HMAC_DOMAIN,
637 payload,
638 (size_t)payload_written,
639 signature);
640 if (result != AUTH_CRYPTO_OK)
641 {
642 return result;
643 }
644
645 char signature_hex[65];
646 auth__hex_encode(signature, sizeof(signature), signature_hex);
647 OPENSSL_cleanse(signature, sizeof(signature));
648
649 int cookie_written = snprintf(
650 cookie,
651 cookie_capacity,
652 "%s.%s",
653 payload,
654 signature_hex);
655 OPENSSL_cleanse(signature_hex, sizeof(signature_hex));
656 if (cookie_written < 0 || (size_t)cookie_written >= cookie_capacity)
657 {
658 cookie[0] = '\0';
659 return AUTH_CRYPTO_BUFFER_TOO_SMALL;
660 }
661
662 return AUTH_CRYPTO_OK;
663 }
664
665 Auth_Crypto_Result Auth_Crypto_Guest_Cookie_Verify(
666 const uint8 *cookie_secret,
667 size_t cookie_secret_length,
668 const char *cookie,
669 uint64 current_unix,
670 const char *expected_ip_binding_digest,
671 Auth_Crypto_Guest_Cookie *guest_cookie)
672 {
673 if (!auth__secret_is_valid(cookie_secret, cookie_secret_length) ||
674 !cookie || !expected_ip_binding_digest || !guest_cookie)
675 {
676 return AUTH_CRYPTO_INVALID_ARGUMENT;
677 }
678 memset(guest_cookie, 0, sizeof(*guest_cookie));
679
680 size_t expected_ip_length = 0;
681 uint8 expected_ip[32];
682 if (!auth__bounded_length(
683 expected_ip_binding_digest,
684 AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE - 1,
685 &expected_ip_length) ||
686 !auth__hex_decode(
687 expected_ip_binding_digest,
688 expected_ip_length,
689 expected_ip,
690 sizeof(expected_ip)))
691 {
692 return AUTH_CRYPTO_INVALID_ARGUMENT;
693 }
694
695 size_t cookie_length = 0;
696 if (!auth__bounded_length(
697 cookie, AUTH_CRYPTO_GUEST_COOKIE_SIZE - 1, &cookie_length))
698 {
699 OPENSSL_cleanse(expected_ip, sizeof(expected_ip));
700 return AUTH_CRYPTO_MALFORMED;
701 }
702
703 const char *segments[5];
704 size_t segment_lengths[5];
705 if (!auth__split_cookie(
706 cookie, cookie_length, segments, segment_lengths) ||
707 segment_lengths[0] != sizeof(AUTH_CRYPTO_COOKIE_VERSION) - 1 ||
708 memcmp(
709 segments[0],
710 AUTH_CRYPTO_COOKIE_VERSION,
711 sizeof(AUTH_CRYPTO_COOKIE_VERSION) - 1) != 0 ||
712 !auth__uuid_is_valid(segments[1], segment_lengths[1]))
713 {
714 OPENSSL_cleanse(expected_ip, sizeof(expected_ip));
715 return AUTH_CRYPTO_MALFORMED;
716 }
717
718 uint64 expiration_unix = 0;
719 uint8 cookie_ip[32];
720 uint8 provided_signature[32];
721 if (!auth__parse_uint64(
722 segments[2], segment_lengths[2], &expiration_unix) ||
723 expiration_unix == 0 ||
724 !auth__hex_decode(
725 segments[3],
726 segment_lengths[3],
727 cookie_ip,
728 sizeof(cookie_ip)) ||
729 !auth__hex_decode(
730 segments[4],
731 segment_lengths[4],
732 provided_signature,
733 sizeof(provided_signature)))
734 {
735 OPENSSL_cleanse(expected_ip, sizeof(expected_ip));
736 return AUTH_CRYPTO_MALFORMED;
737 }
738
739 size_t payload_length = (size_t)(segments[4] - cookie - 1);
740 uint8 calculated_signature[32];
741 Auth_Crypto_Result result = auth__hmac_sha256(
742 cookie_secret,
743 cookie_secret_length,
744 AUTH_CRYPTO_COOKIE_HMAC_DOMAIN,
745 cookie,
746 payload_length,
747 calculated_signature);
748 if (result != AUTH_CRYPTO_OK)
749 {
750 OPENSSL_cleanse(expected_ip, sizeof(expected_ip));
751 OPENSSL_cleanse(cookie_ip, sizeof(cookie_ip));
752 OPENSSL_cleanse(provided_signature, sizeof(provided_signature));
753 return result;
754 }
755
756 int signature_comparison = CRYPTO_memcmp(
757 calculated_signature,
758 provided_signature,
759 sizeof(calculated_signature));
760 int ip_comparison = CRYPTO_memcmp(
761 cookie_ip,
762 expected_ip,
763 sizeof(cookie_ip));
764 OPENSSL_cleanse(calculated_signature, sizeof(calculated_signature));
765 OPENSSL_cleanse(provided_signature, sizeof(provided_signature));
766 OPENSSL_cleanse(cookie_ip, sizeof(cookie_ip));
767 OPENSSL_cleanse(expected_ip, sizeof(expected_ip));
768
769 if (signature_comparison != 0)
770 {
771 return AUTH_CRYPTO_AUTHENTICATION_FAILED;
772 }
773 if (ip_comparison != 0)
774 {
775 return AUTH_CRYPTO_IP_MISMATCH;
776 }
777 if (current_unix >= expiration_unix)
778 {
779 return AUTH_CRYPTO_EXPIRED;
780 }
781
782 memcpy(
783 guest_cookie->guest_uuid,
784 segments[1],
785 AUTH_CRYPTO_GUEST_UUID_SIZE - 1);
786 guest_cookie->guest_uuid[AUTH_CRYPTO_GUEST_UUID_SIZE - 1] = '\0';
787 guest_cookie->expiration_unix = expiration_unix;
788 return AUTH_CRYPTO_OK;
789 }
790
791 const char *Auth_Crypto_Result_String(Auth_Crypto_Result result)
792 {
793 switch (result)
794 {
795 case AUTH_CRYPTO_OK:
796 return "ok";
797 case AUTH_CRYPTO_INVALID_ARGUMENT:
798 return "invalid argument";
799 case AUTH_CRYPTO_BUFFER_TOO_SMALL:
800 return "buffer too small";
801 case AUTH_CRYPTO_PASSWORD_TOO_LONG:
802 return "password too long";
803 case AUTH_CRYPTO_RANDOM_FAILED:
804 return "secure random generation failed";
805 case AUTH_CRYPTO_OPERATION_FAILED:
806 return "cryptographic operation failed";
807 case AUTH_CRYPTO_MALFORMED:
808 return "malformed input";
809 case AUTH_CRYPTO_AUTHENTICATION_FAILED:
810 return "authentication failed";
811 case AUTH_CRYPTO_EXPIRED:
812 return "expired";
813 case AUTH_CRYPTO_IP_MISMATCH:
814 return "IP binding mismatch";
815 }
816
817 return "unknown result";
818 }
819
820 size_t Auth_Crypto_Base64url_Encode(
821 const uint8 *bytes,
822 size_t byte_count,
823 char *out,
824 size_t out_capacity)
825 {
826 if (!bytes || !out || out_capacity == 0)
827 return 0;
828
829 /* Unpadded output length: full groups + partial group chars */
830 size_t full_groups = byte_count / 3;
831 size_t remainder = byte_count % 3;
832 size_t unpadded_len = full_groups * 4 +
833 (remainder == 0 ? 0 : remainder + 1);
834 if (out_capacity < unpadded_len + 1)
835 {
836 out[0] = '\0';
837 return 0;
838 }
839
840 static const char kAlpha[] =
841 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
842
843 size_t in_idx = 0, out_idx = 0;
844
845 /* Full 3-byte groups */
846 while (in_idx + 2 < byte_count)
847 {
848 uint32 v = ((uint32)bytes[in_idx] << 16) |
849 ((uint32)bytes[in_idx + 1] << 8) |
850 bytes[in_idx + 2];
851 out[out_idx++] = kAlpha[(v >> 18) & 0x3f];
852 out[out_idx++] = kAlpha[(v >> 12) & 0x3f];
853 out[out_idx++] = kAlpha[(v >> 6) & 0x3f];
854 out[out_idx++] = kAlpha[(v ) & 0x3f];
855 in_idx += 3;
856 }
857
858 /* Remaining 1 or 2 bytes (no padding) */
859 if (in_idx < byte_count)
860 {
861 uint32 v = (uint32)bytes[in_idx] << 16;
862 if (in_idx + 1 < byte_count)
863 v |= (uint32)bytes[in_idx + 1] << 8;
864 out[out_idx++] = kAlpha[(v >> 18) & 0x3f];
865 out[out_idx++] = kAlpha[(v >> 12) & 0x3f];
866 if (in_idx + 1 < byte_count)
867 out[out_idx++] = kAlpha[(v >> 6) & 0x3f];
868 }
869
870 out[out_idx] = '\0';
871 return out_idx;
872 }