diff --git a/include/mtx/requests.hpp b/include/mtx/requests.hpp index 0177e43d901ec865085ccca46a2592d823f75aef..9c392958faea1047f9a6af1db7a628f0b9889f67 100644 --- a/include/mtx/requests.hpp +++ b/include/mtx/requests.hpp @@ -281,6 +281,22 @@ struct KeySignaturesUpload void to_json(json &obj, const KeySignaturesUpload &req); +//! Upload cross signing keys +struct DeviceSigningUpload +{ + //! Optional. The user's master key. + std::optional<mtx::crypto::CrossSigningKeys> master_key; + //! Optional. The user's self-signing key. Must be signed by the accompanying master key, or by + //! the user's most recently uploaded master key if no master key is included in the request. + std::optional<mtx::crypto::CrossSigningKeys> self_signing_key; + //! Optional. The user's user-signing key. Must be signed by the accompanying master key, or by + //! the user's most recently uploaded master key if no master key is included in the request. + std::optional<mtx::crypto::CrossSigningKeys> user_signing_key; +}; + +void +to_json(json &obj, const DeviceSigningUpload &req); + struct PusherData { //! Required if `kind` is http. The URL to use to send notifications to. diff --git a/include/mtx/responses/common.hpp b/include/mtx/responses/common.hpp index 753991bc32187c0da9b4bac277a971b304670b1c..fdf7b395122d674ef5364c606714a6deb71bd2d0 100644 --- a/include/mtx/responses/common.hpp +++ b/include/mtx/responses/common.hpp @@ -57,6 +57,15 @@ struct FilterId void from_json(const nlohmann::json &obj, FilterId &response); +//! A new room version as returned by the room_keys/version API +struct Version +{ + //! Required: The backup version. This is an opaque string. + std::string version; +}; + +void +from_json(const nlohmann::json &obj, Version &response); //! Different helper for parsing responses. namespace utils { //! Multiple account_data events. diff --git a/include/mtxclient/crypto/client.hpp b/include/mtxclient/crypto/client.hpp index 2542cae12bfa2774437993ef3e569c1ff652bed8..eb4ff14e49e7364e286d2f2b044492095bbb6fb0 100644 --- a/include/mtxclient/crypto/client.hpp +++ b/include/mtxclient/crypto/client.hpp @@ -188,15 +188,22 @@ struct PkSigning { //! Construct from base64 key static PkSigning from_seed(std::string seed); + //! construct a new random key + static PkSigning new_key(); + + //! sign an arbitrary message std::string sign(const std::string &message); //! base64 public key std::string public_key() const { return public_key_; } + //! base64 private key (seed) + std::string seed() const { return seed_; } private: PkSigning() {} std::unique_ptr<OlmPkSigning, OlmDeleter> signing; - std::string public_key_; + std::string public_key_; // base64 + std::string seed_; // base64 }; //! Client for all the cryptography related functionality like olm accounts, session keys @@ -216,6 +223,35 @@ public: //! A signed set of one time keys indexed by `<algorithm>:<key_id>`. using SignedOneTimeKeys = std::map<std::string, requests::SignedOneTimeKey>; + //! Data needed for bootstrapping crosssigning + struct CrossSigningSetup + { + //! The public key objects, signed and ready for upload. + CrossSigningKeys master_key, user_signing_key, self_signing_key; + //! The private keys to store in SSSS + std::string private_master_key, private_user_signing_key, + private_self_signing_key; // base64 + }; + + //! Data needed to setup the online key backup + struct OnlineKeyBackupSetup + { + //! private key to decrypt sessions with. + mtx::crypto::BinaryBuf privateKey; + //! The backup version data including auth data to be sent to the server. + mtx::responses::backup::BackupVersion backupVersion; + }; + //! Data needed to setup SSSS + struct SSSSSetup + { + //! Key to encrypt/decrypt secrets with. + mtx::crypto::BinaryBuf privateKey; + //! The key description to be stored in account data. + mtx::secret_storage::AesHmacSha2KeyDescription keyDescription; + //! The name of this key. + std::string key_name; + }; + //! Set the id of this device. void set_device_id(std::string device_id) { device_id_ = std::move(device_id); } //! Set the id of this user. @@ -259,6 +295,15 @@ public: //! Prepare an empty /keys/upload request. mtx::requests::UploadKeys create_upload_keys_request(); + //! Create the cross-signing keys (including signatures). Needs to be uploaded to the server + //! after this. + std::optional<CrossSigningSetup> create_crosssigning_keys(); + + //! Create a new online key backup. Needs to be uploaded to the server after this. + std::optional<OnlineKeyBackupSetup> create_online_key_backup(const std::string &masterKey); + //! Create a new SSSS storage key. Should be uploaded to account_data. The password is optional. + static std::optional<SSSSSetup> create_ssss_key(const std::string &password = ""); + //! Decrypt a message using megolm. GroupPlaintext decrypt_group_message(OlmInboundGroupSession *session, const std::string &message, diff --git a/include/mtxclient/crypto/utils.hpp b/include/mtxclient/crypto/utils.hpp index 7f0bca7d73722eece1df9df7e4ef2c9c105874a6..868c2581074a3a23f4cf258ee1e2811faee7a015 100644 --- a/include/mtxclient/crypto/utils.hpp +++ b/include/mtxclient/crypto/utils.hpp @@ -53,6 +53,14 @@ to_string(const BinaryBuf &buf) return std::string(reinterpret_cast<const char *>(buf.data()), buf.size()); } +//! Sets bit 63 to 0 to be compatible with other AES implementations. +BinaryBuf +compatible_iv(BinaryBuf incompatible_iv); + +//! encodes a recovery key in base58 with parity and version tag, +std::string +key_to_recoverykey(const BinaryBuf &key); + //! Simple wrapper around the OpenSSL PKCS5_PBKDF2_HMAC function BinaryBuf PBKDF2_HMAC_SHA_512(const std::string pass, @@ -77,6 +85,10 @@ decrypt(const mtx::secret_storage::AesHmacSha2EncryptedData &data, BinaryBuf decryptionKey, const std::string key_name); +//! Encrypt a secret for SSSS +mtx::secret_storage::AesHmacSha2EncryptedData +encrypt(const std::string &data, BinaryBuf decryptionKey, const std::string key_name); + //! HKDF key derivation with SHA256 digest struct HkdfKeys { diff --git a/include/mtxclient/http/client.hpp b/include/mtxclient/http/client.hpp index 94fae087d57268fd3b0c8a1448517b9ebb89d0a9..042e0a97c9d3bdddfbb1a5d77cb1765aa4b8f2ef 100644 --- a/include/mtxclient/http/client.hpp +++ b/include/mtxclient/http/client.hpp @@ -58,6 +58,7 @@ struct CreateRoom; struct EventId; struct RoomId; struct FilterId; +struct Version; struct GroupId; struct GroupProfile; struct JoinedGroups; @@ -117,6 +118,30 @@ template<class Response> using HeadersCallback = std::function<void(const Response &, HeaderFields, RequestErr)>; using TypeErasedCallback = std::function<void(HeaderFields, const std::string_view &, int, int)>; +//! A helper to handle user interactive authentication. This will cache the request and call the +//! prompt every time there is a new stage. Advance the flow by calling next(). +class UIAHandler +{ +public: + //! The callback for when a new UIA stage needs to be completed + using UIAPrompt = + std::function<void(const UIAHandler &, const user_interactive::Unauthorized &)>; + + //! Create a new UIA handler. Pass a callback for when a new stage needs to be completed. + UIAHandler(UIAPrompt prompt_) + : prompt(std::move(prompt_)) + {} + + void next(const user_interactive::Auth &auth) const; + +private: + UIAPrompt prompt; + + std::function<void(const UIAHandler &, const nlohmann::json &)> next_; + + friend class Client; +}; + //! Sync configuration options. struct SyncOpts { @@ -242,6 +267,13 @@ public: const user_interactive::Auth &auth, Callback<mtx::responses::Register> cb); + //! Register with an UIA handler so you don't need to repeat the request manually. + //! register failed with 401 + void registration(const std::string &user, + const std::string &pass, + UIAHandler uia_handler, + Callback<mtx::responses::Register> cb); + //! Check the validity of a registration token void registration_token_validity(const std::string token, Callback<mtx::responses::RegistrationTokenValidity> cb); @@ -564,6 +596,11 @@ public: void keys_signatures_upload(const mtx::requests::KeySignaturesUpload &req, Callback<mtx::responses::KeySignaturesUpload> cb); + //! Upload cross signing keys + void device_signing_upload(const mtx::requests::DeviceSigningUpload, + UIAHandler uia_handler, + ErrCallback cb); + //! Returns the current devices and identity keys for the given users. void query_keys(const mtx::requests::QueryKeys &req, Callback<mtx::responses::QueryKeys> cb); @@ -587,6 +624,9 @@ public: void update_backup_version(const std::string &version, const mtx::responses::backup::BackupVersion &data, ErrCallback cb); + void post_backup_version(const std::string &algorithm, + const std::string &auth_data, + Callback<mtx::responses::Version> cb); void room_keys(const std::string &version, Callback<mtx::responses::backup::KeysBackup> cb); void room_keys(const std::string &version, @@ -620,6 +660,18 @@ public: void secret_storage_key(const std::string &key_id, Callback<mtx::secret_storage::AesHmacSha2KeyDescription> cb); + //! Upload a specific secret + void upload_secret_storage_secret(const std::string &secret_id, + const mtx::secret_storage::Secret &secret, + ErrCallback cb); + //! Upload information about a key + void upload_secret_storage_key(const std::string &key_id, + const mtx::secret_storage::AesHmacSha2KeyDescription &desc, + ErrCallback cb); + + //! Set the default key for the secret storage + void set_secret_storage_default_key(const std::string &key_id, ErrCallback cb); + //! Gets any TURN server URIs and authentication credentials void get_turn_server(Callback<mtx::responses::TurnServer> cb); diff --git a/lib/crypto/client.cpp b/lib/crypto/client.cpp index 10db860ddba1e91f59844ff179e478cf20cdea54..668ae0f4766b93efb9ae76a07c2e6e3ecacd09fd 100644 --- a/lib/crypto/client.cpp +++ b/lib/crypto/client.cpp @@ -206,6 +206,113 @@ OlmClient::create_upload_keys_request(const mtx::crypto::OneTimeKeys &one_time_k return req; } +std::optional<OlmClient::CrossSigningSetup> +OlmClient::create_crosssigning_keys() +{ + auto master = PkSigning::new_key(); + auto user_signing = PkSigning::new_key(); + auto self_signing = PkSigning::new_key(); + + CrossSigningSetup setup{}; + setup.private_master_key = master.seed(); + setup.private_user_signing_key = user_signing.seed(); + setup.private_self_signing_key = self_signing.seed(); + + // master key + setup.master_key.usage = {"master"}; + setup.master_key.user_id = user_id_; + setup.master_key.keys["ed25519:" + master.public_key()] = master.public_key(); + + nlohmann::json master_j = setup.master_key; + master_j.erase("unsigned"); + master_j.erase("signatures"); + setup.master_key.signatures[user_id_]["ed25519:" + master.public_key()] = + master.sign(master_j.dump()); + setup.master_key.signatures[user_id_]["ed25519:" + device_id_] = sign_message(master_j.dump()); + + // user_signing_key + setup.user_signing_key.usage = {"user_signing"}; + setup.user_signing_key.user_id = user_id_; + setup.user_signing_key.keys["ed25519:" + user_signing.public_key()] = user_signing.public_key(); + + nlohmann::json user_signing_j = setup.user_signing_key; + user_signing_j.erase("unsigned"); + user_signing_j.erase("signatures"); + setup.user_signing_key.signatures[user_id_]["ed25519:" + user_signing.public_key()] = + user_signing.sign(user_signing_j.dump()); + setup.user_signing_key.signatures[user_id_]["ed25519:" + master.public_key()] = + master.sign(user_signing_j.dump()); + + // self_signing_key + setup.self_signing_key.usage = {"self_signing"}; + setup.self_signing_key.user_id = user_id_; + setup.self_signing_key.keys["ed25519:" + self_signing.public_key()] = self_signing.public_key(); + + nlohmann::json self_signing_j = setup.self_signing_key; + self_signing_j.erase("unsigned"); + self_signing_j.erase("signatures"); + setup.self_signing_key.signatures[user_id_]["ed25519:" + self_signing.public_key()] = + self_signing.sign(self_signing_j.dump()); + setup.self_signing_key.signatures[user_id_]["ed25519:" + master.public_key()] = + master.sign(self_signing_j.dump()); + + return setup; +} + +std::optional<OlmClient::OnlineKeyBackupSetup> +OlmClient::create_online_key_backup(const std::string &masterKey) +{ + OnlineKeyBackupSetup setup{}; + + auto key = create_buffer(olm_pk_private_key_length()); + setup.privateKey = key; + + json auth_data; + auth_data["public_key"] = bin2base64_unpadded(CURVE25519_public_key_from_private(key)); + auto master = PkSigning::from_seed(masterKey); + + auto sig = master.sign(auth_data.dump()); + auth_data["signatures"][user_id_]["ed25519:" + master.public_key()] = sig; + + setup.backupVersion.auth_data = auth_data.dump(); + setup.backupVersion.algorithm = "m.megolm_backup.v1.curve25519-aes-sha2"; + + return setup; +} + +std::optional<OlmClient::SSSSSetup> +OlmClient::create_ssss_key(const std::string &password) +{ + OlmClient::SSSSSetup setup{}; + + if (password.empty()) { + setup.privateKey = create_buffer(32); + } else { + mtx::secret_storage::PBKDF2 pbkdf2{}; + pbkdf2.algorithm = "m.pbkdf2"; + pbkdf2.iterations = 500'000; + pbkdf2.bits = 256; // 32 * 8 + pbkdf2.salt = bin2base64(to_string(create_buffer(32))); + + setup.privateKey = mtx::crypto::PBKDF2_HMAC_SHA_512( + password, to_binary_buf(pbkdf2.salt), pbkdf2.iterations, pbkdf2.bits / 8); + setup.keyDescription.passphrase = pbkdf2; + } + + setup.keyDescription.algorithm = "m.secret_storage.v1.aes-hmac-sha2"; + setup.keyDescription.name = bin2base58(to_string(create_buffer(16))); // create a random name + setup.keyDescription.iv = bin2base64(to_string(compatible_iv(create_buffer(32)))); + + auto testKeys = HKDF_SHA256(setup.privateKey, BinaryBuf(32, 0), BinaryBuf{}); + + auto encrypted = AES_CTR_256_Encrypt( + std::string(32, '\0'), testKeys.aes, to_binary_buf(base642bin(setup.keyDescription.iv))); + + setup.keyDescription.mac = bin2base64(to_string(HMAC_SHA256(testKeys.mac, encrypted))); + + return setup; +} + OutboundGroupSessionPtr OlmClient::init_outbound_group_session() { @@ -561,10 +668,18 @@ SAS::calculate_mac(std::string input_data, std::string info) return to_string(output_buffer); } +PkSigning +PkSigning::new_key() +{ + auto priv_seed = bin2base64(to_string(create_buffer(olm_pk_signing_seed_length()))); + return from_seed(priv_seed); +} + PkSigning PkSigning::from_seed(std::string seed) { PkSigning s{}; + s.seed_ = seed; s.signing = create_olm_object<PkSigningObject>(); auto seed_ = base642bin(seed); diff --git a/lib/crypto/encoding.cpp b/lib/crypto/encoding.cpp index 5589a8343914a8c7aa2b0f79e8a488042c378205..727bc669e84cc9f78a9b284a25a3dd897b278e63 100644 --- a/lib/crypto/encoding.cpp +++ b/lib/crypto/encoding.cpp @@ -1,5 +1,6 @@ #include <algorithm> #include <array> +#include <cassert> #include <string> #include <vector> @@ -60,23 +61,25 @@ encode_base58(const std::array<char, 58> &alphabet, const std::string &input) if (input.empty()) return ""; - std::vector<uint8_t> digits(input.size() * 137 / 100 + 1); + std::vector<uint8_t> digits(input.size() * 138 / 100 + 1); std::size_t digitslen = 1; - for (uint32_t carry : input) { + for (uint8_t carry_ : input) { + uint32_t carry = static_cast<uint32_t>(carry_); for (size_t j = 0; j < digitslen; j++) { - carry += (uint32_t)(digits[j]) << 8; + carry += (uint32_t)(digits[j]) * 256; digits[j] = static_cast<uint8_t>(carry % 58); carry /= 58; } while (carry > 0) { + assert(digitslen < digits.size()); digits[digitslen++] = static_cast<uint8_t>(carry % 58); carry /= 58; } } - std::size_t resultlen = 0; std::string result(digits.size(), ' '); // leading zero bytes + std::size_t resultlen = 0; for (; resultlen < input.length() && input[resultlen] == 0;) result[resultlen++] = '1'; diff --git a/lib/crypto/utils.cpp b/lib/crypto/utils.cpp index 96ca7a567a39e0cc8b547e0b83f61f7117388694..7457779147968bdffeb89f78afdbf17c1d80f59a 100644 --- a/lib/crypto/utils.cpp +++ b/lib/crypto/utils.cpp @@ -108,6 +108,22 @@ key_from_recoverykey(const std::string &recoverykey, return decryptionKey; } +std::string +key_to_recoverykey(const BinaryBuf &key) +{ + auto buf = BinaryBuf(key.size() + 3); + buf[0] = 0x8b; + buf[1] = 0x01; + std::copy(begin(key), end(key), begin(buf) + 2); + + uint8_t parity = buf[0] ^ buf[1]; + for (uint8_t b : key) + parity ^= b; + buf.back() = parity; + + return bin2base58(to_string(buf)); +}; + std::string decrypt(const mtx::secret_storage::AesHmacSha2EncryptedData &data, BinaryBuf decryptionKey, @@ -126,6 +142,22 @@ decrypt(const mtx::secret_storage::AesHmacSha2EncryptedData &data, return to_string(decryptedSecret); } +mtx::secret_storage::AesHmacSha2EncryptedData +encrypt(const std::string &data, BinaryBuf decryptionKey, const std::string key_name) +{ + mtx::secret_storage::AesHmacSha2EncryptedData encrypted{}; + auto iv = compatible_iv(create_buffer(16)); + encrypted.iv = bin2base64(to_string(iv)); + + auto keys = HKDF_SHA256(decryptionKey, BinaryBuf(32, 0), to_binary_buf(key_name)); + + auto ciphertext = AES_CTR_256_Encrypt(data, keys.aes, iv); + encrypted.ciphertext = bin2base64(to_string(ciphertext)); + encrypted.mac = bin2base64(to_string(HMAC_SHA256(keys.mac, ciphertext))); + + return encrypted; +} + HkdfKeys HKDF_SHA256(const BinaryBuf &key, const BinaryBuf &salt, const BinaryBuf &info) { @@ -170,6 +202,19 @@ HKDF_SHA256(const BinaryBuf &key, const BinaryBuf &salt, const BinaryBuf &info) return {std::move(buf), std::move(macKey)}; } +BinaryBuf +compatible_iv(BinaryBuf incompatible_iv) +{ + // need to set bit 63 to 0 + // Element and everyone else seems to be counting bytes from the back, i.e. iv_data[15] is + // the last byte. So we need to clear byte 15 - 63%8 = 15 - 7 = 8, the highest bit, 1 << 7 + // see: + // https://github.com/matrix-org/matrix-js-sdk/blob/529fe93ab14b93c515e9ab0d0277c1942a5d73c5/src/crypto/aes.ts#L144 + uint8_t *data = incompatible_iv.data(); + data[15 - 63 % 8] &= ~(1UL << (63 / 8)); + return incompatible_iv; +} + BinaryBuf AES_CTR_256_Encrypt(const std::string plaintext, const BinaryBuf aes256Key, BinaryBuf iv) { @@ -180,23 +225,14 @@ AES_CTR_256_Encrypt(const std::string plaintext, const BinaryBuf aes256Key, Bina int ciphertext_len; // 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 - // Element and everyone else seems to be counting bytes from the back, i.e. iv_data[15] is - // the last byte. So we need to clear byte 15 - 63%8 = 15 - 7 = 8, the highest bit, 1 << 7 - // see: - // https://github.com/matrix-org/matrix-js-sdk/blob/529fe93ab14b93c515e9ab0d0277c1942a5d73c5/src/crypto/aes.ts#L144 - iv_data[15 - 63 % 8] &= ~(1UL << (63 / 8)); - //*iv_data &= ~(1UL << (63)); + BinaryBuf encrypted = compatible_iv(create_buffer(plaintext.size() + AES_BLOCK_SIZE)); /* Create and initialise the context */ if (!(ctx = EVP_CIPHER_CTX_new())) { // handleErrors(); } - if (1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, aes256Key.data(), iv_data)) { + if (1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, aes256Key.data(), iv.data())) { // handleErrors(); } diff --git a/lib/http/client.cpp b/lib/http/client.cpp index beb9a58680a9442b13d990fe36b6f57160fbaf39..0d55eb2539a2d784be7d78fbe13c5fe5611ba522 100644 --- a/lib/http/client.cpp +++ b/lib/http/client.cpp @@ -23,6 +23,11 @@ struct ClientPrivate coeurl::Client client; }; +void +UIAHandler::next(const user_interactive::Auth &auth) const +{ + next_(*this, auth); +} } Client::Client(const std::string &server, uint16_t port) @@ -895,6 +900,34 @@ Client::registration(const std::string &user, post<nlohmann::json, mtx::responses::Register>("/client/r0/register", req, callback, false); } +void +Client::registration(const std::string &user, + const std::string &pass, + UIAHandler uia_handler, + Callback<mtx::responses::Register> cb) +{ + nlohmann::json req = {{"username", user}, {"password", pass}}; + + uia_handler.next_ = [this, req, cb](const UIAHandler &h, const nlohmann::json &auth) { + auto request = req; + if (!auth.empty()) + request["auth"] = auth; + + post<nlohmann::json, mtx::responses::Register>( + "/client/r0/register", + request, + [cb, h](auto &r, RequestErr e) { + if (e && e->status_code == 401) + h.prompt(h, e->matrix_error.unauthorized); + else + cb(r, e); + }, + false); + }; + + uia_handler.next_(uia_handler, {}); +} + void Client::registration_token_validity(const std::string token, Callback<mtx::responses::RegistrationTokenValidity> cb) @@ -1069,6 +1102,30 @@ Client::keys_signatures_upload(const mtx::requests::KeySignaturesUpload &req, "/client/unstable/keys/signatures/upload", req, cb); } +void +Client::device_signing_upload(const mtx::requests::DeviceSigningUpload deviceKeys, + UIAHandler uia_handler, + ErrCallback cb) +{ + nlohmann::json req = deviceKeys; + + uia_handler.next_ = [this, req, cb](const UIAHandler &h, const nlohmann::json &auth) { + auto request = req; + if (!auth.empty()) + request["auth"] = auth; + + post<nlohmann::json, mtx::responses::Empty>( + "/client/unstable/keys/device_signing/upload", request, [cb, h](auto &, RequestErr e) { + if (e && e->status_code == 401 && !e->matrix_error.unauthorized.flows.empty()) + h.prompt(h, e->matrix_error.unauthorized); + else + cb(e); + }); + }; + + uia_handler.next_(uia_handler, {}); +} + void Client::query_keys(const mtx::requests::QueryKeys &req, Callback<mtx::responses::QueryKeys> callback) @@ -1136,6 +1193,16 @@ Client::update_backup_version(const std::string &version, put<mtx::responses::backup::BackupVersion>( "/client/r0/room_keys/version/" + mtx::client::utils::url_encode(version), data, cb); } + +void +Client::post_backup_version(const std::string &algorithm, + const std::string &auth_data, + Callback<mtx::responses::Version> cb) +{ + nlohmann::json req = {{"algorithm", algorithm}, + {"auth_data", nlohmann::json::parse(auth_data)}}; + post<nlohmann::json, mtx::responses::Version>("/client/r0/room_keys/version", req, cb); +} void Client::room_keys(const std::string &version, Callback<mtx::responses::backup::KeysBackup> cb) { @@ -1229,6 +1296,40 @@ Client::secret_storage_key(const std::string &key_id, RequestErr err) { cb(res, err); }); } +//! Upload a specific secret +void +Client::upload_secret_storage_secret(const std::string &secret_id, + const mtx::secret_storage::Secret &secret, + ErrCallback cb) +{ + put("/client/r0/user/" + mtx::client::utils::url_encode(user_id_.to_string()) + + "/account_data/" + mtx::client::utils::url_encode(secret_id), + secret, + cb); +} + +//! Upload information about a key +void +Client::upload_secret_storage_key(const std::string &key_id, + const mtx::secret_storage::AesHmacSha2KeyDescription &desc, + ErrCallback cb) +{ + put("/client/r0/user/" + mtx::client::utils::url_encode(user_id_.to_string()) + + "/account_data/m.secret_storage.key." + mtx::client::utils::url_encode(key_id), + desc, + cb); +} + +void +Client::set_secret_storage_default_key(const std::string &key_id, ErrCallback cb) +{ + nlohmann::json key = {{"key", key_id}}; + put("/client/r0/user/" + mtx::client::utils::url_encode(user_id_.to_string()) + + "/account_data/m.secret_storage.default_key", + key, + cb); +} + void Client::enable_encryption(const std::string &room, Callback<mtx::responses::EventId> callback) { diff --git a/lib/structs/requests.cpp b/lib/structs/requests.cpp index 6217dfdb8d09923a9bb344d11c8b7cff0ebb40aa..0c1e10935388eb61acf1714cb492b9a9d2b18070 100644 --- a/lib/structs/requests.cpp +++ b/lib/structs/requests.cpp @@ -196,6 +196,17 @@ to_json(json &obj, const KeySignaturesUpload &req) obj[user_id][key_id] = std::visit([](const auto &e) { return json(e); }, keyVar); } +void +to_json(json &obj, const DeviceSigningUpload &req) +{ + if (req.master_key) + obj["master_key"] = req.master_key.value(); + if (req.self_signing_key) + obj["self_signing_key"] = req.self_signing_key.value(); + if (req.user_signing_key) + obj["user_signing_key"] = req.user_signing_key.value(); +} + void to_json(json &obj, const PusherData &data) { diff --git a/lib/structs/responses/common.cpp b/lib/structs/responses/common.cpp index 49a4e107fc56559bc310f3c3d3077b2cab86e959..366815f7d49a815c5d557fc63d79872dbf773783 100644 --- a/lib/structs/responses/common.cpp +++ b/lib/structs/responses/common.cpp @@ -53,6 +53,12 @@ from_json(const nlohmann::json &obj, FilterId &response) response.filter_id = obj.at("filter_id"); } +void +from_json(const nlohmann::json &obj, Version &response) +{ + response.version = obj.at("version"); +} + namespace utils { void diff --git a/tests/crypto.cpp b/tests/crypto.cpp index 909c9a8ff72253779e407b4dff0160fc1d02ea17..b47923b3c91d13b0929e1a69bc1600ddffe611df 100644 --- a/tests/crypto.cpp +++ b/tests/crypto.cpp @@ -184,6 +184,7 @@ TEST(Base58, EncodingDecoding) EXPECT_EQ(bin2base58("foob"), "3csAg9"); EXPECT_EQ(bin2base58("fooba"), "CZJRhmz"); EXPECT_EQ(bin2base58("foobar"), "t1Zv2yaZ"); + EXPECT_FALSE(bin2base58(to_string(create_buffer(32))).empty()); EXPECT_EQ("", base582bin("")); EXPECT_EQ("f", base582bin("2m")); @@ -371,3 +372,51 @@ TEST(SecretStorage, SecretKey) ASSERT_EQ(desc.signatures["@alice:localhost"]["ed25519:adkfajfgaefkdahfzguerhtgduifghes"], "ksfjvkrfbnrtnwublrjkgnorthgnrdtjbiortbjdlbiutr"); } + +TEST(SecretStorage, CreateSecretKey) +{ + auto ssss1 = mtx::crypto::OlmClient::create_ssss_key(); + ASSERT_TRUE(ssss1.has_value()); + EXPECT_FALSE(ssss1->keyDescription.passphrase.has_value()); + EXPECT_EQ(ssss1->keyDescription.algorithm, "m.secret_storage.v1.aes-hmac-sha2"); + EXPECT_GE(ssss1->keyDescription.iv.length(), 32); + EXPECT_EQ((ssss1->keyDescription.mac.length() - 1) * 3 / 4, 32); + + EXPECT_EQ(key_from_recoverykey(key_to_recoverykey(ssss1->privateKey), ssss1->keyDescription), + ssss1->privateKey); + + auto ssss2 = mtx::crypto::OlmClient::create_ssss_key("some passphrase"); + ASSERT_TRUE(ssss2.has_value()); + ASSERT_TRUE(ssss2->keyDescription.passphrase.has_value()); + EXPECT_EQ(ssss2->keyDescription.algorithm, "m.secret_storage.v1.aes-hmac-sha2"); + EXPECT_GE(ssss2->keyDescription.iv.length(), 32); + EXPECT_EQ((ssss2->keyDescription.mac.length() - 1) * 3 / 4, 32); + + EXPECT_EQ(mtx::crypto::key_from_passphrase("some passphrase", ssss2->keyDescription), + ssss2->privateKey); +} + +TEST(SecretStorage, CreateOnlineKeyBackup) +{ + mtx::crypto::OlmClient account; + account.create_new_account(); + + auto cross = account.create_crosssigning_keys(); + ASSERT_TRUE(cross.has_value()); + + auto okb = account.create_online_key_backup(cross->private_master_key); + ASSERT_TRUE(okb.has_value()); + + mtx::responses::backup::SessionData s; + s.algorithm = mtx::crypto::MEGOLM_ALGO; + s.sender_key = "abc"; + s.session_key = "cde"; + + auto enc1 = + mtx::crypto::encrypt_session(s, json::parse(okb->backupVersion.auth_data)["public_key"]); + EXPECT_FALSE(enc1.ciphertext.empty()); + + auto enc2 = mtx::crypto::encrypt_session( + s, mtx::crypto::CURVE25519_public_key_from_private(okb->privateKey)); + EXPECT_FALSE(enc2.ciphertext.empty()); +} diff --git a/tests/e2ee.cpp b/tests/e2ee.cpp index 8aecd46c51a8ccc2650dc090efd6c17b4bf401dd..da89038908e7b8873c1a05cb688e170056801e17 100644 --- a/tests/e2ee.cpp +++ b/tests/e2ee.cpp @@ -463,6 +463,137 @@ TEST(Encryption, ClaimMultipleDeviceKeys) alice3->close(); } +TEST(Encryption, UploadCrossSigningKeys) +{ + auto alice = make_test_client(); + auto olm_account = std::make_shared<mtx::crypto::OlmClient>(); + + EXPECT_THROW(olm_account->identity_keys(), olm_exception); + + olm_account->create_new_account(); + + alice->login( + "alice", "secret", [](const mtx::responses::Login &, RequestErr err) { check_error(err); }); + + while (alice->access_token().empty()) + sleep(); + + olm_account->set_user_id(alice->user_id().to_string()); + olm_account->set_device_id(alice->device_id()); + + auto id_keys = olm_account->identity_keys(); + + ASSERT_TRUE(id_keys.curve25519.size() > 10); + ASSERT_TRUE(id_keys.curve25519.size() > 10); + + mtx::crypto::OneTimeKeys unused; + auto request = olm_account->create_upload_keys_request(unused); + + // Make the request with the signed identity keys. + alice->upload_keys(request, [](const mtx::responses::UploadKeys &res, RequestErr err) { + check_error(err); + EXPECT_EQ(res.one_time_key_counts.size(), 0); + }); + + auto xsign_keys = olm_account->create_crosssigning_keys(); + ASSERT_TRUE(xsign_keys.has_value()); + mtx::requests::DeviceSigningUpload u; + u.master_key = xsign_keys->master_key; + u.user_signing_key = xsign_keys->user_signing_key; + u.self_signing_key = xsign_keys->self_signing_key; + alice->device_signing_upload( + u, + mtx::http::UIAHandler([](const mtx::http::UIAHandler &h, + const mtx::user_interactive::Unauthorized &unauthorized) { + ASSERT_EQ(unauthorized.flows.size(), 1); + ASSERT_EQ(unauthorized.flows[0].stages.size(), 1); + ASSERT_EQ(unauthorized.flows[0].stages[0], mtx::user_interactive::auth_types::password); + + mtx::user_interactive::Auth auth; + auth.session = unauthorized.session; + mtx::user_interactive::auth::Password pass{}; + pass.password = "secret"; + pass.identifier_user = "alice"; + pass.identifier_type = mtx::user_interactive::auth::Password::IdType::UserId; + auth.content = pass; + h.next(auth); + }), + [](RequestErr e) { check_error(e); }); + + alice->close(); +} + +TEST(Encryption, UploadOnlineBackup) +{ + auto alice = make_test_client(); + auto olm_account = std::make_shared<mtx::crypto::OlmClient>(); + + EXPECT_THROW(olm_account->identity_keys(), olm_exception); + + olm_account->create_new_account(); + + alice->login( + "alice", "secret", [](const mtx::responses::Login &, RequestErr err) { check_error(err); }); + + while (alice->access_token().empty()) + sleep(); + + olm_account->set_user_id(alice->user_id().to_string()); + olm_account->set_device_id(alice->device_id()); + + auto id_keys = olm_account->identity_keys(); + + ASSERT_TRUE(id_keys.curve25519.size() > 10); + ASSERT_TRUE(id_keys.curve25519.size() > 10); + + mtx::crypto::OneTimeKeys unused; + auto request = olm_account->create_upload_keys_request(unused); + + // Make the request with the signed identity keys. + alice->upload_keys(request, [](const mtx::responses::UploadKeys &res, RequestErr err) { + check_error(err); + EXPECT_EQ(res.one_time_key_counts.size(), 0); + }); + + auto xsign_keys = olm_account->create_crosssigning_keys(); + ASSERT_TRUE(xsign_keys.has_value()); + mtx::requests::DeviceSigningUpload u; + u.master_key = xsign_keys->master_key; + u.user_signing_key = xsign_keys->user_signing_key; + u.self_signing_key = xsign_keys->self_signing_key; + alice->device_signing_upload( + u, + mtx::http::UIAHandler([](const mtx::http::UIAHandler &h, + const mtx::user_interactive::Unauthorized &unauthorized) { + ASSERT_EQ(unauthorized.flows.size(), 1); + ASSERT_EQ(unauthorized.flows[0].stages.size(), 1); + ASSERT_EQ(unauthorized.flows[0].stages[0], mtx::user_interactive::auth_types::password); + + mtx::user_interactive::Auth auth; + auth.session = unauthorized.session; + mtx::user_interactive::auth::Password pass{}; + pass.password = "secret"; + pass.identifier_user = "alice"; + pass.identifier_type = mtx::user_interactive::auth::Password::IdType::UserId; + auth.content = pass; + h.next(auth); + }), + [xsign_keys, olm_account, alice](RequestErr e) { + check_error(e); + + auto bk = olm_account->create_online_key_backup(xsign_keys->private_master_key); + alice->post_backup_version(bk->backupVersion.algorithm, + bk->backupVersion.auth_data, + [](const mtx::responses::Version &v, RequestErr e) { + check_error(e); + + EXPECT_FALSE(v.version.empty()); + }); + }); + + alice->close(); +} + TEST(Encryption, KeyChanges) { auto carl = make_test_client();