[feat] add macOS privacy screen and keep remote sessions running

This commit is contained in:
kunkundi
2026-09-13 00:38:40 +08:00
parent 23bf2ff8b8
commit a6e325f190
38 changed files with 1161 additions and 1046 deletions
+9 -10
View File
@@ -7,14 +7,12 @@
#ifndef _PRIVACY_BACKEND_H_
#define _PRIVACY_BACKEND_H_
#include <cstdint>
#include <memory>
#include <string>
namespace crossdesk {
struct PrivacyScreenText {
std::string verification = "Privacy screen verification";
std::string unlock_hint = "To turn off the privacy screen, press";
};
@@ -27,12 +25,13 @@ struct PrivacyCapabilities {
struct PrivacyHealth {
std::string failure;
bool emergency_exit = false;
bool ready = true;
};
// Lifecycle, message pumping and native resource operations run on the
// controller's message thread. Const metadata queries are serialized with
// those operations by the controller mutex and must not call native UI APIs.
// No platform backend is installed on macOS/Linux yet.
// AppKit work is queued to the main thread without waiting under this mutex.
class PrivacyBackend {
public:
virtual ~PrivacyBackend() = default;
@@ -41,23 +40,23 @@ class PrivacyBackend {
std::string& error) = 0;
virtual void Recover() = 0;
virtual bool IsRecovered() const = 0;
virtual PrivacyHealth Poll(bool force_check = false) = 0;
virtual bool RecoveryPending() const { return false; }
virtual PrivacyHealth Poll() = 0;
// Wait on both native window messages and controller commands.
// Wake is the only backend operation callable from another thread.
virtual void WaitForEvents(unsigned timeout_ms) = 0;
virtual void Wake() = 0;
virtual bool PaintChallenge(uint32_t value, const std::string& title,
std::string& error) = 0;
// Remove only the temporary capture probes, keeping all privacy covers up.
// The controller drains queued probe frames before admitting clean video.
virtual bool ClearChallenges(std::string& error) = 0;
virtual bool HasMonitor(int left, int top, int width, int height) const = 0;
};
#ifdef _WIN32
std::unique_ptr<PrivacyBackend> CreateWindowsPrivacyBackend();
bool IsWindowsPrivacyDesktopAvailable();
#endif
#ifdef __APPLE__
std::unique_ptr<PrivacyBackend> CreateMacPrivacyBackend();
// Flush queued AppKit cleanup after joining the controller at app shutdown.
void FlushMacPrivacyTasks();
#endif
} // namespace crossdesk
+82 -376
View File
@@ -1,496 +1,202 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
/* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */
#include "privacy_controller.h"
#include <chrono>
#include <cstring>
#include "rd_log.h"
namespace crossdesk {
uint64_t PrivacyController::Now() {
return static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count());
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
}
PrivacyController::PrivacyController(Factory factory) {
status_.state = PrivacyState::unsupported;
std::strcpy(status_.reason,
"Privacy screen is not available on this platform");
std::strcpy(status_.reason, "Privacy screen is not available on this platform");
#ifdef _WIN32
if (!factory) factory = CreateWindowsPrivacyBackend;
#elif defined(__APPLE__)
if (!factory) factory = CreateMacPrivacyBackend;
#endif
thread_ = std::thread([this, factory] { Run(factory); });
}
PrivacyController::~PrivacyController() { Shutdown(); }
void PrivacyController::WakeLocked() {
if (backend_) backend_->Wake();
wake_.notify_all();
}
void PrivacyController::Shutdown() {
{
std::lock_guard lock(mutex_);
quit_ = true;
remote_paused_.store(true);
WakeLocked();
}
if (thread_.joinable()) thread_.join();
#ifdef __APPLE__
FlushMacPrivacyTasks();
#endif
}
PrivacyStatus PrivacyController::Snapshot() const {
std::lock_guard lock(mutex_);
return status_;
}
void PrivacyController::SetText(const PrivacyScreenText& text) {
std::lock_guard lock(mutex_);
text_ = text;
}
void PrivacyController::SetState(PrivacyState state,
const std::string& reason) {
void PrivacyController::SetState(PrivacyState state, const std::string& reason) {
status_.state = state;
status_.remote_paused = remote_paused_.load();
status_.remote_paused = false; // Wire compatibility; privacy never pauses a session.
++status_.revision;
std::strncpy(status_.reason, reason.c_str(), sizeof(status_.reason) - 1);
status_.reason[sizeof(status_.reason) - 1] = '\0';
LOG_INFO(
"Privacy: state={}, overlay={}, input_blocked={}, paused={}, reason={}",
static_cast<int>(state), status_.overlay_active, status_.input_blocked,
status_.remote_paused, reason);
LOG_INFO("Privacy: state={}, overlay={}, input_blocked={}, reason={}",
static_cast<int>(state), status_.overlay_active, status_.input_blocked, reason);
}
void PrivacyController::Enable(bool block_input) {
std::lock_guard lock(mutex_);
if (quit_) return;
if (status_.state == PrivacyState::on ||
status_.state == PrivacyState::starting ||
status_.state == PrivacyState::stopping ||
status_.state == PrivacyState::failed) {
SetState(status_.state, status_.reason);
return;
}
if (quit_ || engaged_.load() || disable_pending_) return;
if (!status_.supported || (block_input && !status_.input_block_supported)) {
SetState(PrivacyState::unsupported, status_.reason);
SetState(PrivacyState::unsupported, "Privacy screen is unavailable");
return;
}
// Failure recovery is explicit: first turn off, then request a fresh enable.
engaged_.store(true);
remote_paused_.store(true);
block_requested_ = block_input;
automatic_enable_ = false;
enable_pending_ = true;
ever_enabled_ = false;
SetState(PrivacyState::starting,
"Creating privacy windows and checking capture");
failure_.clear();
started_ = Now();
SetState(PrivacyState::starting, "Enabling privacy screen");
WakeLocked();
}
void PrivacyController::EnableOnConnection() {
std::lock_guard lock(mutex_);
if (quit_ || status_.state == PrivacyState::on ||
status_.state == PrivacyState::starting ||
status_.state == PrivacyState::failed)
return;
// Also accept a reconnect while the previous disconnect is still cleaning
// up. Run() keeps the gate closed between recovery and this new enable.
if (quit_ || (engaged_.load() && !disable_pending_)) return;
// A reconnect may arrive before asynchronous AppKit recovery has completed.
engaged_.store(true);
remote_paused_.store(true);
block_requested_ = true;
automatic_enable_ = true;
capture_start_deadline_ = Now() + 10000;
enable_pending_ = true;
ever_enabled_ = false;
SetState(PrivacyState::starting,
"Connected; automatically enabling privacy and verifying capture");
failure_.clear();
started_ = Now();
SetState(PrivacyState::starting, "Enabling privacy on connection");
WakeLocked();
}
void PrivacyController::Disable() {
std::lock_guard lock(mutex_);
if (quit_) return;
if (!engaged_.load() && !status_.overlay_active) {
SetState(status_.state, status_.reason);
return;
}
enable_pending_ = false;
failure_.clear();
disable_pending_ = true;
remote_paused_.store(true);
SetState(PrivacyState::stopping, "Restoring local desktop and input");
WakeLocked();
}
void PrivacyController::Disconnected() {
std::lock_guard lock(mutex_);
if (quit_ ||
(!engaged_.load() && !status_.overlay_active && !remote_paused_.load()))
return;
remote_paused_.store(true);
void PrivacyController::Disconnected() { Disable(); }
void PrivacyController::FailLocked(const std::string& reason) {
if (!engaged_.load() || disable_pending_) return;
failure_ = reason;
enable_pending_ = false;
disable_pending_ = true;
SetState(PrivacyState::stopping,
"Last controller disconnected; restoring desktop, input and session "
"state");
SetState(PrivacyState::failed, reason);
WakeLocked();
}
void PrivacyController::FailLocked(const std::string& reason) {
if (!engaged_.load() || status_.state == PrivacyState::failed ||
status_.state == PrivacyState::stopping)
return;
remote_paused_.store(true);
enable_pending_ = false;
SetState(PrivacyState::failed, reason);
}
void PrivacyController::Fail(const std::string& reason) {
std::lock_guard lock(mutex_);
FailLocked(reason);
}
void PrivacyController::BeginVerification(const std::string& reason) {
remote_paused_.store(true);
verifier_.Reset(true);
verification_phase_ = VerificationPhase::challenge;
clean_frames_ = 0;
last_frame_received_ = 0;
drain_after_recovery_.store(false);
started_ = Now();
last_issue_ = 0;
frame_monitor_set_ = false;
first_verified_frame_logged_ = false;
SetState(PrivacyState::starting,
!reason.empty() ? reason
: automatic_enable_ && !capture_running_
? "Waiting for desktop capture startup"
: "Verifying live desktop through " + capture_backend_);
}
void PrivacyController::CaptureChanged(const std::string& backend,
bool running) {
void PrivacyController::CaptureChanged(bool running) {
std::lock_guard lock(mutex_);
capture_backend_ = backend;
capture_running_ = running;
if (!engaged_.load()) return;
if (ever_enabled_) {
FailLocked(
"Capture backend stopped or rebuilt; remote operation paused. Turn "
"privacy off to recover.");
} else if (status_.state == PrivacyState::starting) {
BeginVerification();
}
if (!running) FailLocked("Capture changed; privacy screen is being turned off");
WakeLocked();
}
void PrivacyController::DisplayChanging() {
std::lock_guard lock(mutex_);
if (status_.state == PrivacyState::on) BeginVerification();
}
void PrivacyController::CaptureInterrupted() {
std::lock_guard lock(mutex_);
if (!engaged_.load()) return;
if (ever_enabled_) {
FailLocked(
"Capture component is rebuilding; remote operation paused. Turn "
"privacy off to recover.");
} else if (status_.state == PrivacyState::starting) {
// The capturer owns recovery and backend selection. Require fresh frames
// from it without letting repeated resets extend verification forever.
const auto attempt_started = started_;
BeginVerification(
"Capture component interrupted; verifying recovered frames");
if (attempt_started) started_ = attempt_started;
}
}
bool PrivacyController::ObserveFrame(const uint8_t* y, size_t size, int width,
int height, int left, int top,
int physical_width, int physical_height) {
if (!engaged_.load() && !drain_after_recovery_.load()) return RemoteAllowed();
std::lock_guard lock(mutex_);
if (!engaged_.load()) {
// Disabling during verification can leave already-captured probe frames
// queued behind HWND destruction. Do not leak them after reporting Off.
if (drain_after_recovery_.load()) {
if (!y || width <= 0 || height <= 0 ||
size < static_cast<size_t>(width) * height)
return false;
if (verifier_.ContainsChallenge(y, size, width, height)) {
clean_frames_ = 0;
return false;
}
if (++clean_frames_ < 2) return false;
drain_after_recovery_.store(false);
verifier_.Reset();
}
return RemoteAllowed();
}
if (!backend_ || !status_.overlay_active ||
(status_.state != PrivacyState::starting &&
status_.state != PrivacyState::on))
return false;
if (!backend_->HasMonitor(left, top, physical_width, physical_height) ||
width != (physical_width & ~1) || height != (physical_height & ~1)) {
if (ever_enabled_)
FailLocked(
"Capture geometry changed or cannot be verified; remote operation "
"paused");
return false;
}
if (frame_monitor_set_ && (frame_left_ != left || frame_top_ != top)) {
BeginVerification();
}
frame_left_ = left;
frame_top_ = top;
frame_monitor_set_ = true;
if (!y || size < static_cast<size_t>(width) * height) {
FailLocked(
"Capture returned an invalid desktop frame; remote operation paused");
return false;
}
last_frame_received_ = Now();
if (verification_phase_ == VerificationPhase::challenge) {
verifier_.Observe(y, size, width, height, last_frame_received_);
return false; // Verification pixels are never sent to the encoder.
}
if (verifier_.ContainsChallenge(y, size, width, height)) {
clean_frames_ = 0;
if (verification_phase_ == VerificationPhase::live) {
drain_after_recovery_.store(true);
FailLocked(
"Capture reintroduced verification pixels; remote operation paused");
}
return false;
}
if (verification_phase_ == VerificationPhase::draining) {
if (clean_frames_ < 2) ++clean_frames_;
return false; // Only the owner thread can publish the verified On state.
}
if (status_.state != PrivacyState::on) return false;
if (!first_verified_frame_logged_) {
first_verified_frame_logged_ = true;
LOG_INFO(
"Privacy: first clean verified desktop frame admitted, backend={}, "
"size={}x{}, origin=({}, {})",
capture_backend_, width, height, left, top);
}
return true;
}
void PrivacyController::Run(Factory factory) {
std::unique_lock lock(mutex_);
try {
if (factory) backend_ = factory();
if (backend_) {
const auto caps = backend_->Query();
last_capability_check_ = Now();
status_.supported = caps.overlay;
status_.input_block_supported = caps.block_input;
// A connection can arm the gate before this thread has initialized.
if (!engaged_.load())
SetState(caps.overlay ? PrivacyState::off : PrivacyState::unsupported,
caps.reason);
}
while (!quit_) {
if (backend_ && !engaged_.load() &&
Now() - last_capability_check_ >= 2000) {
if (last_capability_check_ == 0 ||
(!engaged_.load() && Now() - last_capability_check_ >= 2000)) {
last_capability_check_ = Now();
const auto caps = backend_->Query();
if (status_.supported != caps.overlay ||
status_.input_block_supported != caps.block_input) {
status_.supported = caps.overlay;
status_.input_block_supported = caps.block_input;
SetState(caps.overlay ? PrivacyState::off : PrivacyState::unsupported,
caps.reason);
}
const auto caps = backend_ ? backend_->Query() : PrivacyCapabilities{};
const bool changed = status_.supported != caps.overlay ||
status_.input_block_supported != caps.block_input;
status_.supported = caps.overlay;
status_.input_block_supported = caps.block_input;
if (!engaged_.load() && (changed || status_.revision == 0))
SetState(caps.overlay ? PrivacyState::off : PrivacyState::unsupported, caps.reason);
}
if (disable_pending_) {
if (backend_) backend_->Recover();
if (backend_ && backend_->RecoveryPending()) {
lock.unlock();
backend_->WaitForEvents(25);
lock.lock();
continue;
}
disable_pending_ = false;
if (backend_ && !backend_->IsRecovered()) {
enable_pending_ = false;
SetState(PrivacyState::failed,
"Could not release every privacy resource; remote operation "
"remains paused");
continue;
}
status_.overlay_active = status_.input_blocked = false;
const bool drain = challenges_visible_ ||
verification_phase_ == VerificationPhase::draining ||
drain_after_recovery_.load();
challenges_visible_ = false;
clean_frames_ = 0;
drain_after_recovery_.store(drain);
if (!drain) verifier_.Reset();
verification_phase_ = VerificationPhase::live;
if (enable_pending_) {
SetState(PrivacyState::starting,
"Reconnected; automatically restoring privacy protection");
disable_pending_ = true; // Retry native cleanup without affecting the session.
SetState(PrivacyState::failed, "Could not release every privacy resource");
} else {
ever_enabled_ = false;
automatic_enable_ = false;
block_requested_ = false;
capture_start_deadline_ = 0;
started_ = last_issue_ = 0;
frame_monitor_set_ = false;
first_verified_frame_logged_ = false;
engaged_.store(false);
remote_paused_.store(false);
SetState(PrivacyState::off, "Privacy screen is off");
status_.overlay_active = status_.input_blocked = false;
if (!enable_pending_) {
engaged_.store(false);
SetState(failure_.empty() ? PrivacyState::off : PrivacyState::failed,
failure_.empty() ? "Privacy screen is off" : failure_);
}
}
}
if (enable_pending_) {
enable_pending_ = false;
std::string error;
if (automatic_enable_) {
const auto caps =
backend_ ? backend_->Query()
: PrivacyCapabilities{false, false,
"No privacy backend is installed"};
status_.supported = caps.overlay;
status_.input_block_supported = caps.block_input;
if (!caps.overlay || !caps.block_input)
FailLocked("Automatic privacy unavailable: " + caps.reason);
}
if (status_.state != PrivacyState::failed) {
if (!capture_running_ && !automatic_enable_) {
FailLocked("No running desktop capture; remote operation paused");
} else if (!backend_->Enable(block_requested_, text_, error)) {
backend_->Recover();
status_.overlay_active = !backend_->IsRecovered();
const auto caps = backend_ ? backend_->Query() : PrivacyCapabilities{};
status_.supported = caps.overlay;
status_.input_block_supported = caps.block_input;
if (!caps.overlay || (block_requested_ && !caps.block_input)) {
FailLocked("Privacy screen unavailable: " + caps.reason);
} else if (!capture_running_) {
if (!automatic_enable_ || Now() - started_ > 10000)
FailLocked("No desktop capture available for privacy screen");
} else {
enable_pending_ = false;
std::string error;
if (!backend_->Enable(block_requested_, text_, error)) {
FailLocked(error);
} else {
status_.overlay_active = true;
status_.input_blocked = block_requested_;
BeginVerification();
started_ = Now();
}
}
}
// Pump the native queue even after recovery: the message sink is reused.
const auto health =
backend_ ? backend_->Poll(status_.state == PrivacyState::starting &&
verifier_.Verified())
: PrivacyHealth{};
if (backend_ && engaged_.load()) {
const auto health = backend_ ? backend_->Poll() : PrivacyHealth{};
if (engaged_.load() && !disable_pending_) {
if (health.emergency_exit) {
remote_paused_.store(true);
failure_.clear();
enable_pending_ = false;
disable_pending_ = true;
SetState(PrivacyState::stopping,
"Local privacy exit; restoring desktop and input before "
"resuming remote operation");
continue;
SetState(PrivacyState::stopping, "Local shortcut: restoring desktop and input");
} else if (!health.failure.empty()) {
FailLocked(health.failure);
}
if (status_.overlay_active &&
(status_.state == PrivacyState::starting ||
status_.state == PrivacyState::on)) {
const auto now = Now();
if (verification_phase_ == VerificationPhase::challenge &&
now - last_issue_ >= 250) {
uint32_t challenge;
do {
challenge = random_();
} while (challenge == 0 || challenge == UINT32_MAX);
std::string error;
challenges_visible_ = true;
// Record even a partially painted multi-monitor challenge, so
// cleanup can reject its queued frames if another monitor fails.
verifier_.Issue(challenge, now);
if (!backend_->PaintChallenge(challenge, text_.verification,
error)) {
FailLocked(error);
} else {
last_issue_ = now;
}
}
if (status_.state == PrivacyState::starting &&
verification_phase_ == VerificationPhase::challenge &&
verifier_.Verified() && capture_running_) {
std::string error;
if (!backend_->ClearChallenges(error)) {
FailLocked(error);
} else {
challenges_visible_ = false;
verification_phase_ = VerificationPhase::draining;
clean_frames_ = 0;
started_ = now;
SetState(PrivacyState::starting,
"Capture exclusion verified; waiting for clean desktop "
"frames");
}
}
if (status_.state == PrivacyState::starting &&
verification_phase_ == VerificationPhase::draining &&
clean_frames_ >= 2 && capture_running_) {
verification_phase_ = VerificationPhase::live;
ever_enabled_ = true;
automatic_enable_ = false;
remote_paused_.store(false);
SetState(PrivacyState::on, "Privacy screen verified using " +
capture_backend_ +
"; verification windows removed");
}
if (status_.state == PrivacyState::on &&
now - last_frame_received_ > 1500) {
// Live sessions rely on capture delivery and native window/backend
// health; verification pixels are never painted during operation.
FailLocked(
"Capture stopped delivering desktop frames; remote operation "
"paused");
} else if (status_.state == PrivacyState::starting &&
automatic_enable_ && !capture_running_) {
if (now >= capture_start_deadline_)
FailLocked(
"Desktop capture did not start; automatic privacy remains "
"paused");
} else if (status_.state == PrivacyState::starting &&
now - started_ > 3000) {
if (verification_phase_ == VerificationPhase::draining) {
FailLocked(
"Capture did not clear its verification pixels; remote "
"operation paused");
} else {
FailLocked("Current capture backend " + capture_backend_ +
" did not pass privacy verification; remote operation "
"paused. Turn privacy off to recover.");
}
}
} else if (!enable_pending_ && status_.state == PrivacyState::starting) {
if (health.ready)
SetState(PrivacyState::on, "Privacy screen is on");
else if (Now() - started_ > 3000)
FailLocked("Privacy screen did not become ready");
}
}
if (backend_) {
// Keep window notifications and emergency exits responsive. Low-level
// input hooks are pumped independently by PrivacyInputGuard.
const unsigned timeout_ms = engaged_.load() ? 25 : 2000;
const unsigned timeout = engaged_.load() || disable_pending_ ? 25 : 2000;
lock.unlock();
backend_->WaitForEvents(timeout_ms);
backend_->WaitForEvents(timeout);
lock.lock();
} else {
// Unsupported platforms have no native health or capability polling.
wake_.wait(lock);
wake_.wait(lock, [this] { return quit_ || enable_pending_ || disable_pending_; });
}
}
} catch (const std::exception& e) {
if (!lock.owns_lock()) lock.lock();
remote_paused_.store(true);
SetState(PrivacyState::failed,
std::string("Privacy component failed: ") + e.what());
}
if (backend_) {
backend_->Recover();
backend_.reset();
SetState(PrivacyState::failed, std::string("Privacy component failed: ") + e.what());
}
if (backend_) { backend_->Recover(); backend_.reset(); }
}
} // namespace crossdesk
+7 -47
View File
@@ -1,26 +1,19 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-10
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
/* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */
#ifndef _PRIVACY_CONTROLLER_H_
#define _PRIVACY_CONTROLLER_H_
#include <remote_action.h>
#include <atomic>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <random>
#include <thread>
#include "privacy_backend.h"
#include "privacy_frame_verifier.h"
namespace crossdesk {
// Privacy is optional: it must never gate desktop video, audio or remote input.
// A failed backend is recovered automatically while the session keeps running.
class PrivacyController {
public:
using Factory = std::function<std::unique_ptr<PrivacyBackend>()>;
@@ -28,72 +21,39 @@ class PrivacyController {
~PrivacyController();
PrivacyController(const PrivacyController&) = delete;
PrivacyController& operator=(const PrivacyController&) = delete;
PrivacyStatus Snapshot() const;
// The cover uses a snapshot of these strings when the next session starts.
void SetText(const PrivacyScreenText& text);
void Enable(bool block_input);
// Arms protection synchronously before an incoming connection is published.
// Unlike a manual request, startup/unavailable capture cannot leave the
// newly connected peer with ordinary unprotected remote access.
void EnableOnConnection();
void Disable();
// Called only after the last control session has been removed. Failures keep
// protection while a session exists; ending that session recovers everything
// and must not leave the next connection in a paused/failed state.
void Disconnected();
void Shutdown();
void Fail(const std::string& reason);
bool RemoteAllowed() const { return !remote_paused_.load(); }
bool Engaged() const { return engaged_.load(); }
void CaptureChanged(const std::string& backend, bool running);
void CaptureInterrupted();
void DisplayChanging();
bool ObserveFrame(const uint8_t* y, size_t size, int width, int height,
int left, int top, int physical_width, int physical_height);
void CaptureChanged(bool running);
private:
enum class VerificationPhase { challenge, draining, live };
void Run(Factory factory);
void WakeLocked();
void SetState(PrivacyState state, const std::string& reason);
void FailLocked(const std::string& reason);
void BeginVerification(const std::string& reason = {});
static uint64_t Now();
mutable std::mutex mutex_;
std::condition_variable wake_;
std::thread thread_;
std::unique_ptr<PrivacyBackend> backend_;
PrivacyStatus status_{};
PrivacyFrameVerifier verifier_;
PrivacyScreenText text_;
std::mt19937 random_{std::random_device{}()};
std::atomic<bool> remote_paused_{false};
std::atomic<bool> engaged_{false};
std::atomic<bool> drain_after_recovery_{false};
VerificationPhase verification_phase_ = VerificationPhase::challenge;
bool challenges_visible_ = false;
unsigned clean_frames_ = 0;
uint64_t last_frame_received_ = 0;
bool quit_ = false;
bool enable_pending_ = false;
bool disable_pending_ = false;
bool block_requested_ = false;
bool capture_running_ = false;
bool ever_enabled_ = false;
bool automatic_enable_ = false;
uint64_t capture_start_deadline_ = 0;
std::string capture_backend_;
bool capture_running_ = false;
std::string failure_;
uint64_t started_ = 0;
uint64_t last_issue_ = 0;
uint64_t last_capability_check_ = 0;
int frame_left_ = 0;
int frame_top_ = 0;
bool frame_monitor_set_ = false;
bool first_verified_frame_logged_ = false;
};
} // namespace crossdesk
#endif // _PRIVACY_CONTROLLER_H_
#endif
@@ -1,98 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-10
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_FRAME_VERIFIER_H_
#define _PRIVACY_FRAME_VERIFIER_H_
#include <cstddef>
#include <cstdint>
#include <deque>
#include "privacy_probe_pattern.h"
namespace crossdesk {
// An opaque square code beside localized text, UNDER the privacy cover.
// Used only while transmission is gated during capture validation. Probes are
// then removed, and queued frames containing their tokens must be discarded.
class PrivacyFrameVerifier {
public:
void Reset(bool retain_history = false) {
challenges_.clear();
if (!retain_history) issued_values_.clear();
matches_ = 0;
}
void Issue(uint32_t value, uint64_t now) {
challenges_.push_back({value, now, false});
issued_values_.push_back(value);
// Covers more than the startup/drain deadlines, including backend retries.
while (issued_values_.size() > 128) issued_values_.pop_front();
// A validation attempt is time-bounded. Keep every issued token so an old
// queued frame cannot escape merely because its token expired for matching.
}
bool Observe(const uint8_t* y, size_t size, int width, int height,
uint64_t now) {
uint32_t value = 0;
if (!Decode(y, size, width, height, value)) return false;
for (auto& challenge : challenges_) {
if (challenge.value != value || now - challenge.issued > 750) continue;
if (!challenge.seen) {
challenge.seen = true;
++matches_;
}
return true;
}
return false;
}
bool Verified() const { return matches_ >= 2; }
bool ContainsChallenge(const uint8_t* y, size_t size, int width,
int height) const {
uint32_t value = 0;
if (!Decode(y, size, width, height, value)) return false;
for (const auto issued : issued_values_)
if (issued == value) return true;
return false;
}
private:
static bool Decode(const uint8_t* y, size_t size, int width, int height,
uint32_t& value) {
using Pattern = PrivacyProbePattern;
if (!y || width < Pattern::kCaptureExtent ||
height < Pattern::kCaptureExtent ||
size < static_cast<size_t>(width) * height)
return false;
value = 0;
for (int bit = 0; bit < 32; ++bit) {
const int x = Pattern::kOffset + Pattern::kPadding +
(bit % Pattern::kColumns) * Pattern::kCell +
Pattern::kCell / 2;
const int row = Pattern::kOffset + Pattern::kPadding +
(bit / Pattern::kColumns) * Pattern::kCell +
Pattern::kCell / 2;
const auto pixel = y[static_cast<size_t>(row) * width + x];
const auto inverse =
y[static_cast<size_t>(row + 4 * Pattern::kCell) * width + x];
if ((pixel > 60 && pixel < 190) || (inverse > 60 && inverse < 190) ||
(pixel >= 190) == (inverse >= 190))
return false;
if (pixel >= 190) value |= uint32_t{1} << bit;
}
return true;
}
struct Challenge {
uint32_t value;
uint64_t issued;
bool seen;
};
std::deque<Challenge> challenges_;
std::deque<uint32_t> issued_values_;
unsigned matches_ = 0;
};
} // namespace crossdesk
#endif // _PRIVACY_FRAME_VERIFIER_H_
@@ -1,37 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_PROBE_PATTERN_H_
#define _PRIVACY_PROBE_PATTERN_H_
#include <cstdint>
namespace crossdesk {
// Physical-pixel geometry shared by the renderer and raw-frame verifier.
// Text can scale/wrap independently without moving any sampling location.
struct PrivacyProbePattern {
static constexpr int kOffset = 16;
static constexpr int kPadding = 16;
static constexpr int kGap = 24;
static constexpr int kCell = 12;
static constexpr int kColumns = 8;
static constexpr int kRows = 8;
static constexpr int kSide = kColumns * kCell;
static constexpr int kCaptureExtent = kOffset + kPadding + kSide;
static constexpr int kTextLeft = kPadding + kSide + kGap;
// Top half carries all 32 bits; bottom half carries their complements.
static constexpr bool White(uint32_t value, int column, int row) {
const int index = row * kColumns + column;
const bool bit = (value & (uint32_t{1} << (index % 32))) != 0;
return index < 32 ? bit : !bit;
}
};
} // namespace crossdesk
#endif // _PRIVACY_PROBE_PATTERN_H_