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