[feat] use device keys for server login

This commit is contained in:
dijunkun
2026-11-02 01:15:11 +08:00
parent bb48551d59
commit 49717f0f68
17 changed files with 225 additions and 41 deletions
@@ -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_, &params_, 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
+2 -3
View File
@@ -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_;
};
+5 -3
View File
@@ -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,