diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index 23c58c8b..fcb5ff1e 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -65,6 +66,28 @@ namespace { using namespace std::chrono_literals; +std::optional GenerateRandomPassword(std::string_view current) { + constexpr std::string_view alphabet = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + constexpr auto unbiased_limit = 256 - 256 % alphabet.size(); + std::string password; + do { + password.clear(); + while (password.size() < 6) { + unsigned char value; + if (RAND_bytes(&value, sizeof(value)) != 1) { + return std::nullopt; + } + // Reject the incomplete bucket so every character is equally likely. + if (value < unbiased_limit) { + password.push_back(alphabet[value % alphabet.size()]); + } + } + } while (password == current || + password.find_first_not_of("0123456789") == std::string::npos); + return password; +} + std::string CreatePasswordChangeRequestId(uint64_t sequence) { const auto timestamp = std::chrono::system_clock::now() .time_since_epoch() @@ -1294,78 +1317,17 @@ void GuiApplication::BindMainCallbacks() { ui_->main->set_password_visible(show_password_); }); main->on_reset_password([this](slint::SharedString value) { - const std::string password(value); - if (password.size() != 6) { - return false; - } - - if (!peer_ || !signal_connected_) { - offline_warning_text_ = - localization::signal_disconnected[localization_language_index_]; - show_offline_warning_window_ = true; - return true; - } - - std::string request_id; - { - std::lock_guard lock(password_change_mutex_); - if (password_change_pending_) { - return true; - } - 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(); + return RequestPasswordChange(std::string(value)); + }); + main->on_refresh_password([this] { + const auto password = GenerateRandomPassword(password_saved_); + if (!password) { + LOG_ERROR("Could not generate a random password"); offline_warning_text_ = localization::failed[localization_language_index_]; show_offline_warning_window_ = true; - LOG_ERROR("Could not durably stage password change"); - return true; + return; } - - 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; - } - if (!settings_.ClearPendingPasswordChange()) { - LOG_WARN("Could not clear unsent pending password change"); - } - } - return true; + RequestPasswordChange(*password); }); main->on_connect_requested( [this](slint::SharedString id) { ConnectFromUi(std::string(id)); }); @@ -2039,6 +2001,78 @@ void GuiApplication::ShareLocalCursorState() { } } +bool GuiApplication::RequestPasswordChange(const std::string &password) { + if (password.size() != 6) { + return false; + } + + if (!peer_ || !signal_connected_) { + offline_warning_text_ = + localization::signal_disconnected[localization_language_index_]; + show_offline_warning_window_ = true; + return true; + } + + std::string request_id; + { + std::lock_guard lock(password_change_mutex_); + if (password_change_pending_) { + return true; + } + 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; + } + if (!settings_.ClearPendingPasswordChange()) { + LOG_WARN("Could not clear unsent pending password change"); + } + } + return true; +} + void GuiApplication::HandlePasswordChangeResult() { bool succeeded = false; bool uncertain = false; @@ -2188,6 +2222,10 @@ void GuiApplication::SyncMainWindow() { ui_->main->set_local_id(UiText(FormatPeerId(client_id_))); ui_->main->set_local_password(password_saved_); ui_->main->set_password_visible(show_password_); + { + std::lock_guard lock(password_change_mutex_); + ui_->main->set_password_change_pending(password_change_pending_); + } ui_->main->set_signal_connected(signal_connected_); ui_->main->set_signal_tls_error(signal_status_ == SignalStatus::SignalTlsCertError); diff --git a/apps/desktop/src/gui/application/gui_application.h b/apps/desktop/src/gui/application/gui_application.h index f05d757d..cacd867c 100644 --- a/apps/desktop/src/gui/application/gui_application.h +++ b/apps/desktop/src/gui/application/gui_application.h @@ -49,6 +49,7 @@ private: void BindServerCallbacks(); void Tick(); void ShareLocalCursorState(); + bool RequestPasswordChange(const std::string &password); void HandlePasswordChangeResult(); void HandleCredentialRecovery(); void SyncMainWindow(); @@ -100,4 +101,4 @@ private: } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/apps/desktop/src/gui/assets/localization/localization_data.h b/apps/desktop/src/gui/assets/localization/localization_data.h index 491508b7..af468833 100644 --- a/apps/desktop/src/gui/assets/localization/localization_data.h +++ b/apps/desktop/src/gui/assets/localization/localization_data.h @@ -31,6 +31,8 @@ struct TranslationRow { X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \ u8"Скопировано в буфер обмена") \ X(password, u8"密码", "Password", u8"Пароль") \ + X(refresh_password, u8"刷新随机密码", "Refresh random password", \ + u8"Обновить случайный пароль") \ X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \ X(remote_desktop, u8"远程桌面", "Remote Desktop", \ u8"Удаленный рабочий стол") \ diff --git a/apps/desktop/src/gui/ui/common.slint b/apps/desktop/src/gui/ui/common.slint index eabb71f9..cb65c708 100644 --- a/apps/desktop/src/gui/ui/common.slint +++ b/apps/desktop/src/gui/ui/common.slint @@ -41,6 +41,7 @@ export global FontAwesomeIcons { out property keyboard: "\u{f11c}"; out property minus: "\u{f068}"; out property pen: "\u{f304}"; + out property rotate: "\u{f2f1}"; out property signal: "\u{f012}"; out property shield-halved: "\u{f3ed}"; out property square-full: "\u{f45c}"; @@ -54,6 +55,7 @@ export global FontAwesomeIcons { export component IconButton inherits Rectangle { in property icon; in property tooltip; + in property enabled: true; in property danger: false; in property icon-size: 16px; in property icon-color: #24272d; @@ -69,6 +71,7 @@ export component IconButton inherits Rectangle { : touch.has-hover ? (danger ? #f04438 : root.hover-background) : root.normal-background; border-radius: 4px; + opacity: root.enabled ? 1 : 0.4; Text { text: root.icon; @@ -90,6 +93,7 @@ export component IconButton inherits Rectangle { } touch := TouchArea { + enabled: root.enabled; clicked => { root.clicked(); } } diff --git a/apps/desktop/src/gui/ui/main_window.slint b/apps/desktop/src/gui/ui/main_window.slint index aeb2fafa..a8864f87 100644 --- a/apps/desktop/src/gui/ui/main_window.slint +++ b/apps/desktop/src/gui/ui/main_window.slint @@ -24,6 +24,7 @@ export global UiStrings { in-out property online: "Online"; in-out property offline: "Offline"; in-out property password: "Password"; + in-out property refresh-password: "Refresh random password"; in-out property copied: "Local ID copied to clipboard"; in-out property connect: "Connect"; in-out property settings: "Settings"; @@ -208,6 +209,7 @@ export component MainWindow inherits Window { in property local-password: ""; in-out property remote-id-input: ""; in property password-visible: true; + in property password-change-pending: false; in property signal-connected: false; in property signal-tls-error: false; in property <[RecentConnection]> recent-connections; @@ -262,6 +264,7 @@ export component MainWindow inherits Window { callback copy-local-id; callback toggle-password-visibility; + callback refresh-password; callback reset-password(string) -> bool; callback format-remote-id(string) -> string; callback connect-requested(string); @@ -676,19 +679,31 @@ export component MainWindow inherits Window { IconButton { x: 174px; y: 133px; - width: 29px; + width: 24px; height: 30px; icon: root.password-visible ? FontAwesomeIcons.eye : FontAwesomeIcons.eye-slash; icon-size: 17px; clicked => { root.toggle-password-visibility(); } } IconButton { - x: 214px; + x: 198px; y: 133px; - width: 29px; + width: 24px; + height: 30px; + icon: FontAwesomeIcons.rotate; + icon-size: 15px; + tooltip: UiStrings.refresh-password; + enabled: !root.password-change-pending; + clicked => { root.refresh-password(); } + } + IconButton { + x: 222px; + y: 133px; + width: 24px; height: 30px; icon: FontAwesomeIcons.pen; icon-size: 15px; + enabled: !root.password-change-pending; clicked => { root.reset-password-open = true; } } } diff --git a/apps/desktop/src/gui/ui/ui_localization.h b/apps/desktop/src/gui/ui/ui_localization.h index 8b8deca5..b42b6e27 100644 --- a/apps/desktop/src/gui/ui/ui_localization.h +++ b/apps/desktop/src/gui/ui/ui_localization.h @@ -36,6 +36,7 @@ inline int ApplyMainWindowStrings( strings.set_online(Text(localization::online[language])); strings.set_offline(Text(localization::offline[language])); strings.set_password(Text(localization::password[language])); + strings.set_refresh_password(Text(localization::refresh_password[language])); strings.set_copied( Text(localization::local_id_copied_to_clipboard[language])); strings.set_connect(Text(localization::connect[language])); diff --git a/apps/desktop/xmake/targets.lua b/apps/desktop/xmake/targets.lua index 38ad0fc6..45eedd69 100644 --- a/apps/desktop/xmake/targets.lua +++ b/apps/desktop/xmake/targets.lua @@ -392,7 +392,7 @@ function setup_targets() -- version and use fmt's supported runtime-parser fallback here. add_defines("FMT_CONSTEVAL=") add_packages("slint", {public = true}) - add_packages("libyuv", "tinyfiledialogs") + add_packages("libyuv", "tinyfiledialogs", "openssl3") add_rules("slint") if is_config("CROSSDESK_PORTABLE", true) then -- RuntimeState changes layout in portable builds. Consumers of GUI