mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-28 00:33:06 +08:00
[feat] add mobile privacy controls and Windows secure text input
This commit is contained in:
@@ -21,6 +21,8 @@
|
||||
#include "interactive_desktop.h"
|
||||
#include "service_host.h"
|
||||
#include "unattended_config.h"
|
||||
#include "platform/windows/input/windows_input_injector.h"
|
||||
#include "platform/windows/input/windows_text_input.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -275,11 +277,51 @@ InputOutcome KeyboardController::ApplyRemoteText(const std::string& remote_id,
|
||||
std::lock_guard input_lock(remote_input_mutex_);
|
||||
if (!owner_.IsAuthorizedController(remote_id)) return InputOutcome::ignored;
|
||||
if (!owner_.devices_.SupportsTextInput()) return InputOutcome::failed;
|
||||
#if _WIN32
|
||||
std::vector<INPUT> inputs;
|
||||
if (!BuildWindowsTextInputs(text, inputs)) return InputOutcome::failed;
|
||||
if (owner_.privacy_.Engaged() && !IsWindowsPrivacyDesktopAvailable())
|
||||
owner_.privacy_.SuspendForDesktop();
|
||||
static const bool process_elevated = IsAdministratorProcess();
|
||||
const bool injected = DispatchDesktopInput(
|
||||
[&] {
|
||||
return PreferUserDesktopInput(IsCurrentSessionUserDesktopActive(),
|
||||
owner_.windows_consent_ui_.load(),
|
||||
owner_.local_service_available_.load(),
|
||||
process_elevated);
|
||||
},
|
||||
[&] {
|
||||
const UINT sent = SendWindowsTextInputs(
|
||||
inputs, [](INPUT* batch, UINT count) {
|
||||
return SendInputOnUserDesktop(batch, count);
|
||||
});
|
||||
const DWORD error = GetLastError();
|
||||
const bool blocked =
|
||||
error == ERROR_ACCESS_DENIED || error == ERROR_GEN_FAILURE;
|
||||
// A text commit is a batch: only zero insertion permits a retry.
|
||||
return DesktopInputResult{
|
||||
sent == inputs.size(),
|
||||
sent == 0 && (IsDesktopTransitionInputError(error) || blocked),
|
||||
sent == 0 && blocked};
|
||||
},
|
||||
[&]() -> DesktopInputResult {
|
||||
const auto response = SendCrossDeskSecureDesktopTextInput(text, 1000);
|
||||
const auto json = nlohmann::json::parse(response, nullptr, false);
|
||||
if (json.is_object() && json.value("ok", false)) return {true, false};
|
||||
const std::string error =
|
||||
json.is_object() ? json.value("error", "") : "invalid_response";
|
||||
// No text or raw reply in diagnostics; commits may be passwords.
|
||||
LOG_WARN("Secure desktop text injection failed: error={}", error);
|
||||
return {false, IsDesktopInputSetupPending(error)};
|
||||
});
|
||||
return injected ? InputOutcome::applied : InputOutcome::failed;
|
||||
#else
|
||||
if (!owner_.devices_.SendTextCommand(text)) {
|
||||
LOG_WARN("Failed to inject committed remote text");
|
||||
return InputOutcome::failed;
|
||||
}
|
||||
return InputOutcome::applied;
|
||||
#endif
|
||||
}
|
||||
|
||||
InputOutcome KeyboardController::ApplyRemoteState(const std::string& remote_id,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "windows_input_injector.h"
|
||||
#include "windows_input_marker.h"
|
||||
#include "windows_key_metadata.h"
|
||||
#include "windows_text_input.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
@@ -393,52 +394,17 @@ int PlatformKeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
}
|
||||
|
||||
int PlatformKeyboardCapturer::SendTextCommand(const std::string& text) {
|
||||
if (text.empty() || text.size() > kMaxTextInputBytes ||
|
||||
text.find('\0') != std::string::npos) {
|
||||
return -1;
|
||||
}
|
||||
// Validate the entire UTF-8 commit before injecting any characters.
|
||||
const int byte_count = static_cast<int>(text.size());
|
||||
const int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
|
||||
text.data(), byte_count, nullptr, 0);
|
||||
if (length == 0) return -1;
|
||||
std::wstring characters(length, L'\0');
|
||||
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(),
|
||||
byte_count, characters.data(), length) != length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::vector<INPUT> inputs;
|
||||
inputs.reserve(characters.size() * 2);
|
||||
for (size_t index = 0; index < characters.size(); ++index) {
|
||||
const wchar_t character = characters[index];
|
||||
INPUT input{};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.dwExtraInfo = static_cast<ULONG_PTR>(kInjectedKeyboardInputMarker);
|
||||
if (character == L'\r' || character == L'\n' || character == L'\t') {
|
||||
input.ki.wVk = character == L'\t' ? VK_TAB : VK_RETURN;
|
||||
if (character == L'\r' && index + 1 < characters.size() &&
|
||||
characters[index + 1] == L'\n') {
|
||||
++index;
|
||||
}
|
||||
} else {
|
||||
// VK_PACKET carries UTF-16 units, including both halves of emoji.
|
||||
input.ki.wScan = static_cast<WORD>(character);
|
||||
input.ki.dwFlags = KEYEVENTF_UNICODE;
|
||||
}
|
||||
inputs.push_back(input);
|
||||
input.ki.dwFlags |= KEYEVENTF_KEYUP;
|
||||
inputs.push_back(input);
|
||||
}
|
||||
if (!BuildWindowsTextInputs(text, inputs)) return -1;
|
||||
|
||||
// One batch keeps surrogate pairs and key releases together with the text.
|
||||
const UINT count = static_cast<UINT>(inputs.size());
|
||||
const UINT sent = SendInputOnUserDesktop(inputs.data(), count);
|
||||
const UINT sent = SendWindowsTextInputs(
|
||||
inputs, [](INPUT* batch, UINT size) {
|
||||
return SendInputOnUserDesktop(batch, size);
|
||||
});
|
||||
if (sent != count) {
|
||||
const DWORD error = GetLastError();
|
||||
if (sent < count && sent % 2 != 0) {
|
||||
SendInputOnUserDesktop(inputs[sent]); // Best-effort release.
|
||||
}
|
||||
LOG_WARN(
|
||||
"SendInput failed for committed text, sent={}, expected={}, err={}",
|
||||
sent, count, error);
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-26
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOWS_TEXT_INPUT_H_
|
||||
#define _WINDOWS_TEXT_INPUT_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "remote_action.h"
|
||||
#include "windows_input_marker.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline bool BuildWindowsTextInputs(const std::string& text,
|
||||
std::vector<INPUT>& inputs) {
|
||||
inputs.clear();
|
||||
if (text.empty() || text.size() > kMaxTextInputBytes ||
|
||||
text.find('\0') != std::string::npos) return false;
|
||||
// Validate the entire commit before injecting any characters.
|
||||
const int bytes = static_cast<int>(text.size());
|
||||
const int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
|
||||
text.data(), bytes, nullptr, 0);
|
||||
if (length == 0) return false;
|
||||
std::wstring characters(length, L'\0');
|
||||
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), bytes,
|
||||
characters.data(), length) != length) return false;
|
||||
|
||||
inputs.reserve(characters.size() * 2);
|
||||
for (size_t index = 0; index < characters.size(); ++index) {
|
||||
const wchar_t character = characters[index];
|
||||
INPUT input{};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.dwExtraInfo = static_cast<ULONG_PTR>(kInjectedKeyboardInputMarker);
|
||||
if (character == L'\r' || character == L'\n' || character == L'\t') {
|
||||
input.ki.wVk = character == L'\t' ? VK_TAB : VK_RETURN;
|
||||
if (character == L'\r' && index + 1 < characters.size() &&
|
||||
characters[index + 1] == L'\n') ++index;
|
||||
} else {
|
||||
input.ki.wScan = static_cast<WORD>(character);
|
||||
input.ki.dwFlags = KEYEVENTF_UNICODE;
|
||||
}
|
||||
inputs.push_back(input);
|
||||
input.ki.dwFlags |= KEYEVENTF_KEYUP;
|
||||
inputs.push_back(input);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// A partial batch must never be replayed. Release its final key if necessary,
|
||||
// retaining the original count and error for the desktop-routing decision.
|
||||
template <typename Sender>
|
||||
UINT SendWindowsTextInputs(std::vector<INPUT>& inputs, Sender send) {
|
||||
const UINT count = static_cast<UINT>(inputs.size());
|
||||
SetLastError(ERROR_SUCCESS);
|
||||
const UINT sent = send(inputs.data(), count);
|
||||
if (sent == count) return sent;
|
||||
const DWORD error = GetLastError();
|
||||
if (sent < count && sent % 2 != 0) send(&inputs[sent], 1);
|
||||
SetLastError(error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE);
|
||||
return sent;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-26
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SECURE_TEXT_INPUT_H_
|
||||
#define _SECURE_TEXT_INPUT_H_
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
#include "remote_action.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline constexpr char kSecureTextInputCommandPrefix[] = "text:";
|
||||
// JSON can escape each text byte as six ASCII bytes. Leave room for metadata.
|
||||
inline constexpr size_t kSecureInputCommandBufferBytes =
|
||||
6 * kMaxTextInputBytes + 1024;
|
||||
|
||||
struct SecureTextInputRequest {
|
||||
std::string text;
|
||||
std::string stage;
|
||||
std::string desktop;
|
||||
};
|
||||
|
||||
inline std::string BuildSecureTextInputCommand(
|
||||
const std::string& text, const std::string& stage = {},
|
||||
const std::string& desktop = {}) {
|
||||
return kSecureTextInputCommandPrefix +
|
||||
nlohmann::json{{"text", text}, {"stage", stage},
|
||||
{"desktop", desktop}}.dump();
|
||||
}
|
||||
|
||||
inline bool ParseSecureTextInputCommand(const std::string& command,
|
||||
SecureTextInputRequest& request) {
|
||||
request = {};
|
||||
if (command.size() > kSecureInputCommandBufferBytes ||
|
||||
command.rfind(kSecureTextInputCommandPrefix, 0) != 0) return false;
|
||||
const auto json = nlohmann::json::parse(
|
||||
command.substr(sizeof(kSecureTextInputCommandPrefix) - 1), nullptr, false);
|
||||
if (!json.is_object() || !json.contains("text") ||
|
||||
!json["text"].is_string() || !json.contains("stage") ||
|
||||
!json["stage"].is_string() || !json.contains("desktop") ||
|
||||
!json["desktop"].is_string()) return false;
|
||||
const auto& text = json["text"].get_ref<const std::string&>();
|
||||
const auto& stage = json["stage"].get_ref<const std::string&>();
|
||||
const auto& desktop = json["desktop"].get_ref<const std::string&>();
|
||||
if (text.empty() || text.size() > kMaxTextInputBytes ||
|
||||
text.find('\0') != std::string::npos || stage.size() > 64 ||
|
||||
desktop.size() > 256 || stage.find('\0') != std::string::npos ||
|
||||
desktop.find('\0') != std::string::npos) return false;
|
||||
request = {text, stage, desktop};
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "secure_text_input.h"
|
||||
#include "unattended_config.h"
|
||||
#include "unattended_policy.h"
|
||||
#include "rdp_session_handoff.h"
|
||||
@@ -1421,7 +1422,7 @@ void CrossDeskServiceHost::IpcServerLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
char buffer[1024] = {0};
|
||||
char buffer[kSecureInputCommandBufferBytes] = {0};
|
||||
DWORD bytes_read = 0;
|
||||
DWORD transfer_error = ERROR_SUCCESS;
|
||||
if (pipe_deadline_detail::Transfer(
|
||||
@@ -2235,6 +2236,14 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
|
||||
std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command,
|
||||
DWORD client_session_id,
|
||||
HANDLE client_pipe) {
|
||||
// Password text is case/whitespace sensitive. Parse before normalizing the
|
||||
// legacy command verbs, and never accept the caller's desktop selection.
|
||||
if (command.rfind(kSecureTextInputCommandPrefix, 0) == 0) {
|
||||
SecureTextInputRequest request;
|
||||
if (!ParseSecureTextInputCommand(command, request))
|
||||
return BuildErrorJson("invalid_text_input");
|
||||
return SendSecureDesktopTextInput(client_session_id, request.text);
|
||||
}
|
||||
std::string normalized = ToLower(Trim(command));
|
||||
if (normalized == kRdpHandoffCommand) {
|
||||
bool observed_rdp = false;
|
||||
@@ -2618,6 +2627,16 @@ std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(
|
||||
1000);
|
||||
}
|
||||
|
||||
std::string CrossDeskServiceHost::SendSecureDesktopTextInput(
|
||||
DWORD client_session_id, const std::string& text) {
|
||||
SecureInputTarget target;
|
||||
const auto error = ResolveSecureInputTarget(client_session_id, target);
|
||||
if (!error.empty()) return error;
|
||||
return QueryNamedPipeMessage(
|
||||
GetCrossDeskSecureInputHelperPipeName(target.session_id),
|
||||
BuildSecureTextInputCommand(text, target.stage, target.desktop), 1000);
|
||||
}
|
||||
|
||||
bool InstallCrossDeskService(const std::wstring& binary_path) {
|
||||
SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
|
||||
if (manager == nullptr) {
|
||||
@@ -2892,4 +2911,9 @@ std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel,
|
||||
BuildSecureDesktopMouseIpcCommand(x, y, wheel, flag), timeout_ms);
|
||||
}
|
||||
|
||||
std::string SendCrossDeskSecureDesktopTextInput(const std::string& text,
|
||||
DWORD timeout_ms) {
|
||||
return QueryDesktopInput(BuildSecureTextInputCommand(text), timeout_ms);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -90,6 +90,8 @@ class CrossDeskServiceHost {
|
||||
bool extended = false);
|
||||
std::string SendSecureDesktopMouseInput(DWORD client_session_id,
|
||||
int x, int y, int wheel, int flag);
|
||||
std::string SendSecureDesktopTextInput(DWORD client_session_id,
|
||||
const std::string& text);
|
||||
|
||||
static void WINAPI ServiceMain(DWORD argc, LPWSTR* argv);
|
||||
static BOOL WINAPI ConsoleControlHandler(DWORD control_type);
|
||||
@@ -197,6 +199,8 @@ std::string SendCrossDeskSecureDesktopKeyInput(int key_code, bool is_down,
|
||||
std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel,
|
||||
int flag,
|
||||
DWORD timeout_ms = 1000);
|
||||
std::string SendCrossDeskSecureDesktopTextInput(const std::string& text,
|
||||
DWORD timeout_ms = 1000);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "platform/path_backend.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "secure_text_input.h"
|
||||
#include "secure_desktop_dxgi_capture.h"
|
||||
#include "secure_desktop_frame_pacer.h"
|
||||
#include "secure_desktop_nv12_cache.h"
|
||||
@@ -40,6 +41,7 @@
|
||||
#include "windows_cursor_state.h"
|
||||
#include "cursor_draw.h"
|
||||
#include "windows_input_marker.h"
|
||||
#include "windows_text_input.h"
|
||||
#include "windows_key_metadata.h"
|
||||
#include "../windows_thread_dpi.h"
|
||||
|
||||
@@ -948,6 +950,31 @@ InputInjectionResult InjectKeyboardInput(
|
||||
return BuildInputSuccess(std::move(desktop_switch));
|
||||
}
|
||||
|
||||
InputInjectionResult InjectTextInput(
|
||||
const crossdesk::SecureTextInputRequest& request) {
|
||||
std::vector<INPUT> inputs;
|
||||
if (!crossdesk::BuildWindowsTextInputs(request.text, inputs)) {
|
||||
return BuildInputFailure("invalid_text_input", ERROR_INVALID_PARAMETER,
|
||||
BuildDesktopSwitchDetails(request.stage,
|
||||
request.desktop));
|
||||
}
|
||||
ScopedInteractiveDesktop desktop;
|
||||
DesktopSwitchDetails desktop_switch;
|
||||
if (!EnsureThreadInteractiveDesktopForStage(request.stage, request.desktop,
|
||||
&desktop, &desktop_switch)) {
|
||||
return BuildInputFailure("switch_interactive_desktop_failed", GetLastError(),
|
||||
desktop_switch);
|
||||
}
|
||||
const UINT sent = crossdesk::SendWindowsTextInputs(
|
||||
inputs, [](INPUT* batch, UINT count) {
|
||||
return SendInput(count, batch, sizeof(INPUT));
|
||||
});
|
||||
if (sent != inputs.size()) {
|
||||
return BuildInputFailure("send_input_failed", GetLastError(), desktop_switch);
|
||||
}
|
||||
return BuildInputSuccess(std::move(desktop_switch));
|
||||
}
|
||||
|
||||
void ParseInteractionTail(const std::string& tail,
|
||||
std::string* interactive_stage_out,
|
||||
std::string* interactive_desktop_out) {
|
||||
@@ -1833,6 +1860,21 @@ std::vector<uint8_t> StartSecureDesktopSharedCapture(
|
||||
std::vector<uint8_t> HandleSecureInputHelperCommand(
|
||||
const std::string& command,
|
||||
SecureSharedCaptureState* capture_state) {
|
||||
if (command.rfind(crossdesk::kSecureTextInputCommandPrefix, 0) == 0) {
|
||||
crossdesk::SecureTextInputRequest request;
|
||||
if (!crossdesk::ParseSecureTextInputCommand(command, request))
|
||||
return BuildTextResponseBytes(BuildErrorJson("invalid_text_input"));
|
||||
const auto result = InjectTextInput(request);
|
||||
if (!result.ok) {
|
||||
// Text may contain a login password. Never echo it in logs or replies.
|
||||
LOG_WARN("Secure input helper text injection failed: error={}, code={}, "
|
||||
"stage={}, target={}, current={}",
|
||||
result.error, result.error_code, result.desktop.stage,
|
||||
result.desktop.target_desktop, result.desktop.current_desktop);
|
||||
return BuildTextResponseBytes(BuildInputFailureJson(result).dump());
|
||||
}
|
||||
return BuildTextResponseBytes("{\"ok\":true,\"injected\":\"text\"}");
|
||||
}
|
||||
if (command == "ping") {
|
||||
return BuildTextResponseBytes("{\"ok\":true,\"reply\":\"pong\"}");
|
||||
}
|
||||
@@ -1922,7 +1964,7 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
|
||||
void HandleSecureInputHelperPipeClient(
|
||||
HANDLE pipe, HANDLE event_handle,
|
||||
std::shared_ptr<SecureSharedCaptureState> capture_state) {
|
||||
char buffer[1024] = {0};
|
||||
char buffer[crossdesk::kSecureInputCommandBufferBytes] = {0};
|
||||
DWORD bytes_read = 0;
|
||||
DWORD error = ERROR_SUCCESS;
|
||||
if (crossdesk::pipe_deadline_detail::Transfer(
|
||||
|
||||
Reference in New Issue
Block a user