mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-03 20:47:43 +08:00
[feat] use device keys for server login
This commit is contained in:
@@ -2983,6 +2983,7 @@ bool GuiApplication::RequestPasswordChange(const std::string &password) {
|
||||
password_change_result_ready_ = false;
|
||||
password_change_succeeded_ = false;
|
||||
password_change_result_uncertain_ = false;
|
||||
password_change_recovery_ = false;
|
||||
password_change_requested_at_ = std::chrono::steady_clock::now();
|
||||
pending_password_change_request_id_ = request_id;
|
||||
pending_local_password_ = password;
|
||||
@@ -3062,6 +3063,7 @@ void GuiApplication::HandlePasswordChangeResult() {
|
||||
password_change_result_ready_ = false;
|
||||
password_change_succeeded_ = false;
|
||||
password_change_result_uncertain_ = false;
|
||||
password_change_recovery_ = false;
|
||||
pending_password_change_request_id_.clear();
|
||||
pending_local_password_.clear();
|
||||
password_change_error_.clear();
|
||||
@@ -3108,23 +3110,53 @@ void GuiApplication::HandlePasswordChangeResult() {
|
||||
}
|
||||
|
||||
void GuiApplication::HandleCredentialRecovery() {
|
||||
bool retry_active = false;
|
||||
bool promote_pending = false;
|
||||
bool clear_pending = false;
|
||||
CredentialRecoveryAction action;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
||||
retry_active = credential_recovery_retry_active_;
|
||||
promote_pending = credential_recovery_promote_pending_;
|
||||
clear_pending = credential_recovery_clear_pending_;
|
||||
credential_recovery_retry_active_ = false;
|
||||
credential_recovery_promote_pending_ = false;
|
||||
credential_recovery_clear_pending_ = false;
|
||||
if (retry_active) {
|
||||
action = std::exchange(credential_recovery_action_, CredentialRecoveryAction::None);
|
||||
if (action == CredentialRecoveryAction::RetryActive) {
|
||||
credential_recovery_attempt_pending_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (retry_active) {
|
||||
if (action == CredentialRecoveryAction::ResumePending) {
|
||||
// Key authentication proves device ownership, not which password committed.
|
||||
// Replay the durable request and promote only its successful server reply.
|
||||
const bool self_hosted = config_center_->IsSelfHosted();
|
||||
std::string request_id;
|
||||
const auto identity = settings_.PendingPasswordChangeIdentity(self_hosted,
|
||||
self_hosted ? config_center_->GetSignalServerHost() : config_center_->GetDefaultServerHost(),
|
||||
self_hosted ? config_center_->GetSignalServerPort() : config_center_->GetDefaultSignalServerPort(),
|
||||
&request_id);
|
||||
const auto at = identity.find('@');
|
||||
if (!peer_ || !signal_connected_ || at == std::string::npos || request_id.empty() ||
|
||||
identity.substr(0, at) != client_id_) return;
|
||||
const auto password = identity.substr(at + 1);
|
||||
{
|
||||
std::lock_guard lock(password_change_mutex_);
|
||||
if (password_change_pending_) return;
|
||||
password_change_pending_ = true;
|
||||
password_change_result_ready_ = false;
|
||||
password_change_succeeded_ = false;
|
||||
password_change_result_uncertain_ = false;
|
||||
password_change_recovery_ = true;
|
||||
password_change_requested_at_ = std::chrono::steady_clock::now();
|
||||
pending_password_change_request_id_ = request_id;
|
||||
pending_local_password_ = password;
|
||||
password_change_error_.clear();
|
||||
}
|
||||
const auto message = nlohmann::json{{"type", "change_password"},
|
||||
{"request_id", request_id}, {"new_password", password}}.dump();
|
||||
if (SendSignalMessage(peer_, message.data(), message.size()) != 0) {
|
||||
std::lock_guard lock(password_change_mutex_);
|
||||
password_change_result_ready_ = true;
|
||||
password_change_result_uncertain_ = true;
|
||||
password_change_error_ = "Password recovery connection unavailable";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == CredentialRecoveryAction::RetryActive) {
|
||||
LOG_INFO("Pending credential was not accepted; retrying active credential");
|
||||
if (peer_) {
|
||||
CloseConnectionPeer();
|
||||
@@ -3132,7 +3164,7 @@ void GuiApplication::HandleCredentialRecovery() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (promote_pending) {
|
||||
if (action == CredentialRecoveryAction::PromotePending) {
|
||||
#if defined(__linux__) && !defined(__OHOS__)
|
||||
HeadlessConsole::Instance().NotifyKey("console_password_recovered");
|
||||
#endif
|
||||
@@ -3141,7 +3173,7 @@ void GuiApplication::HandleCredentialRecovery() {
|
||||
} else {
|
||||
LOG_INFO("Recovered password change with pending credential");
|
||||
}
|
||||
} else if (clear_pending) {
|
||||
} else if (action == CredentialRecoveryAction::ClearPending) {
|
||||
#if defined(__linux__) && !defined(__OHOS__)
|
||||
HeadlessConsole::Instance().NotifyKey("console_password_retained");
|
||||
#endif
|
||||
|
||||
@@ -593,14 +593,17 @@ bool SettingsManager::StagePendingPasswordChange(
|
||||
}
|
||||
|
||||
std::string SettingsManager::PendingPasswordChangeIdentity(
|
||||
bool self_hosted, const std::string &server_host, int server_port) const {
|
||||
bool self_hosted, const std::string &server_host, int server_port,
|
||||
std::string* request_id) const {
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
if (request_id) request_id->clear();
|
||||
if (cache_v3_.pending_identity[0] == '\0' ||
|
||||
cache_v3_.pending_self_hosted != self_hosted ||
|
||||
cache_v3_.pending_server_port != server_port ||
|
||||
server_host != cache_v3_.pending_server_host) {
|
||||
return {};
|
||||
}
|
||||
if (request_id) *request_id = cache_v3_.pending_request_id;
|
||||
return cache_v3_.pending_identity;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ public:
|
||||
const std::string &request_id);
|
||||
std::string PendingPasswordChangeIdentity(
|
||||
bool self_hosted, const std::string &server_host,
|
||||
int server_port) const;
|
||||
int server_port, std::string* request_id = nullptr) const;
|
||||
bool PromotePendingPasswordChange();
|
||||
bool ClearPendingPasswordChange();
|
||||
|
||||
|
||||
@@ -709,6 +709,7 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
props->audio_capture_button_pressed_ = !background;
|
||||
memcpy(&props->params_, ¶ms_, sizeof(Params));
|
||||
props->params_.user_id = props->local_id_.c_str();
|
||||
props->params_.on_signal_exchange = nullptr;
|
||||
props->peer_events_ = std::make_shared<PeerEventHandler>(*this);
|
||||
props->params_.user_data = props->peer_events_.get();
|
||||
props->peer_ = CreatePeer(&props->params_);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
namespace crossdesk::device_identity_store {
|
||||
|
||||
// The trusted-device pairing key lives in cache/identity.json. Read the latest
|
||||
// The shared device signing 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,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#include "runtime/device_login.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "runtime/trusted_devices.h"
|
||||
|
||||
namespace crossdesk {
|
||||
int DeviceLogin::Exchange(SignalExchangeStage stage, const nlohmann::json& message,
|
||||
const TrustedDevices* identity, char* response, size_t capacity) {
|
||||
auto output = [&](const nlohmann::json& reply) {
|
||||
const auto text = reply.dump();
|
||||
if (text.empty() || text.size() > capacity) return -1;
|
||||
std::memcpy(response, text.data(), text.size());
|
||||
return static_cast<int>(text.size());
|
||||
};
|
||||
if (stage == SignalExchangeStage::PrepareLogin) {
|
||||
phase_ = Phase::Idle;
|
||||
used_password_ = false;
|
||||
password_.clear();
|
||||
request_ = message;
|
||||
const auto login = message.at("user_id").get<std::string>();
|
||||
if (login.rfind("C-", 0) == 0) return 0;
|
||||
if (!identity) return -1; // Never fall back when local key storage fails.
|
||||
const auto at = login.find('@');
|
||||
request_["user_id"] = login.substr(0, at);
|
||||
if (at != std::string::npos) password_ = login.substr(at + 1);
|
||||
request_["device_key"] = {{"version", 1}, {"public_key", identity->PublicKey()}};
|
||||
request_.erase("reconnect_token");
|
||||
request_.erase("reconnect_version");
|
||||
phase_ = Phase::AwaitingChallenge;
|
||||
return output(request_);
|
||||
}
|
||||
if (!Pending()) return 0;
|
||||
const auto type = message.value("type", "");
|
||||
if (type == "login_challenge") {
|
||||
if (!identity || phase_ != Phase::AwaitingChallenge || message.at("device_key_version") != 1 ||
|
||||
message.at("user_id") != request_.at("user_id")) return -1;
|
||||
const auto nonce = message.at("nonce").get<std::string>();
|
||||
if (nonce.size() != 64 || !std::all_of(nonce.begin(), nonce.end(),
|
||||
[](char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); }))
|
||||
return -1;
|
||||
const auto id = request_.at("user_id").get<std::string>();
|
||||
auto reply = request_;
|
||||
used_password_ = message.at("password_required").get<bool>();
|
||||
if (used_password_) {
|
||||
if (id.empty() || password_.empty()) return -1;
|
||||
reply["user_id"] = id + "@" + password_;
|
||||
}
|
||||
const auto transcript = nlohmann::json::array(
|
||||
{"crossdesk.device-login.v1", id, identity->PublicKey(), nonce}).dump();
|
||||
reply["device_key"]["nonce"] = nonce;
|
||||
reply["device_key"]["signature"] = identity->Sign(transcript);
|
||||
phase_ = Phase::AwaitingResult;
|
||||
password_.clear();
|
||||
return output(reply);
|
||||
}
|
||||
if (type == "login") {
|
||||
if (message.at("status") == "success") {
|
||||
if (phase_ != Phase::AwaitingResult || message.value("device_key_version", 0) != 1) return -1;
|
||||
const auto assigned = message.at("user_id").get<std::string>();
|
||||
const auto expected = request_.at("user_id").get<std::string>();
|
||||
if (assigned.empty() || (!expected.empty() && assigned != expected)) return -1;
|
||||
}
|
||||
password_.clear();
|
||||
request_.clear();
|
||||
phase_ = Phase::Idle;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-11-02
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _DEVICE_LOGIN_H_
|
||||
#define _DEVICE_LOGIN_H_
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk {
|
||||
class TrustedDevices;
|
||||
|
||||
// One signaling connection's application authentication, owned by its callbacks.
|
||||
class DeviceLogin {
|
||||
public:
|
||||
int Exchange(SignalExchangeStage stage, const nlohmann::json& message,
|
||||
const TrustedDevices* identity, char* response, size_t capacity);
|
||||
bool Pending() const { return phase_ != Phase::Idle; }
|
||||
bool UsedPassword() const { return used_password_; }
|
||||
|
||||
private:
|
||||
enum class Phase { Idle, AwaitingChallenge, AwaitingResult };
|
||||
nlohmann::json request_;
|
||||
std::string password_;
|
||||
Phase phase_ = Phase::Idle;
|
||||
bool used_password_ = false;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -190,9 +190,7 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
if (pending_identity.empty()) {
|
||||
credential_recovery_in_progress_ = false;
|
||||
credential_recovery_attempt_pending_ = false;
|
||||
credential_recovery_retry_active_ = false;
|
||||
credential_recovery_promote_pending_ = false;
|
||||
credential_recovery_clear_pending_ = false;
|
||||
credential_recovery_action_ = CredentialRecoveryAction::None;
|
||||
} else if (!credential_recovery_in_progress_) {
|
||||
credential_recovery_in_progress_ = true;
|
||||
credential_recovery_attempt_pending_ = true;
|
||||
@@ -270,6 +268,7 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
|
||||
params_.on_signal_status = PeerEventHandler::OnSignalStatus;
|
||||
params_.on_signal_message = PeerEventHandler::OnSignalMessage;
|
||||
params_.on_signal_exchange = PeerEventHandler::OnSignalExchange;
|
||||
params_.on_connection_status = PeerEventHandler::OnConnectionStatus;
|
||||
params_.on_net_status_report = PeerEventHandler::OnNetStatusReport;
|
||||
|
||||
|
||||
@@ -84,6 +84,24 @@ PeerEventHandler::PeerEventHandler(GuiRuntime& owner) : owner_(owner) {
|
||||
owner_.transfers_.Connected(this, "");
|
||||
}
|
||||
|
||||
int PeerEventHandler::OnSignalExchange(SignalExchangeStage stage, const char* message,
|
||||
size_t size, char* response, size_t capacity, void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
if (!handler || !message) return -1;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return -1;
|
||||
if (stage == SignalExchangeStage::IncomingMessage &&
|
||||
!handler->device_login_.Pending()) return 0;
|
||||
try {
|
||||
return handler->device_login_.Exchange(stage,
|
||||
nlohmann::json::parse(message, message + size),
|
||||
handler->owner_.trusted_devices_.get(), response, capacity);
|
||||
} catch (const std::exception&) {
|
||||
LOG_ERROR("Device login protocol or identity unavailable");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
|
||||
void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
@@ -184,13 +202,16 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
|
||||
j.contains("reason") && j["reason"].is_string()
|
||||
? j["reason"].get<std::string>()
|
||||
: "Password change failed";
|
||||
runtime->password_change_result_uncertain_ = false;
|
||||
// A rejected replay cannot tell us whether the original request committed.
|
||||
runtime->password_change_result_uncertain_ = runtime->password_change_recovery_ &&
|
||||
!runtime->password_change_succeeded_;
|
||||
runtime->password_change_result_ready_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
||||
size_t user_id_size, void* user_data) {
|
||||
using RecoveryAction = GuiRuntime::CredentialRecoveryAction;
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
@@ -212,10 +233,12 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
||||
SendClientInfo(runtime->peer_, client_id);
|
||||
std::lock_guard<std::mutex> lock(runtime->password_change_mutex_);
|
||||
if (runtime->credential_recovery_in_progress_) {
|
||||
if (runtime->credential_recovery_attempt_pending_) {
|
||||
runtime->credential_recovery_promote_pending_ = true;
|
||||
if (!handler->device_login_.UsedPassword()) {
|
||||
runtime->credential_recovery_action_ = RecoveryAction::ResumePending;
|
||||
} else if (runtime->credential_recovery_attempt_pending_) {
|
||||
runtime->credential_recovery_action_ = RecoveryAction::PromotePending;
|
||||
} else {
|
||||
runtime->credential_recovery_clear_pending_ = true;
|
||||
runtime->credential_recovery_action_ = RecoveryAction::ClearPending;
|
||||
}
|
||||
}
|
||||
} else if (SignalStatus::SignalFailed == status) {
|
||||
@@ -223,7 +246,7 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
||||
std::lock_guard<std::mutex> lock(runtime->password_change_mutex_);
|
||||
if (runtime->credential_recovery_in_progress_ &&
|
||||
runtime->credential_recovery_attempt_pending_) {
|
||||
runtime->credential_recovery_retry_active_ = true;
|
||||
runtime->credential_recovery_action_ = RecoveryAction::RetryActive;
|
||||
}
|
||||
} else if (SignalStatus::SignalClosed == status) {
|
||||
runtime->signal_connected_ = false;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "minirtc.h"
|
||||
#include "runtime/session_lifecycle.h"
|
||||
#include "runtime/device_login.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -63,6 +64,9 @@ public:
|
||||
size_t user_id_size, void *user_data);
|
||||
static void OnSignalMessage(const char *message, size_t size,
|
||||
void *user_data);
|
||||
static int OnSignalExchange(SignalExchangeStage stage, const char* message,
|
||||
size_t size, char* response, size_t capacity,
|
||||
void* user_data);
|
||||
static void OnConnectionStatus(ConnectionStatus status, const char *user_id,
|
||||
size_t user_id_size, void *user_data);
|
||||
static void OnNetStatusReport(const char *client_id, size_t client_id_size,
|
||||
@@ -85,6 +89,7 @@ private:
|
||||
size_t early_data_bytes_ = 0;
|
||||
|
||||
GuiRuntime& owner_;
|
||||
DeviceLogin device_login_;
|
||||
gui_detail::SessionCallbackGate callbacks_;
|
||||
};
|
||||
|
||||
|
||||
@@ -167,21 +167,23 @@ struct UserSettingsState {
|
||||
};
|
||||
|
||||
struct PasswordChangeState {
|
||||
enum class CredentialRecoveryAction {
|
||||
None, RetryActive, PromotePending, ClearPending, ResumePending
|
||||
};
|
||||
std::mutex password_change_mutex_;
|
||||
uint64_t next_password_change_request_id_ = 0;
|
||||
bool password_change_pending_ = false;
|
||||
bool password_change_result_ready_ = false;
|
||||
bool password_change_succeeded_ = false;
|
||||
bool password_change_result_uncertain_ = false;
|
||||
bool password_change_recovery_ = false;
|
||||
std::chrono::steady_clock::time_point password_change_requested_at_;
|
||||
std::string pending_password_change_request_id_;
|
||||
std::string pending_local_password_;
|
||||
std::string password_change_error_;
|
||||
bool credential_recovery_in_progress_ = false;
|
||||
bool credential_recovery_attempt_pending_ = false;
|
||||
bool credential_recovery_retry_active_ = false;
|
||||
bool credential_recovery_promote_pending_ = false;
|
||||
bool credential_recovery_clear_pending_ = false;
|
||||
CredentialRecoveryAction credential_recovery_action_ = CredentialRecoveryAction::None;
|
||||
};
|
||||
|
||||
struct ConnectionState {
|
||||
|
||||
@@ -156,15 +156,19 @@ nlohmann::json TrustedDevices::Prove(const std::string& nonce,
|
||||
Require(IsHex(nonce, 32) && Label(device_id) && Label(name));
|
||||
const Device device{public_key_, name, device_id};
|
||||
const auto text = Transcript(nonce, device);
|
||||
return {{"type", "device_trust_proof"}, {"version", 1}, {"nonce", nonce},
|
||||
{"public_key", public_key_}, {"device_id", device_id}, {"name", name},
|
||||
{"signature", Sign(text)}};
|
||||
}
|
||||
|
||||
std::string TrustedDevices::Sign(std::string_view text) const {
|
||||
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())}};
|
||||
return Hex(signature.data(), signature.size());
|
||||
}
|
||||
|
||||
std::optional<TrustedDevices::Device> TrustedDevices::Verify(
|
||||
|
||||
@@ -31,6 +31,9 @@ class TrustedDevices {
|
||||
TrustedDevices(const std::filesystem::path& directory,
|
||||
const std::filesystem::path& cache);
|
||||
std::string Fingerprint() const;
|
||||
const std::string& PublicKey() const { return public_key_; }
|
||||
// Callers supply a canonical, purpose-specific transcript.
|
||||
std::string Sign(std::string_view transcript) const;
|
||||
static std::string Fingerprint(const std::string& public_key);
|
||||
static std::string Nonce();
|
||||
nlohmann::json Prove(const std::string& nonce,
|
||||
|
||||
Reference in New Issue
Block a user