diff --git a/apps/desktop/src/gui/runtime/device_identity_store.cpp b/apps/desktop/src/gui/runtime/device_identity_store.cpp index 0e83a1cc..b329eb33 100644 --- a/apps/desktop/src/gui/runtime/device_identity_store.cpp +++ b/apps/desktop/src/gui/runtime/device_identity_store.cpp @@ -70,28 +70,38 @@ struct PrivateText { void Validate(const nlohmann::json& document) { Require(document.is_object() && document.at("version") == 1); } + +std::optional ReadLocked(const std::filesystem::path& directory) { + const auto path = directory / "identity.json"; + if (std::filesystem::symlink_status(path).type() == + std::filesystem::file_type::not_found) return std::nullopt; + PrivateText text{private_file::Read(path)}; + auto saved = nlohmann::json::parse(text.value, nullptr, false); + Require(!saved.is_discarded()); + Validate(saved); + return saved; +} } // namespace -void Update(const std::filesystem::path& cache, - const std::function& 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); +std::optional Read(const std::filesystem::path& directory) { + private_file::EnsureDirectory(directory, true); + StoreLock lock(directory / "identity.json.lock"); + return ReadLocked(directory); +} + +void Update(const std::filesystem::path& directory, + const std::function& update) { + private_file::EnsureDirectory(directory, true); + StoreLock lock(directory / "identity.json.lock"); + auto previous = ReadLocked(directory); + auto saved = previous ? std::move(*previous) : nlohmann::json{{"version", 1}}; PrivateText before{saved.dump()}; - update(saved); + update(saved, previous.has_value()); Validate(saved); PrivateText after{saved.dump()}; Require(after.value.size() <= 64 * 1024); - if (!exists || before.value != after.value) private_file::Write(path, after.value); + if (!previous || before.value != after.value) + private_file::Write(directory / "identity.json", after.value); } } // namespace crossdesk::device_identity_store diff --git a/apps/desktop/src/gui/runtime/device_identity_store.h b/apps/desktop/src/gui/runtime/device_identity_store.h index 5ccfadfa..23a59d5e 100644 --- a/apps/desktop/src/gui/runtime/device_identity_store.h +++ b/apps/desktop/src/gui/runtime/device_identity_store.h @@ -10,14 +10,17 @@ #include #include #include +#include namespace crossdesk::device_identity_store { -// 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. +// Device signing keys live in the persistent trusted-device directory. +// Read the latest document under a process lock, then atomically persist it. // Throw on corruption, unsafe permissions, lock contention or persistence error. -void Update(const std::filesystem::path& cache, - const std::function& update); +// Read returns nullopt only when the identity file is absent (legacy migration). +std::optional Read(const std::filesystem::path& directory); +void Update(const std::filesystem::path& directory, + const std::function& update); } // namespace crossdesk::device_identity_store diff --git a/apps/desktop/src/gui/runtime/trusted_devices.cpp b/apps/desktop/src/gui/runtime/trusted_devices.cpp index 4a049e35..9ad93246 100644 --- a/apps/desktop/src/gui/runtime/trusted_devices.cpp +++ b/apps/desktop/src/gui/runtime/trusted_devices.cpp @@ -14,8 +14,8 @@ namespace crossdesk { namespace { constexpr size_t kMaxTrustedDevices = 128; -void Require(bool ok) { - if (!ok) throw std::runtime_error("Invalid or unavailable trusted device identity"); +void Require(bool ok, const char* error = "Invalid or unavailable trusted device identity") { + if (!ok) throw std::runtime_error(error); } std::string Hex(const uint8_t* data, size_t size) { constexpr char digits[] = "0123456789abcdef"; @@ -81,7 +81,19 @@ TrustedDevices::TrustedDevices(const std::filesystem::path& directory, } } - device_identity_store::Update(cache, [&](nlohmann::json& identity) { + device_identity_store::Update(directory, [&](nlohmann::json& identity, bool identity_exists) { + bool legacy_identity_exists = false; + if (!identity_exists) { + // The persistent copy is authoritative once migration has completed. + // Leave the old cache copy intact so an interrupted migration is retryable. + if (auto previous = device_identity_store::Read(cache)) { + identity = std::move(*previous); + legacy_identity_exists = true; + } + } else { + Require(identity.contains("trust_private_key"), + "Persistent device signing key is missing; restore the original identity"); + } if (!identity.contains("trust_private_key") && !legacy_key.value.empty()) identity["trust_private_key"] = legacy_key.value; if (identity.contains("trust_private_key")) { @@ -94,6 +106,8 @@ TrustedDevices::TrustedDevices(const std::filesystem::path& directory, secret.data(), secret.size())); OPENSSL_cleanse(secret.data(), secret.size()); } else { + Require(!exists && !legacy_identity_exists, + "Device signing key is missing from an existing installation; restore the original identity"); key_.reset(EVP_PKEY_Q_keygen(nullptr, nullptr, "ED25519")); Require(bool(key_)); std::array secret{}; diff --git a/apps/desktop/src/platform/windows/service/shared_identity.cpp b/apps/desktop/src/platform/windows/service/shared_identity.cpp index 98632f52..c927143e 100644 --- a/apps/desktop/src/platform/windows/service/shared_identity.cpp +++ b/apps/desktop/src/platform/windows/service/shared_identity.cpp @@ -270,7 +270,9 @@ void SharedHostIdentity::Import() { private_file::EnsureDirectory(cache_, true); private_file::EnsureDirectory(trusted_); Json identity{{"version", 1}}; - if (std::filesystem::exists(cache_ / "identity.json")) + if (std::filesystem::exists(trusted_ / "identity.json")) + identity = PrivateDocument(trusted_ / "identity.json"); + else if (std::filesystem::exists(cache_ / "identity.json")) identity = PrivateDocument(cache_ / "identity.json"); Require(identity.is_object() && identity.value("version", 0) == 1, "Invalid existing device identity store"); @@ -284,7 +286,7 @@ void SharedHostIdentity::Import() { cache.checksum = credential_cache::Checksum(&cache, offsetof(credential_cache::CacheV3, checksum)); // Validate everything before replacing any user identity data. Both stores // stay private to this process's user; their DACLs are never relaxed. - private_file::Write(cache_ / "identity.json", identity.dump()); + private_file::Write(trusted_ / "identity.json", identity.dump()); private_file::Write(trusted_ / "devices.json", data.at("trusted_devices").dump()); private_file::Write(cache_ / "secure_cache_v3.enc", std::string(reinterpret_cast(&cache), sizeof(cache))); @@ -303,7 +305,10 @@ bool SharedHostIdentity::Publish(const std::string& host_config, std::string& er std::memset(cache.base.iv, 0, sizeof(cache.base.iv)); cache.checksum = credential_cache::Checksum(&cache, offsetof(credential_cache::CacheV3, checksum)); const std::string identity_cache(reinterpret_cast(&cache), sizeof(cache)); - const auto identity = PrivateDocument(cache_ / "identity.json"); + // CLI setup can publish an installation that has not run GUI migration yet. + const auto identity = PrivateDocument( + std::filesystem::exists(trusted_ / "identity.json") + ? trusted_ / "identity.json" : cache_ / "identity.json"); const auto key = identity.at("trust_private_key").get(); Require(Unhex(key).size() == 32, "Invalid device identity key"); const auto devices = PrivateDocument(trusted_ / "devices.json");