mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-08-26 13:29:31 +08:00
[fix] recover interrupted password changes
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <random>
|
||||||
#include <shared_mutex>
|
#include <shared_mutex>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -51,6 +52,15 @@ namespace {
|
|||||||
|
|
||||||
using namespace std::chrono_literals;
|
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__)
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
bool HasNonEmptyEnvironmentVariable(const char* name) {
|
bool HasNonEmptyEnvironmentVariable(const char* name) {
|
||||||
const char* value = std::getenv(name);
|
const char* value = std::getenv(name);
|
||||||
@@ -1356,21 +1366,47 @@ void GuiApplication::BindMainCallbacks() {
|
|||||||
if (password_change_pending_) {
|
if (password_change_pending_) {
|
||||||
return true;
|
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_pending_ = true;
|
||||||
password_change_result_ready_ = false;
|
password_change_result_ready_ = false;
|
||||||
password_change_succeeded_ = false;
|
password_change_succeeded_ = false;
|
||||||
|
password_change_result_uncertain_ = false;
|
||||||
password_change_requested_at_ = std::chrono::steady_clock::now();
|
password_change_requested_at_ = std::chrono::steady_clock::now();
|
||||||
pending_password_change_request_id_ = request_id;
|
pending_password_change_request_id_ = request_id;
|
||||||
pending_local_password_ = password;
|
pending_local_password_ = password;
|
||||||
password_change_error_.clear();
|
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<std::mutex> 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"},
|
const nlohmann::json request = {{"type", "change_password"},
|
||||||
{"request_id", request_id},
|
{"request_id", request_id},
|
||||||
{"new_password", password}};
|
{"new_password", password}};
|
||||||
const std::string message = request.dump();
|
const std::string message = request.dump();
|
||||||
if (SendSignalMessage(peer_, message.data(), message.size()) != 0) {
|
if (SendSignalMessage(peer_, message.data(), message.size()) != 0) {
|
||||||
|
{
|
||||||
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
||||||
password_change_pending_ = false;
|
password_change_pending_ = false;
|
||||||
pending_password_change_request_id_.clear();
|
pending_password_change_request_id_.clear();
|
||||||
@@ -1379,6 +1415,10 @@ void GuiApplication::BindMainCallbacks() {
|
|||||||
localization::signal_disconnected[localization_language_index_];
|
localization::signal_disconnected[localization_language_index_];
|
||||||
show_offline_warning_window_ = true;
|
show_offline_warning_window_ = true;
|
||||||
}
|
}
|
||||||
|
if (!settings_.ClearPendingPasswordChange()) {
|
||||||
|
LOG_WARN("Could not clear unsent pending password change");
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
main->on_connect_requested(
|
main->on_connect_requested(
|
||||||
@@ -1788,6 +1828,7 @@ void GuiApplication::Tick() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
HandlePasswordChangeResult();
|
HandlePasswordChangeResult();
|
||||||
|
HandleCredentialRecovery();
|
||||||
if (!peer_) {
|
if (!peer_) {
|
||||||
CreateConnectionPeer();
|
CreateConnectionPeer();
|
||||||
}
|
}
|
||||||
@@ -1864,7 +1905,7 @@ void GuiApplication::Tick() {
|
|||||||
|
|
||||||
void GuiApplication::HandlePasswordChangeResult() {
|
void GuiApplication::HandlePasswordChangeResult() {
|
||||||
bool succeeded = false;
|
bool succeeded = false;
|
||||||
std::string new_password;
|
bool uncertain = false;
|
||||||
std::string error;
|
std::string error;
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -1874,6 +1915,7 @@ void GuiApplication::HandlePasswordChangeResult() {
|
|||||||
std::chrono::seconds(10)) {
|
std::chrono::seconds(10)) {
|
||||||
password_change_result_ready_ = true;
|
password_change_result_ready_ = true;
|
||||||
password_change_succeeded_ = false;
|
password_change_succeeded_ = false;
|
||||||
|
password_change_result_uncertain_ = true;
|
||||||
password_change_error_ = "Server did not respond";
|
password_change_error_ = "Server did not respond";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1882,47 +1924,43 @@ void GuiApplication::HandlePasswordChangeResult() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
succeeded = password_change_succeeded_;
|
succeeded = password_change_succeeded_;
|
||||||
new_password = pending_local_password_;
|
uncertain = password_change_result_uncertain_;
|
||||||
error = password_change_error_;
|
error = password_change_error_;
|
||||||
password_change_pending_ = false;
|
password_change_pending_ = false;
|
||||||
password_change_result_ready_ = false;
|
password_change_result_ready_ = false;
|
||||||
password_change_succeeded_ = false;
|
password_change_succeeded_ = false;
|
||||||
|
password_change_result_uncertain_ = false;
|
||||||
pending_password_change_request_id_.clear();
|
pending_password_change_request_id_.clear();
|
||||||
pending_local_password_.clear();
|
pending_local_password_.clear();
|
||||||
password_change_error_.clear();
|
password_change_error_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!succeeded) {
|
if (!succeeded) {
|
||||||
|
if (uncertain) {
|
||||||
|
LOG_WARN("Password change outcome is unknown: {}; re-authenticating",
|
||||||
|
error);
|
||||||
|
} else {
|
||||||
LOG_WARN("Password change failed: {}", error);
|
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_];
|
offline_warning_text_ = localization::failed[localization_language_index_];
|
||||||
if (!error.empty()) {
|
if (!error.empty()) {
|
||||||
offline_warning_text_ += ": " + error;
|
offline_warning_text_ += ": " + error;
|
||||||
}
|
}
|
||||||
show_offline_warning_window_ = true;
|
show_offline_warning_window_ = true;
|
||||||
|
if (uncertain && peer_) {
|
||||||
|
LeaveConnection(peer_, client_id_);
|
||||||
|
DestroyPeer(&peer_);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::memset(password_saved_, 0, sizeof(password_saved_));
|
if (!settings_.PromotePendingPasswordChange()) {
|
||||||
std::strncpy(password_saved_, new_password.c_str(),
|
// The pending credential was persisted before the request was sent, so it
|
||||||
sizeof(password_saved_) - 1);
|
// remains sufficient for recovery even when promotion cannot be written.
|
||||||
|
LOG_WARN("Password changed on server; pending recovery record retained");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Password changed successfully for [{}]", client_id_);
|
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<std::mutex> 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<std::mutex> lock(password_change_mutex_);
|
||||||
|
credential_recovery_in_progress_ = false;
|
||||||
|
credential_recovery_attempt_pending_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
void GuiApplication::UpdateLocalization() {
|
void GuiApplication::UpdateLocalization() {
|
||||||
const int language =
|
const int language =
|
||||||
localization::detail::ClampLanguageIndex(localization_language_index_);
|
localization::detail::ClampLanguageIndex(localization_language_index_);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ private:
|
|||||||
void BindServerCallbacks();
|
void BindServerCallbacks();
|
||||||
void Tick();
|
void Tick();
|
||||||
void HandlePasswordChangeResult();
|
void HandlePasswordChangeResult();
|
||||||
|
void HandleCredentialRecovery();
|
||||||
void SyncMainWindow();
|
void SyncMainWindow();
|
||||||
void SyncConnectionDialog();
|
void SyncConnectionDialog();
|
||||||
void SyncPlatformDialogs();
|
void SyncPlatformDialogs();
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
#include "features/settings/settings_manager.h"
|
#include "features/settings/settings_manager.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <io.h>
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
@@ -14,6 +27,9 @@
|
|||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
constexpr uint32_t kCacheV3Magic = 0x33444358; // "XCD3"
|
||||||
|
constexpr uint32_t kCacheV3Version = 3;
|
||||||
|
|
||||||
template <size_t Size>
|
template <size_t Size>
|
||||||
void CopyString(char (&destination)[Size], const char *source) {
|
void CopyString(char (&destination)[Size], const char *source) {
|
||||||
static_assert(Size > 0);
|
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<const unsigned char *>(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::thread::id>{}(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
|
} // namespace
|
||||||
|
|
||||||
SettingsManager::SettingsManager(GuiRuntime &owner) : owner_(owner) {}
|
SettingsManager::SettingsManager(GuiRuntime &owner) : owner_(owner) {}
|
||||||
@@ -33,36 +142,74 @@ int SettingsManager::Save() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int SettingsManager::SaveLocked() {
|
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,
|
CopyString(cache_v2_.client_id_with_password,
|
||||||
owner_.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_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
|
||||||
std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
||||||
CopyString(cache_v2_.self_hosted_id, owner_.self_hosted_id_);
|
CopyString(cache_v2_.self_hosted_id, owner_.self_hosted_id_);
|
||||||
|
|
||||||
cache_v2_file.write(reinterpret_cast<const char *>(&cache_v2_),
|
cache_v3_.magic = kCacheV3Magic;
|
||||||
sizeof(cache_v2_));
|
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.
|
// 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,
|
CopyString(cache_v1_.client_id_with_password,
|
||||||
owner_.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_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
|
||||||
std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
||||||
cache_v1_file.write(reinterpret_cast<const char *>(&cache_v1_),
|
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache.enc",
|
||||||
sizeof(cache_v1_));
|
&cache_v1_, sizeof(cache_v1_))) {
|
||||||
|
LOG_WARN("Failed to update legacy v1 credential cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
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<char *>(&loaded), sizeof(loaded));
|
||||||
|
if (cache_file.gcount() != static_cast<std::streamsize>(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() {
|
bool SettingsManager::ReadV2Locked() {
|
||||||
std::ifstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc",
|
std::ifstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc",
|
||||||
std::ios::binary);
|
std::ios::binary);
|
||||||
@@ -84,13 +231,20 @@ bool SettingsManager::ReadV2Locked() {
|
|||||||
int SettingsManager::Load() {
|
int SettingsManager::Load() {
|
||||||
std::unique_lock<std::mutex> lock(cache_mutex_);
|
std::unique_lock<std::mutex> lock(cache_mutex_);
|
||||||
|
|
||||||
if (ReadV2Locked()) {
|
const bool loaded_v3 = ReadV3Locked();
|
||||||
|
if (loaded_v3 || ReadV2Locked()) {
|
||||||
CopyString(owner_.client_id_with_password_,
|
CopyString(owner_.client_id_with_password_,
|
||||||
cache_v2_.client_id_with_password);
|
cache_v2_.client_id_with_password);
|
||||||
CopyString(owner_.self_hosted_id_, cache_v2_.self_hosted_id);
|
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_key_, cache_v2_.key, sizeof(cache_v2_.key));
|
||||||
std::memcpy(owner_.aes128_iv_, cache_v2_.iv, sizeof(cache_v2_.iv));
|
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 {
|
} else {
|
||||||
std::ifstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc",
|
std::ifstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc",
|
||||||
std::ios::binary);
|
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_key_, cache_v1_.key, sizeof(cache_v1_.key));
|
||||||
std::memcpy(owner_.aes128_iv_, cache_v1_.iv, sizeof(cache_v1_.iv));
|
std::memcpy(owner_.aes128_iv_, cache_v1_.iv, sizeof(cache_v1_.iv));
|
||||||
|
|
||||||
|
cache_v3_ = {};
|
||||||
SaveLocked();
|
SaveLocked();
|
||||||
LOG_INFO("Migrated settings from v1 to v2 cache file");
|
LOG_INFO("Migrated settings from v1 to v3 cache file");
|
||||||
}
|
}
|
||||||
|
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
@@ -186,7 +341,8 @@ int SettingsManager::Load() {
|
|||||||
|
|
||||||
bool SettingsManager::LoadCachedSelfHostedIdentity() {
|
bool SettingsManager::LoadCachedSelfHostedIdentity() {
|
||||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
std::lock_guard<std::mutex> 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_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
|
||||||
std::memset(owner_.client_id_, 0, sizeof(owner_.client_id_));
|
std::memset(owner_.client_id_, 0, sizeof(owner_.client_id_));
|
||||||
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||||
@@ -227,27 +383,135 @@ bool SettingsManager::ActivateCachedPublicIdentity() {
|
|||||||
return owner_.client_id_[0] != '\0';
|
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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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) {
|
void SettingsManager::PersistSelfHostedIdentity(const char *client_id) {
|
||||||
if (!client_id) {
|
if (!client_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||||
if (!ReadV2Locked()) {
|
CopyString(owner_.self_hosted_id_, client_id);
|
||||||
cache_v2_ = {};
|
if (SaveLocked() != 0) {
|
||||||
}
|
LOG_ERROR("Failed to persist self-hosted identity atomically");
|
||||||
|
|
||||||
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<const char *>(&cache_v2_),
|
|
||||||
sizeof(cache_v2_));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
#ifndef CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
||||||
#define CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
#define CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -39,6 +40,22 @@ public:
|
|||||||
bool ActivateCachedPublicIdentity();
|
bool ActivateCachedPublicIdentity();
|
||||||
void PersistSelfHostedIdentity(const char *client_id);
|
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:
|
private:
|
||||||
struct CacheV1 {
|
struct CacheV1 {
|
||||||
char client_id_with_password[17];
|
char client_id_with_password[17];
|
||||||
@@ -67,12 +84,27 @@ private:
|
|||||||
char self_hosted_id[17];
|
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();
|
int SaveLocked();
|
||||||
|
bool ReadV3Locked();
|
||||||
bool ReadV2Locked();
|
bool ReadV2Locked();
|
||||||
|
void ActivateIdentity(const char *identity, bool self_hosted);
|
||||||
|
|
||||||
GuiRuntime &owner_;
|
GuiRuntime &owner_;
|
||||||
CacheV1 cache_v1_{};
|
CacheV1 cache_v1_{};
|
||||||
CacheV2 cache_v2_{};
|
CacheV2 cache_v2_{};
|
||||||
|
CacheV3 cache_v3_{};
|
||||||
mutable std::mutex cache_mutex_;
|
mutable std::mutex cache_mutex_;
|
||||||
std::unordered_map<std::string, std::string> recent_connection_aliases_;
|
std::unordered_map<std::string, std::string> recent_connection_aliases_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -61,6 +61,40 @@ int GuiRuntime::CreateConnectionPeer() {
|
|||||||
params_.user_id = client_id_with_password_;
|
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<std::mutex> 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
|
// self hosted server config
|
||||||
strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(),
|
strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(),
|
||||||
sizeof(signal_server_ip_self_) - 1);
|
sizeof(signal_server_ip_self_) - 1);
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
|
|||||||
j.contains("reason") && j["reason"].is_string()
|
j.contains("reason") && j["reason"].is_string()
|
||||||
? j["reason"].get<std::string>()
|
? j["reason"].get<std::string>()
|
||||||
: "Password change failed";
|
: "Password change failed";
|
||||||
|
runtime->password_change_result_uncertain_ = false;
|
||||||
runtime->password_change_result_ready_ = true;
|
runtime->password_change_result_ready_ = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,8 +138,21 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
|||||||
runtime->signal_connected_ = true;
|
runtime->signal_connected_ = true;
|
||||||
runtime->need_to_send_recent_connections_ = true;
|
runtime->need_to_send_recent_connections_ = true;
|
||||||
LOG_INFO("[{}] connected to signal server", client_id);
|
LOG_INFO("[{}] connected to signal server", client_id);
|
||||||
|
std::lock_guard<std::mutex> 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) {
|
} else if (SignalStatus::SignalFailed == status) {
|
||||||
runtime->signal_connected_ = false;
|
runtime->signal_connected_ = false;
|
||||||
|
std::lock_guard<std::mutex> 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) {
|
} else if (SignalStatus::SignalClosed == status) {
|
||||||
runtime->signal_connected_ = false;
|
runtime->signal_connected_ = false;
|
||||||
} else if (SignalStatus::SignalReconnecting == status) {
|
} else if (SignalStatus::SignalReconnecting == status) {
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ struct UserSettingsState {
|
|||||||
char password_saved_[7] = "";
|
char password_saved_[7] = "";
|
||||||
char self_hosted_id_[17] = "";
|
char self_hosted_id_[17] = "";
|
||||||
char self_hosted_user_id_[17] = "";
|
char self_hosted_user_id_[17] = "";
|
||||||
|
char connection_login_identity_[17] = "";
|
||||||
int language_button_value_ = 0;
|
int language_button_value_ = 0;
|
||||||
int video_quality_button_value_ = 2;
|
int video_quality_button_value_ = 2;
|
||||||
int video_frame_rate_button_value_ = 1;
|
int video_frame_rate_button_value_ = 1;
|
||||||
@@ -158,10 +159,16 @@ struct PasswordChangeState {
|
|||||||
bool password_change_pending_ = false;
|
bool password_change_pending_ = false;
|
||||||
bool password_change_result_ready_ = false;
|
bool password_change_result_ready_ = false;
|
||||||
bool password_change_succeeded_ = false;
|
bool password_change_succeeded_ = false;
|
||||||
|
bool password_change_result_uncertain_ = false;
|
||||||
std::chrono::steady_clock::time_point password_change_requested_at_;
|
std::chrono::steady_clock::time_point password_change_requested_at_;
|
||||||
std::string pending_password_change_request_id_;
|
std::string pending_password_change_request_id_;
|
||||||
std::string pending_local_password_;
|
std::string pending_local_password_;
|
||||||
std::string password_change_error_;
|
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 {
|
struct ConnectionState {
|
||||||
|
|||||||
Reference in New Issue
Block a user