Line data Source code
1 : /*
2 : * Copyright (C) 2004-2024 Savoir-faire Linux Inc.
3 : * Author : Adrien BĂ©raud <adrien.beraud@savoirfairelinux.com>
4 : *
5 : * This program is free software; you can redistribute it and/or modify
6 : * it under the terms of the GNU General Public License as published by
7 : * the Free Software Foundation; either version 3 of the License, or
8 : * (at your option) any later version.
9 : *
10 : * This program is distributed in the hope that it will be useful,
11 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : * GNU General Public License for more details.
14 : *
15 : * You should have received a copy of the GNU General Public License
16 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 : */
18 : #include "archive_account_manager.h"
19 : #include "accountarchive.h"
20 : #include "fileutils.h"
21 : #include "libdevcrypto/Common.h"
22 : #include "archiver.h"
23 : #include "base64.h"
24 : #include "jami/account_const.h"
25 : #include "account_schema.h"
26 : #include "jamidht/conversation_module.h"
27 : #include "manager.h"
28 :
29 : #include <opendht/dhtrunner.h>
30 : #include <opendht/thread_pool.h>
31 :
32 : #include <memory>
33 : #include <fstream>
34 :
35 : namespace jami {
36 :
37 : const constexpr auto EXPORT_KEY_RENEWAL_TIME = std::chrono::minutes(20);
38 :
39 : void
40 613 : ArchiveAccountManager::initAuthentication(const std::string& accountId,
41 : PrivateKey key,
42 : std::string deviceName,
43 : std::unique_ptr<AccountCredentials> credentials,
44 : AuthSuccessCallback onSuccess,
45 : AuthFailureCallback onFailure,
46 : const OnChangeCallback& onChange)
47 : {
48 613 : auto ctx = std::make_shared<AuthContext>();
49 613 : ctx->accountId = accountId;
50 613 : ctx->key = key;
51 613 : ctx->request = buildRequest(key);
52 613 : ctx->deviceName = std::move(deviceName);
53 613 : ctx->credentials = dynamic_unique_cast<ArchiveAccountCredentials>(std::move(credentials));
54 613 : ctx->onSuccess = std::move(onSuccess);
55 613 : ctx->onFailure = std::move(onFailure);
56 :
57 613 : if (not ctx->credentials) {
58 0 : ctx->onFailure(AuthError::INVALID_ARGUMENTS, "invalid credentials");
59 0 : return;
60 : }
61 :
62 613 : onChange_ = std::move(onChange);
63 :
64 613 : if (ctx->credentials->scheme == "dht") {
65 1 : loadFromDHT(ctx);
66 1 : return;
67 : }
68 :
69 612 : dht::ThreadPool::computation().run([ctx = std::move(ctx), w = weak_from_this()] {
70 612 : auto this_ = std::static_pointer_cast<ArchiveAccountManager>(w.lock());
71 612 : if (not this_) return;
72 : try {
73 612 : if (ctx->credentials->scheme == "file") {
74 : // Import from external archive
75 31 : this_->loadFromFile(*ctx);
76 : } else {
77 : // Create/migrate local account
78 581 : bool hasArchive = not ctx->credentials->uri.empty()
79 1162 : and std::filesystem::is_regular_file(ctx->credentials->uri);
80 581 : if (hasArchive) {
81 : // Create/migrate from local archive
82 4 : if (ctx->credentials->updateIdentity.first
83 8 : and ctx->credentials->updateIdentity.second
84 8 : and needsMigration(ctx->credentials->updateIdentity)) {
85 2 : this_->migrateAccount(*ctx);
86 : } else {
87 2 : this_->loadFromFile(*ctx);
88 : }
89 577 : } else if (ctx->credentials->updateIdentity.first
90 577 : and ctx->credentials->updateIdentity.second) {
91 0 : auto future_keypair = dht::ThreadPool::computation().get<dev::KeyPair>(
92 0 : &dev::KeyPair::create);
93 0 : AccountArchive a;
94 0 : JAMI_WARN("[Auth] converting certificate from old account %s",
95 : ctx->credentials->updateIdentity.first->getPublicKey()
96 : .getId()
97 : .toString()
98 : .c_str());
99 0 : a.id = std::move(ctx->credentials->updateIdentity);
100 : try {
101 0 : a.ca_key = std::make_shared<dht::crypto::PrivateKey>(
102 0 : fileutils::loadFile("ca.key", this_->path_));
103 0 : } catch (...) {
104 0 : }
105 0 : this_->updateCertificates(a, ctx->credentials->updateIdentity);
106 0 : a.eth_key = future_keypair.get().secret().makeInsecure().asBytes();
107 0 : this_->onArchiveLoaded(*ctx, std::move(a));
108 0 : } else {
109 577 : this_->createAccount(*ctx);
110 : }
111 : }
112 0 : } catch (const std::exception& e) {
113 0 : ctx->onFailure(AuthError::UNKNOWN, e.what());
114 0 : }
115 612 : });
116 613 : }
117 :
118 : bool
119 2 : ArchiveAccountManager::updateCertificates(AccountArchive& archive, dht::crypto::Identity& device)
120 : {
121 2 : JAMI_WARN("Updating certificates");
122 : using Certificate = dht::crypto::Certificate;
123 :
124 : // We need the CA key to resign certificates
125 4 : if (not archive.id.first or not *archive.id.first or not archive.id.second or not archive.ca_key
126 4 : or not *archive.ca_key)
127 0 : return false;
128 :
129 : // Currently set the CA flag and update expiration dates
130 2 : bool updated = false;
131 :
132 2 : auto& cert = archive.id.second;
133 2 : auto ca = cert->issuer;
134 : // Update CA if possible and relevant
135 2 : if (not ca or (not ca->issuer and (not ca->isCA() or ca->getExpiration() < clock::now()))) {
136 4 : ca = std::make_shared<Certificate>(
137 6 : Certificate::generate(*archive.ca_key, "Jami CA", {}, true));
138 2 : updated = true;
139 2 : JAMI_DBG("CA CRT re-generated");
140 : }
141 :
142 : // Update certificate
143 2 : if (updated or not cert->isCA() or cert->getExpiration() < clock::now()) {
144 4 : cert = std::make_shared<Certificate>(
145 4 : Certificate::generate(*archive.id.first,
146 : "Jami",
147 4 : dht::crypto::Identity {archive.ca_key, ca},
148 2 : true));
149 2 : updated = true;
150 2 : JAMI_DBG("Jami CRT re-generated");
151 : }
152 :
153 2 : if (updated and device.first and *device.first) {
154 : // update device certificate
155 4 : device.second = std::make_shared<Certificate>(
156 6 : Certificate::generate(*device.first, "Jami device", archive.id));
157 2 : JAMI_DBG("device CRT re-generated");
158 : }
159 :
160 2 : return updated;
161 2 : }
162 :
163 : bool
164 2 : ArchiveAccountManager::setValidity(std::string_view scheme, const std::string& password,
165 : dht::crypto::Identity& device,
166 : const dht::InfoHash& id,
167 : int64_t validity)
168 : {
169 2 : auto archive = readArchive(scheme, password);
170 : // We need the CA key to resign certificates
171 4 : if (not archive.id.first or not *archive.id.first or not archive.id.second or not archive.ca_key
172 4 : or not *archive.ca_key)
173 0 : return false;
174 :
175 2 : auto updated = false;
176 :
177 2 : if (id)
178 0 : JAMI_WARN("Updating validity for certificate with id: %s", id.to_c_str());
179 : else
180 2 : JAMI_WARN("Updating validity for certificates");
181 :
182 2 : auto& cert = archive.id.second;
183 2 : auto ca = cert->issuer;
184 2 : if (not ca)
185 0 : return false;
186 :
187 : // using Certificate = dht::crypto::Certificate;
188 : // Update CA if possible and relevant
189 2 : if (not id or ca->getId() == id) {
190 2 : ca->setValidity(*archive.ca_key, validity);
191 2 : updated = true;
192 2 : JAMI_DBG("CA CRT re-generated");
193 : }
194 :
195 : // Update certificate
196 2 : if (updated or not id or cert->getId() == id) {
197 2 : cert->setValidity(dht::crypto::Identity {archive.ca_key, ca}, validity);
198 2 : device.second->issuer = cert;
199 2 : updated = true;
200 2 : JAMI_DBG("Jami CRT re-generated");
201 : }
202 :
203 2 : if (updated) {
204 2 : archive.save(fileutils::getFullPath(path_, archivePath_), scheme, password);
205 : }
206 :
207 2 : if (updated or not id or device.second->getId() == id) {
208 : // update device certificate
209 2 : device.second->setValidity(archive.id, validity);
210 2 : updated = true;
211 : }
212 :
213 2 : return updated;
214 2 : }
215 :
216 : void
217 577 : ArchiveAccountManager::createAccount(AuthContext& ctx)
218 : {
219 577 : AccountArchive a;
220 577 : auto future_keypair = dht::ThreadPool::computation().get<dev::KeyPair>(&dev::KeyPair::create);
221 1154 : auto ca = dht::crypto::generateIdentity("Jami CA");
222 577 : if (!ca.first || !ca.second) {
223 0 : throw std::runtime_error("Can't generate CA for this account.");
224 : }
225 577 : a.id = dht::crypto::generateIdentity("Jami", ca, 4096, true);
226 577 : if (!a.id.first || !a.id.second) {
227 0 : throw std::runtime_error("Can't generate identity for this account.");
228 : }
229 577 : JAMI_WARN("[Auth] new account: CA: %s, RingID: %s",
230 : ca.second->getId().toString().c_str(),
231 : a.id.second->getId().toString().c_str());
232 577 : a.ca_key = ca.first;
233 577 : auto keypair = future_keypair.get();
234 577 : a.eth_key = keypair.secret().makeInsecure().asBytes();
235 577 : onArchiveLoaded(ctx, std::move(a));
236 577 : }
237 :
238 : void
239 33 : ArchiveAccountManager::loadFromFile(AuthContext& ctx)
240 : {
241 33 : JAMI_WARN("[Auth] loading archive from: %s", ctx.credentials->uri.c_str());
242 33 : AccountArchive archive;
243 : try {
244 33 : archive = AccountArchive(ctx.credentials->uri, ctx.credentials->password_scheme, ctx.credentials->password);
245 0 : } catch (const std::exception& ex) {
246 0 : JAMI_WARN("[Auth] can't read file: %s", ex.what());
247 0 : ctx.onFailure(AuthError::INVALID_ARGUMENTS, ex.what());
248 0 : return;
249 0 : }
250 33 : onArchiveLoaded(ctx, std::move(archive));
251 33 : }
252 :
253 : struct ArchiveAccountManager::DhtLoadContext
254 : {
255 : dht::DhtRunner dht;
256 : std::pair<bool, bool> stateOld {false, true};
257 : std::pair<bool, bool> stateNew {false, true};
258 : bool found {false};
259 : };
260 :
261 : void
262 1 : ArchiveAccountManager::loadFromDHT(const std::shared_ptr<AuthContext>& ctx)
263 : {
264 1 : ctx->dhtContext = std::make_unique<DhtLoadContext>();
265 1 : ctx->dhtContext->dht.run(ctx->credentials->dhtPort, {}, true);
266 2 : for (const auto& bootstrap : ctx->credentials->dhtBootstrap)
267 1 : ctx->dhtContext->dht.bootstrap(bootstrap);
268 1 : auto searchEnded = [ctx]() {
269 1 : if (not ctx->dhtContext or ctx->dhtContext->found) {
270 1 : return;
271 : }
272 0 : auto& s = *ctx->dhtContext;
273 0 : if (s.stateOld.first && s.stateNew.first) {
274 0 : dht::ThreadPool::computation().run(
275 0 : [ctx, network_error = !s.stateOld.second && !s.stateNew.second] {
276 0 : ctx->dhtContext.reset();
277 0 : JAMI_WARN("[Auth] failure looking for archive on DHT: %s",
278 : /**/ network_error ? "network error" : "not found");
279 0 : ctx->onFailure(network_error ? AuthError::NETWORK : AuthError::UNKNOWN, "");
280 0 : });
281 : }
282 1 : };
283 :
284 2 : auto search = [ctx, searchEnded, w=weak_from_this()](bool previous) {
285 2 : std::vector<uint8_t> key;
286 2 : dht::InfoHash loc;
287 2 : auto& s = previous ? ctx->dhtContext->stateOld : ctx->dhtContext->stateNew;
288 :
289 : // compute archive location and decryption keys
290 : try {
291 4 : std::tie(key, loc) = computeKeys(ctx->credentials->password,
292 2 : ctx->credentials->uri,
293 2 : previous);
294 2 : JAMI_DBG("[Auth] trying to load account from DHT with %s at %s",
295 : /**/ ctx->credentials->uri.c_str(),
296 : loc.toString().c_str());
297 2 : if (not ctx->dhtContext or ctx->dhtContext->found) {
298 0 : return;
299 : }
300 6 : ctx->dhtContext->dht.get(
301 : loc,
302 4 : [ctx, key = std::move(key), w](const std::shared_ptr<dht::Value>& val) {
303 1 : std::vector<uint8_t> decrypted;
304 : try {
305 1 : decrypted = archiver::decompress(dht::crypto::aesDecrypt(val->data, key));
306 0 : } catch (const std::exception& ex) {
307 0 : return true;
308 0 : }
309 1 : JAMI_DBG("[Auth] found archive on the DHT");
310 1 : ctx->dhtContext->found = true;
311 3 : dht::ThreadPool::computation().run([ctx,
312 2 : decrypted = std::move(decrypted), w] {
313 : try {
314 1 : auto archive = AccountArchive(decrypted);
315 2 : if (auto sthis = std::static_pointer_cast<ArchiveAccountManager>(w.lock())) {
316 1 : if (ctx->dhtContext) {
317 1 : ctx->dhtContext->dht.join();
318 1 : ctx->dhtContext.reset();
319 : }
320 2 : sthis->onArchiveLoaded(*ctx,
321 1 : std::move(archive) /*, std::move(contacts)*/);
322 1 : }
323 1 : } catch (const std::exception& e) {
324 0 : ctx->onFailure(AuthError::UNKNOWN, "");
325 0 : }
326 1 : });
327 1 : return not ctx->dhtContext->found;
328 1 : },
329 2 : [=, &s](bool ok) {
330 1 : JAMI_DBG("[Auth] DHT archive search ended at %s", /**/ loc.toString().c_str());
331 1 : s.first = true;
332 1 : s.second = ok;
333 1 : searchEnded();
334 1 : });
335 0 : } catch (const std::exception& e) {
336 : // JAMI_ERR("Error computing kedht::ThreadPool::computation().run(ys: %s", e.what());
337 0 : s.first = true;
338 0 : s.second = true;
339 0 : searchEnded();
340 0 : return;
341 0 : }
342 3 : };
343 1 : dht::ThreadPool::computation().run(std::bind(search, true));
344 1 : dht::ThreadPool::computation().run(std::bind(search, false));
345 1 : }
346 :
347 : void
348 2 : ArchiveAccountManager::migrateAccount(AuthContext& ctx)
349 : {
350 2 : JAMI_WARN("[Auth] account migration needed");
351 2 : AccountArchive archive;
352 : try {
353 2 : archive = readArchive(ctx.credentials->password_scheme, ctx.credentials->password);
354 0 : } catch (...) {
355 0 : JAMI_DBG("[Auth] Can't load archive");
356 0 : ctx.onFailure(AuthError::INVALID_ARGUMENTS, "");
357 0 : return;
358 0 : }
359 :
360 2 : updateArchive(archive);
361 :
362 2 : if (updateCertificates(archive, ctx.credentials->updateIdentity)) {
363 : // because updateCertificates already regenerate a device, we do not need to
364 : // regenerate one in onArchiveLoaded
365 2 : onArchiveLoaded(ctx, std::move(archive));
366 : } else
367 0 : ctx.onFailure(AuthError::UNKNOWN, "");
368 2 : }
369 :
370 : void
371 613 : ArchiveAccountManager::onArchiveLoaded(AuthContext& ctx,
372 : AccountArchive&& a)
373 : {
374 1226 : auto ethAccount = dev::KeyPair(dev::Secret(a.eth_key)).address().hex();
375 613 : dhtnet::fileutils::check_dir(path_, 0700);
376 :
377 613 : a.save(fileutils::getFullPath(path_, archivePath_), ctx.credentials ? ctx.credentials->password_scheme : "", ctx.credentials ? ctx.credentials->password : "");
378 :
379 613 : if (not a.id.second->isCA()) {
380 0 : JAMI_ERR("[Auth] trying to sign a certificate with a non-CA.");
381 : }
382 :
383 613 : std::shared_ptr<dht::crypto::Certificate> deviceCertificate;
384 613 : std::unique_ptr<ContactList> contacts;
385 613 : auto usePreviousIdentity = false;
386 : // If updateIdentity got a valid certificate, there is no need for a new cert
387 613 : if (auto oldId = ctx.credentials->updateIdentity.second) {
388 4 : contacts = std::make_unique<ContactList>(ctx.accountId, oldId, path_, onChange_);
389 4 : if (contacts->isValidAccountDevice(*oldId) && ctx.credentials->updateIdentity.first) {
390 3 : deviceCertificate = oldId;
391 3 : usePreviousIdentity = true;
392 3 : JAMI_WARN("[Auth] Using previously generated certificate %s",
393 : deviceCertificate->getLongId().toString().c_str());
394 : } else {
395 1 : contacts.reset();
396 : }
397 613 : }
398 :
399 : // Generate a new device if needed
400 613 : if (!deviceCertificate) {
401 610 : JAMI_WARN("[Auth] creating new device certificate");
402 610 : auto request = ctx.request.get();
403 610 : if (not request->verify()) {
404 0 : JAMI_ERR("[Auth] Invalid certificate request.");
405 0 : ctx.onFailure(AuthError::INVALID_ARGUMENTS, "");
406 0 : return;
407 : }
408 1220 : deviceCertificate = std::make_shared<dht::crypto::Certificate>(
409 1830 : dht::crypto::Certificate::generate(*request, a.id));
410 1830 : JAMI_WARNING("[Auth] created new device: {}",
411 : deviceCertificate->getLongId());
412 610 : }
413 :
414 613 : auto receipt = makeReceipt(a.id, *deviceCertificate, ethAccount);
415 1226 : auto receiptSignature = a.id.first->sign({receipt.first.begin(), receipt.first.end()});
416 :
417 613 : auto info = std::make_unique<AccountInfo>();
418 613 : auto pk = usePreviousIdentity ? ctx.credentials->updateIdentity.first : ctx.key.get();
419 613 : auto sharedPk = pk->getSharedPublicKey();
420 613 : info->identity.first = pk;
421 613 : info->identity.second = deviceCertificate;
422 613 : info->accountId = a.id.second->getId().toString();
423 613 : info->devicePk = sharedPk;
424 613 : info->deviceId = info->devicePk->getLongId().toString();
425 613 : if (ctx.deviceName.empty())
426 0 : ctx.deviceName = info->deviceId.substr(8);
427 :
428 613 : if (!contacts)
429 610 : contacts = std::make_unique<ContactList>(ctx.accountId, a.id.second, path_, onChange_);
430 613 : info->contacts = std::move(contacts);
431 613 : info->contacts->setContacts(a.contacts);
432 613 : info->contacts->foundAccountDevice(deviceCertificate, ctx.deviceName, clock::now());
433 613 : info->ethAccount = ethAccount;
434 613 : info->announce = std::move(receipt.second);
435 613 : ConversationModule::saveConvInfosToPath(path_, a.conversations);
436 613 : ConversationModule::saveConvRequestsToPath(path_, a.conversationsRequests);
437 613 : info_ = std::move(info);
438 :
439 613 : ctx.onSuccess(*info_,
440 613 : std::move(a.config),
441 613 : std::move(receipt.first),
442 613 : std::move(receiptSignature));
443 613 : }
444 :
445 : std::pair<std::vector<uint8_t>, dht::InfoHash>
446 3 : ArchiveAccountManager::computeKeys(const std::string& password,
447 : const std::string& pin,
448 : bool previous)
449 : {
450 : // Compute time seed
451 3 : auto now = std::chrono::duration_cast<std::chrono::seconds>(clock::now().time_since_epoch());
452 3 : auto tseed = now.count() / std::chrono::seconds(EXPORT_KEY_RENEWAL_TIME).count();
453 3 : if (previous)
454 1 : tseed--;
455 3 : std::ostringstream ss;
456 3 : ss << std::hex << tseed;
457 3 : auto tseed_str = ss.str();
458 :
459 : // Generate key for archive encryption, using PIN as the salt
460 3 : std::vector<uint8_t> salt_key;
461 3 : salt_key.reserve(pin.size() + tseed_str.size());
462 3 : salt_key.insert(salt_key.end(), pin.begin(), pin.end());
463 3 : salt_key.insert(salt_key.end(), tseed_str.begin(), tseed_str.end());
464 3 : auto key = dht::crypto::stretchKey(password, salt_key, 256 / 8);
465 :
466 : // Generate public storage location as SHA1(key).
467 3 : auto loc = dht::InfoHash::get(key);
468 :
469 6 : return {key, loc};
470 3 : }
471 :
472 : std::pair<std::string, std::shared_ptr<dht::Value>>
473 613 : ArchiveAccountManager::makeReceipt(const dht::crypto::Identity& id,
474 : const dht::crypto::Certificate& device,
475 : const std::string& ethAccount)
476 : {
477 613 : JAMI_DBG("[Auth] signing device receipt");
478 613 : auto devId = device.getId();
479 613 : DeviceAnnouncement announcement;
480 613 : announcement.dev = devId;
481 613 : announcement.pk = device.getSharedPublicKey();
482 613 : dht::Value ann_val {announcement};
483 613 : ann_val.sign(*id.first);
484 :
485 613 : auto packedAnnoucement = ann_val.getPacked();
486 613 : JAMI_DBG("[Auth] device announcement size: %zu", packedAnnoucement.size());
487 :
488 613 : std::ostringstream is;
489 613 : is << "{\"id\":\"" << id.second->getId() << "\",\"dev\":\"" << devId << "\",\"eth\":\""
490 613 : << ethAccount << "\",\"announce\":\"" << base64::encode(packedAnnoucement) << "\"}";
491 :
492 : // auto announce_ = ;
493 1226 : return {is.str(), std::make_shared<dht::Value>(std::move(ann_val))};
494 613 : }
495 :
496 : bool
497 4 : ArchiveAccountManager::needsMigration(const dht::crypto::Identity& id)
498 : {
499 4 : if (not id.second)
500 0 : return false;
501 4 : auto cert = id.second->issuer;
502 8 : while (cert) {
503 6 : if (not cert->isCA()) {
504 0 : JAMI_WARN("certificate %s is not a CA, needs update.", cert->getId().toString().c_str());
505 0 : return true;
506 : }
507 6 : if (cert->getExpiration() < clock::now()) {
508 2 : JAMI_WARN("certificate %s is expired, needs update.", cert->getId().toString().c_str());
509 2 : return true;
510 : }
511 4 : cert = cert->issuer;
512 : }
513 2 : return false;
514 4 : }
515 :
516 : void
517 669 : ArchiveAccountManager::syncDevices()
518 : {
519 669 : if (not dht_ or not dht_->isRunning()) {
520 0 : JAMI_WARN("Not syncing devices: DHT is not running");
521 0 : return;
522 : }
523 669 : JAMI_DBG("Building device sync from %s", info_->deviceId.c_str());
524 669 : auto sync_data = info_->contacts->getSyncData();
525 :
526 1355 : for (const auto& dev : getKnownDevices()) {
527 : // don't send sync data to ourself
528 686 : if (dev.first.toString() == info_->deviceId)
529 669 : continue;
530 17 : if (!dev.second.certificate) {
531 0 : JAMI_WARNING("Cannot find certificate for {}", dev.first);
532 0 : continue;
533 0 : }
534 17 : auto pk = dev.second.certificate->getSharedPublicKey();
535 17 : JAMI_DBG("sending device sync to %s %s",
536 : dev.second.name.c_str(),
537 : dev.first.toString().c_str());
538 17 : auto syncDeviceKey = dht::InfoHash::get("inbox:" + pk->getId().toString());
539 17 : dht_->putEncrypted(syncDeviceKey, pk, sync_data);
540 17 : }
541 669 : }
542 :
543 : void
544 555 : ArchiveAccountManager::startSync(const OnNewDeviceCb& cb, const OnDeviceAnnouncedCb& dcb, bool publishPresence)
545 : {
546 555 : AccountManager::startSync(std::move(cb), std::move(dcb), publishPresence);
547 :
548 1665 : dht_->listen<DeviceSync>(
549 1110 : dht::InfoHash::get("inbox:" + info_->devicePk->getId().toString()),
550 14 : [this](DeviceSync&& sync) {
551 : // Received device sync data.
552 : // check device certificate
553 14 : findCertificate(sync.from,
554 14 : [this,
555 28 : sync](const std::shared_ptr<dht::crypto::Certificate>& cert) mutable {
556 14 : if (!cert or cert->getId() != sync.from) {
557 0 : JAMI_WARN("Can't find certificate for device %s",
558 : sync.from.toString().c_str());
559 0 : return;
560 : }
561 14 : if (not foundAccountDevice(cert))
562 0 : return;
563 14 : onSyncData(std::move(sync));
564 : });
565 :
566 14 : return true;
567 : });
568 555 : }
569 :
570 : AccountArchive
571 41 : ArchiveAccountManager::readArchive(std::string_view scheme, const std::string& pwd) const
572 : {
573 41 : JAMI_DBG("[Auth] reading account archive");
574 82 : return AccountArchive(fileutils::getFullPath(path_, archivePath_), scheme, pwd);
575 : }
576 :
577 : void
578 36 : ArchiveAccountManager::updateArchive(AccountArchive& archive) const
579 : {
580 : using namespace libjami::Account::ConfProperties;
581 :
582 : // Keys not exported to archive
583 : static const auto filtered_keys = {Ringtone::PATH,
584 : ARCHIVE_PATH,
585 : DEVICE_ID,
586 : DEVICE_NAME,
587 : Conf::CONFIG_DHT_PORT,
588 : DHT_PROXY_LIST_URL,
589 : AUTOANSWER,
590 : PROXY_ENABLED,
591 : PROXY_SERVER,
592 : PROXY_PUSH_TOKEN};
593 :
594 : // Keys with meaning of file path where the contents has to be exported in base64
595 : static const auto encoded_keys = {TLS::CA_LIST_FILE,
596 : TLS::CERTIFICATE_FILE,
597 : TLS::PRIVATE_KEY_FILE};
598 :
599 36 : JAMI_DBG("[Auth] building account archive");
600 2016 : for (const auto& it : onExportConfig_()) {
601 : // filter-out?
602 1980 : if (std::any_of(std::begin(filtered_keys), std::end(filtered_keys), [&](const auto& key) {
603 18720 : return key == it.first;
604 : }))
605 252 : continue;
606 :
607 : // file contents?
608 1728 : if (std::any_of(std::begin(encoded_keys), std::end(encoded_keys), [&](const auto& key) {
609 5076 : return key == it.first;
610 : })) {
611 : try {
612 180 : archive.config.emplace(it.first, base64::encode(fileutils::loadFile(it.second)));
613 36 : } catch (...) {
614 36 : }
615 : } else
616 1620 : archive.config[it.first] = it.second;
617 36 : }
618 36 : if (info_) {
619 : // If migrating from same archive, info_ will be null
620 34 : archive.contacts = info_->contacts->getContacts();
621 : // Note we do not know accountID_ here, use path
622 34 : archive.conversations = ConversationModule::convInfosFromPath(path_);
623 34 : archive.conversationsRequests = ConversationModule::convRequestsFromPath(path_);
624 : }
625 36 : }
626 :
627 : void
628 2 : ArchiveAccountManager::saveArchive(AccountArchive& archive, std::string_view scheme, const std::string& pwd)
629 : {
630 : try {
631 2 : updateArchive(archive);
632 2 : if (archivePath_.empty())
633 0 : archivePath_ = "export.gz";
634 2 : archive.save(fileutils::getFullPath(path_, archivePath_), scheme, pwd);
635 0 : } catch (const std::runtime_error& ex) {
636 0 : JAMI_ERR("[Auth] Can't export archive: %s", ex.what());
637 0 : return;
638 0 : }
639 : }
640 :
641 : bool
642 4 : ArchiveAccountManager::changePassword(const std::string& password_old,
643 : const std::string& password_new)
644 : {
645 : try {
646 4 : auto path = fileutils::getFullPath(path_, archivePath_);
647 6 : AccountArchive(path, fileutils::ARCHIVE_AUTH_SCHEME_PASSWORD, password_old)
648 2 : .save(path, fileutils::ARCHIVE_AUTH_SCHEME_PASSWORD, password_new);
649 2 : return true;
650 6 : } catch (const std::exception&) {
651 2 : return false;
652 2 : }
653 : }
654 :
655 : std::vector<uint8_t>
656 0 : ArchiveAccountManager::getPasswordKey(const std::string& password)
657 : {
658 : try {
659 0 : auto data = dhtnet::fileutils::loadFile(fileutils::getFullPath(path_, archivePath_));
660 : // Try to decrypt to check if password is valid
661 0 : auto key = dht::crypto::aesGetKey(data, password);
662 0 : auto decrypted = dht::crypto::aesDecrypt(dht::crypto::aesGetEncrypted(data), key);
663 0 : return key;
664 0 : } catch (const std::exception& e) {
665 0 : JAMI_ERR("Error loading archive: %s", e.what());
666 0 : }
667 0 : return {};
668 : }
669 :
670 : std::string
671 1 : generatePIN(size_t length = 16, size_t split = 8)
672 : {
673 : static constexpr const char alphabet[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
674 1 : std::random_device rd;
675 1 : std::uniform_int_distribution<size_t> dis(0, sizeof(alphabet) - 2);
676 1 : std::string ret;
677 1 : ret.reserve(length);
678 17 : for (size_t i = 0; i < length; i++) {
679 16 : ret.push_back(alphabet[dis(rd)]);
680 16 : if (i % split == split - 1 and i != length - 1)
681 1 : ret.push_back('-');
682 : }
683 2 : return ret;
684 1 : }
685 :
686 : void
687 2 : ArchiveAccountManager::addDevice(const std::string& password, AddDeviceCallback cb)
688 : {
689 2 : dht::ThreadPool::computation().run([password, cb = std::move(cb), w=weak_from_this()] {
690 2 : auto this_ = std::static_pointer_cast<ArchiveAccountManager>(w.lock());
691 2 : if (not this_) return;
692 :
693 2 : std::vector<uint8_t> key;
694 2 : dht::InfoHash loc;
695 2 : std::string pin_str;
696 2 : AccountArchive a;
697 : try {
698 2 : JAMI_DBG("[Auth] exporting account");
699 :
700 2 : a = this_->readArchive("password", password);
701 :
702 : // Generate random PIN
703 1 : pin_str = generatePIN();
704 :
705 1 : std::tie(key, loc) = computeKeys(password, pin_str);
706 1 : } catch (const std::exception& e) {
707 1 : JAMI_ERR("[Auth] can't export account: %s", e.what());
708 1 : cb(AddDeviceResult::ERROR_CREDENTIALS, {});
709 1 : return;
710 1 : }
711 : // now that key and loc are computed, display to user in lowercase
712 1 : std::transform(pin_str.begin(), pin_str.end(), pin_str.begin(), ::tolower);
713 : try {
714 1 : this_->updateArchive(a);
715 2 : auto encrypted = dht::crypto::aesEncrypt(archiver::compress(a.serialize()), key);
716 1 : if (not this_->dht_ or not this_->dht_->isRunning())
717 0 : throw std::runtime_error("DHT is not running..");
718 1 : JAMI_WARN("[Auth] exporting account with PIN: %s at %s (size %zu)",
719 : pin_str.c_str(),
720 : loc.toString().c_str(),
721 : encrypted.size());
722 1 : this_->dht_->put(loc, encrypted, [cb, pin = std::move(pin_str)](bool ok) {
723 1 : JAMI_DBG("[Auth] account archive published: %s", ok ? "success" : "failure");
724 1 : if (ok)
725 1 : cb(AddDeviceResult::SUCCESS_SHOW_PIN, pin);
726 : else
727 0 : cb(AddDeviceResult::ERROR_NETWORK, {});
728 1 : });
729 1 : } catch (const std::exception& e) {
730 0 : JAMI_ERR("[Auth] can't export account: %s", e.what());
731 0 : cb(AddDeviceResult::ERROR_NETWORK, {});
732 0 : return;
733 0 : }
734 5 : });
735 2 : }
736 :
737 : bool
738 3 : ArchiveAccountManager::revokeDevice(const std::string& device,
739 : std::string_view scheme,
740 : const std::string& password,
741 : RevokeDeviceCallback cb)
742 : {
743 3 : auto fa = dht::ThreadPool::computation().getShared<AccountArchive>(
744 9 : [this, scheme=std::string(scheme), password] { return readArchive(scheme, password); });
745 3 : findCertificate(DeviceId(device),
746 3 : [fa = std::move(fa), scheme=std::string(scheme), password, device, cb, w=weak_from_this()](
747 : const std::shared_ptr<dht::crypto::Certificate>& crt) mutable {
748 3 : if (not crt) {
749 1 : cb(RevokeDeviceResult::ERROR_NETWORK);
750 1 : return;
751 : }
752 2 : auto this_ = std::static_pointer_cast<ArchiveAccountManager>(w.lock());
753 2 : if (not this_) return;
754 2 : this_->info_->contacts->foundAccountDevice(crt);
755 2 : AccountArchive a;
756 : try {
757 2 : a = fa.get();
758 0 : } catch (...) {
759 0 : cb(RevokeDeviceResult::ERROR_CREDENTIALS);
760 0 : return;
761 0 : }
762 : // Add revoked device to the revocation list and resign it
763 2 : if (not a.revoked)
764 2 : a.revoked = std::make_shared<decltype(a.revoked)::element_type>();
765 2 : a.revoked->revoke(*crt);
766 2 : a.revoked->sign(a.id);
767 : // add to CRL cache
768 2 : this_->certStore().pinRevocationList(a.id.second->getId().toString(), a.revoked);
769 2 : this_->certStore().loadRevocations(*a.id.second);
770 :
771 : // Announce CRL immediately
772 2 : auto h = a.id.second->getId();
773 2 : this_->dht_->put(h, a.revoked, dht::DoneCallback {}, {}, true);
774 :
775 2 : this_->saveArchive(a, scheme, password);
776 2 : this_->info_->contacts->removeAccountDevice(crt->getLongId());
777 2 : cb(RevokeDeviceResult::SUCCESS);
778 2 : this_->syncDevices();
779 2 : });
780 3 : return false;
781 3 : }
782 :
783 : bool
784 31 : ArchiveAccountManager::exportArchive(const std::string& destinationPath, std::string_view scheme, const std::string& password)
785 : {
786 : try {
787 : // Save contacts if possible before exporting
788 31 : AccountArchive archive = readArchive(scheme, password);
789 31 : updateArchive(archive);
790 31 : auto archivePath = fileutils::getFullPath(path_, archivePath_);
791 31 : archive.save(archivePath, scheme, password);
792 :
793 : // Export the file
794 31 : std::error_code ec;
795 31 : std::filesystem::copy_file(archivePath, destinationPath, std::filesystem::copy_options::overwrite_existing, ec);
796 31 : return !ec;
797 31 : } catch (const std::runtime_error& ex) {
798 0 : JAMI_ERR("[Auth] Can't export archive: %s", ex.what());
799 0 : return false;
800 0 : } catch (...) {
801 0 : JAMI_ERR("[Auth] Can't export archive: can't read archive");
802 0 : return false;
803 0 : }
804 : }
805 :
806 : bool
807 0 : ArchiveAccountManager::isPasswordValid(const std::string& password)
808 : {
809 : try {
810 0 : readArchive(fileutils::ARCHIVE_AUTH_SCHEME_PASSWORD, password);
811 0 : return true;
812 0 : } catch (...) {
813 0 : return false;
814 0 : }
815 : }
816 :
817 : #if HAVE_RINGNS
818 : void
819 1 : ArchiveAccountManager::registerName(const std::string& name,
820 : std::string_view scheme,
821 : const std::string& password,
822 : RegistrationCallback cb)
823 : {
824 1 : std::string signedName;
825 1 : auto nameLowercase {name};
826 1 : std::transform(nameLowercase.begin(), nameLowercase.end(), nameLowercase.begin(), ::tolower);
827 1 : std::string publickey;
828 1 : std::string accountId;
829 1 : std::string ethAccount;
830 :
831 : try {
832 1 : auto archive = readArchive(scheme, password);
833 1 : auto privateKey = archive.id.first;
834 1 : const auto& pk = privateKey->getPublicKey();
835 1 : publickey = pk.toString();
836 1 : accountId = pk.getId().toString();
837 2 : signedName = base64::encode(
838 3 : privateKey->sign(std::vector<uint8_t>(nameLowercase.begin(), nameLowercase.end())));
839 1 : ethAccount = dev::KeyPair(dev::Secret(archive.eth_key)).address().hex();
840 1 : } catch (const std::exception& e) {
841 : // JAMI_ERR("[Auth] can't export account: %s", e.what());
842 0 : cb(NameDirectory::RegistrationResponse::invalidCredentials);
843 0 : return;
844 0 : }
845 :
846 1 : nameDir_.get().registerName(accountId, nameLowercase, ethAccount, cb, signedName, publickey);
847 1 : }
848 : #endif
849 :
850 : } // namespace jami
|