mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-04 21:12:42 +08:00
[feat] add application-layer trusted device authentication
This commit is contained in:
@@ -36,6 +36,14 @@ ConfigCenter::~ConfigCenter() {}
|
|||||||
int ConfigCenter::Load() {
|
int ConfigCenter::Load() {
|
||||||
SI_Error rc = ini_->LoadFile(config_path_.c_str());
|
SI_Error rc = ini_->LoadFile(config_path_.c_str());
|
||||||
if (rc < 0) {
|
if (rc < 0) {
|
||||||
|
std::error_code error;
|
||||||
|
if (std::filesystem::exists(config_path_, error) || error) {
|
||||||
|
// An unreadable existing configuration must not silently disable access
|
||||||
|
// restrictions or overwrite the user's settings with default values.
|
||||||
|
trusted_devices_only_.store(true);
|
||||||
|
LOG_ERROR("Cannot read existing configuration; restricting incoming connections");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
Save();
|
Save();
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -141,6 +149,9 @@ int ConfigCenter::Load() {
|
|||||||
ini_->GetBoolValue(section_, "privacy_local_unlock_allowed", true));
|
ini_->GetBoolValue(section_, "privacy_local_unlock_allowed", true));
|
||||||
lock_on_disconnect_.store(
|
lock_on_disconnect_.store(
|
||||||
ini_->GetBoolValue(section_, "lock_on_disconnect", true));
|
ini_->GetBoolValue(section_, "lock_on_disconnect", true));
|
||||||
|
trusted_devices_only_.store(
|
||||||
|
ini_->GetValue(section_, "trusted_devices_only", nullptr) != nullptr &&
|
||||||
|
ini_->GetBoolValue(section_, "trusted_devices_only", true));
|
||||||
save_remote_previews_.store(
|
save_remote_previews_.store(
|
||||||
ini_->GetBoolValue(section_, "save_remote_previews", true));
|
ini_->GetBoolValue(section_, "save_remote_previews", true));
|
||||||
portable_service_prompt_suppressed_ =
|
portable_service_prompt_suppressed_ =
|
||||||
@@ -239,6 +250,7 @@ int ConfigCenter::Save() {
|
|||||||
ini_->SetBoolValue(section_, "privacy_local_unlock_allowed",
|
ini_->SetBoolValue(section_, "privacy_local_unlock_allowed",
|
||||||
privacy_local_unlock_allowed_.load());
|
privacy_local_unlock_allowed_.load());
|
||||||
ini_->SetBoolValue(section_, "lock_on_disconnect", lock_on_disconnect_.load());
|
ini_->SetBoolValue(section_, "lock_on_disconnect", lock_on_disconnect_.load());
|
||||||
|
ini_->SetBoolValue(section_, "trusted_devices_only", trusted_devices_only_.load());
|
||||||
ini_->SetBoolValue(section_, "save_remote_previews",
|
ini_->SetBoolValue(section_, "save_remote_previews",
|
||||||
save_remote_previews_.load());
|
save_remote_previews_.load());
|
||||||
ini_->SetBoolValue(section_, "portable_service_prompt_suppressed",
|
ini_->SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||||
@@ -425,6 +437,13 @@ bool ConfigCenter::IsPrivacyLocalUnlockAllowed() const {
|
|||||||
return privacy_local_unlock_allowed_.load();
|
return privacy_local_unlock_allowed_.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int ConfigCenter::SetTrustedDevicesOnly(bool enabled) {
|
||||||
|
if (StoreValues({{"trusted_devices_only", enabled ? "true" : "false"}}) != 0) return -1;
|
||||||
|
trusted_devices_only_.store(enabled);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
bool ConfigCenter::IsTrustedDevicesOnly() const { return trusted_devices_only_.load(); }
|
||||||
|
|
||||||
int ConfigCenter::SetLockOnDisconnect(bool enabled) {
|
int ConfigCenter::SetLockOnDisconnect(bool enabled) {
|
||||||
if (StoreValues({{"lock_on_disconnect", enabled ? "true" : "false"}}) != 0)
|
if (StoreValues({{"lock_on_disconnect", enabled ? "true" : "false"}}) != 0)
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class ConfigCenter {
|
|||||||
int SetPrivacyScreen(bool enable_privacy_screen);
|
int SetPrivacyScreen(bool enable_privacy_screen);
|
||||||
int SetPrivacyLocalUnlock(bool allowed);
|
int SetPrivacyLocalUnlock(bool allowed);
|
||||||
int SetLockOnDisconnect(bool enabled);
|
int SetLockOnDisconnect(bool enabled);
|
||||||
|
int SetTrustedDevicesOnly(bool enabled);
|
||||||
int SetSaveRemotePreviews(bool enabled);
|
int SetSaveRemotePreviews(bool enabled);
|
||||||
int SetPortableServicePromptSuppressed(bool suppressed);
|
int SetPortableServicePromptSuppressed(bool suppressed);
|
||||||
int SetSponsorshipPromptDismissedVersion(const std::string& version);
|
int SetSponsorshipPromptDismissedVersion(const std::string& version);
|
||||||
@@ -80,6 +81,7 @@ class ConfigCenter {
|
|||||||
bool IsEnablePrivacyScreen() const;
|
bool IsEnablePrivacyScreen() const;
|
||||||
bool IsPrivacyLocalUnlockAllowed() const;
|
bool IsPrivacyLocalUnlockAllowed() const;
|
||||||
bool IsLockOnDisconnect() const;
|
bool IsLockOnDisconnect() const;
|
||||||
|
bool IsTrustedDevicesOnly() const;
|
||||||
bool IsSaveRemotePreviews() const;
|
bool IsSaveRemotePreviews() const;
|
||||||
bool IsPortableServicePromptSuppressed() const;
|
bool IsPortableServicePromptSuppressed() const;
|
||||||
std::string GetSponsorshipPromptDismissedVersion() const;
|
std::string GetSponsorshipPromptDismissedVersion() const;
|
||||||
@@ -113,6 +115,7 @@ class ConfigCenter {
|
|||||||
std::atomic<bool> enable_privacy_screen_{true};
|
std::atomic<bool> enable_privacy_screen_{true};
|
||||||
std::atomic<bool> privacy_local_unlock_allowed_{true};
|
std::atomic<bool> privacy_local_unlock_allowed_{true};
|
||||||
std::atomic<bool> lock_on_disconnect_{true};
|
std::atomic<bool> lock_on_disconnect_{true};
|
||||||
|
std::atomic<bool> trusted_devices_only_{false};
|
||||||
std::atomic<bool> save_remote_previews_{true};
|
std::atomic<bool> save_remote_previews_{true};
|
||||||
bool portable_service_prompt_suppressed_ = false;
|
bool portable_service_prompt_suppressed_ = false;
|
||||||
std::string sponsorship_prompt_dismissed_version_;
|
std::string sponsorship_prompt_dismissed_version_;
|
||||||
|
|||||||
@@ -1153,6 +1153,7 @@ int GuiApplication::Run() {
|
|||||||
announcements_.SetStorageDirectory(path_manager_->GetCachePath() /
|
announcements_.SetStorageDirectory(path_manager_->GetCachePath() /
|
||||||
"announcement_reads");
|
"announcement_reads");
|
||||||
config_center_ = std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
|
config_center_ = std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
|
||||||
|
InitializeTrustedDevices(path_manager_->GetConfigPath() / "trusted-devices");
|
||||||
|
|
||||||
LOG_INFO("CrossDesk version: {} (Slint UI)", CROSSDESK_VERSION);
|
LOG_INFO("CrossDesk version: {} (Slint UI)", CROSSDESK_VERSION);
|
||||||
|
|
||||||
@@ -2472,6 +2473,7 @@ void GuiApplication::Tick() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
TickLan();
|
TickLan();
|
||||||
|
TickControllerTrust();
|
||||||
if (LanAvailable()) {
|
if (LanAvailable()) {
|
||||||
const auto address = lan_->identity().settings().value("address", "");
|
const auto address = lan_->identity().settings().value("address", "");
|
||||||
ui_->main->set_lan_listen_all(lan_->identity().listen_all());
|
ui_->main->set_lan_listen_all(lan_->identity().listen_all());
|
||||||
|
|||||||
@@ -99,8 +99,9 @@ void FileTransferManager::ProcessQueue(std::shared_ptr<RemoteSession> props) {
|
|||||||
peer = events ? events->DirectPeer() : nullptr;
|
peer = events ? events->DirectPeer() : nullptr;
|
||||||
}
|
}
|
||||||
auto session = std::make_shared<file_transfer::Session>(
|
auto session = std::make_shared<file_transfer::Session>(
|
||||||
[peer, events, file, props](const char*, const char* data, size_t size) {
|
[this, peer, events, file, props](const char*, const char* data, size_t size) {
|
||||||
if (!peer || !events || (props && props->closing_)) return -1;
|
if (!peer || !events || (props && props->closing_) ||
|
||||||
|
(!props && !owner_.IsAuthorizedController(file.remote_id))) return -1;
|
||||||
auto callback = events->EnterCallback();
|
auto callback = events->EnterCallback();
|
||||||
if (!callback || !events->DataAllowed()) return -1;
|
if (!callback || !events->DataAllowed()) return -1;
|
||||||
const auto wire_id = events->WireId(file.remote_id);
|
const auto wire_id = events->WireId(file.remote_id);
|
||||||
@@ -341,7 +342,8 @@ void FileTransferManager::Receive(const char* data, size_t size,
|
|||||||
(const file_transfer::ReceiveState& progress) mutable {
|
(const file_transfer::ReceiveState& progress) mutable {
|
||||||
UpdateReceiveProgress(progress, notifications, remote);
|
UpdateReceiveProgress(progress, notifications, remote);
|
||||||
});
|
});
|
||||||
entry.session->SetOnSendAck([peer, events, remote](const FileTransferAck& ack) {
|
entry.session->SetOnSendAck([this, peer, events, remote, props](const FileTransferAck& ack) {
|
||||||
|
if (!props && !owner_.IsAuthorizedController(remote)) return -1;
|
||||||
auto callback = events->EnterCallback();
|
auto callback = events->EnterCallback();
|
||||||
if (!callback || !events->DataAllowed()) return -1;
|
if (!callback || !events->DataAllowed()) return -1;
|
||||||
const auto bytes = EncodeFileTransferAck(ack);
|
const auto bytes = EncodeFileTransferAck(ack);
|
||||||
|
|||||||
@@ -443,6 +443,12 @@ void GuiRuntime::CloseConnectionPeer() {
|
|||||||
announcements_.Reset();
|
announcements_.Reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard lock(controller_trust_mutex_);
|
||||||
|
std::erase_if(controller_trust_, [this](const auto& item) {
|
||||||
|
return item.second.events == peer_events_.get();
|
||||||
|
});
|
||||||
|
}
|
||||||
peer_events_->StopAccepting();
|
peer_events_->StopAccepting();
|
||||||
transfers_.Cancel(peer_events_.get());
|
transfers_.Cancel(peer_events_.get());
|
||||||
announcements_.Reset();
|
announcements_.Reset();
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#include "runtime/device_identity_store.h"
|
||||||
|
|
||||||
|
#include <openssl/crypto.h>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "private_file.h"
|
||||||
|
|
||||||
|
namespace crossdesk::device_identity_store {
|
||||||
|
namespace {
|
||||||
|
void Require(bool ok) {
|
||||||
|
if (!ok) throw std::runtime_error("Device identity store unavailable or invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the lock file's inode stable; it contains no credentials.
|
||||||
|
class StoreLock {
|
||||||
|
public:
|
||||||
|
explicit StoreLock(const std::filesystem::path& path) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
handle_ = CreateFileW(path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr,
|
||||||
|
OPEN_ALWAYS, FILE_FLAG_OPEN_REPARSE_POINT, nullptr);
|
||||||
|
BY_HANDLE_FILE_INFORMATION info{};
|
||||||
|
if (handle_ == INVALID_HANDLE_VALUE || !GetFileInformationByHandle(handle_, &info) ||
|
||||||
|
(info.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT))) {
|
||||||
|
if (handle_ != INVALID_HANDLE_VALUE) CloseHandle(handle_);
|
||||||
|
throw std::runtime_error("Device identity store unavailable");
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
handle_ = open(path.c_str(), O_CREAT | O_RDWR | O_NOFOLLOW | O_CLOEXEC, 0600);
|
||||||
|
struct stat info{};
|
||||||
|
if (handle_ < 0 || fstat(handle_, &info) != 0 || !S_ISREG(info.st_mode) ||
|
||||||
|
info.st_uid != getuid() || (info.st_mode & 0077) != 0 ||
|
||||||
|
flock(handle_, LOCK_EX | LOCK_NB) != 0) {
|
||||||
|
if (handle_ >= 0) close(handle_);
|
||||||
|
throw std::runtime_error("Device identity store unavailable");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
~StoreLock() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
CloseHandle(handle_);
|
||||||
|
#else
|
||||||
|
close(handle_);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
StoreLock(const StoreLock&) = delete;
|
||||||
|
StoreLock& operator=(const StoreLock&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
#ifdef _WIN32
|
||||||
|
HANDLE handle_ = INVALID_HANDLE_VALUE;
|
||||||
|
#else
|
||||||
|
int handle_ = -1;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PrivateText {
|
||||||
|
std::string value;
|
||||||
|
~PrivateText() { OPENSSL_cleanse(value.data(), value.size()); }
|
||||||
|
};
|
||||||
|
|
||||||
|
void Validate(const nlohmann::json& document) {
|
||||||
|
Require(document.is_object() && document.at("version") == 1);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void Update(const std::filesystem::path& cache,
|
||||||
|
const std::function<void(nlohmann::json&)>& update) {
|
||||||
|
private_file::EnsureDirectory(cache, true);
|
||||||
|
StoreLock lock(cache / "identity.json.lock");
|
||||||
|
const auto path = cache / "identity.json";
|
||||||
|
const bool exists = std::filesystem::symlink_status(path).type() !=
|
||||||
|
std::filesystem::file_type::not_found;
|
||||||
|
nlohmann::json saved = {{"version", 1}};
|
||||||
|
if (exists) {
|
||||||
|
PrivateText text{private_file::Read(path)};
|
||||||
|
saved = nlohmann::json::parse(text.value, nullptr, false);
|
||||||
|
Require(!saved.is_discarded());
|
||||||
|
}
|
||||||
|
Validate(saved);
|
||||||
|
PrivateText before{saved.dump()};
|
||||||
|
update(saved);
|
||||||
|
Validate(saved);
|
||||||
|
PrivateText after{saved.dump()};
|
||||||
|
Require(after.value.size() <= 64 * 1024);
|
||||||
|
if (!exists || before.value != after.value) private_file::Write(path, after.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk::device_identity_store
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2026-11-01
|
||||||
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _DEVICE_IDENTITY_STORE_H_
|
||||||
|
#define _DEVICE_IDENTITY_STORE_H_
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <functional>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
namespace crossdesk::device_identity_store {
|
||||||
|
|
||||||
|
// The trusted-device pairing key lives in cache/identity.json. Read the latest
|
||||||
|
// document under a process lock, apply an update, then atomically persist it.
|
||||||
|
// Throw on corruption, unsafe permissions, lock contention or persistence error.
|
||||||
|
void Update(const std::filesystem::path& cache,
|
||||||
|
const std::function<void(nlohmann::json&)>& update);
|
||||||
|
|
||||||
|
} // namespace crossdesk::device_identity_store
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
#include "runtime/gui_runtime.h"
|
||||||
|
|
||||||
|
#include "platform.h"
|
||||||
|
#include "rd_log.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
void GuiRuntime::InitializeTrustedDevices(const std::filesystem::path& directory) {
|
||||||
|
try {
|
||||||
|
trusted_devices_ = std::make_unique<TrustedDevices>(directory, cache_path_);
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
LOG_ERROR("Cannot load trusted devices: {}", error.what());
|
||||||
|
// The independent persisted switch stays enabled on identity/store failure.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GuiRuntime::TrustedControllerAllowed(const std::string& id) {
|
||||||
|
if (!config_center_ || !config_center_->IsTrustedDevicesOnly()) return true;
|
||||||
|
std::lock_guard lock(controller_trust_mutex_);
|
||||||
|
const auto it = controller_trust_.find(id);
|
||||||
|
return it != controller_trust_.end() && it->second.IsTrusted(trusted_devices_.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GuiRuntime::BeginControllerTrust(const std::string& id, PeerEventHandler* events) {
|
||||||
|
const bool required = config_center_->IsTrustedDevicesOnly();
|
||||||
|
std::lock_guard lock(controller_trust_mutex_);
|
||||||
|
const auto previous = controller_trust_.find(id);
|
||||||
|
// Non-connected statuses clear this state. Duplicate Connected notifications
|
||||||
|
// in the same application session must not restart the challenge or timeout.
|
||||||
|
if (previous != controller_trust_.end() && previous->second.events == events) {
|
||||||
|
auto& state = previous->second;
|
||||||
|
const bool allowed = !required || state.IsTrusted(trusted_devices_.get());
|
||||||
|
state.pending = !allowed;
|
||||||
|
return allowed;
|
||||||
|
}
|
||||||
|
ControllerTrust state;
|
||||||
|
state.events = events;
|
||||||
|
state.pending = required;
|
||||||
|
state.deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
||||||
|
try {
|
||||||
|
if (!trusted_devices_) state.invalid = true;
|
||||||
|
else state.nonce = TrustedDevices::Nonce();
|
||||||
|
} catch (...) { state.invalid = true; }
|
||||||
|
controller_trust_[id] = std::move(state);
|
||||||
|
return !required;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GuiRuntime::ForgetControllerTrust(const std::string& id, PeerEventHandler* events) {
|
||||||
|
std::lock_guard lock(controller_trust_mutex_);
|
||||||
|
const auto it = controller_trust_.find(id);
|
||||||
|
if (it != controller_trust_.end() && it->second.events == events)
|
||||||
|
controller_trust_.erase(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
int GuiRuntime::SendTrustMessage(const std::string& id, const nlohmann::json& message) {
|
||||||
|
const auto text = message.dump();
|
||||||
|
return WithController(id, [&](PeerPtr* peer, const std::string& wire) {
|
||||||
|
return SendReliableDataFrameToPeer(peer, text.data(), text.size(),
|
||||||
|
control_data_label_.c_str(), wire.data(), wire.size());
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GuiRuntime::HandleDeviceTrustMessage(const std::string& id, PeerEventHandler* events,
|
||||||
|
const char* data, size_t size) {
|
||||||
|
if (!data || size > 4096) return false;
|
||||||
|
const auto message = nlohmann::json::parse(data, data + size, nullptr, false);
|
||||||
|
if (!message.is_object()) return false;
|
||||||
|
const auto type_it = message.find("type");
|
||||||
|
if (type_it == message.end()) return false;
|
||||||
|
if (*type_it == static_cast<int>(ControlType::host_infomation)) {
|
||||||
|
// Advertise through an existing message so disabled restrictions send no
|
||||||
|
// unfamiliar protocol messages to legacy clients. Retain only bounded,
|
||||||
|
// inert metadata until the connection's proof has been accepted.
|
||||||
|
std::lock_guard lock(controller_trust_mutex_);
|
||||||
|
const auto it = controller_trust_.find(id);
|
||||||
|
if (it != controller_trust_.end() && it->second.events == events) {
|
||||||
|
const auto capability = message.find("device_trust_version");
|
||||||
|
if (capability != message.end() && *capability == 1) it->second.capable = true;
|
||||||
|
if (it->second.pending) {
|
||||||
|
it->second.early_host_info.assign(data, size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!type_it->is_string()) return false;
|
||||||
|
const auto type = type_it->get<std::string>();
|
||||||
|
if (type.rfind("device_trust_", 0) != 0) return false;
|
||||||
|
if (!trusted_devices_) return true;
|
||||||
|
try {
|
||||||
|
if (type == "device_trust_challenge") {
|
||||||
|
const auto props = FindRemoteSession(id);
|
||||||
|
if (!props || props->closing_ || props->peer_events_.get() != events ||
|
||||||
|
message.at("version") != 1) return true;
|
||||||
|
const auto wire = events->WireId(id);
|
||||||
|
const auto proof = trusted_devices_->Prove(message.at("nonce"),
|
||||||
|
props->local_id_, GetHostName()).dump();
|
||||||
|
SendReliableDataFrameToPeer(props->peer_, proof.data(), proof.size(),
|
||||||
|
control_data_label_.c_str(), wire.data(), wire.size());
|
||||||
|
} else if (type == "device_trust_proof") {
|
||||||
|
std::lock_guard lock(controller_trust_mutex_);
|
||||||
|
const auto it = controller_trust_.find(id);
|
||||||
|
if (it == controller_trust_.end() || it->second.events != events) return true;
|
||||||
|
auto& state = it->second;
|
||||||
|
if (state.invalid || state.verified || std::chrono::steady_clock::now() >= state.deadline)
|
||||||
|
return true;
|
||||||
|
// Accept once per fresh challenge; malformed/replayed proofs grant nothing.
|
||||||
|
state.verified = TrustedDevices::Verify(message, state.nonce);
|
||||||
|
if (!state.verified) state.invalid = true;
|
||||||
|
}
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
// Untrusted protocol input never escapes the transport callback.
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GuiRuntime::TickControllerTrust() {
|
||||||
|
struct Action {
|
||||||
|
std::string id;
|
||||||
|
PeerEventHandler* events;
|
||||||
|
std::string nonce;
|
||||||
|
int kind;
|
||||||
|
std::string host_info;
|
||||||
|
};
|
||||||
|
std::vector<Action> actions;
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
const bool required = config_center_->IsTrustedDevicesOnly();
|
||||||
|
{
|
||||||
|
std::lock_guard lock(controller_trust_mutex_);
|
||||||
|
for (auto& [id, state] : controller_trust_) {
|
||||||
|
if (state.closing) continue;
|
||||||
|
const bool trusted = state.IsTrusted(trusted_devices_.get());
|
||||||
|
if (required && (state.invalid || (state.verified && !trusted) ||
|
||||||
|
(!state.verified && now >= state.deadline))) {
|
||||||
|
state.invalid = true;
|
||||||
|
state.closing = true;
|
||||||
|
actions.push_back({id, state.events, {}, -1});
|
||||||
|
} else if (state.pending && (!required || trusted)) {
|
||||||
|
state.pending = false;
|
||||||
|
actions.push_back({id, state.events, {}, 1, std::exchange(state.early_host_info, {})});
|
||||||
|
}
|
||||||
|
if ((required || state.capable) && !state.invalid && !state.verified &&
|
||||||
|
now < state.deadline && now >= state.next_challenge) {
|
||||||
|
state.next_challenge = now + std::chrono::seconds(1);
|
||||||
|
actions.push_back({id, state.events, state.nonce, 0});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& action : actions) {
|
||||||
|
const auto events = ControllerEvents(action.id);
|
||||||
|
if (!events || events.get() != action.events) continue;
|
||||||
|
auto callback = events->EnterCallback();
|
||||||
|
if (!callback) continue;
|
||||||
|
if (action.kind < 0) {
|
||||||
|
LOG_WARN("Reject controller [{}]: trusted device verification required", action.id);
|
||||||
|
transfers_.Cancel(events.get(), action.id);
|
||||||
|
remote_input_.Disconnect(action.id);
|
||||||
|
CloseServerController(action.id);
|
||||||
|
} else if (action.kind > 0) {
|
||||||
|
events->ApplyConnectionStatus(ConnectionStatus::Connected, action.id);
|
||||||
|
if (!action.host_info.empty()) {
|
||||||
|
const auto wire = events->WireId(action.id);
|
||||||
|
PeerEventHandler::OnReceiveDataBuffer(action.host_info.data(), action.host_info.size(),
|
||||||
|
wire.data(), wire.size(), control_data_label_.data(), control_data_label_.size(), events.get());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SendTrustMessage(action.id, {{"type", "device_trust_challenge"},
|
||||||
|
{"version", 1}, {"nonce", action.nonce}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<TrustedDevices::Device> GuiRuntime::VerifiedController(const std::string& id) {
|
||||||
|
std::lock_guard lock(controller_trust_mutex_);
|
||||||
|
const auto it = controller_trust_.find(id);
|
||||||
|
return it == controller_trust_.end() || it->second.invalid
|
||||||
|
? std::nullopt : it->second.verified;
|
||||||
|
}
|
||||||
|
bool GuiRuntime::TrustController(const std::string& id, const std::string& expected_key) {
|
||||||
|
if (!trusted_devices_ || !IsAuthorizedController(id)) return false;
|
||||||
|
const auto device = VerifiedController(id);
|
||||||
|
if (!device || device->public_key != expected_key) return false;
|
||||||
|
try { trusted_devices_->Add(*device); return true; }
|
||||||
|
catch (const std::exception& error) { LOG_ERROR("Cannot trust device: {}", error.what()); return false; }
|
||||||
|
}
|
||||||
|
bool GuiRuntime::RemoveTrustedDevice(const std::string& public_key) {
|
||||||
|
if (!trusted_devices_) return false;
|
||||||
|
try { trusted_devices_->Remove(public_key); }
|
||||||
|
catch (const std::exception& error) { LOG_ERROR("Cannot remove trusted device: {}", error.what()); return false; }
|
||||||
|
TickControllerTrust();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
#include "runtime/announcement_inbox.h"
|
#include "runtime/announcement_inbox.h"
|
||||||
#include "runtime/peer_event_handler.h"
|
#include "runtime/peer_event_handler.h"
|
||||||
#include "runtime/privacy_session_policy.h"
|
#include "runtime/privacy_session_policy.h"
|
||||||
|
#include "runtime/trusted_devices.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
@@ -63,7 +64,8 @@ class GuiRuntime : protected gui_detail::GuiState {
|
|||||||
std::string password, bool remember_password = false);
|
std::string password, bool remember_password = false);
|
||||||
int WithController(
|
int WithController(
|
||||||
const std::string& id,
|
const std::string& id,
|
||||||
const std::function<int(PeerPtr*, const std::string&)>& operation);
|
const std::function<int(PeerPtr*, const std::string&)>& operation,
|
||||||
|
bool require_trusted = true);
|
||||||
int SendControllerData(const std::string& id, const char* data, size_t size,
|
int SendControllerData(const std::string& id, const char* data, size_t size,
|
||||||
const char* label);
|
const char* label);
|
||||||
std::shared_ptr<PeerEventHandler> ControllerEvents(const std::string& id);
|
std::shared_ptr<PeerEventHandler> ControllerEvents(const std::string& id);
|
||||||
@@ -105,6 +107,32 @@ class GuiRuntime : protected gui_detail::GuiState {
|
|||||||
void HandlePrivacy();
|
void HandlePrivacy();
|
||||||
void HandleScreenLock();
|
void HandleScreenLock();
|
||||||
bool IsAuthorizedController(const std::string& remote_id);
|
bool IsAuthorizedController(const std::string& remote_id);
|
||||||
|
void InitializeTrustedDevices(const std::filesystem::path& directory);
|
||||||
|
bool TrustedControllerAllowed(const std::string& remote_id);
|
||||||
|
bool BeginControllerTrust(const std::string& remote_id, PeerEventHandler* events);
|
||||||
|
void ForgetControllerTrust(const std::string& remote_id, PeerEventHandler* events);
|
||||||
|
bool HandleDeviceTrustMessage(const std::string& remote_id, PeerEventHandler* events,
|
||||||
|
const char* data, size_t size);
|
||||||
|
void TickControllerTrust();
|
||||||
|
std::optional<TrustedDevices::Device> VerifiedController(const std::string& remote_id);
|
||||||
|
bool TrustController(const std::string& remote_id, const std::string& expected_key);
|
||||||
|
bool RemoveTrustedDevice(const std::string& public_key);
|
||||||
|
int SendTrustMessage(const std::string& remote_id, const nlohmann::json& message);
|
||||||
|
std::unique_ptr<TrustedDevices> trusted_devices_;
|
||||||
|
struct ControllerTrust {
|
||||||
|
PeerEventHandler* events = nullptr;
|
||||||
|
std::string nonce;
|
||||||
|
std::optional<TrustedDevices::Device> verified;
|
||||||
|
std::string early_host_info;
|
||||||
|
std::chrono::steady_clock::time_point deadline{}, next_challenge{};
|
||||||
|
bool pending = false, invalid = false, closing = false, capable = false;
|
||||||
|
bool IsTrusted(const TrustedDevices* devices) const {
|
||||||
|
return devices && !invalid && !closing && verified &&
|
||||||
|
devices->Contains(verified->public_key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
std::mutex controller_trust_mutex_;
|
||||||
|
std::map<std::string, ControllerTrust> controller_trust_;
|
||||||
void SetControllerPrivacySupport(const std::string& remote_id, bool supported);
|
void SetControllerPrivacySupport(const std::string& remote_id, bool supported);
|
||||||
// Caller holds connection_status_mutex_ exclusively. Turns privacy off when
|
// Caller holds connection_status_mutex_ exclusively. Turns privacy off when
|
||||||
// any connected controller lacks support, or on for the first automatic one.
|
// any connected controller lacks support, or on for the first automatic one.
|
||||||
|
|||||||
@@ -180,7 +180,9 @@ std::shared_ptr<PeerEventHandler> GuiRuntime::ControllerEvents(
|
|||||||
}
|
}
|
||||||
int GuiRuntime::WithController(
|
int GuiRuntime::WithController(
|
||||||
const std::string& id,
|
const std::string& id,
|
||||||
const std::function<int(PeerPtr*, const std::string&)>& operation) {
|
const std::function<int(PeerPtr*, const std::string&)>& operation,
|
||||||
|
bool require_trusted) {
|
||||||
|
if (require_trusted && !TrustedControllerAllowed(id)) return -1;
|
||||||
std::shared_lock lock(remote_sessions_mutex_);
|
std::shared_lock lock(remote_sessions_mutex_);
|
||||||
auto it = lan_controllers_.find(id);
|
auto it = lan_controllers_.find(id);
|
||||||
auto events = it == lan_controllers_.end() ? peer_events_ : it->second.events;
|
auto events = it == lan_controllers_.end() ? peer_events_ : it->second.events;
|
||||||
|
|||||||
@@ -133,6 +133,12 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
GuiRuntime* runtime = &handler->owner_;
|
GuiRuntime* runtime = &handler->owner_;
|
||||||
|
|
||||||
std::string source_id = std::string(src_id, src_id_size);
|
std::string source_id = std::string(src_id, src_id_size);
|
||||||
|
const auto remote_id = handler->ConnectionKey(user_id, user_id_size);
|
||||||
|
if (source_id == runtime->control_data_label_ &&
|
||||||
|
runtime->HandleDeviceTrustMessage(remote_id, handler, data, size)) return;
|
||||||
|
const auto viewer = runtime->FindRemoteSession(remote_id);
|
||||||
|
if (viewer && viewer->peer_events_.get() != handler) return;
|
||||||
|
if (!viewer && !runtime->TrustedControllerAllowed(remote_id)) return;
|
||||||
// The service-owned host runs as SYSTEM. Do not expose filesystem or
|
// The service-owned host runs as SYSTEM. Do not expose filesystem or
|
||||||
// clipboard operations in that security context.
|
// clipboard operations in that security context.
|
||||||
if (runtime->unattended_host_ &&
|
if (runtime->unattended_host_ &&
|
||||||
@@ -143,34 +149,30 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
return;
|
return;
|
||||||
if (source_id == web_clipboard::kStream) {
|
if (source_id == web_clipboard::kStream) {
|
||||||
runtime->clipboard_.ReceiveWeb(
|
runtime->clipboard_.ReceiveWeb(
|
||||||
data, size, handler->ConnectionKey(user_id, user_id_size), handler);
|
data, size, remote_id, handler);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (source_id == runtime->file_label_) {
|
if (source_id == runtime->file_label_) {
|
||||||
runtime->transfers_.Receive(data, size, handler,
|
runtime->transfers_.Receive(data, size, handler, remote_id);
|
||||||
handler->ConnectionKey(user_id, user_id_size));
|
|
||||||
return;
|
return;
|
||||||
} else if (source_id == runtime->clipboard_label_) {
|
} else if (source_id == runtime->clipboard_label_) {
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
std::string remote_user_id =
|
if (viewer && !viewer->clipboard_allowed_.load()) {
|
||||||
handler->ConnectionKey(user_id, user_id_size);
|
|
||||||
auto props =
|
|
||||||
runtime->FindRemoteSession(remote_user_id);
|
|
||||||
if (props && !props->clipboard_allowed_.load()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
runtime->clipboard_.QueueRemoteText(
|
runtime->clipboard_.QueueRemoteText(
|
||||||
data, size, props ? remote_user_id : std::string{});
|
data, size, viewer ? remote_id : std::string{}, {},
|
||||||
|
viewer ? std::function<bool()>{} : [runtime, remote_id] {
|
||||||
|
return runtime->IsAuthorizedController(remote_id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} else if (source_id == runtime->file_feedback_label_) {
|
} else if (source_id == runtime->file_feedback_label_) {
|
||||||
runtime->transfers_.HandleAck(
|
runtime->transfers_.HandleAck(data, size, handler, remote_id);
|
||||||
data, size, handler, handler->ConnectionKey(user_id, user_id_size));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string remote_id = handler->ConnectionKey(user_id, user_id_size);
|
|
||||||
const std::string wire_id = handler->WireId(remote_id);
|
const std::string wire_id = handler->WireId(remote_id);
|
||||||
PeerPtr* bound_peer =
|
PeerPtr* bound_peer =
|
||||||
handler->IsDirect() ? handler->DirectPeer() : runtime->peer_;
|
handler->IsDirect() ? handler->DirectPeer() : runtime->peer_;
|
||||||
|
|||||||
@@ -300,6 +300,14 @@ void PeerEventHandler::DispatchConnectionStatus(ConnectionStatus status,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!props) {
|
||||||
|
if (status == ConnectionStatus::Connected) {
|
||||||
|
if (!runtime->BeginControllerTrust(remote_id, handler)) return;
|
||||||
|
} else {
|
||||||
|
runtime->ForgetControllerTrust(remote_id, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (props) {
|
if (props) {
|
||||||
runtime->is_client_mode_ = true;
|
runtime->is_client_mode_ = true;
|
||||||
if (!props->wall_managed_) runtime->show_connection_status_window_ = true;
|
if (!props->wall_managed_) runtime->show_connection_status_window_ = true;
|
||||||
@@ -332,7 +340,9 @@ void PeerEventHandler::DispatchConnectionStatus(ConnectionStatus status,
|
|||||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||||
remote_action.i.display_list[i]);
|
remote_action.i.display_list[i]);
|
||||||
}
|
}
|
||||||
std::string msg = remote_action.to_json();
|
auto information = nlohmann::json::parse(remote_action.to_json());
|
||||||
|
information["device_trust_version"] = 1;
|
||||||
|
std::string msg = information.dump();
|
||||||
const int ret =
|
const int ret =
|
||||||
SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
|
SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
|
||||||
runtime->control_data_label_.c_str());
|
runtime->control_data_label_.c_str());
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
bool GuiRuntime::IsAuthorizedController(const std::string& remote_id) {
|
bool GuiRuntime::IsAuthorizedController(const std::string& remote_id) {
|
||||||
|
if (!TrustedControllerAllowed(remote_id)) return false;
|
||||||
if (remote_id.rfind("lan/", 0) == 0) {
|
if (remote_id.rfind("lan/", 0) == 0) {
|
||||||
const auto events = ControllerEvents(remote_id);
|
const auto events = ControllerEvents(remote_id);
|
||||||
if (!events || !events->DataAllowed()) return false;
|
if (!events || !events->DataAllowed()) return false;
|
||||||
@@ -59,6 +60,7 @@ void GuiRuntime::ApplyPrivacyAdmission(bool automatic_enable) {
|
|||||||
|
|
||||||
void GuiRuntime::QueuePrivacyCommand(const std::string& remote_id,
|
void GuiRuntime::QueuePrivacyCommand(const std::string& remote_id,
|
||||||
const PrivacyCommand& command) {
|
const PrivacyCommand& command) {
|
||||||
|
if (!TrustedControllerAllowed(remote_id)) return;
|
||||||
std::shared_lock session_lock(connection_status_mutex_);
|
std::shared_lock session_lock(connection_status_mutex_);
|
||||||
const auto found = connection_status_.find(remote_id);
|
const auto found = connection_status_.find(remote_id);
|
||||||
if (found == connection_status_.end() ||
|
if (found == connection_status_.end() ||
|
||||||
@@ -86,6 +88,7 @@ void GuiRuntime::HandlePrivacy() {
|
|||||||
commands.swap(privacy_commands_);
|
commands.swap(privacy_commands_);
|
||||||
}
|
}
|
||||||
for (const auto& [remote_id, command] : commands) {
|
for (const auto& [remote_id, command] : commands) {
|
||||||
|
if (!TrustedControllerAllowed(remote_id)) continue;
|
||||||
// Keep admission and activation under the connection lock so an unknown
|
// Keep admission and activation under the connection lock so an unknown
|
||||||
// controller cannot join between the capability check and enabling.
|
// controller cannot join between the capability check and enabling.
|
||||||
std::shared_lock lock(connection_status_mutex_);
|
std::shared_lock lock(connection_status_mutex_);
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
#include "runtime/trusted_devices.h"
|
||||||
|
|
||||||
|
#include <openssl/crypto.h>
|
||||||
|
#include <openssl/rand.h>
|
||||||
|
#include <openssl/sha.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "private_file.h"
|
||||||
|
#include "runtime/device_identity_store.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
namespace {
|
||||||
|
constexpr size_t kMaxTrustedDevices = 128;
|
||||||
|
void Require(bool ok) {
|
||||||
|
if (!ok) throw std::runtime_error("Invalid or unavailable trusted device identity");
|
||||||
|
}
|
||||||
|
std::string Hex(const uint8_t* data, size_t size) {
|
||||||
|
constexpr char digits[] = "0123456789abcdef";
|
||||||
|
std::string result;
|
||||||
|
result.reserve(size * 2);
|
||||||
|
for (size_t i = 0; i < size; ++i) {
|
||||||
|
result += digits[data[i] >> 4]; result += digits[data[i] & 15];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
bool IsHex(const std::string& value, size_t bytes) {
|
||||||
|
return value.size() == bytes * 2 && std::all_of(value.begin(), value.end(),
|
||||||
|
[](char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); });
|
||||||
|
}
|
||||||
|
std::vector<uint8_t> Unhex(const std::string& text, size_t bytes) {
|
||||||
|
Require(IsHex(text, bytes));
|
||||||
|
auto digit = [](char c) { return c <= '9' ? c - '0' : c - 'a' + 10; };
|
||||||
|
std::vector<uint8_t> result(bytes);
|
||||||
|
for (size_t i = 0; i < bytes; ++i)
|
||||||
|
result[i] = (digit(text[2 * i]) << 4) | digit(text[2 * i + 1]);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
bool Label(const std::string& value) {
|
||||||
|
return value.size() <= 128 && std::none_of(value.begin(), value.end(),
|
||||||
|
[](unsigned char c) { return c < 32 || c == 127; });
|
||||||
|
}
|
||||||
|
std::string Transcript(const std::string& nonce, const TrustedDevices::Device& device) {
|
||||||
|
// Canonical encoding and a domain tag prevent ambiguous or cross-protocol use.
|
||||||
|
return nlohmann::json::array({"crossdesk.device-trust.v1", nonce, device.public_key,
|
||||||
|
device.device_id, device.name}).dump();
|
||||||
|
}
|
||||||
|
using Context = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>;
|
||||||
|
struct PrivateText {
|
||||||
|
std::string value;
|
||||||
|
~PrivateText() { OPENSSL_cleanse(value.data(), value.size()); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TrustedDevices::TrustedDevices(const std::filesystem::path& directory,
|
||||||
|
const std::filesystem::path& cache)
|
||||||
|
: path_(directory / "devices.json") {
|
||||||
|
private_file::EnsureDirectory(directory);
|
||||||
|
const bool exists = std::filesystem::symlink_status(path_).type() !=
|
||||||
|
std::filesystem::file_type::not_found;
|
||||||
|
PrivateText legacy_key;
|
||||||
|
if (exists) {
|
||||||
|
PrivateText text{private_file::Read(path_)};
|
||||||
|
auto saved = nlohmann::json::parse(text.value, nullptr, false);
|
||||||
|
Require(!saved.is_discarded());
|
||||||
|
const bool legacy = saved.at("version") == 1;
|
||||||
|
Require((legacy || saved.at("version") == 2) && saved.at("devices").is_array() &&
|
||||||
|
saved.at("devices").size() <= kMaxTrustedDevices);
|
||||||
|
if (legacy) {
|
||||||
|
legacy_key.value = saved.at("private_key").get<std::string>();
|
||||||
|
auto& encoded = saved["private_key"].get_ref<std::string&>();
|
||||||
|
OPENSSL_cleanse(encoded.data(), encoded.size());
|
||||||
|
Require(IsHex(legacy_key.value, 32));
|
||||||
|
} else Require(!saved.contains("private_key"));
|
||||||
|
for (const auto& row : saved.at("devices")) {
|
||||||
|
Device device{row.at("public_key"), row.at("name"), row.at("device_id")};
|
||||||
|
Require(IsHex(device.public_key, 32) && Label(device.name) && Label(device.device_id));
|
||||||
|
Require(devices_.emplace(device.public_key, device).second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
device_identity_store::Update(cache, [&](nlohmann::json& identity) {
|
||||||
|
if (!identity.contains("trust_private_key") && !legacy_key.value.empty())
|
||||||
|
identity["trust_private_key"] = legacy_key.value;
|
||||||
|
if (identity.contains("trust_private_key")) {
|
||||||
|
const auto& encoded = identity.at("trust_private_key").get_ref<const std::string&>();
|
||||||
|
// Interrupted migration may leave both copies. Never replace either key
|
||||||
|
// on a mismatch: that would silently invalidate existing pairings.
|
||||||
|
Require(legacy_key.value.empty() || encoded == legacy_key.value);
|
||||||
|
auto secret = Unhex(encoded, 32);
|
||||||
|
key_.reset(EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr,
|
||||||
|
secret.data(), secret.size()));
|
||||||
|
OPENSSL_cleanse(secret.data(), secret.size());
|
||||||
|
} else {
|
||||||
|
key_.reset(EVP_PKEY_Q_keygen(nullptr, nullptr, "ED25519"));
|
||||||
|
Require(bool(key_));
|
||||||
|
std::array<uint8_t, 32> secret{};
|
||||||
|
size_t size = secret.size();
|
||||||
|
Require(EVP_PKEY_get_raw_private_key(key_.get(), secret.data(), &size) == 1 && size == 32);
|
||||||
|
identity["trust_private_key"] = Hex(secret.data(), secret.size());
|
||||||
|
OPENSSL_cleanse(secret.data(), secret.size());
|
||||||
|
}
|
||||||
|
Require(bool(key_));
|
||||||
|
});
|
||||||
|
|
||||||
|
std::array<uint8_t, 32> public_key{};
|
||||||
|
size_t size = public_key.size();
|
||||||
|
Require(EVP_PKEY_get_raw_public_key(key_.get(), public_key.data(), &size) == 1 && size == 32);
|
||||||
|
public_key_ = Hex(public_key.data(), public_key.size());
|
||||||
|
// The identity update is durable before removing the legacy secret. If this
|
||||||
|
// write fails, reopening can safely retry with the same key and device list.
|
||||||
|
if (!exists || !legacy_key.value.empty()) Save(devices_);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string TrustedDevices::Fingerprint(const std::string& public_key) {
|
||||||
|
const auto bytes = Unhex(public_key, 32);
|
||||||
|
std::array<uint8_t, SHA256_DIGEST_LENGTH> hash{};
|
||||||
|
SHA256(bytes.data(), bytes.size(), hash.data());
|
||||||
|
return Hex(hash.data(), hash.size());
|
||||||
|
}
|
||||||
|
std::string TrustedDevices::Fingerprint() const { return Fingerprint(public_key_); }
|
||||||
|
|
||||||
|
std::string TrustedDevices::PairingInfo(const std::string& device_id,
|
||||||
|
const std::string& name) const {
|
||||||
|
Require(Label(device_id) && Label(name));
|
||||||
|
// Export public information only. Connection admission still requires a fresh
|
||||||
|
// proof of private-key possession; importing this document grants no bypass.
|
||||||
|
return nlohmann::json{{"type", "crossdesk-pairing"}, {"version", 1},
|
||||||
|
{"public_key", public_key_}, {"device_id", device_id}, {"name", name}}.dump();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<TrustedDevices::Device> TrustedDevices::ParsePairingInfo(std::string_view text) {
|
||||||
|
if (text.empty() || text.size() > kMaxPairingInfoSize) return std::nullopt;
|
||||||
|
try {
|
||||||
|
const auto info = nlohmann::json::parse(text.begin(), text.end(), nullptr, false);
|
||||||
|
if (!info.is_object() || info.size() != 5 || info.at("type") != "crossdesk-pairing" ||
|
||||||
|
!info.at("version").is_number_integer() || info.at("version") != 1)
|
||||||
|
return std::nullopt;
|
||||||
|
Device device{info.at("public_key"), info.at("name"), info.at("device_id")};
|
||||||
|
if (IsHex(device.public_key, 32) && Label(device.name) && Label(device.device_id))
|
||||||
|
return device;
|
||||||
|
} catch (const std::exception&) {}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string TrustedDevices::Nonce() {
|
||||||
|
std::array<uint8_t, 32> bytes{};
|
||||||
|
Require(RAND_bytes(bytes.data(), bytes.size()) == 1);
|
||||||
|
return Hex(bytes.data(), bytes.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json TrustedDevices::Prove(const std::string& nonce,
|
||||||
|
const std::string& device_id, const std::string& name) const {
|
||||||
|
Require(IsHex(nonce, 32) && Label(device_id) && Label(name));
|
||||||
|
const Device device{public_key_, name, device_id};
|
||||||
|
const auto text = Transcript(nonce, device);
|
||||||
|
Context context(EVP_MD_CTX_new(), EVP_MD_CTX_free);
|
||||||
|
std::array<uint8_t, 64> signature{};
|
||||||
|
size_t size = signature.size();
|
||||||
|
Require(context && EVP_DigestSignInit(context.get(), nullptr, nullptr, nullptr, key_.get()) == 1 &&
|
||||||
|
EVP_DigestSign(context.get(), signature.data(), &size,
|
||||||
|
reinterpret_cast<const uint8_t*>(text.data()), text.size()) == 1 && size == 64);
|
||||||
|
return {{"type", "device_trust_proof"}, {"version", 1}, {"nonce", nonce},
|
||||||
|
{"public_key", public_key_}, {"device_id", device_id}, {"name", name},
|
||||||
|
{"signature", Hex(signature.data(), signature.size())}};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<TrustedDevices::Device> TrustedDevices::Verify(
|
||||||
|
const nlohmann::json& proof, const std::string& nonce) {
|
||||||
|
try {
|
||||||
|
if (proof.at("version") != 1 || proof.at("nonce") != nonce || !IsHex(nonce, 32))
|
||||||
|
return std::nullopt;
|
||||||
|
Device device{proof.at("public_key"), proof.at("name"), proof.at("device_id")};
|
||||||
|
if (!Label(device.name) || !Label(device.device_id)) return std::nullopt;
|
||||||
|
const auto public_key = Unhex(device.public_key, 32);
|
||||||
|
const auto signature = Unhex(proof.at("signature"), 64);
|
||||||
|
std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key(
|
||||||
|
EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, public_key.data(), public_key.size()),
|
||||||
|
EVP_PKEY_free);
|
||||||
|
Context context(EVP_MD_CTX_new(), EVP_MD_CTX_free);
|
||||||
|
const auto text = Transcript(nonce, device);
|
||||||
|
if (key && context && EVP_DigestVerifyInit(context.get(), nullptr, nullptr, nullptr, key.get()) == 1 &&
|
||||||
|
EVP_DigestVerify(context.get(), signature.data(), signature.size(),
|
||||||
|
reinterpret_cast<const uint8_t*>(text.data()), text.size()) == 1)
|
||||||
|
return device;
|
||||||
|
} catch (const std::exception&) {}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrustedDevices::Contains(const std::string& public_key) const {
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
return devices_.count(public_key) != 0;
|
||||||
|
}
|
||||||
|
std::vector<TrustedDevices::Device> TrustedDevices::List() const {
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
std::vector<Device> result;
|
||||||
|
for (const auto& [key, device] : devices_) result.push_back(device);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
void TrustedDevices::Save(const std::map<std::string, Device>& devices) const {
|
||||||
|
nlohmann::json saved{{"version", 2}, {"devices", nlohmann::json::array()}};
|
||||||
|
for (const auto& [key, device] : devices)
|
||||||
|
saved["devices"].push_back({{"public_key", key}, {"name", device.name}, {"device_id", device.device_id}});
|
||||||
|
const auto text = saved.dump(2);
|
||||||
|
Require(text.size() <= 64 * 1024);
|
||||||
|
private_file::Write(path_, text);
|
||||||
|
}
|
||||||
|
void TrustedDevices::Add(const Device& device) {
|
||||||
|
Require(IsHex(device.public_key, 32) && Label(device.name) && Label(device.device_id));
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
const auto existing = devices_.find(device.public_key);
|
||||||
|
if (existing != devices_.end() && existing->second == device) return;
|
||||||
|
Require(existing != devices_.end() || devices_.size() < kMaxTrustedDevices);
|
||||||
|
auto next = devices_;
|
||||||
|
next[device.public_key] = device;
|
||||||
|
Save(next);
|
||||||
|
devices_.swap(next);
|
||||||
|
}
|
||||||
|
void TrustedDevices::Remove(const std::string& public_key) {
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
if (!devices_.contains(public_key)) return;
|
||||||
|
auto next = devices_;
|
||||||
|
next.erase(public_key);
|
||||||
|
Save(next);
|
||||||
|
devices_.swap(next);
|
||||||
|
}
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2026-11-01
|
||||||
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _TRUSTED_DEVICES_H_
|
||||||
|
#define _TRUSTED_DEVICES_H_
|
||||||
|
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
class TrustedDevices {
|
||||||
|
public:
|
||||||
|
struct Device {
|
||||||
|
std::string public_key, name, device_id;
|
||||||
|
bool operator==(const Device&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
TrustedDevices(const std::filesystem::path& directory,
|
||||||
|
const std::filesystem::path& cache);
|
||||||
|
std::string Fingerprint() const;
|
||||||
|
static std::string Fingerprint(const std::string& public_key);
|
||||||
|
static std::string Nonce();
|
||||||
|
nlohmann::json Prove(const std::string& nonce,
|
||||||
|
const std::string& device_id, const std::string& name) const;
|
||||||
|
static std::optional<Device> Verify(const nlohmann::json& proof,
|
||||||
|
const std::string& nonce);
|
||||||
|
static constexpr size_t kMaxPairingInfoSize = 2048;
|
||||||
|
std::string PairingInfo(const std::string& device_id, const std::string& name) const;
|
||||||
|
static std::optional<Device> ParsePairingInfo(std::string_view text);
|
||||||
|
bool Contains(const std::string& public_key) const;
|
||||||
|
std::vector<Device> List() const;
|
||||||
|
// Call only after a local add action for a verified peer or valid pairing info.
|
||||||
|
void Add(const Device& device);
|
||||||
|
void Remove(const std::string& public_key);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Save(const std::map<std::string, Device>& devices) const;
|
||||||
|
std::filesystem::path path_;
|
||||||
|
std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key_{nullptr, EVP_PKEY_free};
|
||||||
|
std::string public_key_;
|
||||||
|
mutable std::mutex mutex_;
|
||||||
|
std::map<std::string, Device> devices_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -24,6 +24,7 @@ int GuiApplication::RunUnattended(void* stop_event) {
|
|||||||
dll_log_path_ = exec_log_path_;
|
dll_log_path_ = exec_log_path_;
|
||||||
InitializeLogger();
|
InitializeLogger();
|
||||||
config_center_ = std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
|
config_center_ = std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
|
||||||
|
InitializeTrustedDevices(root / "trusted-devices");
|
||||||
if (config_center_->SetPrivacyScreen(false) != 0 ||
|
if (config_center_->SetPrivacyScreen(false) != 0 ||
|
||||||
config_center_->SetSaveRemotePreviews(false) != 0)
|
config_center_->SetSaveRemotePreviews(false) != 0)
|
||||||
return 1;
|
return 1;
|
||||||
@@ -44,6 +45,7 @@ int GuiApplication::RunUnattended(void* stop_event) {
|
|||||||
next_peer_attempt = now + 3000;
|
next_peer_attempt = now + 3000;
|
||||||
if (CreateConnectionPeer() != 0) CloseConnectionPeer();
|
if (CreateConnectionPeer() != 0) CloseConnectionPeer();
|
||||||
}
|
}
|
||||||
|
TickControllerTrust();
|
||||||
HandleConnectionStatusChange();
|
HandleConnectionStatusChange();
|
||||||
HandleScreenLock();
|
HandleScreenLock();
|
||||||
HandleWindowsServiceIntegration();
|
HandleWindowsServiceIntegration();
|
||||||
|
|||||||
Reference in New Issue
Block a user