diff --git a/Makefile b/Makefile index 6d5bbcccd898051280a361c849c31f4c71f68ca5..082cdb3006dd598516c9536d52b057b1c61141fa 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ FILES=`find lib include tests examples -type f -type f \( -iname "*.cpp" -o -iname "*.hpp" \)` -SYNAPSE_IMAGE="avhost/docker-matrix:v0.33.3" +SYNAPSE_IMAGE="avhost/docker-matrix:v0.33.4" DEPS_BUILD_DIR=.deps DEPS_SOURCE_DIR=deps diff --git a/include/mtxclient/crypto/client.hpp b/include/mtxclient/crypto/client.hpp index 3015d95ef4062a916df54a6c694ef618c5bfbea2..ef16c3f5fc2147bf11ce49fa66f85853bb4696fa 100644 --- a/include/mtxclient/crypto/client.hpp +++ b/include/mtxclient/crypto/client.hpp @@ -226,6 +226,21 @@ matches_inbound_session_from(OlmSession *session, const std::string &id_key, const std::string &one_time_key_message); +std::string +encrypt_exported_sessions(const mtx::crypto::ExportedSessionKeys &keys, std::string pass); + +mtx::crypto::ExportedSessionKeys +decrypt_exported_sessions(const std::string &data, std::string pass); + +std::string +base642bin(const std::string &b64); + +std::string +bin2base64(const std::string &b64); + +BinaryBuf +derive_key(const std::string &pass, const BinaryBuf &salt); + //! Verify a signature object as obtained from the response of /keys/query endpoint bool verify_identity_signature(nlohmann::json obj, const DeviceId &device_id, const UserId &user_id); diff --git a/include/mtxclient/crypto/types.hpp b/include/mtxclient/crypto/types.hpp index e0a30cd203ebe8f2f0f4979f56a1f4d1812ce30b..287757512fbb98ca1b9ba388f6c49e500053d3f0 100644 --- a/include/mtxclient/crypto/types.hpp +++ b/include/mtxclient/crypto/types.hpp @@ -10,8 +10,26 @@ STRONG_TYPE(RoomId, std::string) namespace mtx { namespace crypto { -static constexpr const char *ED25519 = "ed25519"; -static constexpr const char *CURVE25519 = "curve25519"; +constexpr auto ED25519 = "ed25519"; +constexpr auto CURVE25519 = "curve25519"; +constexpr auto MEGOLM_ALGO = "m.megolm.v1.aes-sha2"; + +struct ExportedSession +{ + std::map<std::string, std::string> sender_claimed_keys; // currently unused. + std::vector<std::string> forwarding_curve25519_key_chain; // currently unused. + + std::string algorithm = MEGOLM_ALGO; + std::string room_id; + std::string sender_key; + std::string session_id; + std::string session_key; +}; + +struct ExportedSessionKeys +{ + std::vector<ExportedSession> sessions; +}; struct IdentityKeys { @@ -19,6 +37,58 @@ struct IdentityKeys std::string ed25519; }; +struct OneTimeKeys +{ + using KeyId = std::string; + using EncodedKey = std::string; + + std::map<KeyId, EncodedKey> curve25519; +}; + +inline void +to_json(nlohmann::json &obj, const ExportedSession &s) +{ + obj["sender_claimed_keys"] = s.sender_claimed_keys; + obj["forwarding_curve25519_key_chain"] = s.forwarding_curve25519_key_chain; + + obj["algorithm"] = s.algorithm; + obj["room_id"] = s.room_id; + obj["sender_key"] = s.sender_key; + obj["session_id"] = s.session_id; + obj["session_key"] = s.session_key; +} + +inline void +from_json(const nlohmann::json &obj, ExportedSession &s) +{ + s.room_id = obj.at("room_id").get<std::string>(); + s.sender_key = obj.at("sender_key").get<std::string>(); + s.session_id = obj.at("session_id").get<std::string>(); + s.session_key = obj.at("session_key").get<std::string>(); + + using ClaimedKeys = std::map<std::string, std::string>; + using KeyChain = std::vector<std::string>; + + if (obj.find("sender_claimed_keys") != obj.end()) + s.sender_claimed_keys = obj.at("sender_claimed_keys").get<ClaimedKeys>(); + + if (obj.find("forwarding_curve25519_key_chain") != obj.end()) + s.forwarding_curve25519_key_chain = + obj.at("forwarding_curve25519_key_chain").get<KeyChain>(); +} + +inline void +to_json(nlohmann::json &obj, const ExportedSessionKeys &keys) +{ + obj["sessions"] = keys.sessions; +} + +inline void +from_json(const nlohmann::json &obj, ExportedSessionKeys &keys) +{ + keys.sessions = obj.at("sessions").get<std::vector<ExportedSession>>(); +} + inline void to_json(nlohmann::json &obj, const IdentityKeys &keys) { @@ -33,24 +103,17 @@ from_json(const nlohmann::json &obj, IdentityKeys &keys) keys.curve25519 = obj.at(CURVE25519).get<std::string>(); } -struct OneTimeKeys -{ - using KeyId = std::string; - using EncodedKey = std::string; - - std::map<KeyId, EncodedKey> curve25519; -}; - inline void to_json(nlohmann::json &obj, const OneTimeKeys &keys) { - obj["curve25519"] = keys.curve25519; + obj[CURVE25519] = keys.curve25519; } inline void from_json(const nlohmann::json &obj, OneTimeKeys &keys) { - keys.curve25519 = obj.at("curve25519").get<std::map<std::string, std::string>>(); -} -} + keys.curve25519 = obj.at(CURVE25519).get<std::map<std::string, std::string>>(); } + +} // namespace crypto +} // namespace mtx diff --git a/lib/crypto/client.cpp b/lib/crypto/client.cpp index 3254556a017e817b0a38bc77da8f35b003ee15e8..22f95ecd772e77a827efed3982f1c7ef9c1fe77a 100644 --- a/lib/crypto/client.cpp +++ b/lib/crypto/client.cpp @@ -1,6 +1,7 @@ #include <iostream> #include "mtxclient/crypto/client.hpp" +#include "mtxclient/crypto/types.hpp" using json = nlohmann::json; using namespace mtx::crypto; @@ -23,7 +24,7 @@ OlmClient::restore_account(const std::string &saved_data, const std::string &key account_ = unpickle<AccountObject>(saved_data, key); } -IdentityKeys +mtx::crypto::IdentityKeys OlmClient::identity_keys() const { auto tmp_buf = create_buffer(olm_account_identity_keys_length(account_.get())); @@ -80,7 +81,7 @@ OlmClient::generate_one_time_keys(std::size_t number_of_keys) return ret; } -OneTimeKeys +mtx::crypto::OneTimeKeys OlmClient::one_time_keys() { auto buf = create_buffer(olm_account_one_time_keys_length(account_.get())); @@ -497,3 +498,130 @@ mtx::crypto::verify_identity_signature(nlohmann::json obj, return false; } + +std::string +mtx::crypto::encrypt_exported_sessions(const mtx::crypto::ExportedSessionKeys &keys, + std::string pass) +{ + const auto plaintext = json(keys).dump(); + const auto msg_len = plaintext.size(); + const auto ciphertext_len = crypto_secretbox_MACBYTES + msg_len; + + auto nonce = create_buffer(crypto_secretbox_NONCEBYTES); + auto ciphertext = create_buffer(ciphertext_len); + + auto salt = create_buffer(crypto_pwhash_SALTBYTES); + auto key = derive_key(pass, salt); + + crypto_secretbox_easy(reinterpret_cast<unsigned char *>(ciphertext.data()), + reinterpret_cast<const unsigned char *>(plaintext.data()), + msg_len, + nonce.data(), + reinterpret_cast<const unsigned char *>(key.data())); + + // Format of the output buffer: (nonce + salt + ciphertext) + BinaryBuf output{nonce}; + output.insert( + output.end(), std::make_move_iterator(salt.begin()), std::make_move_iterator(salt.end())); + output.insert(output.end(), + std::make_move_iterator(ciphertext.begin()), + std::make_move_iterator(ciphertext.end())); + + return std::string(output.begin(), output.end()); +} + +mtx::crypto::ExportedSessionKeys +mtx::crypto::decrypt_exported_sessions(const std::string &data, std::string pass) +{ + if (data.size() < + crypto_secretbox_MACBYTES + crypto_secretbox_NONCEBYTES + crypto_pwhash_SALTBYTES) + throw std::runtime_error{"decrypt_exported_sessions ciphertext too small"}; + + const auto nonce_start = data.begin(); + const auto nonce_end = nonce_start + crypto_secretbox_NONCEBYTES; + auto nonce = BinaryBuf(nonce_start, nonce_end); + + const auto salt_end = nonce_end + crypto_pwhash_SALTBYTES; + auto salt = BinaryBuf(nonce_end, salt_end); + + auto ciphertext = BinaryBuf(salt_end, data.end()); + auto decrypted = create_buffer(ciphertext.size() - crypto_secretbox_MACBYTES); + + auto key = derive_key(pass, salt); + + if (crypto_secretbox_open_easy(decrypted.data(), + reinterpret_cast<const unsigned char *>(ciphertext.data()), + ciphertext.size(), + nonce.data(), + reinterpret_cast<const unsigned char *>(key.data())) != 0) + throw std::runtime_error{"crypto_secretbox_open_easy: failed to decrypt"}; + + return json::parse(std::string(decrypted.begin(), decrypted.end())); +} + +std::string +mtx::crypto::base642bin(const std::string &b64) +{ + std::size_t bin_maxlen = b64.size(); + std::size_t bin_len; + + const char *max_end; + + auto ciphertext = create_buffer(bin_maxlen); + + const int rc = sodium_base642bin(reinterpret_cast<unsigned char *>(ciphertext.data()), + ciphertext.size(), + b64.data(), + b64.size(), + nullptr, + &bin_len, + &max_end, + sodium_base64_VARIANT_ORIGINAL); + if (rc != 0) + throw std::runtime_error{"base642bin failed"}; + + if (bin_len != bin_maxlen) + ciphertext.resize(bin_len); + + return std::string(std::make_move_iterator(ciphertext.begin()), + std::make_move_iterator(ciphertext.end())); +} + +std::string +mtx::crypto::bin2base64(const std::string &bin) +{ + auto base64buf = + create_buffer(sodium_base64_encoded_len(bin.size(), sodium_base64_VARIANT_ORIGINAL)); + + sodium_bin2base64(reinterpret_cast<char *>(base64buf.data()), + base64buf.size(), + reinterpret_cast<const unsigned char *>(bin.data()), + bin.size(), + sodium_base64_VARIANT_ORIGINAL); + + // Removing the null byte. + return std::string(base64buf.begin(), base64buf.end() - 1); +} + +BinaryBuf +mtx::crypto::derive_key(const std::string &pass, const BinaryBuf &salt) +{ + if (salt.size() != crypto_pwhash_SALTBYTES) + throw std::runtime_error{"derive_key: invalid buffer size for salt"}; + + auto key = create_buffer(crypto_secretbox_KEYBYTES); + + // Derive a key from the user provided password. + if (crypto_pwhash(key.data(), + key.size(), + pass.data(), + pass.size(), + salt.data(), + crypto_pwhash_OPSLIMIT_INTERACTIVE, + crypto_pwhash_MEMLIMIT_INTERACTIVE, + crypto_pwhash_ALG_DEFAULT) != 0) { + throw std::runtime_error{"crypto_pwhash: out of memory"}; + } + + return key; +} diff --git a/lib/crypto/types.cpp b/lib/crypto/types.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b3f93ad5abb438f5b78b136d004d35cb1b6aa3cc --- /dev/null +++ b/lib/crypto/types.cpp @@ -0,0 +1,77 @@ +#include "mtxclient/crypto/types.hpp" + +namespace mtx { +namespace crypto { + +void +to_json(nlohmann::json &obj, const ExportedSession &s) +{ + obj["sender_claimed_keys"] = s.sender_claimed_keys; + obj["forwarding_curve25519_key_chain"] = s.forwarding_curve25519_key_chain; + + obj["algorithm"] = s.algorithm; + obj["room_id"] = s.room_id; + obj["sender_key"] = s.sender_key; + obj["session_id"] = s.session_id; + obj["session_key"] = s.session_key; +} + +void +from_json(const nlohmann::json &obj, ExportedSession &s) +{ + s.room_id = obj.at("room_id").get<std::string>(); + s.sender_key = obj.at("sender_key").get<std::string>(); + s.session_id = obj.at("session_id").get<std::string>(); + s.session_key = obj.at("session_key").get<std::string>(); + + using ClaimedKeys = std::map<std::string, std::string>; + using KeyChain = std::vector<std::string>; + + if (obj.find("sender_claimed_keys") != obj.end()) + s.sender_claimed_keys = obj.at("sender_claimed_keys").get<ClaimedKeys>(); + + if (obj.find("forwarding_curve25519_key_chain") != obj.end()) + s.forwarding_curve25519_key_chain = + obj.at("forwarding_curve25519_key_chain").get<KeyChain>(); +} + +void +to_json(nlohmann::json &obj, const ExportedSessionKeys &keys) +{ + obj["sessions"] = keys.sessions; +} + +void +from_json(const nlohmann::json &obj, ExportedSessionKeys &keys) +{ + keys.sessions = obj.at("sessions").get<std::vector<ExportedSession>>(); +} + +void +to_json(nlohmann::json &obj, const IdentityKeys &keys) +{ + obj[ED25519] = keys.ed25519; + obj[CURVE25519] = keys.curve25519; +} + +void +from_json(const nlohmann::json &obj, IdentityKeys &keys) +{ + keys.ed25519 = obj.at(ED25519).get<std::string>(); + keys.curve25519 = obj.at(CURVE25519).get<std::string>(); +} + +void +to_json(nlohmann::json &obj, const OneTimeKeys &keys) +{ + obj[CURVE25519] = keys.curve25519; +} + +void +from_json(const nlohmann::json &obj, OneTimeKeys &keys) +{ + keys.curve25519 = obj.at(CURVE25519).get<std::map<std::string, std::string>>(); +} + +} // namespace crypto +} // namespace mtx diff --git a/tests/e2ee.cpp b/tests/e2ee.cpp index fdaf9421579dc98d8394df319eac68f1f9dc229e..1a1f0e05ff484de3355680ee5322307c48328fda 100644 --- a/tests/e2ee.cpp +++ b/tests/e2ee.cpp @@ -6,6 +6,7 @@ #include <gtest/gtest.h> #include "mtxclient/crypto/client.hpp" +#include "mtxclient/crypto/types.hpp" #include "mtxclient/http/client.hpp" #include "mtx/requests.hpp" @@ -1035,6 +1036,41 @@ TEST(Encryption, PickleMegolmSessions) EXPECT_EQ(std::string((char *)plaintext.data.data(), plaintext.data.size()), SECRET); } +TEST(Base64, EncodingDecoding) +{ + std::string random_str = + "+7TE+9qmFWHPnrBLd03MtoXsRlhYaQt2tLBg4kZJI+NFcXVxqNUI1S3c97eV8aVgSj1/" + "eo8PsnRNO29c2TgPLXvah2GDl90ehHjzH/" + "vMBJKPdqyE31ch7NYBgvLBVoesrRyDoIYDlbRhHiRDTmLKMC55WN1YvDJu2Pvg3WxZiANobk" + "0EPzHABqOYLaYiVxFrdko7mm8pDZXlatys+dvLv9Zf6lxfd/5MPK1C52m/UhnrZ3shS/" + "XBzxRfBikZQjl7C9IMo7l170ffipN8QHb5LmZlj4V41DUJHCU="; + + EXPECT_EQ(base642bin(bin2base64(random_str)), random_str); + EXPECT_EQ(bin2base64(base642bin(random_str)), random_str); +} + +TEST(ExportSessions, EncryptDecrypt) +{ + constexpr auto PASS = "secret_passphrase"; + + ExportedSession s1; + s1.room_id = "!room_id:example.org"; + s1.session_id = "sid"; + s1.session_key = "skey"; + + ExportedSessionKeys keys; + keys.sessions = {s1, s1, s1}; + + std::string ciphertext = mtx::crypto::encrypt_exported_sessions(keys, PASS); + EXPECT_TRUE(ciphertext.size() > 0); + + auto encoded = bin2base64(ciphertext); + auto decoded = base642bin(encoded); + + auto restored_keys = mtx::crypto::decrypt_exported_sessions(decoded, PASS); + EXPECT_EQ(json(keys).dump(), json(restored_keys).dump()); +} + TEST(Encryption, DISABLED_HandleRoomKeyEvent) {} TEST(Encryption, DISABLED_HandleRoomKeyRequestEvent) {} TEST(Encryption, DISABLED_HandleNewDevices) {}