jamiaccount: add manager configuration property

Change-Id: I32136e7381b2f3f73f206a83d573822c6b291291
This commit is contained in:
Adrien Béraud
2019-09-10 14:25:11 -04:00
committed by Sébastien Blin
parent 75b134cf17
commit 4c899b1e15
15 changed files with 1366 additions and 886 deletions

View File

@ -719,7 +719,9 @@
<ClCompile Include="..\src\im\message_engine.cpp" />
<ClCompile Include="..\src\ip_utils.cpp" />
<ClCompile Include="..\src\jamidht\account_manager.cpp" />
<ClCompile Include="..\src\jamidht\archive_account_manager.cpp" />
<ClCompile Include="..\src\jamidht\contact_list.cpp" />
<ClCompile Include="..\src\jamidht\server_account_manager.cpp" />
<ClCompile Include="..\src\logger.cpp" />
<ClCompile Include="..\src\manager.cpp" />
<ClCompile Include="..\src\media\audio\audiobuffer.cpp" />
@ -879,8 +881,10 @@
<ClInclude Include="..\src\im\message_engine.h" />
<ClInclude Include="..\src\ip_utils.h" />
<ClInclude Include="..\src\jamidht\account_manager.h" />
<ClInclude Include="..\src\jamidht\archive_account_manager.h" />
<ClInclude Include="..\src\jamidht\contact_list.h" />
<ClInclude Include="..\src\jamidht\jami_contact.h" />
<ClInclude Include="..\src\jamidht\server_account_manager.h" />
<ClInclude Include="..\src\logger.h" />
<ClInclude Include="..\src\manager.h" />
<ClInclude Include="..\src\map_utils.h" />

View File

@ -427,6 +427,12 @@
<ClCompile Include="..\src\jamidht\contact_list.cpp">
<Filter>Source Files\jamidht</Filter>
</ClCompile>
<ClCompile Include="..\src\jamidht\archive_account_manager.cpp">
<Filter>Source Files\jamidht</Filter>
</ClCompile>
<ClCompile Include="..\src\jamidht\server_account_manager.cpp">
<Filter>Source Files\jamidht</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\src\account.h">
@ -882,6 +888,12 @@
<ClInclude Include="..\src\jamidht\jami_contact.h">
<Filter>Source Files\jamidht</Filter>
</ClInclude>
<ClInclude Include="..\src\jamidht\archive_account_manager.h">
<Filter>Source Files\jamidht</Filter>
</ClInclude>
<ClInclude Include="..\src\jamidht\server_account_manager.h">
<Filter>Source Files\jamidht</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="..\src\jamidht\eth\libdevcore\Makefile.am">

View File

@ -145,6 +145,7 @@ constexpr static const char PROXY_PUSH_TOKEN [] = "Account.proxyPushToken
constexpr static const char DHT_PEER_DISCOVERY [] = "Account.peerDiscovery";
constexpr static const char ACCOUNT_PEER_DISCOVERY [] = "Account.accountDiscovery";
constexpr static const char ACCOUNT_PUBLISH [] = "Account.accountPublish";
constexpr static const char MANAGER_URI [] = "Account.managerUri";
namespace Audio {

View File

@ -23,7 +23,11 @@ libringacc_la_SOURCES = \
contact_list.h \
contact_list.cpp \
account_manager.h \
account_manager.cpp
account_manager.cpp \
archive_account_manager.h \
archive_account_manager.cpp \
server_account_manager.h \
server_account_manager.cpp
if RINGNS
libringacc_la_SOURCES += \

View File

@ -33,347 +33,6 @@
namespace jami {
const constexpr auto EXPORT_KEY_RENEWAL_TIME = std::chrono::minutes(20);
constexpr static const char* const DHT_TYPE_NS = "cx.ring";
template <typename To, typename From>
std::unique_ptr<To> dynamic_unique_cast(std::unique_ptr<From>&& p) {
if (auto cast = dynamic_cast<To*>(p.get())) {
std::unique_ptr<To> result(cast);
p.release();
return result;
}
return {};
}
void
ArchiveAccountManager::initAuthentication(
CertRequest request,
std::unique_ptr<AccountCredentials> credentials,
AuthSuccessCallback onSuccess,
AuthFailureCallback onFailure,
OnChangeCallback onChange)
{
auto ctx = std::make_shared<AuthContext>();
ctx->request = std::move(request);
ctx->credentials = dynamic_unique_cast<ArchiveAccountCredentials>(std::move(credentials));
ctx->onSuccess = std::move(onSuccess);
ctx->onFailure = std::move(onFailure);
if (not ctx->credentials) {
onFailure(AuthError::INVALID_ARGUMENTS, "invalid credentials");
return;
}
onChange_ = std::move(onChange);
if (ctx->credentials->scheme == "dht") {
loadFromDHT(ctx);
return;
}
dht::ThreadPool::computation().run([
ctx = std::move(ctx),
onAsync = onAsync_
]() mutable {
onAsync([ctx = std::move(ctx)](AccountManager& accountManager) mutable {
auto& this_ = *static_cast<ArchiveAccountManager*>(&accountManager);
try {
if (ctx->credentials->scheme == "file") {
this_.loadFromFile(ctx);
return;
} else {
if (ctx->credentials->updateIdentity.first and ctx->credentials->updateIdentity.second) {
auto future_keypair = dht::ThreadPool::computation().get<dev::KeyPair>(&dev::KeyPair::create);
AccountArchive a;
JAMI_WARN("[Auth] converting certificate from old account %s", ctx->credentials->updateIdentity.first->getPublicKey().getId().toString().c_str());
a.id = std::move(ctx->credentials->updateIdentity);
try {
a.ca_key = std::make_shared<dht::crypto::PrivateKey>(fileutils::loadFile("ca.key", this_.path_));
} catch (...) {}
this_.updateCertificates(a, ctx->credentials->updateIdentity);
auto keypair = future_keypair.get();
a.eth_key = keypair.secret().makeInsecure().asBytes();
this_.onArchiveLoaded(*ctx, std::move(a));
} else {
this_.createAccount(ctx);
}
}
} catch (const std::exception& e) {
ctx->onFailure(AuthError::UNKNOWN, e.what());
}
});
});
}
bool
ArchiveAccountManager::updateCertificates(AccountArchive& archive, dht::crypto::Identity& device)
{
JAMI_WARN("Updating certificates");
using Certificate = dht::crypto::Certificate;
// We need the CA key to resign certificates
if (not archive.id.first or
not *archive.id.first or
not archive.id.second or
not archive.ca_key or
not *archive.ca_key)
return false;
// Currently set the CA flag and update expiration dates
bool updated = false;
auto& cert = archive.id.second;
auto ca = cert->issuer;
// Update CA if possible and relevant
if (not ca or (not ca->issuer and (not ca->isCA() or ca->getExpiration() < clock::now()))) {
ca = std::make_shared<Certificate>(Certificate::generate(*archive.ca_key, "Jami CA", {}, true));
updated = true;
JAMI_DBG("CA CRT re-generated");
}
// Update certificate
if (updated or not cert->isCA() or cert->getExpiration() < clock::now()) {
cert = std::make_shared<Certificate>(Certificate::generate(*archive.id.first, "Jami", dht::crypto::Identity{archive.ca_key, ca}, true));
updated = true;
JAMI_DBG("Jami CRT re-generated");
}
if (updated and device.first and *device.first) {
// update device certificate
device.second = std::make_shared<Certificate>(Certificate::generate(*device.first, "Jami device", archive.id));
JAMI_DBG("device CRT re-generated");
}
return updated;
}
void
ArchiveAccountManager::createAccount(const std::shared_ptr<AuthContext>& ctx)
{
AccountArchive a;
auto future_keypair = dht::ThreadPool::computation().get<dev::KeyPair>(&dev::KeyPair::create);
auto ca = dht::crypto::generateIdentity("Jami CA");
if (!ca.first || !ca.second) {
throw std::runtime_error("Can't generate CA for this account.");
}
a.id = dht::crypto::generateIdentity("Jami", ca, 4096, true);
if (!a.id.first || !a.id.second) {
throw std::runtime_error("Can't generate identity for this account.");
}
JAMI_WARN("[Auth] new account: CA: %s, RingID: %s",
ca.second->getId().toString().c_str(),
a.id.second->getId().toString().c_str());
a.ca_key = ca.first;
auto keypair = future_keypair.get();
a.eth_key = keypair.secret().makeInsecure().asBytes();
onArchiveLoaded(*ctx, std::move(a));
}
void
ArchiveAccountManager::loadFromFile(const std::shared_ptr<AuthContext>& ctx)
{
AccountArchive archive;
try {
archive = AccountArchive(ctx->credentials->uri, ctx->credentials->password);
} catch (const std::exception& ex) {
JAMI_WARN("[Auth] can't read file: %s", ex.what());
ctx->onFailure(AuthError::UNKNOWN, ex.what());
return;
}
onArchiveLoaded(*ctx, std::move(archive));
}
struct ArchiveAccountManager::DhtLoadContext {
dht::DhtRunner dht;
std::pair<bool, bool> stateOld {false, true};
std::pair<bool, bool> stateNew {false, true};
bool found {false};
};
void
ArchiveAccountManager::loadFromDHT(const std::shared_ptr<AuthContext>& ctx)
{
ctx->dhtContext = std::make_unique<DhtLoadContext>();
ctx->dhtContext->dht.run(ctx->credentials->dhtPort, {}, true);
for (const auto& bootstrap : ctx->credentials->dhtBootstrap)
ctx->dhtContext->dht.bootstrap(bootstrap);
auto searchEnded = [ctx]() {
if (not ctx->dhtContext or ctx->dhtContext->found) {
return;
}
auto& s = *ctx->dhtContext;
if (s.stateOld.first && s.stateNew.first) {
dht::ThreadPool::computation().run([ctx, network_error = !s.stateOld.second && !s.stateNew.second]{
ctx->dhtContext.reset();
JAMI_WARN("[Auth] failure looking for archive on DHT: %s", /**/network_error ? "network error" : "not found");
ctx->onFailure(network_error ? AuthError::NETWORK : AuthError::UNKNOWN, "");
});
}
};
auto search = [ctx, searchEnded, onAsync = onAsync_](bool previous) {
std::vector<uint8_t> key;
dht::InfoHash loc;
auto& s = previous ? ctx->dhtContext->stateOld : ctx->dhtContext->stateNew;
// compute archive location and decryption keys
try {
std::tie(key, loc) = computeKeys(ctx->credentials->password, ctx->credentials->uri, previous);
JAMI_DBG("[Auth] trying to load account from DHT with %s at %s", /**/ctx->credentials->uri.c_str(), loc.toString().c_str());
ctx->dhtContext->dht.get(loc, [ctx, key=std::move(key), onAsync](const std::shared_ptr<dht::Value>& val) {
std::vector<uint8_t> decrypted;
try {
decrypted = archiver::decompress(dht::crypto::aesDecrypt(val->data, key));
} catch (const std::exception& ex) {
return true;
}
JAMI_DBG("[Auth] found archive on the DHT");
ctx->dhtContext->found = true;
dht::ThreadPool::computation().run([
ctx,
decrypted = std::move(decrypted),
onAsync
]{
try {
auto archive = AccountArchive(decrypted);
onAsync([&](AccountManager& accountManager) {
auto& this_ = *static_cast<ArchiveAccountManager*>(&accountManager);
if (ctx->dhtContext) {
ctx->dhtContext->dht.join();
ctx->dhtContext.reset();
}
this_.onArchiveLoaded(*ctx, std::move(archive)/*, std::move(contacts)*/);
});
} catch (const std::exception& e) {
ctx->onFailure(AuthError::UNKNOWN, "");
}
});
return not ctx->dhtContext->found;
}, [=, &s](bool ok) {
JAMI_DBG("[Auth] DHT archive search ended at %s", /**/loc.toString().c_str());
s.first = true;
s.second = ok;
searchEnded();
});
} catch (const std::exception& e) {
// JAMI_ERR("Error computing kedht::ThreadPool::computation().run(ys: %s", e.what());
s.first = true;
s.second = true;
searchEnded();
return;
}
};
dht::ThreadPool::computation().run(std::bind(search, true));
dht::ThreadPool::computation().run(std::bind(search, false));
}
void
ArchiveAccountManager::onArchiveLoaded(
AuthContext& ctx,
AccountArchive&& a)
{
auto ethAccount = dev::KeyPair(dev::Secret(a.eth_key)).address().hex();
fileutils::check_dir(path_.c_str(), 0700);
auto path = fileutils::getFullPath(path_, archivePath_);
a.save(path, ctx.credentials ? ctx.credentials->password : "");
if (not a.id.second->isCA()) {
JAMI_ERR("[Auth] trying to sign a certificate with a non-CA.");
}
JAMI_WARN("generating device certificate");
auto request = ctx.request.get();
if (not request->verify()) {
JAMI_ERR("[Auth] Invalid certificate request.");
ctx.onFailure(AuthError::INVALID_ARGUMENTS, "");
return;
}
auto deviceCertificate = std::make_shared<dht::crypto::Certificate>(dht::crypto::Certificate::generate(*request, a.id));
auto receipt = makeReceipt(a.id, *deviceCertificate, ethAccount);
auto receiptSignature = a.id.first->sign({receipt.first.begin(), receipt.first.end()});
auto info = std::make_unique<AccountInfo>();
info->identity.second = deviceCertificate;
info->contacts = std::make_unique<ContactList>(a.id.second, path_, onChange_);
info->contacts->setContacts(a.contacts);
info->accountId = a.id.second->getId().toString();
info->deviceId = deviceCertificate->getPublicKey().getId().toString();
info->ethAccount = ethAccount;
info->announce = std::move(receipt.second);
info_ = std::move(info);
JAMI_WARN("[Auth] created new device: %s", info_->deviceId.c_str());
ctx.onSuccess(*info_, {}, std::move(receipt.first), std::move(receiptSignature));
}
std::pair<std::vector<uint8_t>, dht::InfoHash>
ArchiveAccountManager::computeKeys(const std::string& password, const std::string& pin, bool previous)
{
// Compute time seed
auto now = std::chrono::duration_cast<std::chrono::seconds>(clock::now().time_since_epoch());
auto tseed = now.count() / std::chrono::seconds(EXPORT_KEY_RENEWAL_TIME).count();
if (previous)
tseed--;
std::stringstream ss;
ss << std::hex << tseed;
auto tseed_str = ss.str();
// Generate key for archive encryption, using PIN as the salt
std::vector<uint8_t> salt_key;
salt_key.reserve(pin.size() + tseed_str.size());
salt_key.insert(salt_key.end(), pin.begin(), pin.end());
salt_key.insert(salt_key.end(), tseed_str.begin(), tseed_str.end());
auto key = dht::crypto::stretchKey(password, salt_key, 256/8);
// Generate public storage location as SHA1(key).
auto loc = dht::InfoHash::get(key);
return {key, loc};
}
std::pair<std::string, std::shared_ptr<dht::Value>>
ArchiveAccountManager::makeReceipt(const dht::crypto::Identity& id, const dht::crypto::Certificate& device, const std::string& ethAccount)
{
JAMI_DBG("[Auth] signing device receipt");
auto devId = device.getId();
DeviceAnnouncement announcement;
announcement.dev = devId;
dht::Value ann_val {announcement};
ann_val.sign(*id.first);
std::ostringstream is;
is << "{\"id\":\"" << id.second->getId()
<< "\",\"dev\":\"" << devId
<< "\",\"eth\":\"" << ethAccount
<< "\",\"announce\":\"" << base64::encode(ann_val.getPacked()) << "\"}";
//auto announce_ = ;
return {is.str(), std::make_shared<dht::Value>(std::move(ann_val))};
}
bool
ArchiveAccountManager::needsMigration(const dht::crypto::Identity& id)
{
if (not id.second)
return false;
auto cert = id.second->issuer;
while (cert) {
if (not cert->isCA()){
JAMI_WARN("certificate %s is not a CA, needs update.", cert->getId().toString().c_str());
return true;
}
if (cert->getExpiration() < clock::now()) {
JAMI_WARN("certificate %s is expired, needs update.", cert->getId().toString().c_str());
return true;
}
cert = cert->issuer;
}
return false;
}
dht::crypto::Identity
AccountManager::loadIdentity(const std::string& crt_path, const std::string& key_path, const std::string& key_pwd) const
{
@ -404,6 +63,31 @@ AccountManager::loadIdentity(const std::string& crt_path, const std::string& key
return {};
}
std::shared_ptr<dht::Value>
AccountManager::parseAnnounce(const std::string& announceBase64, const std::string& accountId, const std::string& deviceId)
{
auto announce_val = std::make_shared<dht::Value>();
try {
auto announce = base64::decode(announceBase64);
msgpack::object_handle announce_msg = msgpack::unpack((const char*)announce.data(), announce.size());
announce_val->msgpack_unpack(announce_msg.get());
if (not announce_val->checkSignature()) {
JAMI_ERR("[Auth] announce signature check failed");
return {};
}
DeviceAnnouncement da;
da.unpackValue(*announce_val);
if (da.from.toString() != accountId or da.dev.toString() != deviceId) {
JAMI_ERR("[Auth] device ID mismatch in announce");
return {};
}
} catch (const std::exception& e) {
JAMI_ERR("[Auth] can't read announce: %s", e.what());
return {};
}
return announce_val;
}
const AccountInfo*
AccountManager::useIdentity(
const dht::crypto::Identity& identity,
@ -458,23 +142,8 @@ AccountManager::useIdentity(
return nullptr;
}
dht::Value announce_val;
try {
auto announce = base64::decode(root["announce"].asString());
msgpack::object_handle announce_msg = msgpack::unpack((const char*)announce.data(), announce.size());
announce_val.msgpack_unpack(announce_msg.get());
if (not announce_val.checkSignature()) {
JAMI_ERR("[Auth] announce signature check failed");
return nullptr;
}
DeviceAnnouncement da;
da.unpackValue(announce_val);
if (da.from.toString() != id or da.dev.toString() != dev_id) {
JAMI_ERR("[Auth] device ID mismatch in announce");
return nullptr;
}
} catch (const std::exception& e) {
JAMI_ERR("[Auth] can't read announce: %s", e.what());
auto announce = parseAnnounce(root["announce"].asString(), id, dev_id);
if (not announce) {
return nullptr;
}
@ -486,7 +155,7 @@ AccountManager::useIdentity(
info->contacts->load();
info->accountId = id;
info->deviceId = identity.first->getPublicKey().getId().toString();
info->announce = std::make_shared<dht::Value>(std::move(announce_val));
info->announce = std::move(announce);
info->ethAccount = root["eth"].asString();
info_ = std::move(info);
@ -494,202 +163,6 @@ AccountManager::useIdentity(
return info_.get();
}
AccountArchive
ArchiveAccountManager::readArchive(const std::string& pwd) const
{
JAMI_DBG("[Auth] reading account archive");
return AccountArchive(fileutils::getFullPath(path_, archivePath_), pwd);
}
void
ArchiveAccountManager::updateArchive(AccountArchive& archive) const
{
using namespace DRing::Account::ConfProperties;
// Keys not exported to archive
static const auto filtered_keys = { Ringtone::PATH,
ARCHIVE_PATH,
RING_DEVICE_ID,
RING_DEVICE_NAME,
Conf::CONFIG_DHT_PORT };
// Keys with meaning of file path where the contents has to be exported in base64
static const auto encoded_keys = { TLS::CA_LIST_FILE,
TLS::CERTIFICATE_FILE,
TLS::PRIVATE_KEY_FILE };
JAMI_DBG("[Auth] building account archive");
for (const auto& it : onExportConfig_()) {
// filter-out?
if (std::any_of(std::begin(filtered_keys), std::end(filtered_keys),
[&](const auto& key){ return key == it.first; }))
continue;
// file contents?
if (std::any_of(std::begin(encoded_keys), std::end(encoded_keys),
[&](const auto& key){ return key == it.first; })) {
try {
archive.config.emplace(it.first, base64::encode(fileutils::loadFile(it.second)));
} catch (...) {}
} else
archive.config.insert(it);
}
archive.contacts = info_->contacts->getContacts();
}
void
ArchiveAccountManager::saveArchive(AccountArchive& archive, const std::string& pwd)
{
try {
updateArchive(archive);
if (archivePath_.empty())
archivePath_ = "export.gz";
archive.save(fileutils::getFullPath(path_, archivePath_), pwd);
//archiveHasPassword_ = not pwd.empty();
} catch (const std::runtime_error& ex) {
JAMI_ERR("[Auth] Can't export archive: %s", ex.what());
return;
}
}
bool
ArchiveAccountManager::changePassword(const std::string& password_old, const std::string& password_new)
{
try {
auto path = fileutils::getFullPath(path_, archivePath_);
AccountArchive(path, password_old).save(path, password_new);
return true;
} catch (const std::exception&) {
return false;
}
}
std::string
generatePIN(size_t length = 8)
{
static constexpr const char alphabet[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
dht::crypto::random_device rd;
std::uniform_int_distribution<size_t> dis(0, sizeof(alphabet)-2);
std::string ret;
ret.reserve(length);
for (size_t i=0; i<length; i++)
ret.push_back(alphabet[dis(rd)]);
return ret;
}
void
ArchiveAccountManager::addDevice(const std::string& password, AddDeviceCallback cb)
{
dht::ThreadPool::computation().run([onAsync = onAsync_, password, cb = std::move(cb)]() mutable {
onAsync([password = std::move(password), cb = std::move(cb)](AccountManager& accountManager) mutable {
auto& this_ = *static_cast<ArchiveAccountManager*>(&accountManager);
std::vector<uint8_t> key;
dht::InfoHash loc;
std::string pin_str;
AccountArchive a;
try {
JAMI_DBG("[Auth] exporting account");
a = this_.readArchive(password);
// Generate random PIN
pin_str = generatePIN();
std::tie(key, loc) = computeKeys(password, pin_str);
} catch (const std::exception& e) {
JAMI_ERR("[Auth] can't export account: %s", e.what());
cb(AddDeviceResult::ERROR_CREDENTIALS, {});
return;
}
// now that key and loc are computed, display to user in lowercase
std::transform(pin_str.begin(), pin_str.end(), pin_str.begin(), ::tolower);
try {
this_.updateArchive(a);
auto encrypted = dht::crypto::aesEncrypt(archiver::compress(a.serialize()), key);
if (not this_.dht_->isRunning())
throw std::runtime_error("DHT is not running..");
JAMI_WARN("[Auth] exporting account with PIN: %s at %s (size %zu)", pin_str.c_str(), loc.toString().c_str(), encrypted.size());
this_.dht_->put(loc, encrypted, [cb, pin = std::move(pin_str)](bool ok) {
JAMI_DBG("[Auth] account archive published: %s", ok ? "success" : "failure");
if (ok)
cb(AddDeviceResult::SUCCESS_SHOW_PIN, pin);
else
cb(AddDeviceResult::ERROR_NETWORK, {});
});
} catch (const std::exception& e) {
JAMI_ERR("[Auth] can't export account: %s", e.what());
cb(AddDeviceResult::ERROR_NETWORK, {});
return;
}
});
});
}
bool
ArchiveAccountManager::revokeDevice(const std::string& password, const std::string& device, RevokeDeviceCallback cb)
{
auto fa = dht::ThreadPool::computation().getShared<AccountArchive>([this, password] { return readArchive(password); });
findCertificate(dht::InfoHash(device),
[fa=std::move(fa),password,device,cb,onAsync=onAsync_](const std::shared_ptr<dht::crypto::Certificate>& crt) mutable
{
if (not crt) {
cb(RevokeDeviceResult::ERROR_NETWORK);
return;
}
onAsync([cb, crt=std::move(crt), fa=std::move(fa), password=std::move(password)](AccountManager& accountManager) mutable {
auto& this_ = *static_cast<ArchiveAccountManager*>(&accountManager);
this_.info_->contacts->foundAccountDevice(crt);
AccountArchive a;
try {
a = fa.get();
} catch (...) {
cb(RevokeDeviceResult::ERROR_CREDENTIALS);
return;
}
// Add revoked device to the revocation list and resign it
if (not a.revoked)
a.revoked = std::make_shared<decltype(a.revoked)::element_type>();
a.revoked->revoke(*crt);
a.revoked->sign(a.id);
// add to CRL cache
tls::CertificateStore::instance().pinRevocationList(a.id.second->getId().toString(), a.revoked);
tls::CertificateStore::instance().loadRevocations(*a.id.second);
this_.saveArchive(a, password);
this_.info_->contacts->removeAccountDevice(crt->getId());
cb(RevokeDeviceResult::SUCCESS);
this_.syncDevices();
});
});
return false;
}
bool
ArchiveAccountManager::exportArchive(const std::string& destinationPath, const std::string& password)
{
try {
// Save contacts if possible before exporting
AccountArchive archive = readArchive(password);
updateArchive(archive);
archive.save(fileutils::getFullPath(path_, archivePath_), password);
// Export the file
auto sourcePath = fileutils::getFullPath(path_, archivePath_);
std::ifstream src(sourcePath, std::ios::in | std::ios::binary);
if (!src) return false;
std::ofstream dst(destinationPath, std::ios::out | std::ios::binary);
dst << src.rdbuf();
return true;
} catch (const std::runtime_error& ex) {
JAMI_ERR("[Auth] Can't export archive: %s", ex.what());
return false;
} catch (...) {
JAMI_ERR("[Auth] Can't export archive: can't read archive");
return false;
}
}
const std::map<dht::InfoHash, KnownDevice>&
AccountManager::getKnownDevices() const {
return info_->contacts->getKnownDevices();
@ -800,23 +273,6 @@ AccountManager::getContactDetails(const std::string& uri) const
return info_->contacts->getContactDetails(h);
}
bool
ArchiveAccountManager::findCertificate(const dht::InfoHash& h, std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
{
if (auto cert = tls::CertificateStore::instance().getCertificate(h.toString())) {
if (cb)
cb(cert);
} else {
dht_->findCertificate(h, [cb](const std::shared_ptr<dht::crypto::Certificate>& crt) {
if (crt)
tls::CertificateStore::instance().pinCertificate(crt);
if (cb)
cb(crt);
});
}
return true;
}
bool
AccountManager::setCertificateStatus(const std::string& cert_id, tls::TrustStore::PermissionStatus status)
{
@ -928,168 +384,16 @@ AccountManager::forEachDevice(const dht::InfoHash& to,
});
}
void
ArchiveAccountManager::syncDevices()
{
JAMI_DBG("Building device sync from %s", info_->deviceId.c_str());
auto sync_data = info_->contacts->getSyncData();
for (const auto& dev : getKnownDevices()) {
// don't send sync data to ourself
if (dev.first.toString() == info_->deviceId)
continue;
JAMI_DBG("sending device sync to %s %s", dev.second.name.c_str(), dev.first.toString().c_str());
auto syncDeviceKey = dht::InfoHash::get("inbox:"+dev.first.toString());
dht_->putEncrypted(syncDeviceKey, dev.first, sync_data);
}
}
void
ArchiveAccountManager::startSync()
{
// Put device annoucement
if (info_->announce) {
auto h = dht::InfoHash(info_->accountId);
JAMI_DBG("announcing device at %s", h.toString().c_str());
dht_->put(h, info_->announce, dht::DoneCallback{}, {}, true);
for (const auto& crl : info_->identity.second->issuer->getRevocationLists())
dht_->put(h, crl, dht::DoneCallback{}, {}, true);
dht_->listen<DeviceAnnouncement>(h, [this](DeviceAnnouncement&& dev) {
findCertificate(dev.dev, [this](const std::shared_ptr<dht::crypto::Certificate>& crt) {
foundAccountDevice(crt);
});
return true;
});
dht_->listen<dht::crypto::RevocationList>(h, [this](dht::crypto::RevocationList&& crl) {
if (crl.isSignedBy(*info_->identity.second->issuer)) {
JAMI_DBG("found CRL for account.");
tls::CertificateStore::instance().pinRevocationList(
info_->accountId,
std::make_shared<dht::crypto::RevocationList>(std::move(crl)));
}
return true;
});
syncDevices();
} else {
JAMI_WARN("can't announce device: no annoucement...");
}
auto inboxKey = dht::InfoHash::get("inbox:"+info_->deviceId);
dht_->listen<dht::TrustRequest>(
inboxKey,
[this](dht::TrustRequest&& v) {
if (v.service != DHT_TYPE_NS)
return true;
findCertificate(v.from, [this, v](const std::shared_ptr<dht::crypto::Certificate>& cert) mutable {
// check peer certificate
dht::InfoHash peer_account;
if (not foundPeerDevice(cert, peer_account)) {
return;
}
JAMI_WARN("Got trust request from: %s / %s", peer_account.toString().c_str(), v.from.toString().c_str());
if (info_)
if (info_->contacts->onTrustRequest(peer_account, v.from, time(nullptr), v.confirm, std::move(v.payload))) {
sendTrustRequestConfirm(peer_account);
}
});
return true;
}
);
dht_->listen<DeviceSync>(
inboxKey,
[this](DeviceSync&& sync) {
// Received device sync data.
// check device certificate
findCertificate(sync.from, [this,sync](const std::shared_ptr<dht::crypto::Certificate>& cert) mutable {
if (!cert or cert->getId() != sync.from) {
JAMI_WARN("Can't find certificate for device %s", sync.from.toString().c_str());
return;
}
if (not foundAccountDevice(cert))
return;
onSyncData(std::move(sync));
});
return true;
}
);
}
void
ArchiveAccountManager::onSyncData(DeviceSync&& sync)
{
auto sync_date = clock::time_point(clock::duration(sync.date));
if (not info_->contacts->syncDevice(sync.from, sync_date)) {
return;
}
// Sync known devices
JAMI_DBG("[Contacts] received device sync data (%lu devices, %lu contacts)", sync.devices_known.size(), sync.peers.size());
for (const auto& d : sync.devices_known) {
findCertificate(d.first, [this,d](const std::shared_ptr<dht::crypto::Certificate>& crt) {
if (not crt)
return;
//std::lock_guard<std::mutex> lock(deviceListMutex_);
foundAccountDevice(crt, d.second);
});
}
//saveKnownDevices();
// Sync contacts
for (const auto& peer : sync.peers)
info_->contacts->updateContact(peer.first, peer.second);
//saveContacts();
// Sync trust requests
for (const auto& tr : sync.trust_requests)
info_->contacts->onTrustRequest(tr.first, tr.second.device, tr.second.received, false, {});
}
#if HAVE_RINGNS
void
ArchiveAccountManager::lookupName(const std::string& name, LookupCallback cb)
AccountManager::lookupName(const std::string& name, LookupCallback cb)
{
nameDir_.get().lookupName(name, cb);
}
void
ArchiveAccountManager::lookupAddress(const std::string& addr, LookupCallback cb)
AccountManager::lookupAddress(const std::string& addr, LookupCallback cb)
{
nameDir_.get().lookupAddress(addr, cb);
}
void
ArchiveAccountManager::registerName(const std::string& password, const std::string& name, RegistrationCallback cb)
{
std::string signedName;
auto nameLowercase {name};
std::transform(nameLowercase.begin(), nameLowercase.end(), nameLowercase.begin(), ::tolower);
std::string publickey;
std::string accountId;
std::string ethAccount;
try {
auto archive = readArchive(password);
auto privateKey = archive.id.first;
auto pk = privateKey->getPublicKey();
publickey = pk.toString();
accountId = pk.getId().toString();
signedName = base64::encode(privateKey->sign(std::vector<uint8_t>(nameLowercase.begin(), nameLowercase.end())));
ethAccount = dev::KeyPair(dev::Secret(archive.eth_key)).address().hex();
} catch (const std::exception& e) {
//JAMI_ERR("[Auth] can't export account: %s", e.what());
cb(NameDirectory::RegistrationResponse::invalidCredentials);
return;
}
nameDir_.get().registerName(accountId, nameLowercase, ethAccount, cb, signedName, publickey);
}
#endif
}

View File

@ -50,29 +50,45 @@ struct AccountInfo {
std::string ethAccount;
};
template <typename To, typename From>
std::unique_ptr<To>
dynamic_unique_cast(std::unique_ptr<From>&& p) {
if (auto cast = dynamic_cast<To*>(p.get())) {
std::unique_ptr<To> result(cast);
p.release();
return result;
}
return {};
}
class AccountManager {
public:
using AsyncUser = std::function<void(AccountManager&)>;
using OnAsync = std::function<void(AsyncUser&&)>;
using OnChangeCallback = ContactList::OnChangeCallback;
using OnExportConfig = std::function<std::map<std::string, std::string>()>;
using clock = std::chrono::system_clock;
using time_point = clock::time_point;
AccountManager(
const std::string& path,
OnAsync&& onAsync,
OnExportConfig&& onExportConfig,
std::shared_ptr<dht::DhtRunner> dht)
: path_(path), onAsync_(std::move(onAsync)), onExportConfig_(std::move(onExportConfig)), dht_(std::move(dht)) {};
std::shared_ptr<dht::DhtRunner> dht,
const std::string& nameServer)
: path_(path)
, onAsync_(std::move(onAsync))
, dht_(std::move(dht))
, nameDir_(NameDirectory::instance(nameServer)) {};
virtual ~AccountManager() = default;
constexpr static const char* const DHT_TYPE_NS = "cx.ring";
// Auth
enum class AuthError {
UNKNOWN,
INVALID_ARGUMENTS,
SERVER_ERROR,
NETWORK
};
@ -183,101 +199,23 @@ public:
tls::TrustStore::PermissionStatus getCertificateStatus(const std::string& cert_id) const;
bool isAllowed(const crypto::Certificate& crt, bool allowPublic);
static std::shared_ptr<dht::Value> parseAnnounce(const std::string& announceBase64, const std::string& accountId, const std::string& deviceId);
// Name resolver
#if HAVE_RINGNS
using LookupCallback = NameDirectory::LookupCallback;
using RegistrationCallback = NameDirectory::RegistrationCallback;
virtual void lookupName(const std::string& name, LookupCallback cb) = 0;
virtual void lookupAddress(const std::string& address, LookupCallback cb) = 0;
virtual void lookupName(const std::string& name, LookupCallback cb);
virtual void lookupAddress(const std::string& address, LookupCallback cb);
virtual void registerName(const std::string& password, const std::string& name, RegistrationCallback cb) = 0;
#endif
protected:
std::string path_;
OnAsync onAsync_;
OnExportConfig onExportConfig_;
OnChangeCallback onChange_;
std::unique_ptr<AccountInfo> info_;
std::shared_ptr<dht::DhtRunner> dht_;
};
class ArchiveAccountManager : public AccountManager {
public:
ArchiveAccountManager(
const std::string& path,
std::shared_ptr<dht::DhtRunner> dht,
OnAsync&& onAsync,
OnExportConfig&& onExportConfig,
std::string archivePath,
const std::string& nameServer)
: AccountManager(path, std::move(onAsync), std::move(onExportConfig), std::move(dht)), archivePath_(std::move(archivePath)), nameDir_(NameDirectory::instance(nameServer)) {};
struct ArchiveAccountCredentials : AccountCredentials {
std::string archivePath;
in_port_t dhtPort;
std::vector<std::string> dhtBootstrap;
dht::crypto::Identity updateIdentity;
};
void initAuthentication(
CertRequest request,
std::unique_ptr<AccountCredentials> credentials,
AuthSuccessCallback onSuccess,
AuthFailureCallback onFailure,
OnChangeCallback onChange) override;
void startSync() override;
bool changePassword(const std::string& password_old, const std::string& password_new) override;
void syncDevices() override;
void onSyncData(DeviceSync&& device);
bool findCertificate(const dht::InfoHash& h, std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb = {}) override;
void addDevice(const std::string& password, AddDeviceCallback) override;
bool revokeDevice(const std::string& password, const std::string& device, RevokeDeviceCallback) override;
bool exportArchive(const std::string& destinationPath, const std::string& password);
#if HAVE_RINGNS
void lookupName(const std::string& name, LookupCallback cb) override;
void lookupAddress(const std::string& address, LookupCallback cb) override;
void registerName(const std::string& password, const std::string& name, RegistrationCallback cb) override;
#endif
private:
struct DhtLoadContext;
struct AuthContext {
CertRequest request;
//std::unique_ptr<dht::crypto::CertificateRequest> request;
std::unique_ptr<ArchiveAccountCredentials> credentials;
std::unique_ptr<DhtLoadContext> dhtContext;
AuthSuccessCallback onSuccess;
AuthFailureCallback onFailure;
};
void createAccount(const std::shared_ptr<AuthContext>& ctx);
std::pair<std::string, std::shared_ptr<dht::Value>> makeReceipt(const dht::crypto::Identity& id, const dht::crypto::Certificate& device, const std::string& ethAccount);
void updateArchive(AccountArchive& content/*, const ContactList& syncData*/) const;
void saveArchive(AccountArchive& content, const std::string& pwd);
AccountArchive readArchive(const std::string& pwd) const;
static std::pair<std::vector<uint8_t>, dht::InfoHash> computeKeys(const std::string& password, const std::string& pin, bool previous=false);
bool updateCertificates(AccountArchive& archive, dht::crypto::Identity& device);
static bool needsMigration(const dht::crypto::Identity& id);
void loadFromFile(const std::shared_ptr<AuthContext>& ctx);
void loadFromDHT(const std::shared_ptr<AuthContext>& ctx);
void onArchiveLoaded(AuthContext& ctx, AccountArchive&& a);
std::string archivePath_;
std::reference_wrapper<NameDirectory> nameDir_;
};
class ServerAccountManager : public AccountManager {
};
}

View File

@ -0,0 +1,729 @@
/*
* Copyright (C) 2014-2019 Savoir-faire Linux Inc.
* Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "archive_account_manager.h"
#include "accountarchive.h"
#include "fileutils.h"
#include "libdevcrypto/Common.h"
#include "archiver.h"
#include "base64.h"
#include "dring/account_const.h"
#include "account_schema.h"
#include <opendht/dhtrunner.h>
#include <opendht/thread_pool.h>
#include <memory>
#include <fstream>
namespace jami {
const constexpr auto EXPORT_KEY_RENEWAL_TIME = std::chrono::minutes(20);
void
ArchiveAccountManager::initAuthentication(
CertRequest request,
std::unique_ptr<AccountCredentials> credentials,
AuthSuccessCallback onSuccess,
AuthFailureCallback onFailure,
OnChangeCallback onChange)
{
auto ctx = std::make_shared<AuthContext>();
ctx->request = std::move(request);
ctx->credentials = dynamic_unique_cast<ArchiveAccountCredentials>(std::move(credentials));
ctx->onSuccess = std::move(onSuccess);
ctx->onFailure = std::move(onFailure);
if (not ctx->credentials) {
onFailure(AuthError::INVALID_ARGUMENTS, "invalid credentials");
return;
}
onChange_ = std::move(onChange);
if (ctx->credentials->scheme == "dht") {
loadFromDHT(ctx);
return;
}
dht::ThreadPool::computation().run([
ctx = std::move(ctx),
onAsync = onAsync_
]() mutable {
onAsync([ctx = std::move(ctx)](AccountManager& accountManager) mutable {
auto& this_ = *static_cast<ArchiveAccountManager*>(&accountManager);
try {
if (ctx->credentials->scheme == "file") {
this_.loadFromFile(ctx);
return;
} else {
if (ctx->credentials->updateIdentity.first and ctx->credentials->updateIdentity.second) {
auto future_keypair = dht::ThreadPool::computation().get<dev::KeyPair>(&dev::KeyPair::create);
AccountArchive a;
JAMI_WARN("[Auth] converting certificate from old account %s", ctx->credentials->updateIdentity.first->getPublicKey().getId().toString().c_str());
a.id = std::move(ctx->credentials->updateIdentity);
try {
a.ca_key = std::make_shared<dht::crypto::PrivateKey>(fileutils::loadFile("ca.key", this_.path_));
} catch (...) {}
this_.updateCertificates(a, ctx->credentials->updateIdentity);
auto keypair = future_keypair.get();
a.eth_key = keypair.secret().makeInsecure().asBytes();
this_.onArchiveLoaded(*ctx, std::move(a));
} else {
this_.createAccount(ctx);
}
}
} catch (const std::exception& e) {
ctx->onFailure(AuthError::UNKNOWN, e.what());
}
});
});
}
bool
ArchiveAccountManager::updateCertificates(AccountArchive& archive, dht::crypto::Identity& device)
{
JAMI_WARN("Updating certificates");
using Certificate = dht::crypto::Certificate;
// We need the CA key to resign certificates
if (not archive.id.first or
not *archive.id.first or
not archive.id.second or
not archive.ca_key or
not *archive.ca_key)
return false;
// Currently set the CA flag and update expiration dates
bool updated = false;
auto& cert = archive.id.second;
auto ca = cert->issuer;
// Update CA if possible and relevant
if (not ca or (not ca->issuer and (not ca->isCA() or ca->getExpiration() < clock::now()))) {
ca = std::make_shared<Certificate>(Certificate::generate(*archive.ca_key, "Jami CA", {}, true));
updated = true;
JAMI_DBG("CA CRT re-generated");
}
// Update certificate
if (updated or not cert->isCA() or cert->getExpiration() < clock::now()) {
cert = std::make_shared<Certificate>(Certificate::generate(*archive.id.first, "Jami", dht::crypto::Identity{archive.ca_key, ca}, true));
updated = true;
JAMI_DBG("Jami CRT re-generated");
}
if (updated and device.first and *device.first) {
// update device certificate
device.second = std::make_shared<Certificate>(Certificate::generate(*device.first, "Jami device", archive.id));
JAMI_DBG("device CRT re-generated");
}
return updated;
}
void
ArchiveAccountManager::createAccount(const std::shared_ptr<AuthContext>& ctx)
{
AccountArchive a;
auto future_keypair = dht::ThreadPool::computation().get<dev::KeyPair>(&dev::KeyPair::create);
auto ca = dht::crypto::generateIdentity("Jami CA");
if (!ca.first || !ca.second) {
throw std::runtime_error("Can't generate CA for this account.");
}
a.id = dht::crypto::generateIdentity("Jami", ca, 4096, true);
if (!a.id.first || !a.id.second) {
throw std::runtime_error("Can't generate identity for this account.");
}
JAMI_WARN("[Auth] new account: CA: %s, RingID: %s",
ca.second->getId().toString().c_str(),
a.id.second->getId().toString().c_str());
a.ca_key = ca.first;
auto keypair = future_keypair.get();
a.eth_key = keypair.secret().makeInsecure().asBytes();
onArchiveLoaded(*ctx, std::move(a));
}
void
ArchiveAccountManager::loadFromFile(const std::shared_ptr<AuthContext>& ctx)
{
AccountArchive archive;
try {
archive = AccountArchive(ctx->credentials->uri, ctx->credentials->password);
} catch (const std::exception& ex) {
JAMI_WARN("[Auth] can't read file: %s", ex.what());
ctx->onFailure(AuthError::UNKNOWN, ex.what());
return;
}
onArchiveLoaded(*ctx, std::move(archive));
}
struct ArchiveAccountManager::DhtLoadContext {
dht::DhtRunner dht;
std::pair<bool, bool> stateOld {false, true};
std::pair<bool, bool> stateNew {false, true};
bool found {false};
};
void
ArchiveAccountManager::loadFromDHT(const std::shared_ptr<AuthContext>& ctx)
{
ctx->dhtContext = std::make_unique<DhtLoadContext>();
ctx->dhtContext->dht.run(ctx->credentials->dhtPort, {}, true);
for (const auto& bootstrap : ctx->credentials->dhtBootstrap)
ctx->dhtContext->dht.bootstrap(bootstrap);
auto searchEnded = [ctx]() {
if (not ctx->dhtContext or ctx->dhtContext->found) {
return;
}
auto& s = *ctx->dhtContext;
if (s.stateOld.first && s.stateNew.first) {
dht::ThreadPool::computation().run([ctx, network_error = !s.stateOld.second && !s.stateNew.second]{
ctx->dhtContext.reset();
JAMI_WARN("[Auth] failure looking for archive on DHT: %s", /**/network_error ? "network error" : "not found");
ctx->onFailure(network_error ? AuthError::NETWORK : AuthError::UNKNOWN, "");
});
}
};
auto search = [ctx, searchEnded, onAsync = onAsync_](bool previous) {
std::vector<uint8_t> key;
dht::InfoHash loc;
auto& s = previous ? ctx->dhtContext->stateOld : ctx->dhtContext->stateNew;
// compute archive location and decryption keys
try {
std::tie(key, loc) = computeKeys(ctx->credentials->password, ctx->credentials->uri, previous);
JAMI_DBG("[Auth] trying to load account from DHT with %s at %s", /**/ctx->credentials->uri.c_str(), loc.toString().c_str());
ctx->dhtContext->dht.get(loc, [ctx, key=std::move(key), onAsync](const std::shared_ptr<dht::Value>& val) {
std::vector<uint8_t> decrypted;
try {
decrypted = archiver::decompress(dht::crypto::aesDecrypt(val->data, key));
} catch (const std::exception& ex) {
return true;
}
JAMI_DBG("[Auth] found archive on the DHT");
ctx->dhtContext->found = true;
dht::ThreadPool::computation().run([
ctx,
decrypted = std::move(decrypted),
onAsync
]{
try {
auto archive = AccountArchive(decrypted);
onAsync([&](AccountManager& accountManager) {
auto& this_ = *static_cast<ArchiveAccountManager*>(&accountManager);
if (ctx->dhtContext) {
ctx->dhtContext->dht.join();
ctx->dhtContext.reset();
}
this_.onArchiveLoaded(*ctx, std::move(archive)/*, std::move(contacts)*/);
});
} catch (const std::exception& e) {
ctx->onFailure(AuthError::UNKNOWN, "");
}
});
return not ctx->dhtContext->found;
}, [=, &s](bool ok) {
JAMI_DBG("[Auth] DHT archive search ended at %s", /**/loc.toString().c_str());
s.first = true;
s.second = ok;
searchEnded();
});
} catch (const std::exception& e) {
// JAMI_ERR("Error computing kedht::ThreadPool::computation().run(ys: %s", e.what());
s.first = true;
s.second = true;
searchEnded();
return;
}
};
dht::ThreadPool::computation().run(std::bind(search, true));
dht::ThreadPool::computation().run(std::bind(search, false));
}
void
ArchiveAccountManager::onArchiveLoaded(
AuthContext& ctx,
AccountArchive&& a)
{
auto ethAccount = dev::KeyPair(dev::Secret(a.eth_key)).address().hex();
fileutils::check_dir(path_.c_str(), 0700);
auto path = fileutils::getFullPath(path_, archivePath_);
a.save(path, ctx.credentials ? ctx.credentials->password : "");
if (not a.id.second->isCA()) {
JAMI_ERR("[Auth] trying to sign a certificate with a non-CA.");
}
JAMI_WARN("generating device certificate");
auto request = ctx.request.get();
if (not request->verify()) {
JAMI_ERR("[Auth] Invalid certificate request.");
ctx.onFailure(AuthError::INVALID_ARGUMENTS, "");
return;
}
auto deviceCertificate = std::make_shared<dht::crypto::Certificate>(dht::crypto::Certificate::generate(*request, a.id));
auto receipt = makeReceipt(a.id, *deviceCertificate, ethAccount);
auto receiptSignature = a.id.first->sign({receipt.first.begin(), receipt.first.end()});
auto info = std::make_unique<AccountInfo>();
info->identity.second = deviceCertificate;
info->contacts = std::make_unique<ContactList>(a.id.second, path_, onChange_);
info->contacts->setContacts(a.contacts);
info->accountId = a.id.second->getId().toString();
info->deviceId = deviceCertificate->getPublicKey().getId().toString();
info->ethAccount = ethAccount;
info->announce = std::move(receipt.second);
info_ = std::move(info);
JAMI_WARN("[Auth] created new device: %s", info_->deviceId.c_str());
ctx.onSuccess(*info_, std::move(a.config), std::move(receipt.first), std::move(receiptSignature));
}
std::pair<std::vector<uint8_t>, dht::InfoHash>
ArchiveAccountManager::computeKeys(const std::string& password, const std::string& pin, bool previous)
{
// Compute time seed
auto now = std::chrono::duration_cast<std::chrono::seconds>(clock::now().time_since_epoch());
auto tseed = now.count() / std::chrono::seconds(EXPORT_KEY_RENEWAL_TIME).count();
if (previous)
tseed--;
std::stringstream ss;
ss << std::hex << tseed;
auto tseed_str = ss.str();
// Generate key for archive encryption, using PIN as the salt
std::vector<uint8_t> salt_key;
salt_key.reserve(pin.size() + tseed_str.size());
salt_key.insert(salt_key.end(), pin.begin(), pin.end());
salt_key.insert(salt_key.end(), tseed_str.begin(), tseed_str.end());
auto key = dht::crypto::stretchKey(password, salt_key, 256/8);
// Generate public storage location as SHA1(key).
auto loc = dht::InfoHash::get(key);
return {key, loc};
}
std::pair<std::string, std::shared_ptr<dht::Value>>
ArchiveAccountManager::makeReceipt(const dht::crypto::Identity& id, const dht::crypto::Certificate& device, const std::string& ethAccount)
{
JAMI_DBG("[Auth] signing device receipt");
auto devId = device.getId();
DeviceAnnouncement announcement;
announcement.dev = devId;
dht::Value ann_val {announcement};
ann_val.sign(*id.first);
std::ostringstream is;
is << "{\"id\":\"" << id.second->getId()
<< "\",\"dev\":\"" << devId
<< "\",\"eth\":\"" << ethAccount
<< "\",\"announce\":\"" << base64::encode(ann_val.getPacked()) << "\"}";
//auto announce_ = ;
return {is.str(), std::make_shared<dht::Value>(std::move(ann_val))};
}
bool
ArchiveAccountManager::needsMigration(const dht::crypto::Identity& id)
{
if (not id.second)
return false;
auto cert = id.second->issuer;
while (cert) {
if (not cert->isCA()){
JAMI_WARN("certificate %s is not a CA, needs update.", cert->getId().toString().c_str());
return true;
}
if (cert->getExpiration() < clock::now()) {
JAMI_WARN("certificate %s is expired, needs update.", cert->getId().toString().c_str());
return true;
}
cert = cert->issuer;
}
return false;
}
void
ArchiveAccountManager::syncDevices()
{
JAMI_DBG("Building device sync from %s", info_->deviceId.c_str());
auto sync_data = info_->contacts->getSyncData();
for (const auto& dev : getKnownDevices()) {
// don't send sync data to ourself
if (dev.first.toString() == info_->deviceId)
continue;
JAMI_DBG("sending device sync to %s %s", dev.second.name.c_str(), dev.first.toString().c_str());
auto syncDeviceKey = dht::InfoHash::get("inbox:"+dev.first.toString());
dht_->putEncrypted(syncDeviceKey, dev.first, sync_data);
}
}
void
ArchiveAccountManager::startSync()
{
// Put device annoucement
if (info_->announce) {
auto h = dht::InfoHash(info_->accountId);
JAMI_DBG("announcing device at %s", h.toString().c_str());
dht_->put(h, info_->announce, dht::DoneCallback{}, {}, true);
for (const auto& crl : info_->identity.second->issuer->getRevocationLists())
dht_->put(h, crl, dht::DoneCallback{}, {}, true);
dht_->listen<DeviceAnnouncement>(h, [this](DeviceAnnouncement&& dev) {
findCertificate(dev.dev, [this](const std::shared_ptr<dht::crypto::Certificate>& crt) {
foundAccountDevice(crt);
});
return true;
});
dht_->listen<dht::crypto::RevocationList>(h, [this](dht::crypto::RevocationList&& crl) {
if (crl.isSignedBy(*info_->identity.second->issuer)) {
JAMI_DBG("found CRL for account.");
tls::CertificateStore::instance().pinRevocationList(
info_->accountId,
std::make_shared<dht::crypto::RevocationList>(std::move(crl)));
}
return true;
});
syncDevices();
} else {
JAMI_WARN("can't announce device: no annoucement...");
}
auto inboxKey = dht::InfoHash::get("inbox:"+info_->deviceId);
dht_->listen<dht::TrustRequest>(
inboxKey,
[this](dht::TrustRequest&& v) {
if (v.service != DHT_TYPE_NS)
return true;
findCertificate(v.from, [this, v](const std::shared_ptr<dht::crypto::Certificate>& cert) mutable {
// check peer certificate
dht::InfoHash peer_account;
if (not foundPeerDevice(cert, peer_account)) {
return;
}
JAMI_WARN("Got trust request from: %s / %s", peer_account.toString().c_str(), v.from.toString().c_str());
if (info_)
if (info_->contacts->onTrustRequest(peer_account, v.from, time(nullptr), v.confirm, std::move(v.payload))) {
sendTrustRequestConfirm(peer_account);
}
});
return true;
}
);
dht_->listen<DeviceSync>(
inboxKey,
[this](DeviceSync&& sync) {
// Received device sync data.
// check device certificate
findCertificate(sync.from, [this,sync](const std::shared_ptr<dht::crypto::Certificate>& cert) mutable {
if (!cert or cert->getId() != sync.from) {
JAMI_WARN("Can't find certificate for device %s", sync.from.toString().c_str());
return;
}
if (not foundAccountDevice(cert))
return;
onSyncData(std::move(sync));
});
return true;
}
);
}
void
ArchiveAccountManager::onSyncData(DeviceSync&& sync)
{
auto sync_date = clock::time_point(clock::duration(sync.date));
if (not info_->contacts->syncDevice(sync.from, sync_date)) {
return;
}
// Sync known devices
JAMI_DBG("[Contacts] received device sync data (%lu devices, %lu contacts)", sync.devices_known.size(), sync.peers.size());
for (const auto& d : sync.devices_known) {
findCertificate(d.first, [this,d](const std::shared_ptr<dht::crypto::Certificate>& crt) {
if (not crt)
return;
//std::lock_guard<std::mutex> lock(deviceListMutex_);
foundAccountDevice(crt, d.second);
});
}
//saveKnownDevices();
// Sync contacts
for (const auto& peer : sync.peers)
info_->contacts->updateContact(peer.first, peer.second);
//saveContacts();
// Sync trust requests
for (const auto& tr : sync.trust_requests)
info_->contacts->onTrustRequest(tr.first, tr.second.device, tr.second.received, false, {});
}
AccountArchive
ArchiveAccountManager::readArchive(const std::string& pwd) const
{
JAMI_DBG("[Auth] reading account archive");
return AccountArchive(fileutils::getFullPath(path_, archivePath_), pwd);
}
void
ArchiveAccountManager::updateArchive(AccountArchive& archive) const
{
using namespace DRing::Account::ConfProperties;
// Keys not exported to archive
static const auto filtered_keys = { Ringtone::PATH,
ARCHIVE_PATH,
RING_DEVICE_ID,
RING_DEVICE_NAME,
Conf::CONFIG_DHT_PORT };
// Keys with meaning of file path where the contents has to be exported in base64
static const auto encoded_keys = { TLS::CA_LIST_FILE,
TLS::CERTIFICATE_FILE,
TLS::PRIVATE_KEY_FILE };
JAMI_DBG("[Auth] building account archive");
for (const auto& it : onExportConfig_()) {
// filter-out?
if (std::any_of(std::begin(filtered_keys), std::end(filtered_keys),
[&](const auto& key){ return key == it.first; }))
continue;
// file contents?
if (std::any_of(std::begin(encoded_keys), std::end(encoded_keys),
[&](const auto& key){ return key == it.first; })) {
try {
archive.config.emplace(it.first, base64::encode(fileutils::loadFile(it.second)));
} catch (...) {}
} else
archive.config.insert(it);
}
archive.contacts = info_->contacts->getContacts();
}
void
ArchiveAccountManager::saveArchive(AccountArchive& archive, const std::string& pwd)
{
try {
updateArchive(archive);
if (archivePath_.empty())
archivePath_ = "export.gz";
archive.save(fileutils::getFullPath(path_, archivePath_), pwd);
//archiveHasPassword_ = not pwd.empty();
} catch (const std::runtime_error& ex) {
JAMI_ERR("[Auth] Can't export archive: %s", ex.what());
return;
}
}
bool
ArchiveAccountManager::changePassword(const std::string& password_old, const std::string& password_new)
{
try {
auto path = fileutils::getFullPath(path_, archivePath_);
AccountArchive(path, password_old).save(path, password_new);
return true;
} catch (const std::exception&) {
return false;
}
}
std::string
generatePIN(size_t length = 8)
{
static constexpr const char alphabet[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
dht::crypto::random_device rd;
std::uniform_int_distribution<size_t> dis(0, sizeof(alphabet)-2);
std::string ret;
ret.reserve(length);
for (size_t i=0; i<length; i++)
ret.push_back(alphabet[dis(rd)]);
return ret;
}
void
ArchiveAccountManager::addDevice(const std::string& password, AddDeviceCallback cb)
{
dht::ThreadPool::computation().run([onAsync = onAsync_, password, cb = std::move(cb)]() mutable {
onAsync([password = std::move(password), cb = std::move(cb)](AccountManager& accountManager) mutable {
auto& this_ = *static_cast<ArchiveAccountManager*>(&accountManager);
std::vector<uint8_t> key;
dht::InfoHash loc;
std::string pin_str;
AccountArchive a;
try {
JAMI_DBG("[Auth] exporting account");
a = this_.readArchive(password);
// Generate random PIN
pin_str = generatePIN();
std::tie(key, loc) = computeKeys(password, pin_str);
} catch (const std::exception& e) {
JAMI_ERR("[Auth] can't export account: %s", e.what());
cb(AddDeviceResult::ERROR_CREDENTIALS, {});
return;
}
// now that key and loc are computed, display to user in lowercase
std::transform(pin_str.begin(), pin_str.end(), pin_str.begin(), ::tolower);
try {
this_.updateArchive(a);
auto encrypted = dht::crypto::aesEncrypt(archiver::compress(a.serialize()), key);
if (not this_.dht_->isRunning())
throw std::runtime_error("DHT is not running..");
JAMI_WARN("[Auth] exporting account with PIN: %s at %s (size %zu)", pin_str.c_str(), loc.toString().c_str(), encrypted.size());
this_.dht_->put(loc, encrypted, [cb, pin = std::move(pin_str)](bool ok) {
JAMI_DBG("[Auth] account archive published: %s", ok ? "success" : "failure");
if (ok)
cb(AddDeviceResult::SUCCESS_SHOW_PIN, pin);
else
cb(AddDeviceResult::ERROR_NETWORK, {});
});
} catch (const std::exception& e) {
JAMI_ERR("[Auth] can't export account: %s", e.what());
cb(AddDeviceResult::ERROR_NETWORK, {});
return;
}
});
});
}
bool
ArchiveAccountManager::revokeDevice(const std::string& password, const std::string& device, RevokeDeviceCallback cb)
{
auto fa = dht::ThreadPool::computation().getShared<AccountArchive>([this, password] { return readArchive(password); });
findCertificate(dht::InfoHash(device),
[fa=std::move(fa),password,device,cb,onAsync=onAsync_](const std::shared_ptr<dht::crypto::Certificate>& crt) mutable
{
if (not crt) {
cb(RevokeDeviceResult::ERROR_NETWORK);
return;
}
onAsync([cb, crt=std::move(crt), fa=std::move(fa), password=std::move(password)](AccountManager& accountManager) mutable {
auto& this_ = *static_cast<ArchiveAccountManager*>(&accountManager);
this_.info_->contacts->foundAccountDevice(crt);
AccountArchive a;
try {
a = fa.get();
} catch (...) {
cb(RevokeDeviceResult::ERROR_CREDENTIALS);
return;
}
// Add revoked device to the revocation list and resign it
if (not a.revoked)
a.revoked = std::make_shared<decltype(a.revoked)::element_type>();
a.revoked->revoke(*crt);
a.revoked->sign(a.id);
// add to CRL cache
tls::CertificateStore::instance().pinRevocationList(a.id.second->getId().toString(), a.revoked);
tls::CertificateStore::instance().loadRevocations(*a.id.second);
this_.saveArchive(a, password);
this_.info_->contacts->removeAccountDevice(crt->getId());
cb(RevokeDeviceResult::SUCCESS);
this_.syncDevices();
});
});
return false;
}
bool
ArchiveAccountManager::exportArchive(const std::string& destinationPath, const std::string& password)
{
try {
// Save contacts if possible before exporting
AccountArchive archive = readArchive(password);
updateArchive(archive);
archive.save(fileutils::getFullPath(path_, archivePath_), password);
// Export the file
auto sourcePath = fileutils::getFullPath(path_, archivePath_);
std::ifstream src(sourcePath, std::ios::in | std::ios::binary);
if (!src) return false;
std::ofstream dst(destinationPath, std::ios::out | std::ios::binary);
dst << src.rdbuf();
return true;
} catch (const std::runtime_error& ex) {
JAMI_ERR("[Auth] Can't export archive: %s", ex.what());
return false;
} catch (...) {
JAMI_ERR("[Auth] Can't export archive: can't read archive");
return false;
}
}
bool
ArchiveAccountManager::findCertificate(const dht::InfoHash& h, std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
{
if (auto cert = tls::CertificateStore::instance().getCertificate(h.toString())) {
if (cb)
cb(cert);
} else {
dht_->findCertificate(h, [cb](const std::shared_ptr<dht::crypto::Certificate>& crt) {
if (crt)
tls::CertificateStore::instance().pinCertificate(crt);
if (cb)
cb(crt);
});
}
return true;
}
#if HAVE_RINGNS
void
ArchiveAccountManager::registerName(const std::string& password, const std::string& name, RegistrationCallback cb)
{
std::string signedName;
auto nameLowercase {name};
std::transform(nameLowercase.begin(), nameLowercase.end(), nameLowercase.begin(), ::tolower);
std::string publickey;
std::string accountId;
std::string ethAccount;
try {
auto archive = readArchive(password);
auto privateKey = archive.id.first;
auto pk = privateKey->getPublicKey();
publickey = pk.toString();
accountId = pk.getId().toString();
signedName = base64::encode(privateKey->sign(std::vector<uint8_t>(nameLowercase.begin(), nameLowercase.end())));
ethAccount = dev::KeyPair(dev::Secret(archive.eth_key)).address().hex();
} catch (const std::exception& e) {
//JAMI_ERR("[Auth] can't export account: %s", e.what());
cb(NameDirectory::RegistrationResponse::invalidCredentials);
return;
}
nameDir_.get().registerName(accountId, nameLowercase, ethAccount, cb, signedName, publickey);
}
#endif
}

View File

@ -0,0 +1,101 @@
/*
* Copyright (C) 2014-2019 Savoir-faire Linux Inc.
* Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "account_manager.h"
namespace jami {
class ArchiveAccountManager : public AccountManager {
public:
using OnExportConfig = std::function<std::map<std::string, std::string>()>;
ArchiveAccountManager(
const std::string& path,
std::shared_ptr<dht::DhtRunner> dht,
OnAsync&& onAsync,
OnExportConfig&& onExportConfig,
std::string archivePath,
const std::string& nameServer)
: AccountManager(path, std::move(onAsync), std::move(dht), nameServer)
, onExportConfig_(std::move(onExportConfig))
, archivePath_(std::move(archivePath))
{};
struct ArchiveAccountCredentials : AccountCredentials {
std::string archivePath;
in_port_t dhtPort;
std::vector<std::string> dhtBootstrap;
dht::crypto::Identity updateIdentity;
};
void initAuthentication(
CertRequest request,
std::unique_ptr<AccountCredentials> credentials,
AuthSuccessCallback onSuccess,
AuthFailureCallback onFailure,
OnChangeCallback onChange) override;
void startSync() override;
bool changePassword(const std::string& password_old, const std::string& password_new) override;
void syncDevices() override;
void onSyncData(DeviceSync&& device);
bool findCertificate(const dht::InfoHash& h, std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb = {}) override;
void addDevice(const std::string& password, AddDeviceCallback) override;
bool revokeDevice(const std::string& password, const std::string& device, RevokeDeviceCallback) override;
bool exportArchive(const std::string& destinationPath, const std::string& password);
#if HAVE_RINGNS
/*void lookupName(const std::string& name, LookupCallback cb) override;
void lookupAddress(const std::string& address, LookupCallback cb) override;*/
void registerName(const std::string& password, const std::string& name, RegistrationCallback cb) override;
#endif
private:
struct DhtLoadContext;
struct AuthContext {
CertRequest request;
//std::unique_ptr<dht::crypto::CertificateRequest> request;
std::unique_ptr<ArchiveAccountCredentials> credentials;
std::unique_ptr<DhtLoadContext> dhtContext;
AuthSuccessCallback onSuccess;
AuthFailureCallback onFailure;
};
void createAccount(const std::shared_ptr<AuthContext>& ctx);
std::pair<std::string, std::shared_ptr<dht::Value>> makeReceipt(const dht::crypto::Identity& id, const dht::crypto::Certificate& device, const std::string& ethAccount);
void updateArchive(AccountArchive& content/*, const ContactList& syncData*/) const;
void saveArchive(AccountArchive& content, const std::string& pwd);
AccountArchive readArchive(const std::string& pwd) const;
static std::pair<std::vector<uint8_t>, dht::InfoHash> computeKeys(const std::string& password, const std::string& pin, bool previous=false);
bool updateCertificates(AccountArchive& archive, dht::crypto::Identity& device);
static bool needsMigration(const dht::crypto::Identity& id);
void loadFromFile(const std::shared_ptr<AuthContext>& ctx);
void loadFromDHT(const std::shared_ptr<AuthContext>& ctx);
void onArchiveLoaded(AuthContext& ctx, AccountArchive&& a);
OnExportConfig onExportConfig_;
std::string archivePath_;
};
}

View File

@ -347,6 +347,28 @@ ContactList::saveKnownDevices() const
msgpack::pack(file, devices);
}
void
ContactList::foundAccountDevice(const dht::InfoHash& device, const std::string& name, const time_point& updated)
{
// insert device
auto it = knownDevices_.emplace(device, KnownDevice{{}, name, updated});
if (it.second) {
JAMI_DBG("[Contacts] Found account device: %s %s", name.c_str(),
device.toString().c_str());
saveKnownDevices();
callbacks_.devicesChanged();
} else {
// update device name
if (not name.empty() and it.first->second.name != name) {
JAMI_DBG("[Contacts] updating device name: %s %s", name.c_str(),
device.toString().c_str());
it.first->second.name = name;
saveKnownDevices();
callbacks_.devicesChanged();
}
}
}
bool
ContactList::foundAccountDevice(const std::shared_ptr<dht::crypto::Certificate>& crt, const std::string& name, const time_point& updated)
{
@ -354,8 +376,9 @@ ContactList::foundAccountDevice(const std::shared_ptr<dht::crypto::Certificate>&
return false;
// match certificate chain
if (not accountTrust_.verify(*crt)) {
JAMI_WARN("[Contacts] Found invalid account device: %s", crt->getId().toString().c_str());
auto verifyResult = accountTrust_.verify(*crt);
if (not verifyResult) {
JAMI_WARN("[Contacts] Found invalid account device: %s: %s", crt->getId().toString().c_str(), verifyResult.toString().c_str());
return false;
}

View File

@ -93,6 +93,7 @@ public:
/* Devices */
const std::map<dht::InfoHash, KnownDevice>& getKnownDevices() const { return knownDevices_; }
void foundAccountDevice(const dht::InfoHash& device, const std::string& name = {}, const time_point& last_sync = time_point::min());
bool foundAccountDevice(const std::shared_ptr<dht::crypto::Certificate>& crt, const std::string& name = {}, const time_point& last_sync = time_point::min());
bool removeAccountDevice(const dht::InfoHash& device);
void setAccountDeviceName(const dht::InfoHash& device, const std::string& name);

View File

@ -34,7 +34,8 @@
#include "jami_contact.h"
#include "configkeys.h"
#include "contact_list.h"
#include "account_manager.h"
#include "archive_account_manager.h"
#include "server_account_manager.h"
#include "sip/sdp.h"
#include "sip/sipvoiplink.h"
@ -912,19 +913,28 @@ JamiAccount::loadAccount(const std::string& archive_password, const std::string&
};
try {
accountManager_.reset(new ArchiveAccountManager(getPath(),
dht_,
[w = weak()](AccountManager::AsyncUser&& cb) {
if (auto this_ = w.lock())
cb(*this_->accountManager_);
},
[this]() { return getAccountDetails(); },
archivePath_.empty() ? "archive.gz" : archivePath_,
nameServer_));
auto onAsync = [w = weak()](AccountManager::AsyncUser&& cb) {
if (auto this_ = w.lock())
cb(*this_->accountManager_);
};
if (managerUri_.empty()) {
accountManager_.reset(new ArchiveAccountManager(getPath(),
dht_,
onAsync,
[this]() { return getAccountDetails(); },
archivePath_.empty() ? "archive.gz" : archivePath_,
nameServer_));
} else {
accountManager_.reset(new ServerAccountManager(getPath(),
dht_,
onAsync,
managerUri_,
nameServer_));
}
auto id = accountManager_->loadIdentity(tlsCertificateFile_, tlsPrivateKeyFile_, tlsPassword_);
bool hasArchive = not archivePath_.empty()
and fileutils::isFile(fileutils::getFullPath(idPath_, archivePath_));
/*bool hasArchive = not archivePath_.empty()
and fileutils::isFile(fileutils::getFullPath(idPath_, archivePath_));*/
if (auto info = accountManager_->useIdentity(id, receipt_, receiptSignature_, std::move(callbacks))) {
// normal loading path
@ -936,72 +946,92 @@ JamiAccount::loadAccount(const std::string& archive_password, const std::string&
}
}
else if (isEnabled()) {
setRegistrationState(RegistrationState::INITIALIZING);
auto fDeviceKey = dht::ThreadPool::computation().getShared<std::shared_ptr<dht::crypto::PrivateKey>>([](){
return std::make_shared<dht::crypto::PrivateKey>(dht::crypto::PrivateKey::generate());
});
setRegistrationState(RegistrationState::INITIALIZING);
auto fDeviceKey = dht::ThreadPool::computation().getShared<std::shared_ptr<dht::crypto::PrivateKey>>([](){
return std::make_shared<dht::crypto::PrivateKey>(dht::crypto::PrivateKey::generate());
});
auto fReq = dht::ThreadPool::computation().get<std::unique_ptr<dht::crypto::CertificateRequest>>([fDeviceKey]{
auto request = std::make_unique<dht::crypto::CertificateRequest>();
request->setName("Jami device");
auto deviceKey = fDeviceKey.get();
request->setUID(deviceKey->getPublicKey().getId().toString());
request->sign(*deviceKey);
return request;
});
auto fReq = dht::ThreadPool::computation().get<std::unique_ptr<dht::crypto::CertificateRequest>>([fDeviceKey]{
auto request = std::make_unique<dht::crypto::CertificateRequest>();
request->setName("Jami device");
auto deviceKey = fDeviceKey.get();
request->setUID(deviceKey->getPublicKey().getId().toString());
request->sign(*deviceKey);
return request;
});
auto creds = std::make_unique<ArchiveAccountManager::ArchiveAccountCredentials>();
creds->archivePath = archivePath_.empty() ? "archive.gz" : archivePath_;
std::unique_ptr<AccountManager::AccountCredentials> creds;
if (managerUri_.empty()) {
auto acreds = std::make_unique<ArchiveAccountManager::ArchiveAccountCredentials>();
acreds->archivePath = archivePath_.empty() ? "archive.gz" : archivePath_;
if (not archive_path.empty()) {
creds->scheme = "file";
creds->uri = archive_path;
acreds->scheme = "file";
acreds->uri = archive_path;
}
else if (not archive_pin.empty()) {
creds->scheme = "dht";
creds->uri = archive_pin;
creds->dhtBootstrap = loadBootstrap();
creds->dhtPort = (in_port_t)dhtPortUsed_;
acreds->scheme = "dht";
acreds->uri = archive_pin;
acreds->dhtBootstrap = loadBootstrap();
acreds->dhtPort = (in_port_t)dhtPortUsed_;
}
creds->password = archive_password;
accountManager_->initAuthentication(
std::move(fReq),
std::move(creds),
[this, fDeviceKey](const AccountInfo& info,
const std::map<std::string, std::string>& config,
std::string&& receipt,
std::vector<uint8_t>&& receipt_signature) {
JAMI_WARN("Auth success !");
creds = std::move(acreds);
} else {
auto screds = std::make_unique<ServerAccountManager::ServerAccountCredentials>();
screds->username = username_;
creds = std::move(screds);
}
creds->password = archive_password;
fileutils::check_dir(idPath_.c_str(), 0700);
accountManager_->initAuthentication(
std::move(fReq),
std::move(creds),
[this, fDeviceKey](const AccountInfo& info,
const std::map<std::string, std::string>& config,
std::string&& receipt,
std::vector<uint8_t>&& receipt_signature)
{
JAMI_WARN("Auth success !");
// save the chain including CA
auto id = info.identity;
id.first = std::move(fDeviceKey.get());
std::tie(tlsPrivateKeyFile_, tlsCertificateFile_) = saveIdentity(id, idPath_, "ring_device");
id_ = std::move(id);
tlsPassword_ = {};
fileutils::check_dir(idPath_.c_str(), 0700);
username_ = RING_URI_PREFIX+info.accountId;
// save the chain including CA
auto id = info.identity;
id.first = std::move(fDeviceKey.get());
std::tie(tlsPrivateKeyFile_, tlsCertificateFile_) = saveIdentity(id, idPath_, "ring_device");
id_ = std::move(id);
tlsPassword_ = {};
ringDeviceName_ = ip_utils::getDeviceName();
if (ringDeviceName_.empty())
ringDeviceName_ = info.deviceId.substr(8);
username_ = RING_URI_PREFIX+info.accountId;
receipt_ = std::move(receipt);
receiptSignature_ = std::move(receipt_signature);
accountManager_->foundAccountDevice(info.identity.second, ringDeviceName_, clock::now());
setRegistrationState(RegistrationState::UNREGISTERED);
ringDeviceName_ = ip_utils::getDeviceName();
if (ringDeviceName_.empty())
ringDeviceName_ = info.deviceId.substr(8);
saveConfig();
doRegister();
}, [this](AccountManager::AuthError error, const std::string& message) {
JAMI_WARN("Auth error: %d %s", (int)error, message.c_str());
{
std::lock_guard<std::mutex> lock(configurationMutex_);
setRegistrationState(RegistrationState::ERROR_GENERIC);
}
Manager::instance().removeAccount(getAccountID());
}, std::move(callbacks));
auto nameServerIt = config.find(DRing::Account::ConfProperties::RingNS::ACCOUNT);
if (nameServerIt != config.end()) {
nameServer_ = nameServerIt->second;
}
auto displayNameIt = config.find(DRing::Account::ConfProperties::DISPLAYNAME);
if (displayNameIt != config.end()) {
displayName_ = displayNameIt->second;
}
receipt_ = std::move(receipt);
receiptSignature_ = std::move(receipt_signature);
accountManager_->foundAccountDevice(info.identity.second, ringDeviceName_, clock::now());
setRegistrationState(RegistrationState::UNREGISTERED);
saveConfig();
doRegister();
}, [this](AccountManager::AuthError error, const std::string& message)
{
JAMI_WARN("Auth error: %d %s", (int)error, message.c_str());
{
std::lock_guard<std::mutex> lock(configurationMutex_);
setRegistrationState(RegistrationState::ERROR_GENERIC);
}
Manager::instance().removeAccount(getAccountID());
}, std::move(callbacks));
}
}
catch (const std::exception& e) {
@ -1037,6 +1067,9 @@ JamiAccount::setAccountDetails(const std::map<std::string, std::string>& details
dhtPort_ = getRandomEvenPort(DHT_PORT_RANGE);
dhtPortUsed_ = dhtPort_;
parseString(details, DRing::Account::ConfProperties::MANAGER_URI, managerUri_);
parseString(details, DRing::Account::ConfProperties::USERNAME, username_);
std::string archive_password;
std::string archive_pin;
std::string archive_path;
@ -1119,6 +1152,7 @@ JamiAccount::getAccountDetails() const
a.emplace(DRing::Account::ConfProperties::PROXY_ENABLED, proxyEnabled_ ? TRUE_STR : FALSE_STR);
a.emplace(DRing::Account::ConfProperties::PROXY_SERVER, proxyServer_);
a.emplace(DRing::Account::ConfProperties::PROXY_PUSH_TOKEN, deviceKey_);
a.emplace(DRing::Account::ConfProperties::MANAGER_URI, managerUri_);
#if HAVE_RINGNS
a.emplace(DRing::Account::ConfProperties::RingNS::URI, nameServer_);
#endif
@ -1590,7 +1624,7 @@ JamiAccount::doRegister_()
std::lock_guard<std::mutex> lock(configurationMutex_);
try {
if (not accountManager_->getInfo())
if (not accountManager_ or not accountManager_->getInfo())
throw std::runtime_error("No identity configured for this account.");
loadTreatedCalls();
@ -2091,7 +2125,7 @@ JamiAccount::isMessageTreated(unsigned int id)
std::map<std::string, std::string>
JamiAccount::getKnownDevices() const
{
if (not accountManager_->getInfo())
if (not accountManager_ or not accountManager_->getInfo())
return {};
std::map<std::string, std::string> ids;
for (auto& d : accountManager_->getKnownDevices()) {
@ -2184,7 +2218,7 @@ JamiAccount::generateDhParams()
MatchRank
JamiAccount::matches(const std::string &userName, const std::string &server) const
{
if (not accountManager_->getInfo())
if (not accountManager_ or not accountManager_->getInfo())
return MatchRank::NONE;
if (userName == accountManager_->getInfo()->accountId || server == accountManager_->getInfo()->accountId || userName == accountManager_->getInfo()->deviceId) {

View File

@ -85,7 +85,6 @@ public:
constexpr static const in_port_t DHT_DEFAULT_PORT = 4222;
constexpr static const char* const DHT_DEFAULT_BOOTSTRAP = "bootstrap.jami.net";
constexpr static const char* const DHT_DEFAULT_PROXY = "dhtproxy.jami.net:[80-100]";
constexpr static const char* const DHT_TYPE_NS = "cx.ring";
/* constexpr */ static const std::pair<uint16_t, uint16_t> DHT_PORT_RANGE;
@ -633,6 +632,8 @@ private:
*/
std::string receivedParameter_ {};
std::string managerUri_ {};
/**
* Optional: "rport" parameter from VIA header
*/

View File

@ -55,7 +55,8 @@ public:
alreadyTaken,
error,
incompleteRequest,
signatureVerificationFailed
signatureVerificationFailed,
unsupported
};
using LookupCallback = std::function<void(const std::string& result, Response response)>;

View File

@ -0,0 +1,246 @@
/*
* Copyright (C) 2014-2019 Savoir-faire Linux Inc.
* Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "server_account_manager.h"
#include "base64.h"
#include "dring/account_const.h"
#include <opendht/http.h>
#include <opendht/log.h>
#include "manager.h"
#include <algorithm>
namespace jami {
using Request = dht::http::Request;
static const std::string PATH_AUTH = "/api/auth";
static const std::string PATH_DEVICE_REGISTER = PATH_AUTH + "/device/register";
static const std::string PATH_DEVICES = PATH_AUTH + "/devices";
constexpr const char* const HTTPS_PROTO {"https"};
ServerAccountManager::ServerAccountManager(
const std::string& path,
std::shared_ptr<dht::DhtRunner> dht,
OnAsync&& onAsync,
const std::string& managerHostname,
const std::string& nameServer)
: AccountManager(path, std::move(onAsync), std::move(dht), nameServer)
, managerHostname_(managerHostname)
, logger_(std::make_shared<dht::Logger>(
[](char const* m, va_list args) { Logger::vlog(LOG_ERR, nullptr, 0, true, m, args); },
[](char const* m, va_list args) { Logger::vlog(LOG_WARNING, nullptr, 0, true, m, args); },
[](char const* m, va_list args) { Logger::vlog(LOG_DEBUG, nullptr, 0, true, m, args); }))
{};
void
ServerAccountManager::initAuthentication(
CertRequest csrRequest,
std::unique_ptr<AccountCredentials> credentials,
AuthSuccessCallback onSuccess,
AuthFailureCallback onFailure,
OnChangeCallback onChange)
{
auto ctx = std::make_shared<AuthContext>();
ctx->request = std::move(csrRequest);
ctx->credentials = dynamic_unique_cast<ServerAccountCredentials>(std::move(credentials));
ctx->onSuccess = std::move(onSuccess);
ctx->onFailure = std::move(onFailure);
if (not ctx->credentials or ctx->credentials->username.empty()) {
onFailure(AuthError::INVALID_ARGUMENTS, "invalid credentials");
return;
}
onChange_ = std::move(onChange);
const std::string url = managerHostname_ + PATH_DEVICE_REGISTER;
JAMI_WARN("[Auth] authentication with: %s %s to %s", ctx->credentials->username.c_str(), ctx->credentials->password.c_str(), url.c_str());
auto request = std::make_shared<Request>(*Manager::instance().ioContext(), url, logger_);
auto reqid = request->id();
request->set_connection_type(restinio::http_connection_header_t::close);
request->set_method(restinio::http_method_post());
request->set_auth(ctx->credentials->username, ctx->credentials->password);
{
std::stringstream ss;
auto csr = ctx->request.get()->toString();
string_replace(csr, "\n", "\\n");
string_replace(csr, "\r", "\\r");
ss << "{\"csrRequest\":\"" << csr << "\"}";
JAMI_WARN("[Auth] Sending request: %s", csr.c_str());
request->set_body(ss.str());
}
request->set_header_field(restinio::http_field_t::user_agent, "Jami");
request->set_header_field(restinio::http_field_t::accept, "application/json");
request->set_header_field(restinio::http_field_t::content_type, "application/json");
request->add_on_state_change_callback([reqid, ctx, onAsync = onAsync_]
(Request::State state, const dht::http::Response& response){
JAMI_ERR("[Auth] Got server response: %d", (int)state);
if (state != Request::State::DONE)
return;
if (response.status_code >= 400 && response.status_code < 500)
ctx->onFailure(AuthError::INVALID_ARGUMENTS, "");
else if (response.status_code < 200 || response.status_code > 299)
ctx->onFailure(AuthError::INVALID_ARGUMENTS, "");
else {
try {
Json::Value json;
std::string err;
Json::CharReaderBuilder rbuilder;
auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
if (!reader->parse(response.body.data(), response.body.data() + response.body.size(), &json, &err)){
JAMI_ERR("[Auth] Can't parse server response: %s", err.c_str());
ctx->onFailure(AuthError::SERVER_ERROR, "Can't parse server response");
return;
}
JAMI_WARN("[Auth] Got server response: %s", response.body.c_str());
auto certStr = json["x509Certificate"].asString();
string_replace(certStr, "\\n", "\n");
string_replace(certStr, "\\r", "\r");
auto cert = std::make_shared<dht::crypto::Certificate>(certStr);
auto accountCert = cert->issuer;
if (not accountCert) {
JAMI_ERR("[Auth] Can't parse certificate: no issuer");
ctx->onFailure(AuthError::SERVER_ERROR, "Invalid certificate from server");
return;
}
auto receipt = json["receipt"].asString();
Json::Value receiptJson;
auto receiptReader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
if (!receiptReader->parse(receipt.data(), receipt.data() + receipt.size(), &receiptJson, &err)){
JAMI_ERR("[Auth] Can't parse receipt from server: %s", err.c_str());
ctx->onFailure(AuthError::SERVER_ERROR, "Can't parse receipt from server");
return;
}
onAsync([reqid, ctx,
json=std::move(json),
receiptJson=std::move(receiptJson),
cert,
accountCert,
receipt=std::move(receipt)] (AccountManager& accountManager) mutable
{
auto& this_ = *static_cast<ServerAccountManager*>(&accountManager);
auto receiptSignature = base64::decode(json["receiptSignature"].asString());
auto info = std::make_unique<AccountInfo>();
info->identity.second = cert;
info->contacts = std::make_unique<ContactList>(accountCert, this_.path_, this_.onChange_);
//info->contacts->setContacts(a.contacts);
info->accountId = accountCert->getId().toString();
info->deviceId = cert->getPublicKey().getId().toString();
info->ethAccount = receiptJson["eth"].asString();
info->announce = parseAnnounce(receiptJson["announce"].asString(), info->accountId, info->deviceId);
if (not info->announce) {
ctx->onFailure(AuthError::SERVER_ERROR, "Can't parse announce from server");
}
this_.creds_ = std::move(ctx->credentials);
this_.info_ = std::move(info);
//auto info = useIdentity();
std::map<std::string, std::string> config;
if (json.isMember("nameServer")) {
config.emplace(DRing::Account::ConfProperties::RingNS::ACCOUNT, json["nameServer"].asString());
}
if (json.isMember("displayName")) {
config.emplace(DRing::Account::ConfProperties::DISPLAYNAME, json["displayName"].asString());
}
ctx->onSuccess(*this_.info_, std::move(config), std::move(receipt), std::move(receiptSignature));
this_.syncDevices();
this_.requests_.erase(reqid);
});
}
catch (const std::exception& e) {
JAMI_ERR("Error when loading account: %s", e.what());
ctx->onFailure(AuthError::NETWORK, "");
}
}
});
request->send();
requests_[reqid] = std::move(request);
}
void
ServerAccountManager::syncDevices()
{
if (not creds_)
return;
const std::string url = managerHostname_ + PATH_DEVICES;
JAMI_WARN("[Auth] syncDevices with: %s %s to %s", creds_->username.c_str(), creds_->password.c_str(), url.c_str());
auto request = std::make_shared<Request>(*Manager::instance().ioContext(), url, logger_);
auto reqid = request->id();
request->set_connection_type(restinio::http_connection_header_t::close);
request->set_method(restinio::http_method_get());
request->set_auth(creds_->username, creds_->password);
request->set_header_field(restinio::http_field_t::user_agent, "Jami");
request->set_header_field(restinio::http_field_t::accept, "application/json");
request->set_header_field(restinio::http_field_t::content_type, "application/json");
request->add_on_state_change_callback([reqid, onAsync = onAsync_]
(Request::State state, const dht::http::Response& response){
onAsync([reqid, state, response] (AccountManager& accountManager) {
JAMI_ERR("[Auth] Got server response: %d", (int)state);
auto& this_ = *static_cast<ServerAccountManager*>(&accountManager);
if (state != Request::State::DONE)
return;
if (response.status_code >= 200 || response.status_code < 300) {
try {
Json::Value json;
std::string err;
Json::CharReaderBuilder rbuilder;
auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
if (!reader->parse(response.body.data(), response.body.data() + response.body.size(), &json, &err)){
JAMI_ERR("[Auth] Can't parse server response: %s", err.c_str());
return;
}
JAMI_WARN("[Auth] Got server response: %s", response.body.c_str());
if (not json.isArray()) {
JAMI_ERR("[Auth] Can't parse server response: not an array");
return;
}
for (unsigned i=0, n=json.size(); i<n; i++) {
const auto& e = json[i];
dht::InfoHash deviceId(e["deviceId"].asString());
if (deviceId) {
this_.info_->contacts->foundAccountDevice(deviceId, e["alias"].asString(), clock::now());
}
}
}
catch (const std::exception& e) {
JAMI_ERR("Error when loading device list: %s", e.what());
}
}
this_.requests_.erase(reqid);
});
});
request->send();
requests_[reqid] = std::move(request);
}
void
ServerAccountManager::registerName(const std::string& password, const std::string& name, RegistrationCallback cb)
{
cb(NameDirectory::RegistrationResponse::unsupported);
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright (C) 2014-2019 Savoir-faire Linux Inc.
* Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "account_manager.h"
namespace jami {
class ServerAccountManager : public AccountManager {
public:
ServerAccountManager(
const std::string& path,
std::shared_ptr<dht::DhtRunner> dht,
OnAsync&& onAsync,
const std::string& managerHostname,
const std::string& nameServer);
struct ServerAccountCredentials : AccountCredentials {
std::string username;
std::shared_ptr<dht::crypto::Certificate> ca;
};
void initAuthentication(
CertRequest request,
std::unique_ptr<AccountCredentials> credentials,
AuthSuccessCallback onSuccess,
AuthFailureCallback onFailure,
OnChangeCallback onChange) override;
bool changePassword(const std::string& password_old, const std::string& password_new) {
return false;
}
void syncDevices();
bool findCertificate(const dht::InfoHash& h, std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb = {}) {
return false;
}
/*
void lookupName(const std::string& name, LookupCallback cb) {
}
void lookupAddress(const std::string& address, LookupCallback cb) {
}*/
void registerName(const std::string& password, const std::string& name, RegistrationCallback cb);
private:
struct AuthContext {
CertRequest request;
//std::unique_ptr<dht::crypto::CertificateRequest> request;
std::unique_ptr<ServerAccountCredentials> credentials;
//std::unique_ptr<DhtLoadContext> dhtContext;
AuthSuccessCallback onSuccess;
AuthFailureCallback onFailure;
};
const std::string managerHostname_;
std::shared_ptr<dht::Logger> logger_;
std::map<unsigned int /*id*/, std::shared_ptr<dht::http::Request>> requests_;
std::unique_ptr<ServerAccountCredentials> creds_;
};
}