[feat] add mobile privacy controls and Windows secure text input

This commit is contained in:
dijunkun
2026-10-27 02:37:23 +08:00
parent 1e25ec15e1
commit b15925f857
37 changed files with 795 additions and 80 deletions
@@ -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(