diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index ea4d9fa8..f4485af1 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -366,6 +366,19 @@ slint::SharedString UiText(std::string_view value) { return slint::SharedString(value); } +slint::SharedString TrustFingerprint(std::string fingerprint) { + for (size_t i = 8; i < fingerprint.size(); i += 9) fingerprint.insert(i, " "); + return UiText(fingerprint); +} + +ui::TrustedDeviceEntry TrustRow(const TrustedDevices::Device& device, + const std::string& remote = {}) { + auto id = device.device_id; + if (id.rfind("C-", 0) == 0) id.erase(0, 2); + return {UiText(remote), UiText(device.public_key), + UiText(device.name.empty() ? id : device.name), UiText(id), {}}; +} + void RegisterFontAwesome(slint::Window& window) { if (const auto error = window.window_handle().register_font_from_data( fa_solid_900_ttf, fa_solid_900_ttf_len)) { @@ -1065,6 +1078,11 @@ struct GuiApplication::SlintUi { std::string tab_model_signature; std::vector controller_ids; std::string controller_model_signature; + std::string trusted_device_signature; + std::shared_ptr> trusted_device_model = + std::make_shared>(); + std::shared_ptr> trust_candidate_model = + std::make_shared>(); std::string connection_dialog_remote_id; bool connection_password_initialized = false; bool connection_dialog_gallery = false; @@ -1560,6 +1578,7 @@ void GuiApplication::ResetSettingsUi(bool gallery_window) { config_center_->IsEnablePrivacyScreen()); main->set_privacy_local_unlock_allowed(config_center_->IsPrivacyLocalUnlockAllowed()); main->set_lock_on_disconnect_enabled(config_center_->IsLockOnDisconnect()); + main->set_trusted_devices_only(config_center_->IsTrustedDevicesOnly()); #ifdef _WIN32 main->set_capture_method_visible(true); main->set_capture_method_index( @@ -1933,6 +1952,7 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) { main->set_file_save_path(folder); } }); + BindTrustedDeviceCallbacks(gallery_window); main->on_save_settings([this, main] { return SaveSettingsFromUi(main->get_gallery_mode()); }); @@ -2704,6 +2724,7 @@ void GuiApplication::Tick() { } SyncStreamWindow(); SyncServerWindow(); + SyncTrustedDevices(); SyncFileTransferNotifications(); } @@ -4260,6 +4281,113 @@ void GuiApplication::PositionServerWindows() { } } +void GuiApplication::BindTrustedDeviceCallbacks(bool gallery_window) { + const auto main = ui_->Window(gallery_window); + main->on_trust_action([this, main]( + slint::SharedString action, slint::SharedString id, slint::SharedString key) { + const auto message = [this](const char* text) { + return UiText(localization::Literal(text, localization_language_index_)); + }; + const bool copying = action == "copy"; + const auto set_error = [&](const char* text) { + if (copying) main->set_trust_copy_error(message(text)); + else main->set_trusted_devices_error(message(text)); + }; + if (copying || action == "open" || action == "close") { + main->set_trust_copy_error(""); + main->set_trust_copy_notice(""); + } + if (!copying) { + main->set_trusted_devices_error(""); + main->set_trusted_devices_notice(""); + } + if (action == "open" || action == "close") { + main->set_trust_pairing_input(""); + SyncTrustedDevices(); + return; + } + if (!trusted_devices_) { + set_error("无法读取设备身份或可信列表,请修复后再启用。"); + return; + } + if (action == "edit") return; + try { + if (copying) { + const auto text = trusted_devices_->PairingInfo(client_id_, GetHostName()); + if (SDL_SetClipboardText(text.c_str())) + main->set_trust_copy_notice(message("配对信息已复制。")); + else set_error("无法访问剪贴板,请重试。"); + } else if (action == "import") { + const auto input = std::string(main->get_trust_pairing_input()); + if (input.empty()) return; + const auto device = TrustedDevices::ParsePairingInfo(input); + if (!device) { + set_error("配对信息无效,请从控制端重新复制。"); + return; + } + // The Add action approves the current input. Import only updates the + // list; a live connection must still complete its own authentication. + if (trusted_devices_->Contains(device->public_key)) { + main->set_trusted_devices_notice(message("已在可信列表中")); + } else { + trusted_devices_->Add(*device); + main->set_trusted_devices_notice(message("已加入可信设备。")); + } + main->set_trust_pairing_input(""); + } else if ((action == "add" && !TrustController(std::string(id), std::string(key))) || + (action == "remove" && !RemoveTrustedDevice(std::string(key)))) { + set_error("无法更新可信设备,请检查列表容量和存储权限后重试。"); + } + } catch (const std::exception&) { + set_error(copying ? "无法复制配对信息,请重试。" + : "无法更新可信设备,请检查列表容量和存储权限后重试。"); + } + SyncTrustedDevices(); + }); +} + +void GuiApplication::SyncTrustedDevices() { + if (!ui_->main->get_trusted_devices_open() && !ui_->wall->get_trusted_devices_open()) return; + std::vector saved, candidates; + if (trusted_devices_) { + for (const auto& device : trusted_devices_->List()) saved.push_back(TrustRow(device)); + for (const auto& id : ConnectedControllers()) + if (const auto device = VerifiedController(id); + device && !trusted_devices_->Contains(device->public_key)) + candidates.push_back(TrustRow(*device, id)); + } + std::sort(candidates.begin(), candidates.end(), [](const auto& a, const auto& b) { + return std::string(a.remote_id) < std::string(b.remote_id); + }); + std::string signature = trusted_devices_ ? "ready" : "unavailable"; + for (const auto* rows : {&saved, &candidates}) { + signature += "|"; + for (const auto& device : *rows) + signature += std::string(device.remote_id) + '\0' + std::string(device.public_key) + '\0' + + std::string(device.display_name) + '\0' + std::string(device.device_id) + '\0'; + } + if (signature != ui_->trusted_device_signature) { + // Tick runs frequently. Hash keys and update Slint only when the rows change. + for (auto* rows : {&saved, &candidates}) + for (auto& device : *rows) + device.fingerprint = TrustFingerprint(TrustedDevices::Fingerprint(std::string(device.public_key))); + ui_->trusted_device_model->set_vector(std::move(saved)); + ui_->trust_candidate_model->set_vector(std::move(candidates)); + ui_->trusted_device_signature = std::move(signature); + const auto fingerprint = TrustFingerprint(trusted_devices_ ? trusted_devices_->Fingerprint() : ""); + for (auto& window : {ui_->main, ui_->wall}) { + window->set_trusted_devices(ui_->trusted_device_model); + window->set_trust_candidates(ui_->trust_candidate_model); + window->set_device_fingerprint(fingerprint); + } + } + if (!trusted_devices_) { + for (auto& window : {ui_->main, ui_->wall}) + window->set_trusted_devices_error(UiText(localization::Literal( + "无法读取设备身份或可信列表,请修复后再启用。", localization_language_index_))); + } +} + void GuiApplication::SyncServerWindow() { // The connection map is authoritative. Lifecycle flags record callback // intent, but callbacks for different controllers can cross each other, so @@ -4612,6 +4740,11 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) { const auto previous_connection_mode = config_center_->GetConnectionMode(); auto& main = ui_->Window(gallery_window); main->set_settings_save_error(""); + if (main->get_trusted_devices_only() && !trusted_devices_) { + main->set_settings_save_error(UiText(localization::Literal( + "无法读取设备身份或可信列表,请修复后再启用。", localization_language_index_))); + return false; + } const bool has_endpoint = main->get_self_hosted_enabled() || !std::string(main->get_server_host()).empty() || !std::string(main->get_server_port()).empty(); @@ -4771,6 +4904,12 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) { props->preview_needs_sync_ = true; ui_->gallery_synced = {}; } + if (config_center_->SetTrustedDevicesOnly(main->get_trusted_devices_only()) != 0) { + main->set_settings_save_error(UiText(localization::Literal( + "无法保存可信设备设置,请重试。", localization_language_index_))); + return false; + } + TickControllerTrust(); ApplySettingsFromConfig(previous_server != GetSignalServerSettings(*config_center_)); ResetSettingsUi(gallery_window); diff --git a/apps/desktop/src/gui/application/gui_application.h b/apps/desktop/src/gui/application/gui_application.h index 0aa4bcf0..92b0663e 100644 --- a/apps/desktop/src/gui/application/gui_application.h +++ b/apps/desktop/src/gui/application/gui_application.h @@ -54,6 +54,7 @@ private: void ShowMainWindow(const std::string& activation_token = {}); bool MinimizeMainWindowToTray(); void BindMainCallbacks(bool gallery_window = false); + void BindTrustedDeviceCallbacks(bool gallery_window = false); void BindStreamCallbacks(); void BindServerCallbacks(); void Tick(); @@ -92,6 +93,7 @@ private: void SyncStreamKeyboardFocus(); void SetStreamKeyboardFocus(bool focused); void SyncServerWindow(); + void SyncTrustedDevices(); void SyncFileTransferNotifications(); void CancelFileTransfer(uint32_t file_id); void SetServerPanelExpanded(bool expanded); diff --git a/apps/desktop/src/gui/assets/localization/localization_data.h b/apps/desktop/src/gui/assets/localization/localization_data.h index cbb96fea..b813af26 100644 --- a/apps/desktop/src/gui/assets/localization/localization_data.h +++ b/apps/desktop/src/gui/assets/localization/localization_data.h @@ -494,6 +494,31 @@ inline constexpr TranslationRow kTranslationRows[] = { {":同步键鼠到所有已连接设备", {":同步键鼠到所有已连接设备", ": mirror input to all connected devices", ": ввод на всех подключённых устройствах", ":同步鍵鼠到所有已連線裝置", ": 接続されているすべてのデバイスへのミラー入力", ": 연결된 모든 장치에 대한 미러 입력", ": Eingang auf alle angeschlossenen Geräte spiegeln", ": entrée miroir sur tous les appareils connectés", ": entrada espejo para todos los dispositivos conectados", ": entrada espelhada para todos os dispositivos conectados"}}, {"监听所有 IP 接口", {"监听所有 IP 接口", "Listen on all IP interfaces", "Все IP-интерфейсы", "監聽所有 IP 介面", "すべての IP で待ち受け", "모든 IP 인터페이스 수신", "Alle IP-Schnittstellen", "Toutes les interfaces IP", "Todas las interfaces IP", "Todas as interfaces IP"}}, {"所有接口共用一个端口,包括 VPN。分享时请选择实际 IP;关闭后恢复各 IP 的独立端口。", {"所有接口共用一个端口,包括 VPN。分享时请选择实际 IP;关闭后恢复各 IP 的独立端口。", "Use one port on all interfaces, including VPNs. Select an actual IP to share. Turn off to restore separate ports per IP.", "Один порт для всех интерфейсов, включая VPN. Для подключения выберите реальный IP. Отключение вернёт отдельные порты для каждого IP.", "所有介面共用一個連接埠,包括 VPN。分享時請選擇實際 IP;關閉後恢復各 IP 的獨立連接埠。", "VPN を含む全インターフェースで同じポートを使います。共有には実際の IP を選択してください。オフにすると IP ごとのポートに戻ります。", "VPN을 포함한 모든 인터페이스가 하나의 포트를 사용합니다. 공유할 실제 IP를 선택하세요. 끄면 IP별 포트가 복원됩니다.", "Ein Port für alle Schnittstellen, einschließlich VPN. Zum Teilen eine tatsächliche IP wählen. Ausschalten stellt die Ports pro IP wieder her.", "Un port commun à toutes les interfaces, VPN inclus. Choisissez une IP réelle à partager. Désactivez pour rétablir les ports propres à chaque IP.", "Un puerto para todas las interfaces, incluidas las VPN. Seleccione una IP real para compartir. Desactive para restaurar los puertos de cada IP.", "Uma porta para todas as interfaces, incluindo VPNs. Selecione um IP real para compartilhar. Desative para restaurar as portas de cada IP."}}, + {"可信设备", {"可信设备", "Trusted devices", "Доверенные устройства", "可信裝置", "信頼済みデバイス", "신뢰하는 기기", "Vertrauenswürdige Geräte", "Appareils de confiance", "Dispositivos de confianza", "Dispositivos confiáveis"}}, + {"仅允许可信设备控制", {"仅允许可信设备控制", "Only allow trusted devices", "Разрешать только доверенные устройства", "僅允許可信裝置控制", "信頼済みデバイスのみ許可", "신뢰하는 기기만 제어 허용", "Nur vertrauenswürdige Geräte zulassen", "Autoriser uniquement les appareils de confiance", "Permitir solo dispositivos de confianza", "Permitir apenas dispositivos confiáveis"}}, + {"管理可信设备", {"管理可信设备", "Manage trusted devices", "Управление доверенными устройствами", "管理可信裝置", "信頼済みデバイスを管理", "신뢰하는 기기 관리", "Vertrauenswürdige Geräte verwalten", "Gérer les appareils de confiance", "Administrar dispositivos de confianza", "Gerenciar dispositivos confiáveis"}}, + {"可添加的已连接设备", {"可添加的已连接设备", "Connected devices available to trust", "Подключённые устройства для добавления", "可新增的已連線裝置", "追加可能な接続中のデバイス", "추가 가능한 연결된 기기", "Verbundene Geräte zum Hinzufügen", "Appareils connectés à ajouter", "Dispositivos conectados que puedes añadir", "Dispositivos conectados disponíveis"}}, + {"信任此设备", {"信任此设备", "Trust this device", "Доверять устройству", "信任此裝置", "このデバイスを信頼", "이 기기 신뢰", "Diesem Gerät vertrauen", "Faire confiance à cet appareil", "Confiar en este dispositivo", "Confiar neste dispositivo"}}, + {"移除信任", {"移除信任", "Remove trust", "Отозвать доверие", "移除信任", "信頼を解除", "신뢰 해제", "Vertrauen entfernen", "Retirer la confiance", "Retirar confianza", "Remover confiança"}}, + {"无法读取设备身份或可信列表,请修复后再启用。", {"无法读取设备身份或可信列表,请修复后再启用。", "Could not load the device identity or trusted list. Repair the local data before enabling this restriction.", "Не удалось загрузить идентификатор устройства или список доверия. Восстановите локальные данные перед включением ограничения.", "無法讀取裝置身分或可信清單,請修復後再啟用。", "デバイス情報または信頼リストを読み込めません。ローカルデータを修復してから制限を有効にしてください。", "기기 ID 또는 신뢰 목록을 불러올 수 없습니다. 로컬 데이터를 복구한 후 제한을 켜세요.", "Geräteidentität oder Vertrauensliste konnte nicht geladen werden. Lokale Daten vor dem Aktivieren reparieren.", "Impossible de charger l’identité ou la liste de confiance. Réparez les données locales avant d’activer la restriction.", "No se pudo cargar la identidad o la lista de confianza. Repara los datos locales antes de activar la restricción.", "Não foi possível carregar a identidade ou a lista confiável. Repare os dados locais antes de ativar a restrição."}}, + {"无法保存可信设备设置,请重试。", {"无法保存可信设备设置,请重试。", "Could not save trusted device settings. Try again.", "Не удалось сохранить настройки доверенных устройств. Повторите попытку.", "無法儲存可信裝置設定,請重試。", "信頼済みデバイスの設定を保存できません。再試行してください。", "신뢰하는 기기 설정을 저장할 수 없습니다. 다시 시도하세요.", "Einstellungen für vertrauenswürdige Geräte konnten nicht gespeichert werden. Erneut versuchen.", "Impossible d’enregistrer les paramètres de confiance. Réessayez.", "No se pudo guardar la configuración de confianza. Inténtalo de nuevo.", "Não foi possível salvar as configurações de confiança. Tente novamente."}}, + {"粘贴", {"粘贴", "Paste", "Вставить", "貼上", "貼り付け", "붙여넣기", "Einfügen", "Coller", "Pegar", "Colar"}}, + {"粘贴控制端的配对信息", {"粘贴控制端的配对信息", "Paste the controller’s pairing information", "Вставьте данные сопряжения управляющего устройства", "貼上控制端的配對資訊", "操作側のペアリング情報を貼り付け", "제어 장치의 페어링 정보 붙여넣기", "Kopplungsdaten des steuernden Geräts einfügen", "Collez les informations d’association de l’appareil de contrôle", "Pegue la información de emparejamiento del dispositivo de control", "Cole as informações de pareamento do dispositivo controlador"}}, + {"已在可信列表中", {"已在可信列表中", "Already trusted", "Уже доверенное", "已在可信清單中", "信頼済み", "이미 신뢰하는 장치", "Bereits vertrauenswürdig", "Déjà approuvé", "Ya es de confianza", "Já é confiável"}}, + {"无法访问剪贴板,请重试。", {"无法访问剪贴板,请重试。", "Cannot access the clipboard. Please try again.", "Не удалось получить доступ к буферу обмена. Повторите попытку.", "無法存取剪貼簿,請重試。", "クリップボードにアクセスできません。もう一度お試しください。", "클립보드에 접근할 수 없습니다. 다시 시도하세요.", "Kein Zugriff auf die Zwischenablage. Bitte erneut versuchen.", "Impossible d’accéder au presse-papiers. Réessayez.", "No se puede acceder al portapapeles. Inténtelo de nuevo.", "Não foi possível acessar a área de transferência. Tente novamente."}}, + {"配对信息无效,请从控制端重新复制。", {"配对信息无效,请从控制端重新复制。", "Invalid pairing information. Copy it again from the controller.", "Недопустимые данные сопряжения. Скопируйте их заново с управляющего устройства.", "配對資訊無效,請從控制端重新複製。", "ペアリング情報が無効です。操作側からコピーし直してください。", "유효하지 않은 페어링 정보입니다. 제어 장치에서 다시 복사하세요.", "Ungültige Kopplungsdaten. Kopieren Sie sie erneut vom steuernden Gerät.", "Informations d’association invalides. Copiez-les à nouveau depuis l’appareil de contrôle.", "Información de emparejamiento no válida. Cópiela de nuevo del dispositivo de control.", "Informações de pareamento inválidas. Copie-as novamente do dispositivo controlador."}}, + {"已加入可信设备。", {"已加入可信设备。", "Device added to the trusted list.", "Устройство добавлено в список доверенных.", "已加入可信裝置。", "信頼済みデバイスに追加しました。", "신뢰 장치 목록에 추가했습니다.", "Gerät zur Vertrauensliste hinzugefügt.", "Appareil ajouté à la liste de confiance.", "Dispositivo añadido a la lista de confianza.", "Dispositivo adicionado à lista de confiança."}}, + {"无法更新可信设备,请检查列表容量和存储权限后重试。", {"无法更新可信设备,请检查列表容量和存储权限后重试。", "Cannot update trusted devices. Check the list capacity and storage permissions, then try again.", "Не удалось обновить список доверенных устройств. Проверьте его размер и права доступа к хранилищу.", "無法更新可信裝置,請檢查清單容量和儲存權限後重試。", "信頼済みデバイスを更新できません。リストの上限と保存先の権限を確認してください。", "신뢰 장치를 업데이트할 수 없습니다. 목록 용량과 저장 권한을 확인한 후 다시 시도하세요.", "Vertrauensliste konnte nicht aktualisiert werden. Prüfen Sie die Listenbegrenzung und Speicherberechtigungen.", "Impossible de mettre à jour les appareils de confiance. Vérifiez la capacité de la liste et les autorisations de stockage.", "No se pueden actualizar los dispositivos de confianza. Compruebe la capacidad de la lista y los permisos de almacenamiento.", "Não foi possível atualizar os dispositivos confiáveis. Verifique a capacidade da lista e as permissões de armazenamento."}}, + {"复制配对信息", {"复制配对信息", "Copy pairing info", "Копировать данные", "複製配對資訊", "ペアリング情報をコピー", "페어링 정보 복사", "Kopplungsdaten kopieren", "Copier les informations", "Copiar información", "Copiar informações"}}, + {"添加可信设备", {"添加可信设备", "Add a trusted device", "Добавить доверенное устройство", "新增可信裝置", "信頼するデバイスを追加", "신뢰 장치 추가", "Vertrauenswürdiges Gerät hinzufügen", "Ajouter un appareil de confiance", "Añadir dispositivo de confianza", "Adicionar dispositivo confiável"}}, + {"已信任的设备", {"已信任的设备", "Trusted devices", "Доверенные устройства", "已信任的裝置", "信頼済みデバイス", "신뢰하는 장치", "Vertrauenswürdige Geräte", "Appareils de confiance", "Dispositivos de confianza", "Dispositivos confiáveis"}}, + {"本机配对信息", {"本机配对信息", "This device’s pairing info", "Данные этого устройства", "本機配對資訊", "このデバイスのペアリング情報", "이 장치의 페어링 정보", "Kopplungsdaten dieses Geräts", "Informations de cet appareil", "Información de este dispositivo", "Informações deste dispositivo"}}, + {"查看指纹", {"查看指纹", "View fingerprint", "Показать отпечаток", "查看指紋", "フィンガープリントを表示", "지문 보기", "Fingerabdruck anzeigen", "Afficher l’empreinte", "Ver huella", "Ver impressão digital"}}, + {"收起指纹", {"收起指纹", "Hide fingerprint", "Скрыть отпечаток", "收起指紋", "フィンガープリントを隠す", "지문 숨기기", "Fingerabdruck ausblenden", "Masquer l’empreinte", "Ocultar huella", "Ocultar impressão digital"}}, + {"暂无可信设备", {"暂无可信设备", "No trusted devices", "Нет доверенных устройств", "暫無可信裝置", "信頼済みデバイスはありません", "신뢰하는 장치 없음", "Keine vertrauenswürdigen Geräte", "Aucun appareil de confiance", "No hay dispositivos de confianza", "Nenhum dispositivo confiável"}}, + {"请核对控制端指纹。", {"请核对控制端指纹。", "Check the fingerprint on the controller.", "Сверьте отпечаток с управляющим устройством.", "請核對控制端指紋。", "操作側のフィンガープリントと照合してください。", "제어 장치의 지문과 대조하세요.", "Mit dem Fingerabdruck des steuernden Geräts vergleichen.", "Vérifiez l’empreinte sur l’appareil de contrôle.", "Verifique la huella en el dispositivo de control.", "Confira a impressão digital no dispositivo controlador."}}, + {"配对信息已复制。", {"配对信息已复制。", "Pairing information copied.", "Данные сопряжения скопированы.", "配對資訊已複製。", "ペアリング情報をコピーしました。", "페어링 정보를 복사했습니다.", "Kopplungsdaten kopiert.", "Informations d’association copiées.", "Información de emparejamiento copiada.", "Informações de pareamento copiadas."}}, + {"无法复制配对信息,请重试。", {"无法复制配对信息,请重试。", "Unable to copy pairing information. Please try again.", "Не удалось скопировать данные сопряжения. Повторите попытку.", "無法複製配對資訊,請重試。", "ペアリング情報をコピーできません。もう一度お試しください。", "페어링 정보를 복사할 수 없습니다. 다시 시도하세요.", "Kopplungsdaten konnten nicht kopiert werden. Bitte erneut versuchen.", "Impossible de copier les informations d’association. Réessayez.", "No se pudo copiar la información de emparejamiento. Inténtelo de nuevo.", "Não foi possível copiar as informações de pareamento. Tente novamente."}}, }; struct NamedTranslation { const char* name; const char* literal; }; diff --git a/apps/desktop/src/gui/ui/crossdesk_ui.slint b/apps/desktop/src/gui/ui/crossdesk_ui.slint index 2302dcb5..8d825d74 100644 --- a/apps/desktop/src/gui/ui/crossdesk_ui.slint +++ b/apps/desktop/src/gui/ui/crossdesk_ui.slint @@ -12,3 +12,5 @@ export { GalleryStrings } from "device_strings.slint"; export { LanStrings } from "lan_strings.slint"; export { GalleryNetworkDevice } from "gallery_network_dialog.slint"; export { DeviceGroupOption } from "device_group_dialog.slint"; + +export { TrustedDeviceEntry, TrustStrings } from "trusted_devices.slint"; diff --git a/apps/desktop/src/gui/ui/main_window.slint b/apps/desktop/src/gui/ui/main_window.slint index ab823f9b..fc40bbb1 100644 --- a/apps/desktop/src/gui/ui/main_window.slint +++ b/apps/desktop/src/gui/ui/main_window.slint @@ -1,4 +1,5 @@ import { SurfaceSeparator, FlatSurface, UiSurface } from "ui_surface.slint"; +import { TrustedDevicesDialog, TrustedDeviceEntry, TrustStrings } from "trusted_devices.slint"; import { LanStrings } from "lan_strings.slint"; import { AboutPanel, AboutPage, AboutComponent, AboutStrings, AboutNavigationRow, AboutNavigationGroup, AboutRowSeparator } from "about_panel.slint"; import { UiRadius } from "ui_radius.slint"; @@ -660,6 +661,17 @@ export component MainWindow inherits Window { in-out property privacy-local-unlock-allowed: true; in property lock-on-disconnect-available: false; in-out property lock-on-disconnect-enabled: true; + in-out property trusted-devices-only: false; + in-out property trusted-devices-open: false; + in property <[TrustedDeviceEntry]> trusted-devices; + in property <[TrustedDeviceEntry]> trust-candidates; + in property device-fingerprint; + in property trusted-devices-error; + in property trusted-devices-notice; + in property trust-copy-error; + in property trust-copy-notice; + in-out property trust-pairing-input; + callback trust-action(string, string, string); in-out property file-save-path: ""; in-out property server-host: ""; in-out property server-port: ""; @@ -1087,7 +1099,7 @@ export component MainWindow inherits Window { if event.text == Key.Escape { root.messages-open = false; } return accept; } - if root.settings-open && !root.self-host-settings-open { + if root.settings-open && !root.self-host-settings-open && !root.trusted-devices-open { if event.text == Key.Escape { root.cancel-settings(); root.settings-open = false; } return accept; } @@ -2758,10 +2770,20 @@ export component MainWindow inherits Window { font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap; } SettingsGroup { - title: GalleryStrings.texts[120]; rows: 3; + title: GalleryStrings.texts[120]; rows: 5; SettingsToggleRow { label: UiStrings.privacy-on-connect; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; } SettingsToggleRow { label: UiStrings.privacy-local-unlock; checked <=> root.privacy-local-unlock-allowed; enabled: root.privacy-setting-available; } - SettingsToggleRow { label: UiStrings.lock-on-disconnect; checked <=> root.lock-on-disconnect-enabled; enabled: root.lock-on-disconnect-available; divider: false; } + SettingsToggleRow { label: UiStrings.lock-on-disconnect; checked <=> root.lock-on-disconnect-enabled; enabled: root.lock-on-disconnect-available; } + SettingsToggleRow { label: TrustStrings.only; checked <=> root.trusted-devices-only; } + SettingsRow { + label: TrustStrings.title; divider: false; + SettingsButton { + x: parent.width - self.width - 12px; y: 4px; + width: max(104px, self.label-width); height: 28px; + text: TrustStrings.manage; + clicked => { root.trusted-devices-open = true; root.trust-action("open", "", ""); } + } + } } Text { text: GalleryStrings.texts[121]; @@ -2871,6 +2893,20 @@ export component MainWindow inherits Window { } } + if root.trusted-devices-open: TrustedDevicesDialog { + z: 51; + devices: root.trusted-devices; + candidates: root.trust-candidates; + local-fingerprint: root.device-fingerprint; + error: root.trusted-devices-error; + notice: root.trusted-devices-notice; + copy-error: root.trust-copy-error; + copy-notice: root.trust-copy-notice; + pairing-input <=> root.trust-pairing-input; + close => { root.trust-action("close", "", ""); root.trusted-devices-open = false; } + action(action, id, key) => { root.trust-action(action, id, key); } + } + if root.self-host-settings-open: Rectangle { background: #15223835; z: 49; diff --git a/apps/desktop/src/gui/ui/settings_controls.slint b/apps/desktop/src/gui/ui/settings_controls.slint index 6f046050..6d951ba8 100644 --- a/apps/desktop/src/gui/ui/settings_controls.slint +++ b/apps/desktop/src/gui/ui/settings_controls.slint @@ -6,11 +6,13 @@ import { WorkbenchStyle } from "workbench_controls.slint"; // Shared visual treatment for the global settings pages. export component SettingsButton inherits FlatSurface { in property text; + in property text-size: 12px; in property icon; in property primary: false; in property quiet: false; in property align-left: false; in property enabled: true; + in property destructive: false; out property label-width: button-label.preferred-width + 2 * root.content-padding + (root.has-icon ? 24px : 0px); private property has-icon: root.icon.width > 0; private property content-padding: root.has-icon ? 8px : 12px; @@ -18,7 +20,8 @@ export component SettingsButton inherits FlatSurface { width: 88px; height: 32px; border-radius: UiRadius.control; background: !root.enabled ? ControlStyle.disabled : root.primary ? (touch.pressed ? #1e4fa3 : touch.has-hover ? #2866c9 : #3275dc) - : touch.pressed ? ButtonStyle.pressed : touch.has-hover ? ButtonStyle.hover + : touch.pressed ? (root.destructive ? #f6babb : ButtonStyle.pressed) + : touch.has-hover ? (root.destructive ? #fbe0e0 : ButtonStyle.hover) : root.quiet ? transparent : ButtonStyle.surface; outline-enabled: !root.quiet && !root.primary; outline-color: !root.enabled ? ButtonStyle.disabled-border @@ -44,8 +47,9 @@ export component SettingsButton inherits FlatSurface { button-label := Text { x: root.has-icon ? 24px : 0px; width: parent.width - self.x; height: parent.height; - text: root.text; font-size: 12px; font-weight: root.primary ? 600 : 400; - color: !root.enabled ? #98a2b3 : root.primary ? white : root.quiet ? #65758b : #3c4d63; + text: root.text; font-size: root.text-size; font-weight: root.primary ? 600 : 400; + color: !root.enabled ? #98a2b3 : root.primary ? white + : root.destructive ? #c43c43 : root.quiet ? #65758b : #3c4d63; horizontal-alignment: root.align-left ? left : center; vertical-alignment: center; overflow: elide; } } diff --git a/apps/desktop/src/gui/ui/trusted_devices.slint b/apps/desktop/src/gui/ui/trusted_devices.slint new file mode 100644 index 00000000..93c97179 --- /dev/null +++ b/apps/desktop/src/gui/ui/trusted_devices.slint @@ -0,0 +1,257 @@ +import { SettingsDialog, SettingsButton } from "settings_controls.slint"; +import { WorkbenchStyle } from "workbench_controls.slint"; +import { ScrollView } from "std-widgets.slint"; +import { FlatSurface, UiSurface } from "ui_surface.slint"; +import { UiRadius } from "ui_radius.slint"; +import { RoundedLineEdit } from "rounded_line_edit.slint"; + +export struct TrustedDeviceEntry { + remote-id: string, + public-key: string, + display-name: string, + device-id: string, + fingerprint: string, +} + +export global TrustStrings { + in property title: "Trusted devices"; + in property only: "Only allow trusted devices"; + in property manage: "Manage"; + in property copy: "Copy pairing info"; + in property import-title: "Add a trusted device"; + in property add: "Add"; + in property placeholder: "Paste the controller's pairing information"; + in property confirm: "Check the fingerprint on the controller."; + in property empty: "No trusted devices"; + in property candidates: "Connected devices available to trust"; + in property trust: "Trust"; + in property remove: "Remove"; + in property saved: "Trusted devices"; + in property sharing: "This device’s pairing info"; + in property show-fingerprint: "View fingerprint"; + in property hide-fingerprint: "Hide fingerprint"; + in property close: "Close"; +} + +component FingerprintBlock inherits Rectangle { + in property fingerprint; + background: #f0f4fa; border-radius: UiRadius.control; + height: content.preferred-height; + content := VerticalLayout { + width: parent.width; height: self.preferred-height; + padding: 10px; + Text { text: root.fingerprint; color: #465a76; font-size: 11px; wrap: word-wrap; } + } +} + +component DeviceRow inherits FlatSurface { + in property device; + in property action-text; + in property confirming: false; + private property fingerprint-open: false; + callback action; + height: content.preferred-height; + background: white; outline-color: UiSurface.edge; + border-radius: UiRadius.control; + content := VerticalLayout { + width: parent.width; height: self.preferred-height; + padding: 12px; spacing: 8px; + padding-top: root.confirming ? 12px : 8px; + padding-bottom: root.confirming ? 12px : 8px; + HorizontalLayout { + spacing: 6px; + VerticalLayout { + spacing: 2px; horizontal-stretch: 1; min-width: 0px; + Text { + text: root.device.display-name; + font-size: root.confirming ? 13px : 12px; font-weight: root.confirming ? 600 : 500; + color: WorkbenchStyle.text; overflow: elide; + } + Text { + text: "ID: " + (root.device.device-id == "" ? "—" : root.device.device-id); + font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; + } + } + if !root.confirming: SettingsButton { + width: min(144px, self.label-width); height: 28px; quiet: true; + text-size: 11px; + text: root.fingerprint-open ? TrustStrings.hide-fingerprint : TrustStrings.show-fingerprint; + clicked => { root.fingerprint-open = !root.fingerprint-open; } + } + if !root.confirming: SettingsButton { + width: min(128px, max(64px, self.label-width)); height: 28px; + text-size: 11px; + quiet: true; text: root.action-text; + destructive: true; + clicked => { root.action(); } + } + } + if root.confirming || root.fingerprint-open: FingerprintBlock { + fingerprint: root.device.fingerprint; + } + if root.confirming: HorizontalLayout { + spacing: 12px; + Text { + text: TrustStrings.confirm; font-size: 11px; + color: WorkbenchStyle.muted; wrap: word-wrap; + horizontal-stretch: 1; min-width: 0px; vertical-alignment: center; + } + SettingsButton { + width: min(parent.width * 0.6, max(112px, self.label-width)); height: 30px; + primary: true; text: root.action-text; + clicked => { root.action(); } + } + } + } +} + +export component TrustedDevicesDialog inherits Rectangle { + in property <[TrustedDeviceEntry]> devices; + in property <[TrustedDeviceEntry]> candidates; + in property local-fingerprint; + in property error; + in property notice; + in property copy-error; + in property copy-notice; + in-out property pairing-input; + private property local-fingerprint-open: false; + callback close; + callback action(string, string, string); + background: #15223835; + TouchArea { } + forward-focus: focus; + focus := FocusScope { + key-pressed(event) => { + if event.text == Key.Escape { root.close(); return accept; } + return reject; + } + } + dialog := SettingsDialog { + x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; + width: min(540px, parent.width - 32px); + height: min(400px, parent.height - 32px); + min-width: 0px; max-width: parent.width - 32px; + min-height: 0px; max-height: parent.height - 32px; + title: TrustStrings.title; close-label: TrustStrings.close; + close => { root.close(); } + ScrollView { + x: 24px; y: 64px; width: parent.width - 48px; height: parent.height - 84px; + min-width: 0px; min-height: 0px; + horizontal-scrollbar-policy: always-off; + viewport-width: self.visible-width; + viewport-height: content.preferred-height; + content := VerticalLayout { + width: parent.visible-width - 8px; height: self.preferred-height; + spacing: 16px; padding: 0px; alignment: start; + FlatSurface { + background: #f1f6ff; outline-color: #d7e3f7; border-radius: UiRadius.surface; + height: add-content.preferred-height; + add-content := VerticalLayout { + width: parent.width; height: self.preferred-height; padding: 14px; spacing: 10px; + Text { + text: TrustStrings.import-title; font-size: 14px; font-weight: 600; + color: WorkbenchStyle.text; min-width: 0px; wrap: word-wrap; + } + HorizontalLayout { + spacing: 8px; + RoundedLineEdit { + horizontal-stretch: 1; height: 32px; text <=> root.pairing-input; + placeholder-text: TrustStrings.placeholder; surrounding-background: #f1f6ff; + edited => { root.action("edit", "", ""); } + accepted => { root.action("import", "", ""); } + } + SettingsButton { + width: max(64px, self.label-width); height: 32px; primary: true; + text: TrustStrings.add; enabled: root.pairing-input != "" && root.local-fingerprint != ""; + clicked => { root.action("import", "", ""); } + } + } + if root.error != "": Text { + text: root.error; color: #b33c39; font-size: 11px; wrap: word-wrap; + } + if root.notice != "": Text { + text: root.notice; color: #28734a; font-size: 11px; wrap: word-wrap; + } + } + } + VerticalLayout { + spacing: 6px; + Rectangle { + // Align mixed font sizes by their baselines rather than their top edges. + private property baseline: max(saved-title.font-metrics.ascent, saved-count.font-metrics.ascent); + height: max(saved-title.y + saved-title.preferred-height, saved-count.y + saved-count.preferred-height); + saved-title := Text { + x: 0px; width: self.preferred-width; height: self.preferred-height; + y: parent.baseline - self.font-metrics.ascent; + text: TrustStrings.saved; font-size: 13px; font-weight: 600; color: WorkbenchStyle.text; + } + saved-count := Text { + x: saved-title.preferred-width + 6px; + width: self.preferred-width; height: self.preferred-height; + y: parent.baseline - self.font-metrics.ascent; + text: "" + root.devices.length; font-size: 12px; color: WorkbenchStyle.muted; + } + } + if root.devices.length == 0: FlatSurface { + height: 48px; background: white; + outline-color: UiSurface.edge; border-radius: UiRadius.control; + Text { + x: 14px; width: parent.width - 28px; height: parent.height; + text: TrustStrings.empty; font-size: 12px; color: WorkbenchStyle.muted; + horizontal-alignment: center; vertical-alignment: center; wrap: word-wrap; + } + } + for device in root.devices: DeviceRow { + device: device; action-text: TrustStrings.remove; + action => { root.action("remove", device.remote-id, device.public-key); } + } + } + if root.candidates.length > 0: VerticalLayout { + spacing: 8px; + Text { text: TrustStrings.candidates; font-size: 13px; font-weight: 600; color: WorkbenchStyle.text; wrap: word-wrap; } + for device in root.candidates: DeviceRow { + device: device; confirming: true; action-text: TrustStrings.trust; + action => { root.action("add", device.remote-id, device.public-key); } + } + } + FlatSurface { + height: sharing-content.preferred-height; background: white; + outline-color: UiSurface.edge; border-radius: UiRadius.control; + sharing-content := VerticalLayout { + width: parent.width; height: self.preferred-height; padding: 14px; spacing: 8px; + HorizontalLayout { + spacing: 12px; + Text { + horizontal-stretch: 1; min-width: 0px; + text: TrustStrings.sharing; font-size: 13px; font-weight: 600; + color: WorkbenchStyle.text; wrap: word-wrap; vertical-alignment: center; + } + SettingsButton { + width: min(160px, self.label-width); height: 30px; text: TrustStrings.copy; + enabled: root.local-fingerprint != ""; clicked => { root.action("copy", "", ""); } + } + } + HorizontalLayout { + spacing: 8px; + SettingsButton { + width: self.label-width; height: 22px; quiet: true; align-left: true; + text: root.local-fingerprint-open ? TrustStrings.hide-fingerprint : TrustStrings.show-fingerprint; + enabled: root.local-fingerprint != ""; + clicked => { root.local-fingerprint-open = !root.local-fingerprint-open; } + } + Text { + text: root.copy-error != "" ? root.copy-error : root.copy-notice; + color: root.copy-error != "" ? #b33c39 : #28734a; font-size: 11px; + horizontal-stretch: 1; min-width: 0px; wrap: word-wrap; + horizontal-alignment: right; vertical-alignment: center; + } + } + if root.local-fingerprint-open: FingerprintBlock { + fingerprint: root.local-fingerprint; + } + } + } + } + } + } +} diff --git a/apps/desktop/src/gui/ui/ui_localization.h b/apps/desktop/src/gui/ui/ui_localization.h index aefbd26e..465fc5b6 100644 --- a/apps/desktop/src/gui/ui/ui_localization.h +++ b/apps/desktop/src/gui/ui/ui_localization.h @@ -55,6 +55,24 @@ inline int ApplyMainWindowStrings( languages.push_back(Text(option.display_name)); languages.push_back(Text(localization::Literal("跟随系统", language))); window->set_language_options(std::make_shared>(languages)); + auto& trust = window->global(); + trust.set_title(Text(localization::Literal("可信设备", language))); + trust.set_only(Text(localization::Literal("仅允许可信设备控制", language))); + trust.set_manage(Text(localization::Literal("管理可信设备", language))); + trust.set_copy(Text(localization::Literal("复制配对信息", language))); + trust.set_import_title(Text(localization::Literal("添加可信设备", language))); + trust.set_add(Text(localization::Literal("添加", language))); + trust.set_placeholder(Text(localization::Literal("粘贴控制端的配对信息", language))); + trust.set_confirm(Text(localization::Literal("请核对控制端指纹。", language))); + trust.set_saved(Text(localization::Literal("已信任的设备", language))); + trust.set_sharing(Text(localization::Literal("本机配对信息", language))); + trust.set_show_fingerprint(Text(localization::Literal("查看指纹", language))); + trust.set_hide_fingerprint(Text(localization::Literal("收起指纹", language))); + trust.set_empty(Text(localization::Literal("暂无可信设备", language))); + trust.set_candidates(Text(localization::Literal("可添加的已连接设备", language))); + trust.set_trust(Text(localization::Literal("信任此设备", language))); + trust.set_remove(Text(localization::Literal("移除信任", language))); + trust.set_close(Text(localization::message_close[language])); auto& strings = window->global(); strings.set_window_minimize(Text(localization::window_minimize[language])); strings.set_window_close(Text(localization::window_close[language])); diff --git a/docs/connection-architecture.md b/docs/connection-architecture.md index 1988d75b..d11b2698 100644 --- a/docs/connection-architecture.md +++ b/docs/connection-architecture.md @@ -4,6 +4,8 @@ 本文描述当前已实现的桌面客户端架构,代码依据为 CrossDesk `cc48eb65`、MiniRTC `040114d`,核对日期为 **2026-10-25**。ID 模式的服务端行为按客户端协议说明;服务端内部实现由独立的 CrossDesk Server 项目维护。 +可信设备认证及配对身份文件的说明于 **2026-11-01** 按当前工作区更新;服务器登录沿用原有 ID、密码及重连令牌流程。 + ## 1. 两种模式的边界 两种模式复用桌面采集、编码、音视频传输、键鼠、文件和剪贴板处理。差别主要在**如何找到设备、在哪里验证密码,以及通过什么通道交换连接参数**。 @@ -217,6 +219,24 @@ IP 直连采用 **TLS 内提交密码,由被控端本地验证**: 对应代码:[密码验证](../apps/desktop/src/network/lan/lan_password.cpp)、[本地身份和凭据存储](../apps/desktop/src/network/lan/lan_identity.cpp)、[认证限流](../apps/desktop/src/network/lan/lan_auth_limiter.cpp)、[DTLS/SRTP 就绪处理](../deps/submodules/minirtc/src/transport/ice_transport.cpp)。 +### 4.3 可选可信设备认证 + +可信设备限制默认关闭,认证及授权全部由 CrossDesk 应用层完成。被控端为每次新会话生成 32 字节随机挑战,控制端签署协议域 `crossdesk.device-trust.v1`、挑战随机数、本机公钥、设备 ID 和名称。被控端验证签名后,还必须确认公钥在本地可信名单中,才开放控制权限;相同 ID 或名称不能替代公钥匹配。 + +首次添加支持离线配对:在控制端复制本机配对信息,通过双方认可的渠道交给被控端;被控端粘贴后点击“添加”即校验并写入可信名单,也支持添加本机,无需预览和二次确认。已存在的公钥不会重复添加,完整指纹可在列表中查看。复制结果显示在“本机配对信息”栏目,添加结果显示在添加区域。配对信息只包含公钥和展示信息,不含私钥或密码,也不需要先建立远程连接或关闭可信设备限制。导入仅更新名单,后续连接仍须完成原有密码验证及实时签名认证;配对信息本身不能证明来源,用户通过传递渠道确认其来源。也可以继续从已连接且完成身份验证的控制端中选择添加。 + +挑战有效期为 10 秒,只接受一次成功证明。重复的已连接通知沿用当前挑战及截止时间;断开或重新进入连接阶段时清除认证状态,下次连接重新生成挑战。开启限制时,未完成证明、签名无效或公钥不在名单中的设备不能获得控制权限;不支持该应用层协议的旧客户端同样不能通过。关闭限制时保持原连接行为。 + +MiniRTC 仅通过现有可靠数据通道收发这些应用消息,不提供可信设备专用接口,也不参与名单、签名或授权判断。认证证明不包含 DTLS 连接绑定:随机挑战可防止历史证明重放,但不防护攻击者在两条连接之间实时转发挑战和签名的情形。原有传输加密及其兼容规则仍由第 4.1、4.2 节描述。 + +可信设备配对私钥保存在缓存目录中的 `identity.json`,仅用于控制端与被控端之间的签名认证。服务器登录使用原有 ID、密码缓存及重连令牌,不使用这个私钥。macOS/Linux 文件权限为 `0600`,Windows 使用当前用户 ACL;身份存储通过 `identity.json.lock` 保护读写,并基于最新内容原子更新。锁文件不含凭据,无需备份。 + +可信列表保存在配置目录的 `trusted-devices/devices.json`,新版格式只含可信设备的公钥、名称和 ID,可包含本机。旧格式中的本机私钥会自动迁入 `identity.json`,持久写入成功后才从列表文件移除,因此原有指纹和配对关系保持不变。迁移中断可重试;发现两处密钥冲突时停止加载,不自动覆盖。Windows 无人值守服务使用其独立资料目录中的同一布局。 + +只删除普通 ID/密码缓存不会删除配对私钥,但配对私钥不能恢复服务器分配的 ID;后续身份分配沿用原有缓存流程。删除整个缓存目录会丢失配对私钥,重新生成后需要其他设备重新信任本机的新指纹。备份配对身份应保存 `identity.json`,保留允许控制本机的设备名单还应备份可信列表。身份文件损坏或权限不安全时停止加载;已开启的可信设备限制不会因此自动关闭。 + +对应代码:[设备签名与名单存储](../apps/desktop/src/gui/runtime/trusted_devices.cpp)、[应用层认证及授权](../apps/desktop/src/gui/runtime/device_trust_runtime.cpp)、[连接状态与认证生命周期](../apps/desktop/src/gui/runtime/peer_event_handler.cpp)。 + ## 5. 运行时复用、线程与关闭 ```mermaid diff --git a/localization/catalog.json b/localization/catalog.json index cb281831..27680381 100644 --- a/localization/catalog.json +++ b/localization/catalog.json @@ -10283,6 +10283,294 @@ "fr": "Un port commun à toutes les interfaces, VPN inclus. Choisissez une IP réelle à partager. Désactivez pour rétablir les ports propres à chaque IP.", "es": "Un puerto para todas las interfaces, incluidas las VPN. Seleccione una IP real para compartir. Desactive para restaurar los puertos de cada IP.", "pt-BR": "Uma porta para todas as interfaces, incluindo VPNs. Selecione um IP real para compartilhar. Desative para restaurar as portas de cada IP." + }, + "可信设备": { + "zh-Hans": "可信设备", + "en": "Trusted devices", + "ru": "Доверенные устройства", + "zh-Hant": "可信裝置", + "ja": "信頼済みデバイス", + "ko": "신뢰하는 기기", + "de": "Vertrauenswürdige Geräte", + "fr": "Appareils de confiance", + "es": "Dispositivos de confianza", + "pt-BR": "Dispositivos confiáveis" + }, + "仅允许可信设备控制": { + "zh-Hans": "仅允许可信设备控制", + "en": "Only allow trusted devices", + "ru": "Разрешать только доверенные устройства", + "zh-Hant": "僅允許可信裝置控制", + "ja": "信頼済みデバイスのみ許可", + "ko": "신뢰하는 기기만 제어 허용", + "de": "Nur vertrauenswürdige Geräte zulassen", + "fr": "Autoriser uniquement les appareils de confiance", + "es": "Permitir solo dispositivos de confianza", + "pt-BR": "Permitir apenas dispositivos confiáveis" + }, + "管理可信设备": { + "zh-Hans": "管理可信设备", + "en": "Manage trusted devices", + "ru": "Управление доверенными устройствами", + "zh-Hant": "管理可信裝置", + "ja": "信頼済みデバイスを管理", + "ko": "신뢰하는 기기 관리", + "de": "Vertrauenswürdige Geräte verwalten", + "fr": "Gérer les appareils de confiance", + "es": "Administrar dispositivos de confianza", + "pt-BR": "Gerenciar dispositivos confiáveis" + }, + "可添加的已连接设备": { + "zh-Hans": "可添加的已连接设备", + "en": "Connected devices available to trust", + "ru": "Подключённые устройства для добавления", + "zh-Hant": "可新增的已連線裝置", + "ja": "追加可能な接続中のデバイス", + "ko": "추가 가능한 연결된 기기", + "de": "Verbundene Geräte zum Hinzufügen", + "fr": "Appareils connectés à ajouter", + "es": "Dispositivos conectados que puedes añadir", + "pt-BR": "Dispositivos conectados disponíveis" + }, + "信任此设备": { + "zh-Hans": "信任此设备", + "en": "Trust this device", + "ru": "Доверять устройству", + "zh-Hant": "信任此裝置", + "ja": "このデバイスを信頼", + "ko": "이 기기 신뢰", + "de": "Diesem Gerät vertrauen", + "fr": "Faire confiance à cet appareil", + "es": "Confiar en este dispositivo", + "pt-BR": "Confiar neste dispositivo" + }, + "移除信任": { + "zh-Hans": "移除信任", + "en": "Remove trust", + "ru": "Отозвать доверие", + "zh-Hant": "移除信任", + "ja": "信頼を解除", + "ko": "신뢰 해제", + "de": "Vertrauen entfernen", + "fr": "Retirer la confiance", + "es": "Retirar confianza", + "pt-BR": "Remover confiança" + }, + "无法读取设备身份或可信列表,请修复后再启用。": { + "zh-Hans": "无法读取设备身份或可信列表,请修复后再启用。", + "en": "Could not load the device identity or trusted list. Repair the local data before enabling this restriction.", + "ru": "Не удалось загрузить идентификатор устройства или список доверия. Восстановите локальные данные перед включением ограничения.", + "zh-Hant": "無法讀取裝置身分或可信清單,請修復後再啟用。", + "ja": "デバイス情報または信頼リストを読み込めません。ローカルデータを修復してから制限を有効にしてください。", + "ko": "기기 ID 또는 신뢰 목록을 불러올 수 없습니다. 로컬 데이터를 복구한 후 제한을 켜세요.", + "de": "Geräteidentität oder Vertrauensliste konnte nicht geladen werden. Lokale Daten vor dem Aktivieren reparieren.", + "fr": "Impossible de charger l’identité ou la liste de confiance. Réparez les données locales avant d’activer la restriction.", + "es": "No se pudo cargar la identidad o la lista de confianza. Repara los datos locales antes de activar la restricción.", + "pt-BR": "Não foi possível carregar a identidade ou a lista confiável. Repare os dados locais antes de ativar a restrição." + }, + "无法保存可信设备设置,请重试。": { + "zh-Hans": "无法保存可信设备设置,请重试。", + "en": "Could not save trusted device settings. Try again.", + "ru": "Не удалось сохранить настройки доверенных устройств. Повторите попытку.", + "zh-Hant": "無法儲存可信裝置設定,請重試。", + "ja": "信頼済みデバイスの設定を保存できません。再試行してください。", + "ko": "신뢰하는 기기 설정을 저장할 수 없습니다. 다시 시도하세요.", + "de": "Einstellungen für vertrauenswürdige Geräte konnten nicht gespeichert werden. Erneut versuchen.", + "fr": "Impossible d’enregistrer les paramètres de confiance. Réessayez.", + "es": "No se pudo guardar la configuración de confianza. Inténtalo de nuevo.", + "pt-BR": "Não foi possível salvar as configurações de confiança. Tente novamente." + }, + "粘贴控制端的配对信息": { + "zh-Hans": "粘贴控制端的配对信息", + "en": "Paste the controller’s pairing information", + "ru": "Вставьте данные сопряжения управляющего устройства", + "zh-Hant": "貼上控制端的配對資訊", + "ja": "操作側のペアリング情報を貼り付け", + "ko": "제어 장치의 페어링 정보 붙여넣기", + "de": "Kopplungsdaten des steuernden Geräts einfügen", + "fr": "Collez les informations d’association de l’appareil de contrôle", + "es": "Pegue la información de emparejamiento del dispositivo de control", + "pt-BR": "Cole as informações de pareamento do dispositivo controlador" + }, + "已在可信列表中": { + "zh-Hans": "已在可信列表中", + "en": "Already trusted", + "ru": "Уже доверенное", + "zh-Hant": "已在可信清單中", + "ja": "信頼済み", + "ko": "이미 신뢰하는 장치", + "de": "Bereits vertrauenswürdig", + "fr": "Déjà approuvé", + "es": "Ya es de confianza", + "pt-BR": "Já é confiável" + }, + "无法访问剪贴板,请重试。": { + "zh-Hans": "无法访问剪贴板,请重试。", + "en": "Cannot access the clipboard. Please try again.", + "ru": "Не удалось получить доступ к буферу обмена. Повторите попытку.", + "zh-Hant": "無法存取剪貼簿,請重試。", + "ja": "クリップボードにアクセスできません。もう一度お試しください。", + "ko": "클립보드에 접근할 수 없습니다. 다시 시도하세요.", + "de": "Kein Zugriff auf die Zwischenablage. Bitte erneut versuchen.", + "fr": "Impossible d’accéder au presse-papiers. Réessayez.", + "es": "No se puede acceder al portapapeles. Inténtelo de nuevo.", + "pt-BR": "Não foi possível acessar a área de transferência. Tente novamente." + }, + "配对信息无效,请从控制端重新复制。": { + "zh-Hans": "配对信息无效,请从控制端重新复制。", + "en": "Invalid pairing information. Copy it again from the controller.", + "ru": "Недопустимые данные сопряжения. Скопируйте их заново с управляющего устройства.", + "zh-Hant": "配對資訊無效,請從控制端重新複製。", + "ja": "ペアリング情報が無効です。操作側からコピーし直してください。", + "ko": "유효하지 않은 페어링 정보입니다. 제어 장치에서 다시 복사하세요.", + "de": "Ungültige Kopplungsdaten. Kopieren Sie sie erneut vom steuernden Gerät.", + "fr": "Informations d’association invalides. Copiez-les à nouveau depuis l’appareil de contrôle.", + "es": "Información de emparejamiento no válida. Cópiela de nuevo del dispositivo de control.", + "pt-BR": "Informações de pareamento inválidas. Copie-as novamente do dispositivo controlador." + }, + "已加入可信设备。": { + "zh-Hans": "已加入可信设备。", + "en": "Device added to the trusted list.", + "ru": "Устройство добавлено в список доверенных.", + "zh-Hant": "已加入可信裝置。", + "ja": "信頼済みデバイスに追加しました。", + "ko": "신뢰 장치 목록에 추가했습니다.", + "de": "Gerät zur Vertrauensliste hinzugefügt.", + "fr": "Appareil ajouté à la liste de confiance.", + "es": "Dispositivo añadido a la lista de confianza.", + "pt-BR": "Dispositivo adicionado à lista de confiança." + }, + "无法更新可信设备,请检查列表容量和存储权限后重试。": { + "zh-Hans": "无法更新可信设备,请检查列表容量和存储权限后重试。", + "en": "Cannot update trusted devices. Check the list capacity and storage permissions, then try again.", + "ru": "Не удалось обновить список доверенных устройств. Проверьте его размер и права доступа к хранилищу.", + "zh-Hant": "無法更新可信裝置,請檢查清單容量和儲存權限後重試。", + "ja": "信頼済みデバイスを更新できません。リストの上限と保存先の権限を確認してください。", + "ko": "신뢰 장치를 업데이트할 수 없습니다. 목록 용량과 저장 권한을 확인한 후 다시 시도하세요.", + "de": "Vertrauensliste konnte nicht aktualisiert werden. Prüfen Sie die Listenbegrenzung und Speicherberechtigungen.", + "fr": "Impossible de mettre à jour les appareils de confiance. Vérifiez la capacité de la liste et les autorisations de stockage.", + "es": "No se pueden actualizar los dispositivos de confianza. Compruebe la capacidad de la lista y los permisos de almacenamiento.", + "pt-BR": "Não foi possível atualizar os dispositivos confiáveis. Verifique a capacidade da lista e as permissões de armazenamento." + }, + "复制配对信息": { + "zh-Hans": "复制配对信息", + "en": "Copy pairing info", + "ru": "Копировать данные", + "zh-Hant": "複製配對資訊", + "ja": "ペアリング情報をコピー", + "ko": "페어링 정보 복사", + "de": "Kopplungsdaten kopieren", + "fr": "Copier les informations", + "es": "Copiar información", + "pt-BR": "Copiar informações" + }, + "添加可信设备": { + "zh-Hans": "添加可信设备", + "en": "Add a trusted device", + "ru": "Добавить доверенное устройство", + "zh-Hant": "新增可信裝置", + "ja": "信頼するデバイスを追加", + "ko": "신뢰 장치 추가", + "de": "Vertrauenswürdiges Gerät hinzufügen", + "fr": "Ajouter un appareil de confiance", + "es": "Añadir dispositivo de confianza", + "pt-BR": "Adicionar dispositivo confiável" + }, + "已信任的设备": { + "zh-Hans": "已信任的设备", + "en": "Trusted devices", + "ru": "Доверенные устройства", + "zh-Hant": "已信任的裝置", + "ja": "信頼済みデバイス", + "ko": "신뢰하는 장치", + "de": "Vertrauenswürdige Geräte", + "fr": "Appareils de confiance", + "es": "Dispositivos de confianza", + "pt-BR": "Dispositivos confiáveis" + }, + "本机配对信息": { + "zh-Hans": "本机配对信息", + "en": "This device’s pairing info", + "ru": "Данные этого устройства", + "zh-Hant": "本機配對資訊", + "ja": "このデバイスのペアリング情報", + "ko": "이 장치의 페어링 정보", + "de": "Kopplungsdaten dieses Geräts", + "fr": "Informations de cet appareil", + "es": "Información de este dispositivo", + "pt-BR": "Informações deste dispositivo" + }, + "查看指纹": { + "zh-Hans": "查看指纹", + "en": "View fingerprint", + "ru": "Показать отпечаток", + "zh-Hant": "查看指紋", + "ja": "フィンガープリントを表示", + "ko": "지문 보기", + "de": "Fingerabdruck anzeigen", + "fr": "Afficher l’empreinte", + "es": "Ver huella", + "pt-BR": "Ver impressão digital" + }, + "收起指纹": { + "zh-Hans": "收起指纹", + "en": "Hide fingerprint", + "ru": "Скрыть отпечаток", + "zh-Hant": "收起指紋", + "ja": "フィンガープリントを隠す", + "ko": "지문 숨기기", + "de": "Fingerabdruck ausblenden", + "fr": "Masquer l’empreinte", + "es": "Ocultar huella", + "pt-BR": "Ocultar impressão digital" + }, + "暂无可信设备": { + "zh-Hans": "暂无可信设备", + "en": "No trusted devices", + "ru": "Нет доверенных устройств", + "zh-Hant": "暫無可信裝置", + "ja": "信頼済みデバイスはありません", + "ko": "신뢰하는 장치 없음", + "de": "Keine vertrauenswürdigen Geräte", + "fr": "Aucun appareil de confiance", + "es": "No hay dispositivos de confianza", + "pt-BR": "Nenhum dispositivo confiável" + }, + "请核对控制端指纹。": { + "zh-Hans": "请核对控制端指纹。", + "en": "Check the fingerprint on the controller.", + "ru": "Сверьте отпечаток с управляющим устройством.", + "zh-Hant": "請核對控制端指紋。", + "ja": "操作側のフィンガープリントと照合してください。", + "ko": "제어 장치의 지문과 대조하세요.", + "de": "Mit dem Fingerabdruck des steuernden Geräts vergleichen.", + "fr": "Vérifiez l’empreinte sur l’appareil de contrôle.", + "es": "Verifique la huella en el dispositivo de control.", + "pt-BR": "Confira a impressão digital no dispositivo controlador." + }, + "配对信息已复制。": { + "zh-Hans": "配对信息已复制。", + "en": "Pairing information copied.", + "ru": "Данные сопряжения скопированы.", + "zh-Hant": "配對資訊已複製。", + "ja": "ペアリング情報をコピーしました。", + "ko": "페어링 정보를 복사했습니다.", + "de": "Kopplungsdaten kopiert.", + "fr": "Informations d’association copiées.", + "es": "Información de emparejamiento copiada.", + "pt-BR": "Informações de pareamento copiadas." + }, + "无法复制配对信息,请重试。": { + "zh-Hans": "无法复制配对信息,请重试。", + "en": "Unable to copy pairing information. Please try again.", + "ru": "Не удалось скопировать данные сопряжения. Повторите попытку.", + "zh-Hant": "無法複製配對資訊,請重試。", + "ja": "ペアリング情報をコピーできません。もう一度お試しください。", + "ko": "페어링 정보를 복사할 수 없습니다. 다시 시도하세요.", + "de": "Kopplungsdaten konnten nicht kopiert werden. Bitte erneut versuchen.", + "fr": "Impossible de copier les informations d’association. Réessayez.", + "es": "No se pudo copiar la información de emparejamiento. Inténtelo de nuevo.", + "pt-BR": "Não foi possível copiar as informações de pareamento. Tente novamente." } }, "desktop": { @@ -11261,7 +11549,32 @@ "(仅在交互终端显示)", ":同步键鼠到所有已连接设备", "监听所有 IP 接口", - "所有接口共用一个端口,包括 VPN。分享时请选择实际 IP;关闭后恢复各 IP 的独立端口。" + "所有接口共用一个端口,包括 VPN。分享时请选择实际 IP;关闭后恢复各 IP 的独立端口。", + "可信设备", + "仅允许可信设备控制", + "管理可信设备", + "可添加的已连接设备", + "信任此设备", + "移除信任", + "无法读取设备身份或可信列表,请修复后再启用。", + "无法保存可信设备设置,请重试。", + "粘贴", + "粘贴控制端的配对信息", + "已在可信列表中", + "无法访问剪贴板,请重试。", + "配对信息无效,请从控制端重新复制。", + "已加入可信设备。", + "无法更新可信设备,请检查列表容量和存储权限后重试。", + "复制配对信息", + "添加可信设备", + "已信任的设备", + "本机配对信息", + "查看指纹", + "收起指纹", + "暂无可信设备", + "请核对控制端指纹。", + "配对信息已复制。", + "无法复制配对信息,请重试。" ], "android": [ " · 已完成",