/work/toxcore/shared_key_cache.c
Line | Count | Source (jump to first uncovered line) |
1 | | /* SPDX-License-Identifier: GPL-3.0-or-later |
2 | | * Copyright © 2022-2025 The TokTok team. |
3 | | */ |
4 | | |
5 | | #include "shared_key_cache.h" |
6 | | |
7 | | #include <stdint.h> |
8 | | #include <string.h> // memcpy(...) |
9 | | |
10 | | #include "attributes.h" |
11 | | #include "ccompat.h" |
12 | | #include "crypto_core.h" |
13 | | #include "logger.h" |
14 | | #include "mem.h" |
15 | | #include "mono_time.h" |
16 | | |
17 | | typedef struct Shared_Key { |
18 | | uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]; |
19 | | uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; |
20 | | uint64_t time_last_requested; |
21 | | } Shared_Key; |
22 | | |
23 | | struct Shared_Key_Cache { |
24 | | Shared_Key *keys; |
25 | | const uint8_t *self_secret_key; |
26 | | uint64_t timeout; /** After this time (in seconds), a key is erased on the next housekeeping cycle */ |
27 | | const Mono_Time *mono_time; |
28 | | const Memory *mem; |
29 | | const Logger *log; |
30 | | uint8_t keys_per_slot; |
31 | | }; |
32 | | |
33 | | static bool shared_key_is_empty(const Logger *_Nonnull log, const Shared_Key *_Nonnull k) |
34 | 2.79M | { |
35 | 2.79M | LOGGER_ASSERT(log, k != nullptr, "shared key must not be NULL"); |
36 | | /* |
37 | | * Since time can never be 0, we use that to determine if a key slot is empty. |
38 | | * Additionally this allows us to use crypto_memzero and leave the slot in a valid state. |
39 | | */ |
40 | 2.79M | return k->time_last_requested == 0; |
41 | 2.79M | } |
42 | | |
43 | | static void shared_key_set_empty(const Logger *_Nonnull log, Shared_Key *_Nonnull k) |
44 | 4.00k | { |
45 | 4.00k | crypto_memzero(k, sizeof(Shared_Key)); |
46 | 4.00k | LOGGER_ASSERT(log, shared_key_is_empty(log, k), "shared key must be empty after clearing it"); |
47 | 4.00k | } |
48 | | |
49 | | Shared_Key_Cache *shared_key_cache_new(const Logger *log, const Mono_Time *mono_time, const Memory *mem, const uint8_t *self_secret_key, uint64_t timeout, uint8_t keys_per_slot) |
50 | 20.5k | { |
51 | 20.5k | if (mono_time == nullptr || self_secret_key == nullptr || timeout == 0 || keys_per_slot == 0) { |
52 | 0 | return nullptr; |
53 | 0 | } |
54 | | |
55 | | // Time must not be zero, since we use that as special value for empty slots |
56 | 20.5k | if (mono_time_get(mono_time) == 0) { |
57 | | // Fail loudly in debug environments |
58 | 0 | LOGGER_FATAL(log, "time must not be zero (mono_time not initialised?)"); |
59 | 0 | return nullptr; |
60 | 0 | } |
61 | | |
62 | 20.5k | Shared_Key_Cache *res = (Shared_Key_Cache *)mem_alloc(mem, sizeof(Shared_Key_Cache)); |
63 | 20.5k | if (res == nullptr) { |
64 | 121 | return nullptr; |
65 | 121 | } |
66 | | |
67 | 20.4k | res->self_secret_key = self_secret_key; |
68 | 20.4k | res->mono_time = mono_time; |
69 | 20.4k | res->mem = mem; |
70 | 20.4k | res->log = log; |
71 | 20.4k | res->keys_per_slot = keys_per_slot; |
72 | | |
73 | | // We take one byte from the public key for each bucket and store keys_per_slot elements there |
74 | 20.4k | const size_t cache_size = 256 * keys_per_slot; |
75 | 20.4k | Shared_Key *keys = (Shared_Key *)mem_valloc(mem, cache_size, sizeof(Shared_Key)); |
76 | | |
77 | 20.4k | if (keys == nullptr) { |
78 | 119 | mem_delete(mem, res); |
79 | 119 | return nullptr; |
80 | 119 | } |
81 | | |
82 | 20.3k | crypto_memlock(keys, cache_size * sizeof(Shared_Key)); |
83 | | |
84 | 20.3k | res->keys = keys; |
85 | | |
86 | 20.3k | return res; |
87 | 20.4k | } |
88 | | |
89 | | void shared_key_cache_free(Shared_Key_Cache *cache) |
90 | 14.5k | { |
91 | 14.5k | if (cache == nullptr) { |
92 | 320 | return; |
93 | 320 | } |
94 | | |
95 | 14.2k | const size_t cache_size = 256 * cache->keys_per_slot; |
96 | | // Don't leave key material in memory |
97 | 14.2k | crypto_memzero(cache->keys, cache_size * sizeof(Shared_Key)); |
98 | 14.2k | crypto_memunlock(cache->keys, cache_size * sizeof(Shared_Key)); |
99 | 14.2k | mem_delete(cache->mem, cache->keys); |
100 | 14.2k | mem_delete(cache->mem, cache); |
101 | 14.2k | } |
102 | | |
103 | | /* NOTE: On each lookup housekeeping is performed to evict keys that did timeout. */ |
104 | | const uint8_t *shared_key_cache_lookup(Shared_Key_Cache *cache, const uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]) |
105 | 530k | { |
106 | | // caching the time is not necessary, but calls to mono_time_get(...) are not free |
107 | 530k | const uint64_t cur_time = mono_time_get(cache->mono_time); |
108 | | // We can't use the first and last bytes because they are masked in curve25519. Selected 8 for good alignment. |
109 | 530k | const uint8_t bucket_idx = public_key[8]; |
110 | 530k | Shared_Key *bucket_start = &cache->keys[bucket_idx * cache->keys_per_slot]; |
111 | | |
112 | 530k | const uint8_t *found = nullptr; |
113 | | |
114 | | // Perform lookup |
115 | 711k | for (size_t i = 0; i < cache->keys_per_slot; ++i) { |
116 | 667k | if (shared_key_is_empty(cache->log, &bucket_start[i])) { |
117 | 175k | continue; |
118 | 175k | } |
119 | | |
120 | 492k | if (pk_equal(public_key, bucket_start[i].public_key)) { |
121 | 485k | found = bucket_start[i].shared_key; |
122 | 485k | bucket_start[i].time_last_requested = cur_time; |
123 | 485k | break; |
124 | 485k | } |
125 | 492k | } |
126 | | |
127 | | // Perform housekeeping for this bucket |
128 | 2.65M | for (size_t i = 0; i < cache->keys_per_slot; ++i) { |
129 | 2.12M | if (shared_key_is_empty(cache->log, &bucket_start[i])) { |
130 | 1.62M | continue; |
131 | 1.62M | } |
132 | | |
133 | 494k | const bool timed_out = (bucket_start[i].time_last_requested + cache->timeout) < cur_time; |
134 | 494k | if (timed_out) { |
135 | 4.00k | shared_key_set_empty(cache->log, &bucket_start[i]); |
136 | 4.00k | } |
137 | 494k | } |
138 | | |
139 | 530k | if (found == nullptr) { |
140 | | // Insert into cache |
141 | | |
142 | 44.3k | uint64_t oldest_timestamp = UINT64_MAX; |
143 | 44.3k | size_t oldest_index = 0; |
144 | | |
145 | | /* |
146 | | * Find least recently used entry, unused entries are prioritised, |
147 | | * because their time_last_requested field is zeroed. |
148 | | */ |
149 | 221k | for (size_t i = 0; i < cache->keys_per_slot; ++i) { |
150 | 177k | if (bucket_start[i].time_last_requested < oldest_timestamp) { |
151 | 46.0k | oldest_timestamp = bucket_start[i].time_last_requested; |
152 | 46.0k | oldest_index = i; |
153 | 46.0k | } |
154 | 177k | } |
155 | | |
156 | | // Compute the shared key for the cache |
157 | 44.3k | if (encrypt_precompute(public_key, cache->self_secret_key, bucket_start[oldest_index].shared_key) != 0) { |
158 | | // Don't put anything in the cache on error |
159 | 0 | return nullptr; |
160 | 0 | } |
161 | | |
162 | | // update cache entry |
163 | 44.3k | memcpy(bucket_start[oldest_index].public_key, public_key, CRYPTO_PUBLIC_KEY_SIZE); |
164 | 44.3k | bucket_start[oldest_index].time_last_requested = cur_time; |
165 | 44.3k | found = bucket_start[oldest_index].shared_key; |
166 | 44.3k | } |
167 | | |
168 | 530k | return found; |
169 | 530k | } |