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