From 6c87fbd6fdc1ab2c267b30ef1f7ffd77ef057a07 Mon Sep 17 00:00:00 2001
From: Nicolas Werner <nicolas.werner@hotmail.de>
Date: Fri, 8 Oct 2021 14:47:12 +0200
Subject: [PATCH] Add online key backup bootstrapping

---
 include/mtx/responses/common.hpp  |  9 ++++
 include/mtxclient/http/client.hpp |  4 ++
 lib/crypto/client.cpp             | 21 +++++++++
 lib/http/client.cpp               | 10 +++++
 lib/structs/responses/common.cpp  |  6 +++
 tests/crypto.cpp                  | 25 +++++++++++
 tests/e2ee.cpp                    | 73 +++++++++++++++++++++++++++++--
 7 files changed, 145 insertions(+), 3 deletions(-)

diff --git a/include/mtx/responses/common.hpp b/include/mtx/responses/common.hpp
index 753991bc3..fdf7b3951 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/http/client.hpp b/include/mtxclient/http/client.hpp
index 4a4dba843..9c25ac2c5 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;
@@ -623,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,
diff --git a/lib/crypto/client.cpp b/lib/crypto/client.cpp
index fe55f43ec..0511179e7 100644
--- a/lib/crypto/client.cpp
+++ b/lib/crypto/client.cpp
@@ -259,6 +259,27 @@ OlmClient::create_crosssigning_keys()
     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)
 {
diff --git a/lib/http/client.cpp b/lib/http/client.cpp
index 13731ea37..d03ffb0f0 100644
--- a/lib/http/client.cpp
+++ b/lib/http/client.cpp
@@ -1193,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)
 {
diff --git a/lib/structs/responses/common.cpp b/lib/structs/responses/common.cpp
index 49a4e107f..366815f7d 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 6420bb060..b47923b3c 100644
--- a/tests/crypto.cpp
+++ b/tests/crypto.cpp
@@ -395,3 +395,28 @@ TEST(SecretStorage, CreateSecretKey)
     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 c06e2d7a9..ae2201827 100644
--- a/tests/e2ee.cpp
+++ b/tests/e2ee.cpp
@@ -495,7 +495,6 @@ TEST(Encryption, UploadCrossSigningKeys)
         EXPECT_EQ(res.one_time_key_counts.size(), 0);
     });
 
-    atomic<bool> done = false;
     auto xsign_keys   = olm_account->create_crosssigning_keys();
     ASSERT_TRUE(xsign_keys.has_value());
     mtx::requests::DeviceSigningUpload u;
@@ -519,9 +518,77 @@ TEST(Encryption, UploadCrossSigningKeys)
           auth.content         = pass;
           h.next(auth);
       }),
-      [&done](RequestErr e) {
-          done = true;
+      [](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();
-- 
GitLab