Line data Source code
1 : /*
2 : * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 : #pragma once
18 :
19 : #include "string_utils.h"
20 : #include "timestamp.h"
21 :
22 : #include <opendht/infohash.h>
23 : #include <opendht/value.h>
24 : #include <opendht/default_types.h>
25 :
26 : #include <msgpack.hpp>
27 : #include <json/json.h>
28 :
29 : #include <map>
30 : #include <limits>
31 : #include <optional>
32 : #include <string>
33 : #include <string_view>
34 :
35 : namespace jami {
36 :
37 : namespace ContactMapKeys {
38 : static constexpr const char* ADDED {"added"};
39 : static constexpr const char* REMOVED {"removed"};
40 : static constexpr const char* CONFIRMED {"confirmed"};
41 : static constexpr const char* BANNED {"banned"};
42 : static constexpr const char* CONVERSATIONID {"conversationId"};
43 : static constexpr const char* DEVICE {"device"};
44 : static constexpr const char* RECEIVED {"received"};
45 : static constexpr const char* PAYLOAD {"payload"};
46 : // Millisecond-resolution variants. Legacy keys above keep carrying seconds so
47 : // that older devices (which ignore unknown keys) remain compatible.
48 : static constexpr const char* ADDED_MS {"addedMs"};
49 : static constexpr const char* REMOVED_MS {"removedMs"};
50 : static constexpr const char* RECEIVED_MS {"receivedMs"};
51 : // Sender-embedded invite timestamp (see jami::TrustRequestMsg::invitedMs), so that a re-sync
52 : // of a pending trust request to another one of our own devices doesn't lose the original
53 : // invite time and fall back to comparing against local receive times instead.
54 : static constexpr const char* INVITED_MS {"invitedMs"};
55 : } // namespace ContactMapKeys
56 :
57 : struct Contact
58 : {
59 : /** Time of contact addition */
60 : TimePoint added {};
61 :
62 : /** Time of contact removal */
63 : TimePoint removed {};
64 :
65 : /** True if we got confirmation that this contact also added us */
66 : bool confirmed {false};
67 :
68 : /** True if the contact is banned (if not active) */
69 : bool banned {false};
70 :
71 : /** Non empty if a swarm is linked */
72 : std::string conversationId {};
73 :
74 : /** True if the contact is an active contact (not banned nor removed) */
75 603 : bool isActive() const { return added > removed; }
76 211 : bool isBanned() const { return not isActive() and banned; }
77 :
78 150 : Contact() = default;
79 5 : Contact(const Json::Value& json)
80 5 : {
81 : // Prefer the millisecond keys, fall back to the legacy seconds keys
82 : // (written by older devices).
83 5 : if (json.isMember(ContactMapKeys::ADDED_MS))
84 3 : added = timePointFromMilliseconds(json[ContactMapKeys::ADDED_MS].asLargestInt());
85 : else
86 2 : added = timePointFromSeconds(json[ContactMapKeys::ADDED].asLargestInt());
87 5 : if (json.isMember(ContactMapKeys::REMOVED_MS))
88 1 : removed = timePointFromMilliseconds(json[ContactMapKeys::REMOVED_MS].asLargestInt());
89 : else
90 4 : removed = timePointFromSeconds(json[ContactMapKeys::REMOVED].asLargestInt());
91 5 : confirmed = json[ContactMapKeys::CONFIRMED].asBool();
92 5 : banned = json[ContactMapKeys::BANNED].asBool();
93 5 : conversationId = json[ContactMapKeys::CONVERSATIONID].asString();
94 5 : }
95 :
96 : /**
97 : * Update this contact using other known contact information,
98 : * return true if contact state was changed.
99 : */
100 56 : bool update(const Contact& c)
101 : {
102 56 : const auto copy = *this;
103 56 : auto isMoreRecent = std::max(c.added, c.removed) > std::max(added, removed);
104 56 : if (isMoreRecent) {
105 4 : added = c.added;
106 4 : removed = c.removed;
107 4 : banned = c.banned;
108 4 : conversationId = c.conversationId;
109 4 : confirmed = c.confirmed;
110 52 : } else if (isActive() && added == c.added) {
111 48 : confirmed = confirmed or c.confirmed;
112 : }
113 112 : return hasDifferentState(copy);
114 56 : }
115 :
116 56 : bool hasDifferentState(const Contact& other) const
117 : {
118 56 : return other.isActive() != isActive() or other.isBanned() != isBanned() or other.confirmed != confirmed;
119 : }
120 :
121 6 : Json::Value toJson() const
122 : {
123 6 : Json::Value json;
124 6 : json[ContactMapKeys::ADDED] = Json::Int64(toSecondsSinceEpoch(added));
125 6 : json[ContactMapKeys::ADDED_MS] = Json::Int64(toMillisecondsSinceEpoch(added));
126 6 : if (removed != TimePoint {}) {
127 1 : json[ContactMapKeys::REMOVED] = Json::Int64(toSecondsSinceEpoch(removed));
128 1 : json[ContactMapKeys::REMOVED_MS] = Json::Int64(toMillisecondsSinceEpoch(removed));
129 : }
130 6 : if (confirmed)
131 4 : json[ContactMapKeys::CONFIRMED] = confirmed;
132 6 : if (banned)
133 0 : json[ContactMapKeys::BANNED] = banned;
134 6 : json[ContactMapKeys::CONVERSATIONID] = conversationId;
135 6 : return json;
136 0 : }
137 :
138 6 : std::map<std::string, std::string> toMap() const
139 : {
140 6 : std::map<std::string, std::string> result {{"added", std::to_string(toSecondsSinceEpoch(added))},
141 12 : {"removed", std::to_string(toSecondsSinceEpoch(removed))},
142 30 : {"conversationId", conversationId}};
143 :
144 6 : if (isActive())
145 6 : result.emplace("confirmed", confirmed ? TRUE_STR : FALSE_STR);
146 6 : if (isBanned())
147 0 : result.emplace("banned", TRUE_STR);
148 :
149 6 : return result;
150 18 : }
151 :
152 : // Hand-written msgpack serialization (replaces MSGPACK_DEFINE_MAP) to emit
153 : // dual keys: legacy seconds (added/removed) + milliseconds
154 : // (addedMs/removedMs). Readers prefer the ms keys and fall back to
155 : // seconds * 1000.
156 : template<typename Packer>
157 268 : void msgpack_pack(Packer& pk) const
158 : {
159 268 : int64_t addedSec = toSecondsSinceEpoch(added);
160 268 : int64_t removedSec = toSecondsSinceEpoch(removed);
161 268 : int64_t addedMs = toMillisecondsSinceEpoch(added);
162 268 : int64_t removedMs = toMillisecondsSinceEpoch(removed);
163 : msgpack::type::make_define_map(ContactMapKeys::ADDED,
164 : addedSec,
165 : ContactMapKeys::REMOVED,
166 : removedSec,
167 : ContactMapKeys::CONFIRMED,
168 268 : confirmed,
169 : ContactMapKeys::BANNED,
170 268 : banned,
171 : ContactMapKeys::CONVERSATIONID,
172 268 : conversationId,
173 : ContactMapKeys::ADDED_MS,
174 : addedMs,
175 : ContactMapKeys::REMOVED_MS,
176 : removedMs)
177 268 : .msgpack_pack(pk);
178 268 : }
179 :
180 71 : void msgpack_unpack(const msgpack::object& o)
181 : {
182 71 : if (o.type != msgpack::type::MAP)
183 0 : throw msgpack::type_error();
184 71 : int64_t addedSec = 0, removedSec = 0;
185 71 : std::optional<int64_t> addedMs, removedMs;
186 566 : for (uint32_t i = 0; i < o.via.map.size; ++i) {
187 495 : const auto& kv = o.via.map.ptr[i];
188 495 : if (kv.key.type != msgpack::type::STR)
189 0 : continue;
190 495 : std::string_view key(kv.key.via.str.ptr, kv.key.via.str.size);
191 495 : if (key == ContactMapKeys::ADDED)
192 71 : kv.val.convert(addedSec);
193 424 : else if (key == ContactMapKeys::REMOVED)
194 71 : kv.val.convert(removedSec);
195 353 : else if (key == ContactMapKeys::ADDED_MS)
196 70 : addedMs = kv.val.as<int64_t>();
197 283 : else if (key == ContactMapKeys::REMOVED_MS)
198 70 : removedMs = kv.val.as<int64_t>();
199 213 : else if (key == ContactMapKeys::CONFIRMED)
200 71 : kv.val.convert(confirmed);
201 142 : else if (key == ContactMapKeys::BANNED)
202 71 : kv.val.convert(banned);
203 71 : else if (key == ContactMapKeys::CONVERSATIONID)
204 71 : kv.val.convert(conversationId);
205 : }
206 71 : added = addedMs ? timePointFromMilliseconds(*addedMs) : timePointFromSeconds(addedSec);
207 71 : removed = removedMs ? timePointFromMilliseconds(*removedMs) : timePointFromSeconds(removedSec);
208 71 : }
209 :
210 : template<typename MSGPACK_OBJECT>
211 : void msgpack_object(MSGPACK_OBJECT* o, msgpack::zone& z) const
212 : {
213 : int64_t addedSec = toSecondsSinceEpoch(added);
214 : int64_t removedSec = toSecondsSinceEpoch(removed);
215 : int64_t addedMs = toMillisecondsSinceEpoch(added);
216 : int64_t removedMs = toMillisecondsSinceEpoch(removed);
217 : msgpack::type::make_define_map(ContactMapKeys::ADDED,
218 : addedSec,
219 : ContactMapKeys::REMOVED,
220 : removedSec,
221 : ContactMapKeys::CONFIRMED,
222 : confirmed,
223 : ContactMapKeys::BANNED,
224 : banned,
225 : ContactMapKeys::CONVERSATIONID,
226 : conversationId,
227 : ContactMapKeys::ADDED_MS,
228 : addedMs,
229 : ContactMapKeys::REMOVED_MS,
230 : removedMs)
231 : .msgpack_object(o, z);
232 : }
233 : };
234 :
235 : struct TrustRequest
236 : {
237 : std::shared_ptr<dht::crypto::PublicKey> device;
238 : std::string conversationId;
239 : TimePoint received {};
240 : std::vector<uint8_t> payload;
241 : TimePoint invited {};
242 :
243 : // Hand-written msgpack serialization (replaces MSGPACK_DEFINE_MAP) to emit
244 : // dual keys: legacy seconds (received) + milliseconds (receivedMs). Readers
245 : // prefer the ms key and fall back to seconds * 1000.
246 : template<typename Packer>
247 62 : void msgpack_pack(Packer& pk) const
248 : {
249 62 : int64_t receivedSec = toSecondsSinceEpoch(received);
250 62 : int64_t receivedMs = toMillisecondsSinceEpoch(received);
251 62 : int64_t invitedMs = toMillisecondsSinceEpoch(invited);
252 : msgpack::type::make_define_map(ContactMapKeys::DEVICE,
253 62 : device,
254 : ContactMapKeys::CONVERSATIONID,
255 62 : conversationId,
256 : ContactMapKeys::RECEIVED,
257 : receivedSec,
258 : ContactMapKeys::PAYLOAD,
259 62 : payload,
260 : ContactMapKeys::RECEIVED_MS,
261 : receivedMs,
262 : ContactMapKeys::INVITED_MS,
263 : invitedMs)
264 62 : .msgpack_pack(pk);
265 62 : }
266 :
267 7 : void msgpack_unpack(const msgpack::object& o)
268 : {
269 7 : if (o.type != msgpack::type::MAP)
270 0 : throw msgpack::type_error();
271 7 : int64_t receivedSec = 0;
272 7 : std::optional<int64_t> receivedMs;
273 7 : int64_t invitedMs = 0;
274 45 : for (uint32_t i = 0; i < o.via.map.size; ++i) {
275 38 : const auto& kv = o.via.map.ptr[i];
276 38 : if (kv.key.type != msgpack::type::STR)
277 0 : continue;
278 38 : std::string_view key(kv.key.via.str.ptr, kv.key.via.str.size);
279 38 : if (key == ContactMapKeys::DEVICE)
280 7 : kv.val.convert(device);
281 31 : else if (key == ContactMapKeys::CONVERSATIONID)
282 7 : kv.val.convert(conversationId);
283 24 : else if (key == ContactMapKeys::RECEIVED)
284 7 : kv.val.convert(receivedSec);
285 17 : else if (key == ContactMapKeys::RECEIVED_MS)
286 5 : receivedMs = kv.val.as<int64_t>();
287 12 : else if (key == ContactMapKeys::PAYLOAD)
288 7 : kv.val.convert(payload);
289 5 : else if (key == ContactMapKeys::INVITED_MS)
290 5 : kv.val.convert(invitedMs);
291 : }
292 7 : received = receivedMs ? timePointFromMilliseconds(*receivedMs) : timePointFromSeconds(receivedSec);
293 7 : invited = invitedMs > 0 ? timePointFromMilliseconds(invitedMs) : TimePoint {};
294 7 : }
295 :
296 : template<typename MSGPACK_OBJECT>
297 : void msgpack_object(MSGPACK_OBJECT* o, msgpack::zone& z) const
298 : {
299 : int64_t receivedSec = toSecondsSinceEpoch(received);
300 : int64_t receivedMs = toMillisecondsSinceEpoch(received);
301 : int64_t invitedMs = toMillisecondsSinceEpoch(invited);
302 : msgpack::type::make_define_map(ContactMapKeys::DEVICE,
303 : device,
304 : ContactMapKeys::CONVERSATIONID,
305 : conversationId,
306 : ContactMapKeys::RECEIVED,
307 : receivedSec,
308 : ContactMapKeys::PAYLOAD,
309 : payload,
310 : ContactMapKeys::RECEIVED_MS,
311 : receivedMs,
312 : ContactMapKeys::INVITED_MS,
313 : invitedMs)
314 : .msgpack_object(o, z);
315 : }
316 : };
317 :
318 : struct DeviceAnnouncement : public dht::SignedValue<DeviceAnnouncement>
319 : {
320 : private:
321 : using BaseClass = dht::SignedValue<DeviceAnnouncement>;
322 :
323 : public:
324 : static const constexpr dht::ValueType& TYPE = dht::ValueType::USER_DATA;
325 : dht::InfoHash dev;
326 : std::shared_ptr<dht::crypto::PublicKey> pk;
327 6036 : MSGPACK_DEFINE_MAP(dev, pk)
328 : };
329 :
330 : struct KnownDeviceSync
331 : {
332 : std::string name;
333 779 : MSGPACK_DEFINE_MAP(name)
334 : };
335 :
336 : struct DeviceSync : public dht::EncryptedValue<DeviceSync>
337 : {
338 : static const constexpr dht::ValueType& TYPE = dht::ValueType::USER_DATA;
339 : uint64_t date;
340 : std::string device_name;
341 : std::map<dht::PkId, KnownDeviceSync> devices;
342 : std::map<dht::InfoHash, Contact> peers;
343 : std::map<dht::InfoHash, TrustRequest> trust_requests;
344 742 : MSGPACK_DEFINE_MAP(date, device_name, devices, peers, trust_requests)
345 : };
346 :
347 : struct TrustRequestMsg : public dht::EncryptedValue<TrustRequestMsg>
348 : {
349 : static const constexpr dht::ValueType& TYPE = dht::TrustRequest::TYPE;
350 :
351 : std::string service;
352 : std::string conversationId;
353 : std::vector<uint8_t> payload;
354 : bool confirm {false};
355 : // Millisecond timestamp of when the sender originally issued this invite, so that
356 : // passive DHT redeliveries/retries aren't mistaken for a brand new invitation.
357 : int64_t invitedMs {0};
358 297 : MSGPACK_DEFINE_MAP(service, conversationId, payload, confirm, invitedMs)
359 : };
360 :
361 : // On-disk representation of a known device entry. Older daemons stored it as a
362 : // msgpack array [name, lastSyncSeconds] (std::pair). The current format stores
363 : // a self-describing map carrying milliseconds; the reader accepts both layouts
364 : // so upgrading keeps existing knownDevices files working (a downgrade simply
365 : // fails to parse the new map and re-discovers devices through sync).
366 : struct KnownDeviceData
367 : {
368 : std::string name;
369 : int64_t lastSyncMs {0};
370 :
371 : template<typename Packer>
372 1008100 : void msgpack_pack(Packer& pk) const
373 : {
374 1008100 : pk.pack_map(2);
375 1008100 : pk.pack("name");
376 1008100 : pk.pack(name);
377 1008100 : pk.pack("syncMs");
378 1008100 : pk.pack(lastSyncMs);
379 1008100 : }
380 :
381 23 : void msgpack_unpack(const msgpack::object& o)
382 : {
383 23 : if (o.type == msgpack::type::ARRAY) {
384 : // Legacy layout: [name, lastSyncSeconds]
385 4 : if (o.via.array.size > 0)
386 4 : o.via.array.ptr[0].convert(name);
387 4 : if (o.via.array.size > 1)
388 4 : lastSyncMs = readLegacySyncSeconds(o.via.array.ptr[1]);
389 19 : } else if (o.type == msgpack::type::MAP) {
390 57 : for (uint32_t i = 0; i < o.via.map.size; ++i) {
391 38 : const auto& kv = o.via.map.ptr[i];
392 38 : if (kv.key.type != msgpack::type::STR)
393 0 : continue;
394 38 : std::string_view key(kv.key.via.str.ptr, kv.key.via.str.size);
395 38 : if (key == "name")
396 19 : kv.val.convert(name);
397 19 : else if (key == "syncMs")
398 19 : lastSyncMs = readSyncMs(kv.val);
399 : }
400 : } else {
401 0 : throw msgpack::type_error();
402 : }
403 23 : }
404 :
405 : private:
406 : // Devices that were never synced carry time_point::min(), which daemons
407 : // predating the millisecond migration serialized through time_t into an
408 : // unsigned field: the negative value wrapped around and was stored as a
409 : // uint64 far beyond INT64_MAX. Reading such an entry as int64_t throws
410 : // msgpack::type_error (a std::bad_cast), which used to abort the whole
411 : // knownDevices file. Treat any out-of-range or negative timestamp as
412 : // "never synced" (0) instead.
413 4 : static int64_t readLegacySyncSeconds(const msgpack::object& o)
414 : {
415 : static constexpr int64_t MAX_SYNC_SECONDS = std::numeric_limits<int64_t>::max() / 1000;
416 4 : int64_t seconds = 0;
417 4 : if (o.type == msgpack::type::POSITIVE_INTEGER) {
418 4 : auto raw = o.as<uint64_t>();
419 4 : if (raw > static_cast<uint64_t>(MAX_SYNC_SECONDS))
420 2 : return 0;
421 2 : seconds = static_cast<int64_t>(raw);
422 0 : } else if (o.type == msgpack::type::NEGATIVE_INTEGER) {
423 0 : return 0;
424 : } else {
425 0 : throw msgpack::type_error();
426 : }
427 2 : return seconds * 1000;
428 : }
429 :
430 19 : static int64_t readSyncMs(const msgpack::object& o)
431 : {
432 19 : if (o.type == msgpack::type::POSITIVE_INTEGER) {
433 19 : auto raw = o.as<uint64_t>();
434 19 : if (raw > static_cast<uint64_t>(std::numeric_limits<int64_t>::max()))
435 0 : return 0;
436 19 : return static_cast<int64_t>(raw);
437 : }
438 0 : if (o.type == msgpack::type::NEGATIVE_INTEGER)
439 0 : return 0;
440 0 : throw msgpack::type_error();
441 : }
442 : };
443 :
444 : struct KnownDevice
445 : {
446 : using clock = std::chrono::system_clock;
447 : using time_point = clock::time_point;
448 :
449 : /** Device certificate */
450 : std::shared_ptr<dht::crypto::Certificate> certificate;
451 :
452 : /** Device name */
453 : std::string name {};
454 :
455 : /** Time of last received device sync */
456 : time_point last_sync {time_point::min()};
457 :
458 4039 : KnownDevice(const std::shared_ptr<dht::crypto::Certificate>& cert,
459 : const std::string& n = {},
460 : time_point sync = time_point::min())
461 4039 : : certificate(cert)
462 4039 : , name(n)
463 4039 : , last_sync(sync)
464 4039 : {}
465 : };
466 :
467 : } // namespace jami
|