From cf9877575491771251bdaa83dc4e1c761e2c310a Mon Sep 17 00:00:00 2001 From: dijunkun Date: Wed, 21 Oct 2026 02:08:31 +0800 Subject: [PATCH] [fix] secure Linux credential cache permissions --- .../src/gui/application/gui_application.cpp | 11 ++ .../features/settings/settings_manager.cpp | 164 +++++++++++++++++- apps/desktop/src/gui/runtime/gui_runtime.cpp | 14 +- 3 files changed, 185 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index 8b91d70b..bd4929ad 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -1113,7 +1114,17 @@ int GuiApplication::Run() { "%d", signal_port); } +#if defined(__linux__) && !defined(__OHOS__) + try { + InitializeSettings(); + } catch (const std::system_error&) { + // Credential I/O errors are logged by SettingsManager. Exit cleanly rather + // than initializing a new identity or crashing with an uncaught exception. + return -1; + } +#else InitializeSettings(); +#endif #if defined(__linux__) && !defined(__OHOS__) HeadlessConsole::Instance().BindSettings( config_center_.get(), [this] { diff --git a/apps/desktop/src/gui/features/settings/settings_manager.cpp b/apps/desktop/src/gui/features/settings/settings_manager.cpp index 7804ca07..13fdb1c7 100644 --- a/apps/desktop/src/gui/features/settings/settings_manager.cpp +++ b/apps/desktop/src/gui/features/settings/settings_manager.cpp @@ -1,5 +1,6 @@ #include "features/settings/settings_manager.h" +#include #include #include #include @@ -11,6 +12,7 @@ #include #include #include +#include #include #if defined(_WIN32) @@ -20,6 +22,9 @@ #include #include #endif +#if defined(__linux__) && !defined(__OHOS__) +#include +#endif #include "localization.h" #include "rd_log.h" @@ -31,6 +36,92 @@ namespace { constexpr uint32_t kCacheV3Magic = 0x33444358; // "XCD3" constexpr uint32_t kCacheV3Version = 3; +#if defined(__linux__) && !defined(__OHOS__) +constexpr const char *kCredentialCacheNames[] = { + "secure_cache_v3.enc", "secure_cache_v2.enc", "secure_cache.enc"}; + +bool RestrictCachePermissions(const std::filesystem::path &path, + bool directory = false) { + const int flags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK | + (directory ? O_DIRECTORY : 0); + const int fd = open(path.c_str(), flags); + if (fd < 0 && errno == ENOENT) return true; + int error = fd < 0 ? errno : 0; + if (fd >= 0) { + struct stat info {}; + if (fstat(fd, &info) != 0) { + error = errno; + } else if (info.st_uid != geteuid() || + (directory ? !S_ISDIR(info.st_mode) : !S_ISREG(info.st_mode))) { + error = EPERM; + } else { + const mode_t mode = directory ? 0700 : 0600; + if ((info.st_mode & 07777) != mode && fchmod(fd, mode) != 0) + error = errno; + } + close(fd); + } + if (error) { + LOG_WARN("Cannot restrict credential cache permissions for {}: {}", + path.string(), + std::error_code(error, std::generic_category()).message()); + } + return error == 0; +} + +[[noreturn]] void CacheReadError(const std::filesystem::path &path, int error) { + const std::system_error failure( + error, std::generic_category(), + "Cannot read credential cache; existing data was kept: " + path.string()); + LOG_ERROR("{}", failure.what()); + throw failure; +} + +bool ReadCredentialCache(const std::filesystem::path &path, void *data, + size_t size) { + const int fd = open(path.c_str(), O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK); + if (fd < 0) { + if (errno == ENOENT) return false; + CacheReadError(path, errno); + } + struct stat info {}; + if (fstat(fd, &info) != 0) { + const int error = errno; + close(fd); + CacheReadError(path, error); + } + if (!S_ISREG(info.st_mode)) { + close(fd); + CacheReadError(path, EINVAL); + } + size_t received = 0; + while (received < size) { + const auto count = + read(fd, static_cast(data) + received, size - received); + if (count < 0) { + if (errno == EINTR) continue; + const int error = errno; + close(fd); + CacheReadError(path, error); + } + if (count == 0) break; + received += static_cast(count); + } + close(fd); + return received == size; +} + +bool HasExistingCredentialCache(const std::filesystem::path &directory) { + for (const char *name : kCredentialCacheNames) { + const auto path = directory / name; + struct stat info {}; + if (lstat(path.c_str(), &info) == 0) return true; + if (errno != ENOENT) CacheReadError(path, errno); + } + return false; +} +#endif + template void CopyString(char (&destination)[Size], const char *source) { static_assert(Size > 0); @@ -110,11 +201,41 @@ bool WriteFileAtomically(const std::filesystem::path &target, if (ec) { return false; } +#if defined(__linux__) && !defined(__OHOS__) + if (!RestrictCachePermissions(parent, true)) return false; +#endif } +#if defined(__linux__) && !defined(__OHOS__) + // Never adopt another user's file or replace a link while saving credentials. + struct stat previous {}; + if (lstat(target.c_str(), &previous) == 0) { + if (!S_ISREG(previous.st_mode) || previous.st_uid != geteuid()) return false; + } else if (errno != ENOENT) { + return false; + } +#endif + const std::filesystem::path temporary = TemporaryPathFor(target); #if defined(_WIN32) FILE *file = _wfopen(temporary.c_str(), L"wb"); +#elif defined(__linux__) && !defined(__OHOS__) + // Set permissions before writing any secret, including under umask(0000). + // Exclusive creation also prevents following/reusing a pre-existing temp file. + const int fd = open(temporary.c_str(), + O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600); + if (fd < 0) return false; + if (fchmod(fd, 0600) != 0) { + close(fd); + std::filesystem::remove(temporary, ec); + return false; + } + FILE *file = fdopen(fd, "wb"); + if (!file) { + close(fd); + std::filesystem::remove(temporary, ec); + return false; + } #else FILE *file = std::fopen(temporary.c_str(), "wb"); #endif @@ -184,16 +305,26 @@ int SettingsManager::SaveLocked() { } bool SettingsManager::ReadV3Locked() { + CacheV3 loaded{}; +#if defined(__linux__) && !defined(__OHOS__) + if (!ReadCredentialCache(owner_.cache_path_ + "/secure_cache_v3.enc", + &loaded, sizeof(loaded))) { + return false; + } +#else std::ifstream cache_file(owner_.cache_path_ + "/secure_cache_v3.enc", std::ios::binary); if (!cache_file) { return false; } - CacheV3 loaded{}; cache_file.read(reinterpret_cast(&loaded), sizeof(loaded)); - if (cache_file.gcount() != static_cast(sizeof(loaded)) || - loaded.magic != kCacheV3Magic || + if (cache_file.gcount() != static_cast(sizeof(loaded))) { + LOG_WARN("Ignore invalid v3 credential cache"); + return false; + } +#endif + if (loaded.magic != kCacheV3Magic || loaded.version != kCacheV3Version || loaded.checksum != CacheChecksum(&loaded, offsetof(CacheV3, checksum))) { LOG_WARN("Ignore invalid v3 credential cache"); @@ -212,6 +343,12 @@ bool SettingsManager::ReadV3Locked() { } bool SettingsManager::ReadV2Locked() { +#if defined(__linux__) && !defined(__OHOS__) + if (!ReadCredentialCache(owner_.cache_path_ + "/secure_cache_v2.enc", + &cache_v2_, sizeof(cache_v2_))) { + return false; + } +#else std::ifstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc", std::ios::binary); if (!cache_v2_file) { @@ -222,6 +359,7 @@ bool SettingsManager::ReadV2Locked() { static_cast(sizeof(cache_v2_))) { return false; } +#endif cache_v2_ .client_id_with_password[sizeof(cache_v2_.client_id_with_password) - 1] = '\0'; @@ -239,6 +377,14 @@ std::string SettingsManager::ActiveIdentity() const { int SettingsManager::Load() { std::unique_lock lock(cache_mutex_); +#if defined(__linux__) && !defined(__OHOS__) + // Migrate all generations in place, even when only v3 is used. A chmod + // failure must not turn a readable identity into a missing/invalid cache. + RestrictCachePermissions(owner_.cache_path_, true); + for (const char *name : kCredentialCacheNames) + RestrictCachePermissions(std::filesystem::path(owner_.cache_path_) / name); +#endif + const bool loaded_v3 = ReadV3Locked(); if (loaded_v3 || ReadV2Locked()) { CopyString(owner_.client_id_with_password_, @@ -254,9 +400,19 @@ int SettingsManager::Load() { LOG_INFO("Migrated settings from v2 to v3 cache file"); } } else { +#if defined(__linux__) && !defined(__OHOS__) + const bool loaded_v1 = ReadCredentialCache( + owner_.cache_path_ + "/secure_cache.enc", &cache_v1_, sizeof(cache_v1_)); + if (!loaded_v1) { + // Permission/I/O errors throw above. Invalid existing files also must + // never trigger first-run identity generation or thumbnail deletion. + if (HasExistingCredentialCache(owner_.cache_path_)) + CacheReadError(owner_.cache_path_, EINVAL); +#else std::ifstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc", std::ios::binary); if (!cache_v1_file) { +#endif lock.unlock(); std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_)); @@ -273,7 +429,9 @@ int SettingsManager::Load() { return -1; } +#if !defined(__linux__) || defined(__OHOS__) cache_v1_file.read(reinterpret_cast(&cache_v1_), sizeof(cache_v1_)); +#endif cache_v1_ .client_id_with_password[sizeof(cache_v1_.client_id_with_password) - 1] = '\0'; diff --git a/apps/desktop/src/gui/runtime/gui_runtime.cpp b/apps/desktop/src/gui/runtime/gui_runtime.cpp index f4e2fe52..31e2016f 100644 --- a/apps/desktop/src/gui/runtime/gui_runtime.cpp +++ b/apps/desktop/src/gui/runtime/gui_runtime.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -96,7 +97,18 @@ int GuiRuntime::CreateConnectionPeer() { signal_server_port = config_center_->GetSignalServerPort(); std::string current_self_hosted_ip = config_center_->GetSignalServerHost(); - const bool use_cached_id = settings_.LoadCachedSelfHostedIdentity(); + bool use_cached_id = false; +#if defined(__linux__) && !defined(__OHOS__) + try { + use_cached_id = settings_.LoadCachedSelfHostedIdentity(); + } catch (const std::system_error&) { + // Keep the cached identity and avoid requesting a replacement identity + // when its file is temporarily inaccessible. SettingsManager logs why. + return -1; + } +#else + use_cached_id = settings_.LoadCachedSelfHostedIdentity(); +#endif if (!use_cached_id) { LOG_INFO( "secure_cache_v2.enc not found, will use empty id to get new id from "