diff --git a/src/gui/application/gui_application.cpp b/src/gui/application/gui_application.cpp index e79c9b5..cffd195 100644 --- a/src/gui/application/gui_application.cpp +++ b/src/gui/application/gui_application.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -51,6 +52,15 @@ namespace { using namespace std::chrono_literals; +std::string CreatePasswordChangeRequestId(uint64_t sequence) { + const auto timestamp = std::chrono::system_clock::now() + .time_since_epoch() + .count(); + std::random_device random; + return std::to_string(timestamp) + "-" + std::to_string(random()) + "-" + + std::to_string(sequence); +} + #if defined(__linux__) && !defined(__APPLE__) bool HasNonEmptyEnvironmentVariable(const char* name) { const char* value = std::getenv(name); @@ -1356,28 +1366,58 @@ void GuiApplication::BindMainCallbacks() { if (password_change_pending_) { return true; } - request_id = std::to_string(++next_password_change_request_id_); + request_id = + CreatePasswordChangeRequestId(++next_password_change_request_id_); password_change_pending_ = true; password_change_result_ready_ = false; password_change_succeeded_ = false; + password_change_result_uncertain_ = false; password_change_requested_at_ = std::chrono::steady_clock::now(); pending_password_change_request_id_ = request_id; pending_local_password_ = password; password_change_error_.clear(); } + const bool self_hosted = config_center_->IsSelfHosted(); + const std::string server_host = + self_hosted ? config_center_->GetSignalServerHost() + : config_center_->GetDefaultServerHost(); + const int server_port = + self_hosted ? config_center_->GetSignalServerPort() + : config_center_->GetDefaultSignalServerPort(); + const std::string pending_identity = + std::string(client_id_) + "@" + password; + if (!settings_.StagePendingPasswordChange( + pending_identity, self_hosted, server_host, server_port, + request_id)) { + std::lock_guard lock(password_change_mutex_); + password_change_pending_ = false; + pending_password_change_request_id_.clear(); + pending_local_password_.clear(); + password_change_error_.clear(); + offline_warning_text_ = localization::failed[localization_language_index_]; + show_offline_warning_window_ = true; + LOG_ERROR("Could not durably stage password change"); + return true; + } + const nlohmann::json request = {{"type", "change_password"}, {"request_id", request_id}, {"new_password", password}}; const std::string message = request.dump(); if (SendSignalMessage(peer_, message.data(), message.size()) != 0) { - std::lock_guard lock(password_change_mutex_); - password_change_pending_ = false; - pending_password_change_request_id_.clear(); - pending_local_password_.clear(); - offline_warning_text_ = - localization::signal_disconnected[localization_language_index_]; - show_offline_warning_window_ = true; + { + std::lock_guard lock(password_change_mutex_); + password_change_pending_ = false; + pending_password_change_request_id_.clear(); + pending_local_password_.clear(); + offline_warning_text_ = + localization::signal_disconnected[localization_language_index_]; + show_offline_warning_window_ = true; + } + if (!settings_.ClearPendingPasswordChange()) { + LOG_WARN("Could not clear unsent pending password change"); + } } return true; }); @@ -1788,6 +1828,7 @@ void GuiApplication::Tick() { return; } HandlePasswordChangeResult(); + HandleCredentialRecovery(); if (!peer_) { CreateConnectionPeer(); } @@ -1864,7 +1905,7 @@ void GuiApplication::Tick() { void GuiApplication::HandlePasswordChangeResult() { bool succeeded = false; - std::string new_password; + bool uncertain = false; std::string error; { @@ -1874,6 +1915,7 @@ void GuiApplication::HandlePasswordChangeResult() { std::chrono::seconds(10)) { password_change_result_ready_ = true; password_change_succeeded_ = false; + password_change_result_uncertain_ = true; password_change_error_ = "Server did not respond"; } @@ -1882,47 +1924,43 @@ void GuiApplication::HandlePasswordChangeResult() { } succeeded = password_change_succeeded_; - new_password = pending_local_password_; + uncertain = password_change_result_uncertain_; error = password_change_error_; password_change_pending_ = false; password_change_result_ready_ = false; password_change_succeeded_ = false; + password_change_result_uncertain_ = false; pending_password_change_request_id_.clear(); pending_local_password_.clear(); password_change_error_.clear(); } if (!succeeded) { - LOG_WARN("Password change failed: {}", error); + if (uncertain) { + LOG_WARN("Password change outcome is unknown: {}; re-authenticating", + error); + } else { + LOG_WARN("Password change failed: {}", error); + if (!settings_.ClearPendingPasswordChange()) { + LOG_WARN("Could not clear rejected pending password change"); + } + } offline_warning_text_ = localization::failed[localization_language_index_]; if (!error.empty()) { offline_warning_text_ += ": " + error; } show_offline_warning_window_ = true; + if (uncertain && peer_) { + LeaveConnection(peer_, client_id_); + DestroyPeer(&peer_); + } return; } - std::memset(password_saved_, 0, sizeof(password_saved_)); - std::strncpy(password_saved_, new_password.c_str(), - sizeof(password_saved_) - 1); - - const std::string identity = std::string(client_id_) + "@" + new_password; - if (config_center_->IsSelfHosted()) { - std::memset(self_hosted_id_, 0, sizeof(self_hosted_id_)); - std::strncpy(self_hosted_id_, identity.c_str(), - sizeof(self_hosted_id_) - 1); - } else { - std::memset(client_id_with_password_, 0, - sizeof(client_id_with_password_)); - std::strncpy(client_id_with_password_, identity.c_str(), - sizeof(client_id_with_password_) - 1); - } - - if (settings_.Save() != 0) { - LOG_ERROR("Password changed on server but could not be saved locally"); - offline_warning_text_ = localization::failed[localization_language_index_]; - show_offline_warning_window_ = true; - return; + if (!settings_.PromotePendingPasswordChange()) { + // The pending credential was persisted before the request was sent, so it + // remains sufficient for recovery even when promotion cannot be written. + LOG_WARN("Password changed on server; pending recovery record retained"); } LOG_INFO("Password changed successfully for [{}]", client_id_); @@ -1932,6 +1970,53 @@ void GuiApplication::HandlePasswordChangeResult() { } } +void GuiApplication::HandleCredentialRecovery() { + bool retry_active = false; + bool promote_pending = false; + bool clear_pending = false; + { + std::lock_guard lock(password_change_mutex_); + retry_active = credential_recovery_retry_active_; + promote_pending = credential_recovery_promote_pending_; + clear_pending = credential_recovery_clear_pending_; + credential_recovery_retry_active_ = false; + credential_recovery_promote_pending_ = false; + credential_recovery_clear_pending_ = false; + if (retry_active) { + credential_recovery_attempt_pending_ = false; + } + } + + if (retry_active) { + LOG_INFO("Pending credential was not accepted; retrying active credential"); + if (peer_) { + DestroyPeer(&peer_); + } + return; + } + + if (promote_pending) { + if (!settings_.PromotePendingPasswordChange()) { + LOG_WARN("Recovered credential is active but promotion remains pending"); + } else { + LOG_INFO("Recovered password change with pending credential"); + } + } else if (clear_pending) { + if (!settings_.ClearPendingPasswordChange()) { + LOG_WARN("Active credential recovered but pending record could not be " + "cleared"); + } else { + LOG_INFO("Password change was not committed; retained active credential"); + } + } else { + return; + } + + std::lock_guard lock(password_change_mutex_); + credential_recovery_in_progress_ = false; + credential_recovery_attempt_pending_ = false; +} + void GuiApplication::UpdateLocalization() { const int language = localization::detail::ClampLanguageIndex(localization_language_index_); diff --git a/src/gui/application/gui_application.h b/src/gui/application/gui_application.h index ff4fadf..e2dbb0a 100644 --- a/src/gui/application/gui_application.h +++ b/src/gui/application/gui_application.h @@ -32,6 +32,7 @@ private: void BindServerCallbacks(); void Tick(); void HandlePasswordChangeResult(); + void HandleCredentialRecovery(); void SyncMainWindow(); void SyncConnectionDialog(); void SyncPlatformDialogs(); diff --git a/src/gui/features/settings/settings_manager.cpp b/src/gui/features/settings/settings_manager.cpp index 633a0e8..b41e795 100644 --- a/src/gui/features/settings/settings_manager.cpp +++ b/src/gui/features/settings/settings_manager.cpp @@ -1,11 +1,24 @@ #include "features/settings/settings_manager.h" +#include +#include +#include #include #include #include #include +#include #include #include +#include + +#if defined(_WIN32) +#include +#include +#else +#include +#include +#endif #include "localization.h" #include "rd_log.h" @@ -14,6 +27,9 @@ namespace crossdesk { namespace { +constexpr uint32_t kCacheV3Magic = 0x33444358; // "XCD3" +constexpr uint32_t kCacheV3Version = 3; + template void CopyString(char (&destination)[Size], const char *source) { static_assert(Size > 0); @@ -23,6 +39,99 @@ void CopyString(char (&destination)[Size], const char *source) { } } +uint32_t CacheChecksum(const void *data, size_t size) { + const auto *bytes = static_cast(data); + uint32_t hash = 2166136261u; + for (size_t i = 0; i < size; ++i) { + hash ^= bytes[i]; + hash *= 16777619u; + } + return hash; +} + +std::filesystem::path TemporaryPathFor( + const std::filesystem::path &target) { + const auto timestamp = std::chrono::steady_clock::now() + .time_since_epoch() + .count(); + const auto thread_id = + std::hash{}(std::this_thread::get_id()); + std::filesystem::path temporary = target; + temporary += ".tmp-" + std::to_string(timestamp) + "-" + + std::to_string(thread_id); + return temporary; +} + +bool FlushFileToDisk(FILE *file) { + if (!file || std::fflush(file) != 0) { + return false; + } +#if defined(_WIN32) + return _commit(_fileno(file)) == 0; +#else + return fsync(fileno(file)) == 0; +#endif +} + +bool ReplaceFileAtomically(const std::filesystem::path &temporary, + const std::filesystem::path &target) { +#if defined(_WIN32) + return MoveFileExW(temporary.c_str(), target.c_str(), + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; +#else + if (::rename(temporary.c_str(), target.c_str()) != 0) { + return false; + } + + const std::filesystem::path parent = target.parent_path().empty() + ? std::filesystem::path(".") + : target.parent_path(); + int directory_flags = O_RDONLY; +#if defined(O_DIRECTORY) + directory_flags |= O_DIRECTORY; +#endif + const int directory = open(parent.c_str(), directory_flags); + if (directory >= 0) { + const bool synced = fsync(directory) == 0; + close(directory); + return synced; + } + return false; +#endif +} + +bool WriteFileAtomically(const std::filesystem::path &target, + const void *data, size_t size) { + std::error_code ec; + const std::filesystem::path parent = target.parent_path(); + if (!parent.empty()) { + std::filesystem::create_directories(parent, ec); + if (ec) { + return false; + } + } + + const std::filesystem::path temporary = TemporaryPathFor(target); +#if defined(_WIN32) + FILE *file = _wfopen(temporary.c_str(), L"wb"); +#else + FILE *file = std::fopen(temporary.c_str(), "wb"); +#endif + if (!file) { + return false; + } + + const bool written = std::fwrite(data, 1, size, file) == size; + const bool flushed = written && FlushFileToDisk(file); + const bool closed = std::fclose(file) == 0; + if (!written || !flushed || !closed || + !ReplaceFileAtomically(temporary, target)) { + std::filesystem::remove(temporary, ec); + return false; + } + return true; +} + } // namespace SettingsManager::SettingsManager(GuiRuntime &owner) : owner_(owner) {} @@ -33,36 +142,74 @@ int SettingsManager::Save() { } int SettingsManager::SaveLocked() { - std::ofstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc", - std::ios::binary); - if (!cache_v2_file) { - return -1; - } - CopyString(cache_v2_.client_id_with_password, owner_.client_id_with_password_); std::memcpy(cache_v2_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_)); std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_)); CopyString(cache_v2_.self_hosted_id, owner_.self_hosted_id_); - cache_v2_file.write(reinterpret_cast(&cache_v2_), - sizeof(cache_v2_)); + cache_v3_.magic = kCacheV3Magic; + cache_v3_.version = kCacheV3Version; + cache_v3_.base = cache_v2_; + cache_v3_.pending_identity[sizeof(cache_v3_.pending_identity) - 1] = '\0'; + cache_v3_.pending_server_host[sizeof(cache_v3_.pending_server_host) - 1] = + '\0'; + cache_v3_.pending_request_id[sizeof(cache_v3_.pending_request_id) - 1] = + '\0'; + cache_v3_.checksum = + CacheChecksum(&cache_v3_, offsetof(CacheV3, checksum)); + + if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache_v3.enc", + &cache_v3_, sizeof(cache_v3_))) { + return -1; + } + + if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache_v2.enc", + &cache_v2_, sizeof(cache_v2_))) { + LOG_WARN("Failed to update legacy v2 credential cache"); + } // Keep writing the legacy cache while older installations may still read it. - std::ofstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc", - std::ios::binary); - if (cache_v1_file) { - CopyString(cache_v1_.client_id_with_password, - owner_.client_id_with_password_); - std::memcpy(cache_v1_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_)); - std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_)); - cache_v1_file.write(reinterpret_cast(&cache_v1_), - sizeof(cache_v1_)); + CopyString(cache_v1_.client_id_with_password, + owner_.client_id_with_password_); + std::memcpy(cache_v1_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_)); + std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_)); + if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache.enc", + &cache_v1_, sizeof(cache_v1_))) { + LOG_WARN("Failed to update legacy v1 credential cache"); } return 0; } +bool SettingsManager::ReadV3Locked() { + std::ifstream cache_file(owner_.cache_path_ + "/secure_cache_v3.enc", + std::ios::binary); + if (!cache_file) { + return false; + } + + CacheV3 loaded{}; + cache_file.read(reinterpret_cast(&loaded), sizeof(loaded)); + if (cache_file.gcount() != static_cast(sizeof(loaded)) || + loaded.magic != kCacheV3Magic || + loaded.version != kCacheV3Version || + loaded.checksum != CacheChecksum(&loaded, offsetof(CacheV3, checksum))) { + LOG_WARN("Ignore invalid v3 credential cache"); + return false; + } + + loaded.base.client_id_with_password + [sizeof(loaded.base.client_id_with_password) - 1] = '\0'; + loaded.base.self_hosted_id[sizeof(loaded.base.self_hosted_id) - 1] = '\0'; + loaded.pending_identity[sizeof(loaded.pending_identity) - 1] = '\0'; + loaded.pending_server_host[sizeof(loaded.pending_server_host) - 1] = '\0'; + loaded.pending_request_id[sizeof(loaded.pending_request_id) - 1] = '\0'; + cache_v3_ = loaded; + cache_v2_ = loaded.base; + return true; +} + bool SettingsManager::ReadV2Locked() { std::ifstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc", std::ios::binary); @@ -84,13 +231,20 @@ bool SettingsManager::ReadV2Locked() { int SettingsManager::Load() { std::unique_lock lock(cache_mutex_); - if (ReadV2Locked()) { + const bool loaded_v3 = ReadV3Locked(); + if (loaded_v3 || ReadV2Locked()) { CopyString(owner_.client_id_with_password_, cache_v2_.client_id_with_password); CopyString(owner_.self_hosted_id_, cache_v2_.self_hosted_id); std::memcpy(owner_.aes128_key_, cache_v2_.key, sizeof(cache_v2_.key)); std::memcpy(owner_.aes128_iv_, cache_v2_.iv, sizeof(cache_v2_.iv)); - LOG_INFO("Load settings from v2 cache file"); + if (loaded_v3) { + LOG_INFO("Load settings from v3 cache file"); + } else { + cache_v3_ = {}; + SaveLocked(); + LOG_INFO("Migrated settings from v2 to v3 cache file"); + } } else { std::ifstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc", std::ios::binary); @@ -128,8 +282,9 @@ int SettingsManager::Load() { std::memcpy(owner_.aes128_key_, cache_v1_.key, sizeof(cache_v1_.key)); std::memcpy(owner_.aes128_iv_, cache_v1_.iv, sizeof(cache_v1_.iv)); + cache_v3_ = {}; SaveLocked(); - LOG_INFO("Migrated settings from v1 to v2 cache file"); + LOG_INFO("Migrated settings from v1 to v3 cache file"); } lock.unlock(); @@ -186,7 +341,8 @@ int SettingsManager::Load() { bool SettingsManager::LoadCachedSelfHostedIdentity() { std::lock_guard lock(cache_mutex_); - if (!ReadV2Locked() || cache_v2_.self_hosted_id[0] == '\0') { + if ((!ReadV3Locked() && !ReadV2Locked()) || + cache_v2_.self_hosted_id[0] == '\0') { std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_)); std::memset(owner_.client_id_, 0, sizeof(owner_.client_id_)); std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_)); @@ -227,27 +383,135 @@ bool SettingsManager::ActivateCachedPublicIdentity() { return owner_.client_id_[0] != '\0'; } +void SettingsManager::ActivateIdentity(const char *identity, + bool self_hosted) { + if (!identity) { + return; + } + + if (self_hosted) { + CopyString(owner_.self_hosted_id_, identity); + } else { + CopyString(owner_.client_id_with_password_, identity); + } + + const char *at_pos = std::strchr(identity, '@'); + if (at_pos == nullptr) { + CopyString(owner_.client_id_, identity); + std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_)); + return; + } + + const std::string id(identity, at_pos - identity); + CopyString(owner_.client_id_, id.c_str()); + CopyString(owner_.password_saved_, at_pos + 1); +} + +bool SettingsManager::StagePendingPasswordChange( + const std::string &identity, bool self_hosted, + const std::string &server_host, int server_port, + const std::string &request_id) { + if (identity.empty() || identity.size() >= sizeof(cache_v3_.pending_identity) || + server_host.empty() || + server_host.size() >= sizeof(cache_v3_.pending_server_host) || + server_port <= 0 || request_id.empty() || + request_id.size() >= sizeof(cache_v3_.pending_request_id)) { + return false; + } + + std::lock_guard lock(cache_mutex_); + if (cache_v3_.pending_identity[0] != '\0' && + (cache_v3_.pending_self_hosted != self_hosted || + cache_v3_.pending_server_port != server_port || + server_host != cache_v3_.pending_server_host || + request_id != cache_v3_.pending_request_id)) { + LOG_WARN("Refuse to overwrite an unresolved password change"); + return false; + } + const CacheV3 previous = cache_v3_; + CopyString(cache_v3_.pending_identity, identity.c_str()); + CopyString(cache_v3_.pending_server_host, server_host.c_str()); + cache_v3_.pending_server_port = server_port; + cache_v3_.pending_self_hosted = self_hosted; + CopyString(cache_v3_.pending_request_id, request_id.c_str()); + if (SaveLocked() != 0) { + cache_v3_ = previous; + return false; + } + return true; +} + +std::string SettingsManager::PendingPasswordChangeIdentity( + bool self_hosted, const std::string &server_host, int server_port) const { + std::lock_guard lock(cache_mutex_); + if (cache_v3_.pending_identity[0] == '\0' || + cache_v3_.pending_self_hosted != self_hosted || + cache_v3_.pending_server_port != server_port || + server_host != cache_v3_.pending_server_host) { + return {}; + } + return cache_v3_.pending_identity; +} + +bool SettingsManager::PromotePendingPasswordChange() { + std::lock_guard lock(cache_mutex_); + if (cache_v3_.pending_identity[0] == '\0') { + return true; + } + + const CacheV3 previous = cache_v3_; + const std::string identity = cache_v3_.pending_identity; + const bool self_hosted = cache_v3_.pending_self_hosted; + ActivateIdentity(identity.c_str(), self_hosted); + std::memset(cache_v3_.pending_identity, 0, + sizeof(cache_v3_.pending_identity)); + std::memset(cache_v3_.pending_server_host, 0, + sizeof(cache_v3_.pending_server_host)); + cache_v3_.pending_server_port = 0; + cache_v3_.pending_self_hosted = false; + std::memset(cache_v3_.pending_request_id, 0, + sizeof(cache_v3_.pending_request_id)); + if (SaveLocked() != 0) { + // The already-durable pending credential remains the recovery source on + // disk. Keep it in memory as well so a reconnect in this process retries + // the credential that the server has accepted. + cache_v3_ = previous; + return false; + } + return true; +} + +bool SettingsManager::ClearPendingPasswordChange() { + std::lock_guard lock(cache_mutex_); + if (cache_v3_.pending_identity[0] == '\0') { + return true; + } + + const CacheV3 previous = cache_v3_; + std::memset(cache_v3_.pending_identity, 0, + sizeof(cache_v3_.pending_identity)); + std::memset(cache_v3_.pending_server_host, 0, + sizeof(cache_v3_.pending_server_host)); + cache_v3_.pending_server_port = 0; + cache_v3_.pending_self_hosted = false; + std::memset(cache_v3_.pending_request_id, 0, + sizeof(cache_v3_.pending_request_id)); + if (SaveLocked() != 0) { + cache_v3_ = previous; + return false; + } + return true; +} + void SettingsManager::PersistSelfHostedIdentity(const char *client_id) { if (!client_id) { return; } std::lock_guard lock(cache_mutex_); - if (!ReadV2Locked()) { - cache_v2_ = {}; - } - - CopyString(cache_v2_.self_hosted_id, client_id); - CopyString(cache_v2_.client_id_with_password, - owner_.client_id_with_password_); - std::memcpy(cache_v2_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_)); - std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_)); - - std::ofstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc", - std::ios::binary); - if (cache_v2_file) { - cache_v2_file.write(reinterpret_cast(&cache_v2_), - sizeof(cache_v2_)); + CopyString(owner_.self_hosted_id_, client_id); + if (SaveLocked() != 0) { + LOG_ERROR("Failed to persist self-hosted identity atomically"); } } diff --git a/src/gui/features/settings/settings_manager.h b/src/gui/features/settings/settings_manager.h index dc36c8a..6780b83 100644 --- a/src/gui/features/settings/settings_manager.h +++ b/src/gui/features/settings/settings_manager.h @@ -1,6 +1,7 @@ #ifndef CROSSDESK_GUI_SETTINGS_MANAGER_H_ #define CROSSDESK_GUI_SETTINGS_MANAGER_H_ +#include #include #include #include @@ -39,6 +40,22 @@ public: bool ActivateCachedPublicIdentity(); void PersistSelfHostedIdentity(const char *client_id); + // Password rotation is deliberately persisted in two phases. The active + // credential remains usable while the pending credential records the value + // that may already have been committed by the server. On the next launch the + // caller can try the pending credential first and safely fall back to the + // active credential when the request never reached the server. + bool StagePendingPasswordChange(const std::string &identity, + bool self_hosted, + const std::string &server_host, + int server_port, + const std::string &request_id); + std::string PendingPasswordChangeIdentity( + bool self_hosted, const std::string &server_host, + int server_port) const; + bool PromotePendingPasswordChange(); + bool ClearPendingPasswordChange(); + private: struct CacheV1 { char client_id_with_password[17]; @@ -67,12 +84,27 @@ private: char self_hosted_id[17]; }; + struct CacheV3 { + uint32_t magic; + uint32_t version; + CacheV2 base; + char pending_identity[17]; + char pending_server_host[256]; + int pending_server_port; + bool pending_self_hosted; + char pending_request_id[64]; + uint32_t checksum; + }; + int SaveLocked(); + bool ReadV3Locked(); bool ReadV2Locked(); + void ActivateIdentity(const char *identity, bool self_hosted); GuiRuntime &owner_; CacheV1 cache_v1_{}; CacheV2 cache_v2_{}; + CacheV3 cache_v3_{}; mutable std::mutex cache_mutex_; std::unordered_map recent_connection_aliases_; }; diff --git a/src/gui/runtime/gui_runtime.cpp b/src/gui/runtime/gui_runtime.cpp index b0238e6..f262ad9 100644 --- a/src/gui/runtime/gui_runtime.cpp +++ b/src/gui/runtime/gui_runtime.cpp @@ -61,6 +61,40 @@ int GuiRuntime::CreateConnectionPeer() { params_.user_id = client_id_with_password_; } + const bool self_hosted = config_center_->IsSelfHosted(); + const std::string pending_identity = + settings_.PendingPasswordChangeIdentity( + self_hosted, signal_server_ip, signal_server_port); + bool try_pending_identity = false; + { + std::lock_guard lock(password_change_mutex_); + if (pending_identity.empty()) { + credential_recovery_in_progress_ = false; + credential_recovery_attempt_pending_ = false; + credential_recovery_retry_active_ = false; + credential_recovery_promote_pending_ = false; + credential_recovery_clear_pending_ = false; + } else if (!credential_recovery_in_progress_) { + credential_recovery_in_progress_ = true; + credential_recovery_attempt_pending_ = true; + LOG_INFO("Recovering an interrupted password change for [{}]", + client_id_); + } + try_pending_identity = credential_recovery_in_progress_ && + credential_recovery_attempt_pending_ && + !pending_identity.empty(); + } + + const char *active_identity = + self_hosted ? self_hosted_user_id_ : client_id_with_password_; + const std::string login_identity = + try_pending_identity ? pending_identity : std::string(active_identity); + std::memset(connection_login_identity_, 0, + sizeof(connection_login_identity_)); + std::strncpy(connection_login_identity_, login_identity.c_str(), + sizeof(connection_login_identity_) - 1); + params_.user_id = connection_login_identity_; + // self hosted server config strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(), sizeof(signal_server_ip_self_) - 1); diff --git a/src/gui/runtime/peer_event_handler.cpp b/src/gui/runtime/peer_event_handler.cpp index d07b9cb..20d5393 100644 --- a/src/gui/runtime/peer_event_handler.cpp +++ b/src/gui/runtime/peer_event_handler.cpp @@ -116,6 +116,7 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size, j.contains("reason") && j["reason"].is_string() ? j["reason"].get() : "Password change failed"; + runtime->password_change_result_uncertain_ = false; runtime->password_change_result_ready_ = true; } } @@ -137,8 +138,21 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id, runtime->signal_connected_ = true; runtime->need_to_send_recent_connections_ = true; LOG_INFO("[{}] connected to signal server", client_id); + std::lock_guard lock(runtime->password_change_mutex_); + if (runtime->credential_recovery_in_progress_) { + if (runtime->credential_recovery_attempt_pending_) { + runtime->credential_recovery_promote_pending_ = true; + } else { + runtime->credential_recovery_clear_pending_ = true; + } + } } else if (SignalStatus::SignalFailed == status) { runtime->signal_connected_ = false; + std::lock_guard lock(runtime->password_change_mutex_); + if (runtime->credential_recovery_in_progress_ && + runtime->credential_recovery_attempt_pending_) { + runtime->credential_recovery_retry_active_ = true; + } } else if (SignalStatus::SignalClosed == status) { runtime->signal_connected_ = false; } else if (SignalStatus::SignalReconnecting == status) { diff --git a/src/gui/runtime/runtime_state.h b/src/gui/runtime/runtime_state.h index 751b19b..53faad0 100644 --- a/src/gui/runtime/runtime_state.h +++ b/src/gui/runtime/runtime_state.h @@ -116,6 +116,7 @@ struct UserSettingsState { char password_saved_[7] = ""; char self_hosted_id_[17] = ""; char self_hosted_user_id_[17] = ""; + char connection_login_identity_[17] = ""; int language_button_value_ = 0; int video_quality_button_value_ = 2; int video_frame_rate_button_value_ = 1; @@ -158,10 +159,16 @@ struct PasswordChangeState { bool password_change_pending_ = false; bool password_change_result_ready_ = false; bool password_change_succeeded_ = false; + bool password_change_result_uncertain_ = false; std::chrono::steady_clock::time_point password_change_requested_at_; std::string pending_password_change_request_id_; std::string pending_local_password_; std::string password_change_error_; + bool credential_recovery_in_progress_ = false; + bool credential_recovery_attempt_pending_ = false; + bool credential_recovery_retry_active_ = false; + bool credential_recovery_promote_pending_ = false; + bool credential_recovery_clear_pending_ = false; }; struct ConnectionState {