account: add import/export API

- do not read dhtPort when deserializing a Ring account, use a new
one at runtime
- cache directory is created when generating dhParams
- use key stretching for archive encryption
- bump OpenDHT to add key stretching support

This commit comes with a necessary bump of OpenDHT

Tuleap: #335
Change-Id: Iee67569d378baaa33e9acd7cd9557422ab8e0471
This commit is contained in:
Alexandre Lision
2016-02-12 17:25:01 -05:00
committed by Adrien Béraud
parent 25cdcd8b44
commit ae5d1adad7
13 changed files with 475 additions and 7 deletions

View File

@ -193,6 +193,54 @@
</arg>
</method>
<method name="importAccounts" tp:name-for-bindings="importAccounts">
<tp:docstring>
Import previously exported accounts
</tp:docstring>
<arg type="s" name="path" direction="in">
<tp:docstring>
Path of the file to import
</tp:docstring>
</arg>
<arg type="s" name="password" direction="in">
<tp:docstring>
Decryption password
</tp:docstring>
</arg>
<arg type="i" direction="out">
<tp:docstring>
<p>Return code, 0 for success.</p>
</tp:docstring>
</arg>
</method>
<method name="exportAccounts" tp:name-for-bindings="exportAccounts">
<tp:docstring>
Export account configuration to an encrypted file.
</tp:docstring>
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="VectorString"/>
<arg type="as" name="accountIDs" direction="in">
<tp:docstring>
A list of account IDs
</tp:docstring>
</arg>
<arg type="s" name="filepath" direction="in">
<tp:docstring>
Where to export the account
</tp:docstring>
</arg>
<arg type="s" name="password" direction="in">
<tp:docstring>
File encryption password
</tp:docstring>
</arg>
<arg type="i" direction="out">
<tp:docstring>
<p>Return code, 0 for success.</p>
</tp:docstring>
</arg>
</method>
<method name="registerAllAccounts" tp:name-for-bindings="registerAllAccounts">
<tp:docstring>
Send account registration (REGISTER) for all accounts, even if they are not enabled.

View File

@ -527,3 +527,15 @@ DBusConfigurationManager::getVolume(const std::string& device) -> decltype(DRing
{
return DRing::getVolume(device);
}
auto
DBusConfigurationManager::exportAccounts(const std::vector<std::string>& accountIDs, const std::string& filepath, const std::string& password) -> decltype(DRing::exportAccounts(accountIDs, filepath, password))
{
return DRing::exportAccounts(accountIDs, filepath, password);
}
auto
DBusConfigurationManager::importAccounts(const std::string& archivePath, const std::string& password) -> decltype(DRing::importAccounts(archivePath, password))
{
return DRing::importAccounts(archivePath, password);
}

View File

@ -137,6 +137,8 @@ class DBusConfigurationManager :
bool acceptTrustRequest(const std::string& accountId, const std::string& from);
bool discardTrustRequest(const std::string& accountId, const std::string& from);
void sendTrustRequest(const std::string& accountId, const std::string& to, const std::vector<uint8_t>& payload);
int exportAccounts(const std::vector<std::string>& accountIDs, const std::string& filepath, const std::string& password);
int importAccounts(const std::string& archivePath, const std::string& password);
};
#endif // __RING_DBUSCONFIGURATIONMANAGER_H__

View File

@ -157,6 +157,7 @@ AC_TYPE_SIZE_T
AC_HEADER_TIME
AC_C_VOLATILE
AC_CHECK_TYPES([ptrdiff_t])
AC_CHECK_LIB(zlib, zlib)
PKG_PROG_PKG_CONFIG()
@ -236,7 +237,7 @@ AS_IF([test -n "${CONTRIB_DIR}"], [
export PKG_CONFIG_PATH_CUSTOM
])
export PKG_CONFIG_PATH="${CONTRIB_DIR}/lib/pkgconfig:${CONTRIB_DIR}/lib64/pkgconfig:$PKG_CONFIG_PATH"
LDFLAGS="${LDFLAGS} -L${CONTRIB_DIR}/lib"
LDFLAGS="${LDFLAGS} -L${CONTRIB_DIR}/lib -lz"
AS_IF([test "${SYS}" = "darwin"], [
export LD_LIBRARY_PATH="${CONTRIB_DIR}/lib:$LD_LIBRARY_PATH"

View File

@ -1,5 +1,5 @@
# OPENDHT
OPENDHT_VERSION := 281b62dfd529a226e94d0da19e01acf07871a797
OPENDHT_VERSION := 8ae95f1284f3c00c41832ef4d76196481543f59e
OPENDHT_URL := https://github.com/savoirfairelinux/opendht/archive/$(OPENDHT_VERSION).tar.gz
PKGS += opendht

View File

@ -116,6 +116,7 @@ libring_la_SOURCES = \
account.cpp \
logger.cpp \
fileutils.cpp \
archiver.cpp \
threadloop.cpp \
ip_utils.h \
ip_utils.cpp \
@ -137,6 +138,7 @@ libring_la_SOURCES = \
call.h \
logger.h \
fileutils.h \
archiver.h \
noncopyable.h \
utf8_utils.h \
ring_types.h \

280
src/archiver.cpp Normal file
View File

@ -0,0 +1,280 @@
/*
* Copyright (C) 2016 Savoir-faire Linux Inc.
*
* Author: Alexandre Lision <alexandre.lision@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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "archiver.h"
#include <json/json.h>
#include "client/ring_signal.h"
#include "account_const.h"
#include "configurationmanager_interface.h"
#include "manager.h"
#include "fileutils.h"
#include "logger.h"
#include <opendht/crypto.h>
#include <fstream>
#include <sys/stat.h>
namespace ring {
Archiver&
Archiver::instance()
{
// Meyers singleton
static Archiver instance_;
return instance_;
}
Archiver::Archiver()
{
}
int
Archiver::exportAccounts(std::vector<std::string> accountIDs,
std::string filepath,
std::string password)
{
if (filepath.empty() || !accountIDs.size()) {
RING_ERR("Missing arguments");
return EINVAL;
}
std::size_t found = filepath.find_last_of(DIR_SEPARATOR_STR);
auto toDir = filepath.substr(0,found);
auto filename = filepath.substr(found+1);
if (!fileutils::isDirectory(toDir)) {
RING_ERR("%s is not a directory", toDir.c_str());
return ENOTDIR;
}
// Add
Json::Value root;
Json::Value array;
for (size_t i = 0; i < accountIDs.size(); ++i) {
auto detailsMap = Manager::instance().getAccountDetails(accountIDs[i]);
if (detailsMap.empty()) {
RING_WARN("Can't export account %s", accountIDs[i].c_str());
continue;
}
auto jsonAccount = accountToJsonValue(detailsMap);
array.append(jsonAccount);
}
root["accounts"] = array;
Json::FastWriter fastWriter;
std::string output = fastWriter.write(root);
// Compress
std::vector<uint8_t> compressed;
try {
compressed = compress(output);
} catch (const std::runtime_error& ex) {
RING_ERR("Export failed: %s", ex.what());
return 1;
}
// Encrypt using provided password
auto encrypted = dht::crypto::aesEncrypt(compressed, password);
// Write
try {
fileutils::saveFile(toDir + DIR_SEPARATOR_STR + filename, encrypted);
} catch (const std::runtime_error& ex) {
RING_ERR("Export failed: %s", ex.what());
return EIO;
}
return 0;
}
Json::Value
Archiver::accountToJsonValue(std::map<std::string, std::string> details) {
Json::Value root;
std::map<std::string, std::string>::iterator iter;
for (iter = details.begin(); iter != details.end(); ++iter) {
if (iter->first.compare(DRing::Account::ConfProperties::Ringtone::PATH) == 0) {
// Ringtone path is not exportable
} else if (iter->first.compare(DRing::Account::ConfProperties::TLS::CA_LIST_FILE) == 0 ||
iter->first.compare(DRing::Account::ConfProperties::TLS::CERTIFICATE_FILE) == 0 ||
iter->first.compare(DRing::Account::ConfProperties::TLS::PRIVATE_KEY_FILE) == 0) {
// replace paths by the files content
std::ifstream ifs(iter->second);
std::string fileContent((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
root[iter->first] = fileContent;
} else
root[iter->first] = iter->second;
}
return root;
}
int
Archiver::importAccounts(std::string archivePath, std::string password)
{
if (archivePath.empty()) {
RING_ERR("Missing arguments");
return EINVAL;
}
// Read file
std::vector<uint8_t> file;
try {
file = fileutils::loadFile(archivePath);
} catch (const std::exception& ex) {
RING_ERR("Read failed: %s", ex.what());
return ENOENT;
}
// Decrypt
try {
file = dht::crypto::aesDecrypt(file, password);
} catch (const std::exception& ex) {
RING_ERR("Decryption failed: %s", ex.what());
return EPERM;
}
// Decompress
try {
file = decompress(file);
} catch (const std::exception& ex) {
RING_ERR("Decompression failed: %s", ex.what());
return ERANGE;
}
try {
// Decode string
std::string decoded {file.begin(), file.end()};
// Add
Json::Value root;
Json::Reader reader;
if (!reader.parse(decoded.c_str(),root)) {
RING_ERR("Failed to parse %s", reader.getFormattedErrorMessages().c_str());
return ERANGE;
}
auto& accounts = root["accounts"];
for (int i = 0, n = accounts.size(); i < n; ++i) {
// Generate a new account id
auto accountId = ring::Manager::instance().getNewAccountId();
auto details = jsonValueToAccount(accounts[i], accountId);
ring::Manager::instance().addAccount(details, accountId);
}
} catch (const std::exception& ex) {
RING_ERR("Import failed: %s", ex.what());
return ERANGE;
}
return 0;
}
std::map<std::string, std::string>
Archiver::jsonValueToAccount(Json::Value& value, const std::string& accountId) {
auto idPath_ = fileutils::get_data_dir() + DIR_SEPARATOR_STR + accountId;
fileutils::check_dir(idPath_.c_str(), 0700);
auto detailsMap = DRing::getAccountTemplate(value[DRing::Account::ConfProperties::TYPE].asString());
for( Json::ValueIterator itr = value.begin() ; itr != value.end() ; itr++ ) {
if (itr->asString().empty())
continue;
if (itr.key().asString().compare(DRing::Account::ConfProperties::TLS::CA_LIST_FILE) == 0) {
std::string fileContent(itr->asString());
fileutils::saveFile(idPath_ + DIR_SEPARATOR_STR "ca.key", {fileContent.begin(), fileContent.end()}, 0600);
} else if (itr.key().asString().compare(DRing::Account::ConfProperties::TLS::PRIVATE_KEY_FILE) == 0) {
std::string fileContent(itr->asString());
fileutils::saveFile(idPath_ + DIR_SEPARATOR_STR "dht.key", {fileContent.begin(), fileContent.end()}, 0600);
} else if (itr.key().asString().compare(DRing::Account::ConfProperties::TLS::CERTIFICATE_FILE) == 0) {
std::string fileContent(itr->asString());
fileutils::saveFile(idPath_ + DIR_SEPARATOR_STR "dht.crt", {fileContent.begin(), fileContent.end()}, 0600);
} else
detailsMap[itr.key().asString()] = itr->asString();
}
return detailsMap;
}
std::vector<uint8_t>
Archiver::compress(const std::string& str, int compressionlevel)
{
auto destSize = compressBound(str.size());
std::vector<uint8_t> outbuffer(destSize);
int ret = ::compress(reinterpret_cast<Bytef*>(outbuffer.data()), &destSize, (Bytef*)str.data(), str.size());
if (ret != Z_OK) {
std::ostringstream oss;
oss << "Exception during zlib compression: (" << ret << ") ";
throw(std::runtime_error(oss.str()));
}
return outbuffer;
}
std::vector<uint8_t>
Archiver::decompress(const std::vector<uint8_t>& str)
{
z_stream zs; // z_stream is zlib's control structure
memset(&zs, 0, sizeof(zs));
if (inflateInit(&zs) != Z_OK)
throw(std::runtime_error("inflateInit failed while decompressing."));
zs.next_in = (Bytef*)str.data();
zs.avail_in = str.size();
int ret;
std::vector<uint8_t> out;
// get the decompressed bytes blockwise using repeated calls to inflate
do {
std::array<uint8_t, 32768> outbuffer;
zs.next_out = reinterpret_cast<Bytef*>(outbuffer.data());
zs.avail_out = outbuffer.size();
ret = inflate(&zs, 0);
if (ret == Z_DATA_ERROR || ret == Z_MEM_ERROR)
break;
if (out.size() < zs.total_out) {
// append the block to the output string
out.insert(out.end(), outbuffer.begin(), outbuffer.begin() + zs.total_out - out.size());
}
} while (ret == Z_OK);
inflateEnd(&zs);
// an error occurred that was not EOF
if (ret != Z_STREAM_END) {
std::ostringstream oss;
oss << "Exception during zlib decompression: (" << ret << ") " << zs.msg;
throw(std::runtime_error(oss.str()));
}
return out;
}
} // namespace ring

83
src/archiver.h Normal file
View File

@ -0,0 +1,83 @@
/*
* Copyright (C) 2016 Savoir-faire Linux Inc.
*
* Author: Alexandre Lision <alexandre.lision@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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "noncopyable.h"
#include <string>
#include <vector>
#include <map>
#include <zlib.h>
namespace Json {
class Value;
};
namespace ring {
/**
* Archiver is used to generate/read encrypted archives
*/
class Archiver {
public:
static Archiver& instance();
Archiver();
/**
* Create a protected archive containing a list of accounts
* @param accountIDs The accounts to exports
* @param filepath The filepath where to put the resulting archive
* @param password The mandatory password to set on the archive
* @returns 0 for OK, error code otherwise
*/
int exportAccounts(std::vector<std::string> accountIDs,
std::string filepath,
std::string password);
/**
* Read a protected archive and add accounts found in it
* Warning: this function must be called from a registered pjsip thread
* @param archivePath The path to the archive file
* @param password The password to read the archive
* @returns 0 for OK, error code otherwise
*/
int importAccounts(std::string archivePath, std::string password);
/**
* Compress a STL string using zlib with given compression level and return
* the binary data.
*/
std::vector<uint8_t> compress(const std::string& str, int compressionlevel = Z_BEST_COMPRESSION);
/**
* Decompress an STL string using zlib and return the original data.
*/
std::vector<uint8_t> decompress(const std::vector<uint8_t>& dat);
private:
NON_COPYABLE(Archiver);
static Json::Value accountToJsonValue(std::map<std::string, std::string> details);
static std::map<std::string, std::string> jsonValueToAccount(Json::Value& value, const std::string& accountId);
};
} // namespace ring

View File

@ -34,6 +34,7 @@
#endif
#include "logger.h"
#include "fileutils.h"
#include "archiver.h"
#include "ip_utils.h"
#include "sip/sipaccount.h"
#include "ringdht/ringaccount.h"
@ -320,6 +321,21 @@ sendTrustRequest(const std::string& accountId, const std::string& to, const std:
acc->sendTrustRequest(to, payload);
}
/*
* Import/Export accounts
*/
int
exportAccounts(std::vector<std::string> accountIDs, std::string filepath, std::string password)
{
return ring::Archiver::instance().exportAccounts(accountIDs, filepath, password);
}
int
importAccounts(std::string archivePath, std::string password)
{
return ring::Archiver::instance().importAccounts(archivePath, password);
}
///This function is used as a base for new accounts for clients that support it
std::map<std::string, std::string>
getAccountTemplate(const std::string& accountType)

View File

@ -150,6 +150,12 @@ bool discardTrustRequest(const std::string& accountId, const std::string& from);
void sendTrustRequest(const std::string& accountId, const std::string& to, const std::vector<uint8_t>& payload = {});
/*
* Import/Export accounts
*/
int exportAccounts(std::vector<std::string> accountIDs, std::string filepath, std::string password);
int importAccounts(std::string archivePath, std::string password);
struct AudioSignal {
struct DeviceEvent {
constexpr static const char* name = "audioDeviceEvent";

View File

@ -2433,9 +2433,8 @@ Manager::setAccountDetails(const std::string& accountID,
}
std::string
Manager::addAccount(const std::map<std::string, std::string>& details)
Manager::getNewAccountId()
{
/** @todo Deal with both the accountMap_ and the Configuration */
std::string newAccountID;
static std::uniform_int_distribution<uint64_t> rand_acc_id;
@ -2448,8 +2447,16 @@ Manager::addAccount(const std::map<std::string, std::string>& details)
} while (std::find(accountList.begin(), accountList.end(), newAccountID)
!= accountList.end());
// Get the type
return newAccountID;
}
std::string
Manager::addAccount(const std::map<std::string, std::string>& details, const std::string& accountId)
{
/** @todo Deal with both the accountMap_ and the Configuration */
auto newAccountID = accountId.empty() ? getNewAccountId() : accountId;
// Get the type
const char* accountType;
if (details.find(Conf::CONFIG_ACCOUNT_TYPE) != details.end())
accountType = (*details.find(Conf::CONFIG_ACCOUNT_TYPE)).second.c_str();

View File

@ -499,12 +499,20 @@ class Manager {
void setAccountActive(const std::string& accountID, bool active);
/**
* Return a new random accountid that is not present in the list
* @return A brand new accountid
*/
std::string getNewAccountId();
/**
* Add a new account, and give it a new account ID automatically
* @param details The new account parameters
* @param accountId optionnal predetermined accountid to use
* @return The account Id given to the new account
*/
std::string addAccount(const std::map<std::string, std::string> &details);
std::string addAccount(const std::map<std::string, std::string> &details,
const std::string& accountId = {});
/**
* Delete an existing account, unregister VoIPLink associated, and

View File

@ -425,7 +425,7 @@ void RingAccount::unserialize(const YAML::Node &node)
using yaml_utils::parseValue;
SIPAccountBase::unserialize(node);
parseValue(node, Conf::DHT_PORT_KEY, dhtPort_);
parseValue(node, Conf::DHT_ALLOW_PEERS_FROM_HISTORY, allowPeersFromHistory_);
parseValue(node, Conf::DHT_ALLOW_PEERS_FROM_CONTACT, allowPeersFromContact_);
parseValue(node, Conf::DHT_ALLOW_PEERS_FROM_TRUSTED, allowPeersFromTrusted_);
@ -1251,6 +1251,9 @@ RingAccount::loadDhParams(const std::string path)
void
RingAccount::generateDhParams()
{
//make sure cachePath_ is writable
fileutils::check_dir(cachePath_.c_str(), 0700);
std::packaged_task<decltype(loadDhParams)> task(loadDhParams);
dhParams_ = task.get_future();
std::thread task_td(std::move(task), cachePath_ + DIR_SEPARATOR_STR "dhParams");