diff --git a/.ci/install.sh b/.ci/install.sh index 1cb1657dc16d3546d380da9e337891d34c218ee7..2826c63d7062f10a61b706637d3e6daefce94afb 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -5,7 +5,7 @@ set -ex if [ $TRAVIS_OS_NAME == osx ]; then brew update || true brew upgrade boost || true - brew install libsodium clang-format + brew install libsodium clang-format ninja brew tap nlohmann/json # the nlohmann install seems to make travis angry # because of the number of log messages diff --git a/.travis.yml b/.travis.yml index cff7bacd93f673297f21a6fac91d2560f6f61a27..1661291115c0547fe0a9ca0df119a4acab7e9383 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,18 +13,20 @@ notifications: matrix: include: - os: osx - osx_image: xcode9 + # osx_image: xcode9 # don't specify an image here to use the default compiler: clang - os: linux compiler: gcc env: - CXX_VERSION=g++-8 - CC_VERSION=gcc-8 + - CMAKE_GENERATOR=Ninja - os: linux compiler: clang env: - CXX_VERSION=clang++-6.0 - CC_VERSION=clang-6.0 + - CMAKE_GENERATOR=Ninja - COVERAGE=ON install: diff --git a/CMakeLists.txt b/CMakeLists.txt index eaaaec2fd54d1018081d0e54606dad67efbc8bb2..9e19afe0b6477fc200b4ef82181d278921ed03d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,7 +92,7 @@ set_package_properties(nlohmann_json PROPERTIES set(Boost_USE_STATIC_LIBS OFF) set(Boost_USE_STATIC_RUNTIME OFF) set(Boost_USE_MULTITHREADED ON) -find_package(Boost 1.66 +find_package(Boost 1.70 COMPONENTS atomic chrono date_time diff --git a/Dockerfile b/Dockerfile index ee3842047a6c053c953949dfde24be296eebefaa..b87283c0754c74aa414310ecbcf587662dbc049b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,12 +4,12 @@ ENV LIBSODIUM_VERSION=1.0.16 ENV SPDLOG_VERSION=1.1.0 ENV OLM_VERSION=2.2.2 ENV NLOHMANN_VERSION=v3.2.0 -ENV CMAKE_VERSION=3.12.1 -ENV CMAKE_SHORT_VERSION=3.12 +ENV CMAKE_VERSION=3.15.5 +ENV CMAKE_SHORT_VERSION=3.15 RUN \ apt-get update -qq && \ - apt-get install -y --no-install-recommends apt-transport-https software-properties-common curl && \ + apt-get install -y --no-install-recommends apt-transport-https software-properties-common curl ninja-build && \ # cmake curl https://cmake.org/files/v${CMAKE_SHORT_VERSION}/cmake-${CMAKE_VERSION}-Linux-x86_64.sh -o cmake-install.sh && \ bash cmake-install.sh --skip-license --prefix=/usr/local && \ @@ -52,8 +52,8 @@ RUN \ cmake --build build --target install && \ # boost mkdir -p /build/boost && cd /build/boost && \ - curl -L https://dl.bintray.com/boostorg/release/1.68.0/source/boost_1_68_0.tar.gz -o boost_1_68_0.tar.gz && \ - tar xfz boost_1_68_0.tar.gz && cd /build/boost/boost_1_68_0/ && \ + curl -L https://dl.bintray.com/boostorg/release/1.70.0/source/boost_1_70_0.tar.gz -o boost_1_70_0.tar.gz && \ + tar xfz boost_1_70_0.tar.gz && cd /build/boost/boost_1_70_0/ && \ ./bootstrap.sh --with-libraries=random,thread,system,iostreams,atomic,chrono,date_time,regex && \ ./b2 -d0 cxxstd=14 variant=release link=static threading=multi --layout=system && \ ./b2 -d0 install && \ diff --git a/README.md b/README.md index 3a5a4b86339e88211cb91e7b3e1d942e8a3ad807..2f0ac223b31c1a832571eb298eb6eb5141d55379 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ Client API library for the Matrix protocol, built on top of Boost.Asio. ### Dependencies -- Boost 1.66 (includes Boost.Beast) +- Boost 1.70 (includes Boost.Beast and makes the strand interface usable) - OpenSSL - C++ 14 compiler -- CMake 3.1 or greater +- CMake 3.15 or greater (lower versions can work, but they tend to mess up linking the right boost libraries) - Google Test (for testing) - libsodium 1.0.14 or greater diff --git a/appveyor.yml b/appveyor.yml index 780d8d7727724eb96999851c490d4498d4285f45..bce5d3447615689ef0acf3973a3faabcd4be56e8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ --- -version: 0.2.0-{build} +version: 0.3.0-{build} configuration: Release image: Visual Studio 2017 @@ -30,6 +30,7 @@ install: openssl:%PLATFORM%-windows spdlog:%PLATFORM%-windows zlib:%PLATFORM%-windows + - vcpkg upgrade --no-dry-run build_script: - cmake --version diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 1644d66fd84f5be02a3c6a424c870a9c7e0ec34d..7d930075a014fd44da2501f2c0d86553d5a2ad9c 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -40,9 +40,9 @@ if(USE_BUNDLED_BOOST) endif() set(BOOST_URL - https://dl.bintray.com/boostorg/release/1.69.0/source/boost_1_69_0.tar.bz2) + https://dl.bintray.com/boostorg/release/1.70.0/source/boost_1_70_0.tar.bz2) set(BOOST_SHA256 - 8f32d4617390d1c2d16f26a27ab60d97807b35440d45891fa340fc2648b04406) + 430ae8354789de4fd19ee52f3b1f739e1fba576f0aded0897c3c2bc00fb38778) set(GTEST_URL https://github.com/google/googletest/archive/release-1.8.0.tar.gz) set(GTEST_SHA1 e7e646a6204638fe8e87e165292b8dd9cd4c36ed) diff --git a/include/mtx/common.hpp b/include/mtx/common.hpp index 6dbb8038768736273110c85f9eeee7cd1f763960..37c6c95028fa09cccd9ca26bd3f1d12d272d846a 100644 --- a/include/mtx/common.hpp +++ b/include/mtx/common.hpp @@ -54,5 +54,46 @@ from_json(const json &obj, DeviceKeys &res); void to_json(json &obj, const DeviceKeys &res); +struct JWK +{ + //! Required. Key type. Must be oct. + std::string kty; + //! Required. Key operations. Must at least contain encrypt and decrypt. + std::vector<std::string> key_ops; + //! Required. Algorithm. Must be A256CTR. + std::string alg; + //! Required. The key, encoded as urlsafe unpadded base64. + std::string k; + //! Required. Extractable. Must be true. This is a W3C extension. + bool ext; +}; + +void +from_json(const json &obj, JWK &res); + +void +to_json(json &obj, const JWK &res); + +struct EncryptedFile +{ + //! Required. The URL to the file. + std::string url; + //! Required. A JSON Web Key object. (The encryption key) + JWK key; + //! Required. The Initialisation Vector used by AES-CTR, encoded as unpadded base64. + std::string iv; + //! Required. A map from an algorithm name to a hash of the ciphertext, encoded as unpadded + //! base64. Clients should support the SHA-256 hash, which uses the key sha256. + std::map<std::string, std::string> hashes; + //! Required. Version of the encrypted attachments protocol. Must be v2. + std::string v; +}; + +void +from_json(const json &obj, EncryptedFile &res); + +void +to_json(json &obj, const EncryptedFile &res); + } // namespace crypto } // namespace mtx diff --git a/include/mtx/events/common.hpp b/include/mtx/events/common.hpp index e98f86da969f337f7abc90b3d686250d948d708c..9daf4fbb85c37f0e1cde55240d5ea475692e87cf 100644 --- a/include/mtx/events/common.hpp +++ b/include/mtx/events/common.hpp @@ -1,7 +1,10 @@ #pragma once +#include <boost/optional.hpp> #include <nlohmann/json.hpp> +#include "mtx/common.hpp" + using json = nlohmann::json; namespace mtx { @@ -47,6 +50,8 @@ struct ImageInfo std::string thumbnail_url; //! The mimetype of the image, `e.g. image/jpeg`. std::string mimetype; + //! Encryption members. If present, they replace thumbnail_url. + boost::optional<crypto::EncryptedFile> thumbnail_file; }; //! Deserialization method needed by @p nlohmann::json. @@ -68,6 +73,8 @@ struct FileInfo std::string thumbnail_url; //! The mimetype of the file e.g `application/pdf`. std::string mimetype; + //! Encryption members. If present, they replace thumbnail_url. + boost::optional<crypto::EncryptedFile> thumbnail_file; }; //! Deserialization method needed by @p nlohmann::json. @@ -114,6 +121,8 @@ struct VideoInfo std::string thumbnail_url; //! Metadata about the image referred to in @p thumbnail_url. ThumbnailInfo thumbnail_info; + //! Encryption members. If present, they replace thumbnail_url. + boost::optional<crypto::EncryptedFile> thumbnail_file; }; //! Deserialization method needed by @p nlohmann::json. diff --git a/include/mtx/events/messages/audio.hpp b/include/mtx/events/messages/audio.hpp index f671a38571db4455d4172cc5f78135b96629ed0d..8451f2cb80b28f6ca8a07abb6a8f7f2ab195eaec 100644 --- a/include/mtx/events/messages/audio.hpp +++ b/include/mtx/events/messages/audio.hpp @@ -1,8 +1,10 @@ #pragma once +#include <boost/optional.hpp> #include <nlohmann/json.hpp> #include <string> +#include "mtx/common.hpp" #include "mtx/events/common.hpp" using json = nlohmann::json; @@ -24,6 +26,8 @@ struct Audio std::string url; // Metadata for the audio clip referred to in url. common::AudioInfo info; + // Encryption members. If present, they replace url. + boost::optional<crypto::EncryptedFile> file; }; void diff --git a/include/mtx/events/messages/file.hpp b/include/mtx/events/messages/file.hpp index 9db6730925c4aa6df7c8df82904198df8c31881c..e871951a0aa2dc384968683bd959b0d6c87798b7 100644 --- a/include/mtx/events/messages/file.hpp +++ b/include/mtx/events/messages/file.hpp @@ -3,6 +3,9 @@ #include <nlohmann/json.hpp> #include <string> +#include <boost/optional.hpp> + +#include "mtx/common.hpp" #include "mtx/events/common.hpp" using json = nlohmann::json; @@ -27,6 +30,8 @@ struct File std::string url; // Information about the file referred to in the url. common::FileInfo info; + // Encryption members. If present, they replace url. + boost::optional<crypto::EncryptedFile> file; }; void diff --git a/include/mtx/events/messages/image.hpp b/include/mtx/events/messages/image.hpp index a42a9c368eee59f5b7ca3fd540700ca77e6fb66b..baf7484c4e13294ac5f382a3f93a88a757eaf61a 100644 --- a/include/mtx/events/messages/image.hpp +++ b/include/mtx/events/messages/image.hpp @@ -1,8 +1,10 @@ #pragma once +#include <boost/optional.hpp> #include <nlohmann/json.hpp> #include <string> +#include "mtx/common.hpp" #include "mtx/events/common.hpp" using json = nlohmann::json; @@ -25,6 +27,8 @@ struct Image std::string url; // Metadata about the image referred to in `url`. common::ImageInfo info; + // Encryption members. If present, they replace url. + boost::optional<crypto::EncryptedFile> file; }; struct StickerImage @@ -37,6 +41,8 @@ struct StickerImage std::string url; // Metadata about the image referred to in `url`. common::ImageInfo info; + // Encryption members. If present, they replace url. + boost::optional<crypto::EncryptedFile> file; }; void diff --git a/include/mtx/events/messages/video.hpp b/include/mtx/events/messages/video.hpp index 2231de964965d5ff7d272182127433a4af63bcdb..965a403a03dc621cf84019ce31bdad85dd9321a3 100644 --- a/include/mtx/events/messages/video.hpp +++ b/include/mtx/events/messages/video.hpp @@ -1,8 +1,10 @@ #pragma once +#include <boost/optional.hpp> #include <nlohmann/json.hpp> #include <string> +#include "mtx/common.hpp" #include "mtx/events/common.hpp" using json = nlohmann::json; @@ -24,6 +26,8 @@ struct Video std::string url; // Metadata for the video clip referred to in url. common::VideoInfo info; + // Encryption members. If present, they replace url. + boost::optional<crypto::EncryptedFile> file; }; void diff --git a/include/mtxclient/crypto/client.hpp b/include/mtxclient/crypto/client.hpp index 7b7197299a58d52a98b9234716971cc1a33b48be..cb626241a49615485a9c069e97e5df29525a0543 100644 --- a/include/mtxclient/crypto/client.hpp +++ b/include/mtxclient/crypto/client.hpp @@ -55,19 +55,6 @@ private: std::string msg_; }; -class sodium_exception : public std::exception -{ -public: - sodium_exception(std::string func, const char *msg) - : msg_(func + ": " + std::string(msg)) - {} - - virtual const char *what() const throw() { return msg_.c_str(); } - -private: - std::string msg_; -}; - template<class T> std::string pickle(typename T::olm_type *object, const std::string &key) @@ -244,12 +231,6 @@ encrypt_exported_sessions(const mtx::crypto::ExportedSessionKeys &keys, std::str 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); diff --git a/include/mtxclient/crypto/utils.hpp b/include/mtxclient/crypto/utils.hpp index f8af46f7556f5b72bc31c78aa3a5a697f40bbefd..fc81969c486042be0b5b8da530839cebd741c2ba 100644 --- a/include/mtxclient/crypto/utils.hpp +++ b/include/mtxclient/crypto/utils.hpp @@ -1,20 +1,28 @@ #pragma once -#include <algorithm> #include <string> #include <vector> -#include <openssl/aes.h> -#include <openssl/evp.h> -#include <openssl/hmac.h> -#include <openssl/sha.h> - #include <sodium.h> #include <boost/algorithm/string.hpp> +#include "mtx/common.hpp" + namespace mtx { namespace crypto { +class sodium_exception : public std::exception +{ +public: + sodium_exception(std::string func, const char *msg) + : msg_(func + ": " + std::string(msg)) + {} + + virtual const char *what() const throw() { return msg_.c_str(); } + +private: + std::string msg_; +}; //! Data representation used to interact with libolm. using BinaryBuf = std::vector<uint8_t>; @@ -32,6 +40,19 @@ create_buffer(std::size_t nbytes) return buf; } +inline BinaryBuf +to_binary_buf(const std::string &str) +{ + return BinaryBuf(reinterpret_cast<const uint8_t *>(str.data()), + reinterpret_cast<const uint8_t *>(str.data()) + str.size()); +} + +inline std::string +to_string(const BinaryBuf &buf) +{ + return std::string(reinterpret_cast<const char *>(buf.data()), buf.size()); +} + //! Simple wrapper around the OpenSSL PKCS5_PBKDF2_HMAC function BinaryBuf PBKDF2_HMAC_SHA_512(const std::string pass, const BinaryBuf salt, uint32_t iterations); @@ -45,6 +66,18 @@ AES_CTR_256_Decrypt(const std::string ciphertext, const BinaryBuf aes256Key, Bin BinaryBuf HMAC_SHA256(const BinaryBuf hmacKey, const BinaryBuf data); +std::string +sha256(const std::string &data); + +//! Decrypt matrix EncryptedFile +BinaryBuf +decrypt_file(const std::string &ciphertext, const mtx::crypto::EncryptedFile &encryption_info); + +//! Encrypt matrix EncryptedFile +// Remember to set the url member of the EncryptedFile struct! +std::pair<BinaryBuf, mtx::crypto::EncryptedFile> +encrypt_file(const std::string &plaintext); + //! Translates the data back into the binary buffer, taking care //! to remove the header and footer elements. std::string @@ -63,5 +96,23 @@ uint32_to_uint8(uint8_t b[4], uint32_t u32); void print_binary_buf(const BinaryBuf buf); +std::string +base642bin(const std::string &b64); + +std::string +bin2base64(const std::string &bin); + +std::string +base642bin_unpadded(const std::string &b64); + +std::string +bin2base64_unpadded(const std::string &bin); + +std::string +base642bin_urlsafe_unpadded(const std::string &b64); + +std::string +bin2base64_urlsafe_unpadded(const std::string &bin); + } // namespace crypto -} // namespace mtx \ No newline at end of file +} // namespace mtx diff --git a/lib/crypto/client.cpp b/lib/crypto/client.cpp index ac180273a7e428bd13883bfce8418a8195a6e2bd..6f60d5c316f36be7e87a9d1b628022b0df0ff670 100644 --- a/lib/crypto/client.cpp +++ b/lib/crypto/client.cpp @@ -1,5 +1,8 @@ #include <iostream> +#include <openssl/aes.h> +#include <openssl/sha.h> + #include "mtxclient/crypto/client.hpp" #include "mtxclient/crypto/types.hpp" #include "mtxclient/crypto/utils.hpp" @@ -669,50 +672,6 @@ mtx::crypto::decrypt_exported_sessions(const std::string &data, std::string pass return json::parse(plaintext); } -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 sodium_exception{"sodium_base642bin", "encoding 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) { diff --git a/lib/crypto/utils.cpp b/lib/crypto/utils.cpp index 2833835395df8c9ce962369de9109d3a220c6041..cd5b79e26f7425a3dabf8b50d3e83e8fcee73878 100644 --- a/lib/crypto/utils.cpp +++ b/lib/crypto/utils.cpp @@ -1,5 +1,12 @@ #include "mtxclient/crypto/utils.hpp" +#include <openssl/aes.h> +#include <openssl/evp.h> +#include <openssl/hmac.h> +#include <openssl/sha.h> + +#include <algorithm> +#include <iomanip> #include <iostream> namespace mtx { @@ -32,7 +39,8 @@ AES_CTR_256_Encrypt(const std::string plaintext, const BinaryBuf aes256Key, Bina int ciphertext_len; - BinaryBuf encrypted = create_buffer(plaintext.size()); + // The ciphertext expand up to block size, which is 128 for AES256 + BinaryBuf encrypted = create_buffer(plaintext.size() + AES_BLOCK_SIZE); uint8_t *iv_data = iv.data(); // need to set bit 63 to 0 @@ -67,6 +75,7 @@ AES_CTR_256_Encrypt(const std::string plaintext, const BinaryBuf aes256Key, Bina } ciphertext_len += len; + encrypted.resize(ciphertext_len); /* Clean up */ EVP_CIPHER_CTX_free(ctx); @@ -118,6 +127,7 @@ AES_CTR_256_Decrypt(const std::string ciphertext, const BinaryBuf aes256Key, Bin // handleErrors(); } plaintext_len += len; + decrypted.resize(plaintext_len); /* Clean up */ EVP_CIPHER_CTX_free(ctx); @@ -125,6 +135,98 @@ AES_CTR_256_Decrypt(const std::string ciphertext, const BinaryBuf aes256Key, Bin return decrypted; } +std::string +sha256(const std::string &data) +{ + bool success = false; + std::string hashed; + +#if OPENSSL_VERSION_NUMBER < 0x10100000L + EVP_MD_CTX *context = EVP_MD_CTX_create(); +#else + EVP_MD_CTX *context = EVP_MD_CTX_new(); +#endif + + if (context != NULL) { + if (EVP_DigestInit_ex(context, EVP_sha256(), NULL)) { + if (EVP_DigestUpdate(context, data.c_str(), data.length())) { + unsigned char hash[EVP_MAX_MD_SIZE]; + unsigned int lengthOfHash = 0; + + if (EVP_DigestFinal_ex(context, hash, &lengthOfHash)) { + hashed = std::string(hash, hash + lengthOfHash); + success = true; + } + } + } + +#if OPENSSL_VERSION_NUMBER < 0x10100000L + EVP_MD_CTX_destroy(context); +#else + EVP_MD_CTX_free(context); +#endif + } + + if (success) + return hashed; + throw std::runtime_error("sha256 failed!"); +} + +BinaryBuf +decrypt_file(const std::string &ciphertext, const mtx::crypto::EncryptedFile &encryption_info) +{ + if (encryption_info.v != "v2") + throw std::invalid_argument("Unsupported encrypted file version"); + + if (encryption_info.key.kty != "oct") + throw std::invalid_argument("Unsupported key type"); + + if (encryption_info.key.alg != "A256CTR") + throw std::invalid_argument("Unsupported algorithm"); + + // Be careful, the key should be urlsafe and unpadded, the iv and sha only need to + // be unpadded + if (bin2base64_unpadded(sha256(ciphertext)) != encryption_info.hashes.at("sha256")) + throw std::invalid_argument( + "sha256 of encrypted file does not match the ciphertext, expected '" + + bin2base64_unpadded(sha256(ciphertext)) + "', got '" + + encryption_info.hashes.at("sha256") + "'"); + + return AES_CTR_256_Decrypt( + ciphertext, + to_binary_buf(base642bin_urlsafe_unpadded(encryption_info.key.k)), + to_binary_buf(base642bin_unpadded(encryption_info.iv))); +} + +std::pair<BinaryBuf, mtx::crypto::EncryptedFile> +encrypt_file(const std::string &plaintext) +{ + mtx::crypto::EncryptedFile encryption_info; + + // not sure if 16 bytes would be enough, 32 seems to be safe though + BinaryBuf key = create_buffer(32); + BinaryBuf iv = create_buffer(32); + + BinaryBuf cyphertext = AES_CTR_256_Encrypt(plaintext, key, iv); + + // Be careful, the key should be urlsafe and unpadded, the iv and sha only need to + // be unpadded + JWK web_key; + web_key.ext = true; + web_key.kty = "oct"; + web_key.key_ops = {"encrypt", "decrypt"}; + web_key.alg = "A256CTR"; + web_key.k = bin2base64_urlsafe_unpadded(to_string(key)); + web_key.ext = true; + + encryption_info.key = web_key; + encryption_info.iv = bin2base64_unpadded(to_string(iv)); + encryption_info.hashes["sha256"] = bin2base64_unpadded(sha256(to_string(cyphertext))); + encryption_info.v = "v2"; + + return std::make_pair(cyphertext, encryption_info); +} + template<typename T> void remove_substrs(std::basic_string<T> &s, const std::basic_string<T> &p) @@ -182,5 +284,135 @@ uint32_to_uint8(uint8_t b[4], uint32_t u32) b[0] = (uint8_t)(u32 >>= 8); } +std::string +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 sodium_exception{"sodium_base642bin", "encoding 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 +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); +} +std::string +base642bin_unpadded(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_NO_PADDING); + if (rc != 0) + throw sodium_exception{"sodium_base642bin", "encoding 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 +bin2base64_unpadded(const std::string &bin) +{ + auto base64buf = create_buffer( + sodium_base64_encoded_len(bin.size(), sodium_base64_VARIANT_ORIGINAL_NO_PADDING)); + + sodium_bin2base64(reinterpret_cast<char *>(base64buf.data()), + base64buf.size(), + reinterpret_cast<const unsigned char *>(bin.data()), + bin.size(), + sodium_base64_VARIANT_ORIGINAL_NO_PADDING); + + // Removing the null byte. + return std::string(base64buf.begin(), base64buf.end() - 1); +} +std::string +base642bin_urlsafe_unpadded(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_URLSAFE_NO_PADDING); + if (rc != 0) + throw sodium_exception{"sodium_base642bin", "encoding 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 +bin2base64_urlsafe_unpadded(const std::string &bin) +{ + auto base64buf = create_buffer( + sodium_base64_encoded_len(bin.size(), sodium_base64_VARIANT_URLSAFE_NO_PADDING)); + + sodium_bin2base64(reinterpret_cast<char *>(base64buf.data()), + base64buf.size(), + reinterpret_cast<const unsigned char *>(bin.data()), + bin.size(), + sodium_base64_VARIANT_URLSAFE_NO_PADDING); + + // Removing the null byte. + return std::string(base64buf.begin(), base64buf.end() - 1); +} + } // namespace crypto } // namespace mtx diff --git a/lib/structs/common.cpp b/lib/structs/common.cpp index 6a1870a0d8236752b59ae4066ce10492add3c3be..20601e9ad8e8d761eb7dcfb3fba172832256eb77 100644 --- a/lib/structs/common.cpp +++ b/lib/structs/common.cpp @@ -44,5 +44,45 @@ to_json(json &obj, const DeviceKeys &res) if (!res.unsigned_info.device_display_name.empty()) obj["unsigned"] = res.unsigned_info; } + +void +from_json(const json &obj, JWK &res) +{ + res.kty = obj.at("kty").get<std::string>(); + res.key_ops = obj.at("key_ops").get<std::vector<std::string>>(); + res.alg = obj.at("alg").get<std::string>(); + res.k = obj.at("k").get<std::string>(); + res.ext = obj.at("ext").get<bool>(); +} + +void +to_json(json &obj, const JWK &res) +{ + obj["kty"] = res.kty; + obj["key_ops"] = res.key_ops; + obj["alg"] = res.alg; + obj["k"] = res.k; + obj["ext"] = res.ext; +} + +void +from_json(const json &obj, EncryptedFile &res) +{ + res.url = obj.at("url").get<std::string>(); + res.key = obj.at("key").get<JWK>(); + res.iv = obj.at("iv").get<std::string>(); + res.hashes = obj.at("hashes").get<std::map<std::string, std::string>>(); + res.v = obj.at("v").get<std::string>(); +} + +void +to_json(json &obj, const EncryptedFile &res) +{ + obj["url"] = res.url; + obj["key"] = res.key; + obj["iv"] = res.iv; + obj["hashes"] = res.hashes; + obj["v"] = res.v; +} } } diff --git a/lib/structs/events/common.cpp b/lib/structs/events/common.cpp index 6f36b59506a71a6004498e7f79ac41901bdf29ec..a1d1b60d18e0f766abdd996f0133b71b444e3aa2 100644 --- a/lib/structs/events/common.cpp +++ b/lib/structs/events/common.cpp @@ -52,6 +52,9 @@ from_json(const json &obj, ImageInfo &info) if (obj.find("thumbnail_info") != obj.end()) info.thumbnail_info = obj.at("thumbnail_info").get<ThumbnailInfo>(); + + if (obj.find("thumbnail_file") != obj.end()) + info.thumbnail_file = obj.at("thumbnail_file").get<crypto::EncryptedFile>(); } void @@ -63,6 +66,8 @@ to_json(json &obj, const ImageInfo &info) obj["mimetype"] = info.mimetype; obj["thumbnail_url"] = info.thumbnail_url; obj["thumbnail_info"] = info.thumbnail_info; + if (info.thumbnail_file) + obj["thumbnail_file"] = info.thumbnail_file.value(); } void @@ -79,6 +84,9 @@ from_json(const json &obj, FileInfo &info) if (obj.find("thumbnail_info") != obj.end()) info.thumbnail_info = obj.at("thumbnail_info").get<ThumbnailInfo>(); + + if (obj.find("thumbnail_file") != obj.end()) + info.thumbnail_file = obj.at("thumbnail_file").get<crypto::EncryptedFile>(); } void @@ -88,6 +96,8 @@ to_json(json &obj, const FileInfo &info) obj["mimetype"] = info.mimetype; obj["thumbnail_url"] = info.thumbnail_url; obj["thumbnail_info"] = info.thumbnail_info; + if (info.thumbnail_file) + obj["thumbnail_file"] = info.thumbnail_file.value(); } void @@ -134,6 +144,9 @@ from_json(const json &obj, VideoInfo &info) if (obj.find("thumbnail_info") != obj.end()) info.thumbnail_info = obj.at("thumbnail_info").get<ThumbnailInfo>(); + + if (obj.find("thumbnail_file") != obj.end()) + info.thumbnail_file = obj.at("thumbnail_file").get<crypto::EncryptedFile>(); } void @@ -146,6 +159,8 @@ to_json(json &obj, const VideoInfo &info) obj["thumbnail_url"] = info.thumbnail_url; obj["thumbnail_info"] = info.thumbnail_info; obj["mimetype"] = info.mimetype; + if (info.thumbnail_file) + obj["thumbnail_file"] = info.thumbnail_file.value(); } void diff --git a/lib/structs/events/messages/audio.cpp b/lib/structs/events/messages/audio.cpp index a2cd2269aec984d49f998af7d707ef1d566cde00..e425a590e2c9d358cd5cfe20e626d03572ba9fc4 100644 --- a/lib/structs/events/messages/audio.cpp +++ b/lib/structs/events/messages/audio.cpp @@ -22,6 +22,9 @@ from_json(const json &obj, Audio &content) if (obj.find("info") != obj.end()) content.info = obj.at("info").get<common::AudioInfo>(); + + if (obj.find("file") != obj.end()) + content.file = obj.at("file").get<crypto::EncryptedFile>(); } void @@ -31,6 +34,9 @@ to_json(json &obj, const Audio &content) obj["body"] = content.body; obj["url"] = content.url; obj["info"] = content.info; + + if (content.file) + obj["file"] = content.file.value(); } } // namespace msg diff --git a/lib/structs/events/messages/file.cpp b/lib/structs/events/messages/file.cpp index add735a9cb0dbf11b367b57157d39b6da242bab9..dd36030cd52ca4f238b3cff4f6a342481c26cf81 100644 --- a/lib/structs/events/messages/file.cpp +++ b/lib/structs/events/messages/file.cpp @@ -25,6 +25,9 @@ from_json(const json &obj, File &content) if (obj.find("info") != obj.end()) content.info = obj.at("info").get<common::FileInfo>(); + + if (obj.find("file") != obj.end()) + content.file = obj.at("file").get<crypto::EncryptedFile>(); } void @@ -35,6 +38,9 @@ to_json(json &obj, const File &content) obj["filename"] = content.filename; obj["url"] = content.url; obj["info"] = content.info; + + if (content.file) + obj["file"] = content.file.value(); } } // namespace msg diff --git a/lib/structs/events/messages/image.cpp b/lib/structs/events/messages/image.cpp index f0e5200738994cac464853dc803fc5afde9a2270..c586d38be2fd6e96fb9b7b99940dbca938bda7a6 100644 --- a/lib/structs/events/messages/image.cpp +++ b/lib/structs/events/messages/image.cpp @@ -22,6 +22,9 @@ from_json(const json &obj, Image &content) if (obj.find("info") != obj.end()) content.info = obj.at("info").get<common::ImageInfo>(); + + if (obj.find("file") != obj.end()) + content.file = obj.at("file").get<crypto::EncryptedFile>(); } void @@ -31,6 +34,9 @@ to_json(json &obj, const Image &content) obj["body"] = content.body; obj["url"] = content.url; obj["info"] = content.info; + + if (content.file) + obj["file"] = content.file.value(); } void @@ -43,6 +49,9 @@ from_json(const json &obj, StickerImage &content) if (obj.find("info") != obj.end()) content.info = obj.at("info").get<common::ImageInfo>(); + + if (obj.find("file") != obj.end()) + content.file = obj.at("file").get<crypto::EncryptedFile>(); } void @@ -51,6 +60,9 @@ to_json(json &obj, const StickerImage &content) obj["body"] = content.body; obj["url"] = content.url; obj["info"] = content.info; + + if (content.file) + obj["file"] = content.file.value(); } } // namespace msg diff --git a/lib/structs/events/messages/video.cpp b/lib/structs/events/messages/video.cpp index bac206acc245ee6e41025e37b681f0aedd225f37..0d4749a6d54e1b63b2f2d41972f7dfa940a9da43 100644 --- a/lib/structs/events/messages/video.cpp +++ b/lib/structs/events/messages/video.cpp @@ -23,6 +23,9 @@ from_json(const json &obj, Video &content) if (obj.find("info") != obj.end()) content.info = obj.at("info").get<common::VideoInfo>(); + + if (obj.find("file") != obj.end()) + content.file = obj.at("file").get<crypto::EncryptedFile>(); } void @@ -32,6 +35,9 @@ to_json(json &obj, const Video &content) obj["body"] = content.body; obj["url"] = content.url; obj["info"] = content.info; + + if (content.file) + obj["file"] = content.file.value(); } } // namespace msg diff --git a/tests/crypto.cpp b/tests/crypto.cpp index 2c4871ffe36ad93380fd405539af2813e0bd701a..d9cd67c0aa1f22df5be2acd31c0769fdce58c36a 100644 --- a/tests/crypto.cpp +++ b/tests/crypto.cpp @@ -62,3 +62,34 @@ TEST(Crypto, DeviceKeys) EXPECT_EQ(device2.unsigned_info.device_display_name, "Alice's mobile phone"); } + +TEST(Crypto, EncryptedFile) +{ + json j = R"({ + "url": "mxc://example.org/FHyPlCeYUSFFxlgbQYZmoEoe", + "v": "v2", + "key": { + "alg": "A256CTR", + "ext": true, + "k": "aWF6-32KGYaC3A_FEUCk1Bt0JA37zP0wrStgmdCaW-0", + "key_ops": ["encrypt","decrypt"], + "kty": "oct" + }, + "iv": "w+sE15fzSc0AAAAAAAAAAA", + "hashes": { + "sha256": "fdSLu/YkRx3Wyh3KQabP3rd6+SFiKg5lsJZQHtkSAYA" + }})"_json; + + EncryptedFile file = j; + // json j2 = file; + + // EXPECT_EQ(j, j2); + EXPECT_EQ(file.v, "v2"); + EXPECT_EQ(file.iv, "w+sE15fzSc0AAAAAAAAAAA"); + EXPECT_EQ(file.hashes.at("sha256"), "fdSLu/YkRx3Wyh3KQabP3rd6+SFiKg5lsJZQHtkSAYA"); + EXPECT_EQ(file.key.alg, "A256CTR"); + EXPECT_EQ(file.key.ext, true); + EXPECT_EQ(file.key.k, "aWF6-32KGYaC3A_FEUCk1Bt0JA37zP0wrStgmdCaW-0"); + EXPECT_EQ(file.key.key_ops.size(), 2); + EXPECT_EQ(file.key.kty, "oct"); +} diff --git a/tests/e2ee.cpp b/tests/e2ee.cpp index dd0aa9ed8bb323f78ee4d4e9ff55351775322ddd..885789b096aa8230efde2b478c67a81b10e1b667 100644 --- a/tests/e2ee.cpp +++ b/tests/e2ee.cpp @@ -24,6 +24,8 @@ using namespace mtx::responses; using namespace std; +using namespace nlohmann; + struct OlmCipherContent { std::string body; @@ -1130,6 +1132,60 @@ TEST(ExportSessions, InboundMegolmSessions) ASSERT_EQ(restored_output_str, secret_message); } +TEST(Encryption, EncryptedFile) +{ + std::string plaintext = "This is some plain text payload"; + auto encryption_data = mtx::crypto::encrypt_file(plaintext); + ASSERT_NE(plaintext, mtx::crypto::to_string(encryption_data.first)); + ASSERT_EQ(plaintext, + mtx::crypto::to_string(mtx::crypto::decrypt_file( + mtx::crypto::to_string(encryption_data.first), encryption_data.second))); + + json j = R"({ + "type": "m.room.message", + "content": { + "body": "test.txt", + "info": { + "size": 8, + "mimetype": "text/plain" + }, + "msgtype": "m.file", + "file": { + "v": "v2", + "key": { + "alg": "A256CTR", + "ext": true, + "k": "6osKLzUKV1YZ06WEX0b77D784Te8oAj5eNU-gAgkjs4", + "key_ops": [ + "encrypt", + "decrypt" + ], + "kty": "oct" + }, + "iv": "7zRP/t89YWcAAAAAAAAAAA", + "hashes": { + "sha256": "5g41hn7n10sCw3+2j7CQ9SJl6R/v5EBT4MshdFgHhzo" + }, + "url": "mxc://neko.dev/WPKoOAPfPlcHiZZTEoaIoZhN", + "mimetype": "text/plain" + } + }, + "event_id": "$1575320135447DEPky:neko.dev", + "origin_server_ts": 1575320135324, + "sender": "@test:neko.dev", + "unsigned": { + "age": 1081, + "transaction_id": "m1575320142400.8" + }, + "room_id": "!YnUlhwgbBaGcAFsJOJ:neko.dev" +})"_json; + mtx::events::RoomEvent<mtx::events::msg::File> ev = j; + + ASSERT_EQ("abcdefg\n", + mtx::crypto::to_string(mtx::crypto::decrypt_file("=\xFDX\xAB\xCA\xEB\x8F\xFF", + ev.content.file.value()))); +} + TEST(Encryption, DISABLED_HandleRoomKeyEvent) {} TEST(Encryption, DISABLED_HandleRoomKeyRequestEvent) {} TEST(Encryption, DISABLED_HandleNewDevices) {} diff --git a/tests/messages.cpp b/tests/messages.cpp index a8d4eb1115fdce86750e24966aad90db71ef16bc..23b5f334ec53c4eda4cbd7f52e330b9efa7575a8 100644 --- a/tests/messages.cpp +++ b/tests/messages.cpp @@ -142,6 +142,86 @@ TEST(RoomEvents, FileMessage) EXPECT_EQ(event.content.info.size, 40565); } +TEST(RoomEvents, EncryptedImageMessage) +{ + json data = R"( +{ + "content": { + "body": "something-important.jpg", + "file": { + "url": "mxc://example.org/FHyPlCeYUSFFxlgbQYZmoEoe", + "mimetype": "image/jpeg", + "v": "v2", + "key": { + "alg": "A256CTR", + "ext": true, + "k": "aWF6-32KGYaC3A_FEUCk1Bt0JA37zP0wrStgmdCaW-0", + "key_ops": ["encrypt","decrypt"], + "kty": "oct" + }, + "iv": "w+sE15fzSc0AAAAAAAAAAA", + "hashes": { + "sha256": "fdSLu/YkRx3Wyh3KQabP3rd6+SFiKg5lsJZQHtkSAYA" + } + }, + "info": { + "mimetype": "image/jpeg", + "h": 1536, + "size": 422018, + "thumbnail_file": { + "hashes": { + "sha256": "/NogKqW5bz/m8xHgFiH5haFGjCNVmUIPLzfvOhHdrxY" + }, + "iv": "U+k7PfwLr6UAAAAAAAAAAA", + "key": { + "alg": "A256CTR", + "ext": true, + "k": "RMyd6zhlbifsACM1DXkCbioZ2u0SywGljTH8JmGcylg", + "key_ops": ["encrypt", "decrypt"], + "kty": "oct" + }, + "mimetype": "image/jpeg", + "url": "mxc://example.org/pmVJxyxGlmxHposwVSlOaEOv", + "v": "v2" + }, + "thumbnail_info": { + "h": 768, + "mimetype": "image/jpeg", + "size": 211009, + "w": 432 + }, + "w": 864 + }, + "msgtype": "m.image" + }, + "event_id": "$143273582443PhrSn:example.org", + "origin_server_ts": 1432735824653, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "sender": "@example:example.org", + "type": "m.room.message", + "unsigned": { + "age": 1234 + } +})"_json; + RoomEvent<msg::Image> event = data; + + EXPECT_EQ(event.type, EventType::RoomMessage); + EXPECT_EQ(event.event_id, "$143273582443PhrSn:example.org"); + EXPECT_EQ(event.room_id, "!jEsUZKDJdhlrceRyVU:example.org"); + EXPECT_EQ(event.sender, "@example:example.org"); + EXPECT_EQ(event.origin_server_ts, 1432735824653L); + EXPECT_EQ(event.unsigned_data.age, 1234); + + EXPECT_EQ(event.content.body, "something-important.jpg"); + EXPECT_EQ(event.content.msgtype, "m.image"); + EXPECT_EQ(event.content.url, ""); + EXPECT_EQ(event.content.info.mimetype, "image/jpeg"); + EXPECT_EQ(event.content.info.size, 422018); + EXPECT_EQ(event.content.file.value().url, "mxc://example.org/FHyPlCeYUSFFxlgbQYZmoEoe"); + EXPECT_EQ(event.content.info.thumbnail_file.value().url, + "mxc://example.org/pmVJxyxGlmxHposwVSlOaEOv"); +} + TEST(RoomEvents, ImageMessage) { json data = R"({