[refactor] share private file storage helpers

This commit is contained in:
dijunkun
2026-11-01 23:25:53 +08:00
parent 53a3df6133
commit b999a01e66
4 changed files with 276 additions and 184 deletions
+244
View File
@@ -0,0 +1,244 @@
#include "private_file.h"
#include <openssl/rand.h>
#include <fstream>
#include <memory>
#include <stdexcept>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#include <Aclapi.h>
#include <sddl.h>
#else
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
namespace crossdesk::private_file {
namespace {
void Require(bool ok, const char* error) {
if (!ok) throw std::runtime_error(error);
}
std::string RandomSuffix() {
unsigned char bytes[16];
Require(RAND_bytes(bytes, sizeof(bytes)) == 1, "Secure random source unavailable");
constexpr char hex[] = "0123456789abcdef";
std::string value;
for (auto byte : bytes) { value += hex[byte >> 4]; value += hex[byte & 15]; }
return value;
}
#ifdef _WIN32
std::vector<unsigned char> CurrentUser() {
HANDLE token = nullptr;
Require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token),
"Cannot open user token");
DWORD size = 0;
GetTokenInformation(token, TokenUser, nullptr, 0, &size);
std::vector<unsigned char> data(size);
bool ok = GetTokenInformation(token, TokenUser, data.data(), size, &size);
CloseHandle(token);
Require(ok, "Cannot read user token");
return data;
}
PSECURITY_DESCRIPTOR PrivateSecurityDescriptor(bool directory) {
auto user = CurrentUser();
LPWSTR sid = nullptr;
Require(ConvertSidToStringSidW(
reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid, &sid),
"Cannot read user SID");
// An elevated token can default to Administrators as the owner even when
// its DACL grants access only to TokenUser. Always set both explicitly.
const auto acl = L"O:" + std::wstring(sid) + L"D:P(A;" +
(directory ? L"OICI" : L"") + L";FA;;;" + sid + L")";
LocalFree(sid);
PSECURITY_DESCRIPTOR descriptor = nullptr;
Require(ConvertStringSecurityDescriptorToSecurityDescriptorW(
acl.c_str(), SDDL_REVISION_1, &descriptor, nullptr),
"Cannot protect private file");
return descriptor;
}
#endif
} // namespace
void Write(const std::filesystem::path& path, const std::string& text) {
const auto temporary = path.parent_path() / (".write-" + RandomSuffix());
#ifdef _WIN32
auto descriptor = PrivateSecurityDescriptor(false);
SECURITY_ATTRIBUTES security{sizeof(security), descriptor, FALSE};
HANDLE file = CreateFileW(temporary.c_str(), GENERIC_WRITE, 0, &security,
CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr);
LocalFree(descriptor);
Require(file != INVALID_HANDLE_VALUE, "Cannot create private file");
DWORD written = 0;
bool ok = WriteFile(file, text.data(), static_cast<DWORD>(text.size()),
&written, nullptr) &&
written == text.size() && FlushFileBuffers(file);
CloseHandle(file);
if (!ok || !MoveFileExW(temporary.c_str(), path.c_str(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
DeleteFileW(temporary.c_str());
throw std::runtime_error("Cannot persist private file");
}
#else
int fd =
open(temporary.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
Require(fd >= 0, "Cannot create private file");
size_t offset = 0;
while (offset < text.size()) {
const auto count = write(fd, text.data() + offset, text.size() - offset);
if (count <= 0) break;
offset += static_cast<size_t>(count);
}
bool ok = offset == text.size() && fsync(fd) == 0;
close(fd);
if (!ok || rename(temporary.c_str(), path.c_str()) != 0) {
unlink(temporary.c_str());
throw std::runtime_error("Cannot persist private file");
}
const int directory = open(path.parent_path().c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
const bool durable = directory >= 0 && fsync(directory) == 0;
if (directory >= 0) close(directory);
Require(durable, "Cannot persist private directory entry");
#endif
}
namespace {
#ifdef _WIN32
using FileHandle = std::unique_ptr<void, decltype(&CloseHandle)>;
FileHandle OpenPrivatePath(const std::filesystem::path& path, DWORD access) {
// Keep the object open without FILE_SHARE_DELETE so validation and legacy
// owner repair cannot operate on a replacement at the same path.
HANDLE file = CreateFileW(
path.c_str(), access, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
nullptr);
Require(file != INVALID_HANDLE_VALUE, "Cannot open private path");
return {file, CloseHandle};
}
void CheckPrivateWindowsPath(const std::filesystem::path& path) {
auto file = OpenPrivatePath(path, READ_CONTROL);
BY_HANDLE_FILE_INFORMATION info{};
Require(GetFileInformationByHandle(file.get(), &info) &&
!(info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT),
"private path cannot be a reparse point");
auto user = CurrentUser();
auto sid = reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid;
PSID owner = nullptr;
PACL acl = nullptr;
PSECURITY_DESCRIPTOR descriptor = nullptr;
Require(GetSecurityInfo(
file.get(), SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, &owner,
nullptr, &acl, nullptr, &descriptor) == ERROR_SUCCESS,
"Cannot read private file permissions");
std::unique_ptr<void, decltype(&LocalFree)> security(descriptor, LocalFree);
bool ok = acl && acl->AceCount > 0;
// Only the exact protected, full-access DACL written by older versions is
// eligible for owner repair. Never adopt a shared or foreign-user profile.
SECURITY_DESCRIPTOR_CONTROL control{};
DWORD revision = 0;
bool legacy = owner && IsWellKnownSid(owner, WinBuiltinAdministratorsSid) &&
GetSecurityDescriptorControl(descriptor, &control, &revision) &&
(control & SE_DACL_PROTECTED) && acl && acl->AceCount == 1;
for (DWORD i = 0; ok && i < acl->AceCount; ++i) {
void* raw = nullptr;
if (!GetAce(acl, i, &raw)) {
ok = false;
break;
}
const auto* ace = static_cast<ACCESS_ALLOWED_ACE*>(raw);
ok = ace->Header.AceType == ACCESS_ALLOWED_ACE_TYPE &&
EqualSid(const_cast<DWORD*>(&ace->SidStart), sid);
const BYTE flags = (info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
? OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE
: 0;
legacy =
legacy && ace->Mask == FILE_ALL_ACCESS && ace->Header.AceFlags == flags;
}
Require(ok && owner && (EqualSid(owner, sid) || legacy),
"private files must be accessible only by the current user");
if (legacy) {
auto writable = OpenPrivatePath(path, WRITE_OWNER);
BY_HANDLE_FILE_INFORMATION current{};
Require(GetFileInformationByHandle(writable.get(), &current) &&
current.dwVolumeSerialNumber == info.dwVolumeSerialNumber &&
current.nFileIndexHigh == info.nFileIndexHigh &&
current.nFileIndexLow == info.nFileIndexLow &&
SetSecurityInfo(writable.get(), SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION, sid, nullptr,
nullptr, nullptr) == ERROR_SUCCESS,
"Cannot repair private file owner");
}
}
void CreatePrivateDirectory(const std::filesystem::path& path, bool protect_existing) {
if (!std::filesystem::exists(path)) {
auto descriptor = PrivateSecurityDescriptor(true);
SECURITY_ATTRIBUTES security{sizeof(security), descriptor, FALSE};
bool ok = CreateDirectoryW(path.c_str(), &security);
LocalFree(descriptor);
Require(ok, "Cannot create private directory");
}
if (protect_existing) {
// The cache may predate identity storage and inherit a normal profile ACL.
// Elevated older builds may have used Administrators as the owner. The
// private-path check below repairs that owner after applying the user ACL.
auto file = OpenPrivatePath(path, READ_CONTROL | WRITE_DAC);
BY_HANDLE_FILE_INFORMATION info{};
Require(GetFileInformationByHandle(file.get(), &info) &&
(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
!(info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT),
"Invalid cache directory");
PSID owner = nullptr;
PSECURITY_DESCRIPTOR original = nullptr;
Require(GetSecurityInfo(file.get(), SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION,
&owner, nullptr, nullptr, nullptr, &original) == ERROR_SUCCESS,
"Cannot read cache directory owner");
std::unique_ptr<void, decltype(&LocalFree)> original_guard(original, LocalFree);
auto user = CurrentUser();
Require(owner && (EqualSid(owner, reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid) ||
IsWellKnownSid(owner, WinBuiltinAdministratorsSid)),
"Cache directory belongs to another user");
auto descriptor = PrivateSecurityDescriptor(true);
std::unique_ptr<void, decltype(&LocalFree)> descriptor_guard(descriptor, LocalFree);
BOOL present = FALSE, defaulted = FALSE;
PACL acl = nullptr;
Require(GetSecurityDescriptorDacl(descriptor, &present, &acl, &defaulted) && present &&
SetSecurityInfo(file.get(), SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
nullptr, nullptr, acl, nullptr) == ERROR_SUCCESS,
"Cannot protect cache directory");
}
CheckPrivateWindowsPath(path);
}
#endif
} // namespace
std::string Read(const std::filesystem::path& path) {
#ifdef _WIN32
CheckPrivateWindowsPath(path);
#else
struct stat status{};
Require(lstat(path.c_str(), &status) == 0 && S_ISREG(status.st_mode) &&
status.st_uid == getuid() && (status.st_mode & 0077) == 0,
"private file must be owned by this user with mode 0600");
#endif
Require(std::filesystem::file_size(path) <= 64 * 1024, "private file too large");
std::ifstream input(path, std::ios::binary);
Require(input.good(), "Cannot read private file");
return {std::istreambuf_iterator<char>(input), {}};
}
void EnsureDirectory(const std::filesystem::path& directory, bool protect_existing) {
std::filesystem::create_directories(directory.parent_path());
#ifdef _WIN32
CreatePrivateDirectory(directory, protect_existing);
#else
std::filesystem::create_directories(directory);
struct stat status{};
Require(lstat(directory.c_str(), &status) == 0 && S_ISDIR(status.st_mode) &&
status.st_uid == getuid(), "Invalid private directory owner or type");
Require(chmod(directory.c_str(), 0700) == 0, "Cannot protect private directory");
#endif
}
} // namespace crossdesk::private_file
+21
View File
@@ -0,0 +1,21 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-11-01
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVATE_FILE_H_
#define _PRIVATE_FILE_H_
#include <filesystem>
#include <string>
namespace crossdesk::private_file {
// User-only files, atomic replacement, and rejection of shared/linked stores.
void EnsureDirectory(const std::filesystem::path& directory,
bool protect_existing = false);
std::string Read(const std::filesystem::path& path);
void Write(const std::filesystem::path& path, const std::string& text);
}
#endif
+9 -183
View File
@@ -1,4 +1,5 @@
#include "lan_identity.h"
#include "private_file.h"
#include <openssl/crypto.h>
#include <openssl/pem.h>
@@ -8,7 +9,6 @@
#include <algorithm>
#include <array>
#include <charconv>
#include <fstream>
#include <memory>
#include <stdexcept>
#include <string_view>
@@ -16,23 +16,18 @@
#include "direct_address.h"
#include "lan_password.h"
#ifdef _WIN32
// Winsock must precede windows.h; the networking/ACL headers need its types.
// Winsock must precede windows.h; the networking headers need its types.
// clang-format off
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#include <Aclapi.h>
#include <iphlpapi.h>
#include <netioapi.h>
#include <sddl.h>
// clang-format on
#else
#include <arpa/inet.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <sys/stat.h>
#include <unistd.h>
#ifdef __APPLE__
#include <net/if_dl.h>
#include <net/if_types.h>
@@ -59,166 +54,8 @@ bool IsHex(const std::string& text, size_t size) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
});
}
#ifdef _WIN32
std::vector<unsigned char> CurrentUser() {
HANDLE token = nullptr;
Require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token),
"Cannot open user token");
DWORD size = 0;
GetTokenInformation(token, TokenUser, nullptr, 0, &size);
std::vector<unsigned char> data(size);
bool ok = GetTokenInformation(token, TokenUser, data.data(), size, &size);
CloseHandle(token);
Require(ok, "Cannot read user token");
return data;
}
PSECURITY_DESCRIPTOR PrivateSecurityDescriptor(bool directory) {
auto user = CurrentUser();
LPWSTR sid = nullptr;
Require(ConvertSidToStringSidW(
reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid, &sid),
"Cannot read user SID");
// An elevated token can default to Administrators as the owner even when
// its DACL grants access only to TokenUser. Always set both explicitly.
const auto acl = L"O:" + std::wstring(sid) + L"D:P(A;" +
(directory ? L"OICI" : L"") + L";FA;;;" + sid + L")";
LocalFree(sid);
PSECURITY_DESCRIPTOR descriptor = nullptr;
Require(ConvertStringSecurityDescriptorToSecurityDescriptorW(
acl.c_str(), SDDL_REVISION_1, &descriptor, nullptr),
"Cannot protect LAN file");
return descriptor;
}
#endif
void PrivateWrite(const std::filesystem::path& path, const std::string& text) {
const auto temporary = path.parent_path() / (".write-" + RandomHex(16));
#ifdef _WIN32
auto descriptor = PrivateSecurityDescriptor(false);
SECURITY_ATTRIBUTES security{sizeof(security), descriptor, FALSE};
HANDLE file = CreateFileW(temporary.c_str(), GENERIC_WRITE, 0, &security,
CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr);
LocalFree(descriptor);
Require(file != INVALID_HANDLE_VALUE, "Cannot create LAN file");
DWORD written = 0;
bool ok = WriteFile(file, text.data(), static_cast<DWORD>(text.size()),
&written, nullptr) &&
written == text.size() && FlushFileBuffers(file);
CloseHandle(file);
if (!ok || !MoveFileExW(temporary.c_str(), path.c_str(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
DeleteFileW(temporary.c_str());
throw std::runtime_error("Cannot persist LAN file");
}
#else
int fd =
open(temporary.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
Require(fd >= 0, "Cannot create private LAN file");
size_t offset = 0;
while (offset < text.size()) {
const auto count = write(fd, text.data() + offset, text.size() - offset);
if (count <= 0) break;
offset += static_cast<size_t>(count);
}
bool ok = offset == text.size() && fsync(fd) == 0;
close(fd);
if (!ok || rename(temporary.c_str(), path.c_str()) != 0) {
unlink(temporary.c_str());
throw std::runtime_error("Cannot persist LAN file");
}
#endif
}
#ifdef _WIN32
using FileHandle = std::unique_ptr<void, decltype(&CloseHandle)>;
FileHandle OpenLanPath(const std::filesystem::path& path, DWORD access) {
// Keep the object open without FILE_SHARE_DELETE so validation and legacy
// owner repair cannot operate on a replacement at the same path.
HANDLE file = CreateFileW(
path.c_str(), access, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
nullptr);
Require(file != INVALID_HANDLE_VALUE, "Cannot open LAN path");
return {file, CloseHandle};
}
void CheckPrivateWindowsPath(const std::filesystem::path& path) {
auto file = OpenLanPath(path, READ_CONTROL);
BY_HANDLE_FILE_INFORMATION info{};
Require(GetFileInformationByHandle(file.get(), &info) &&
!(info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT),
"LAN path cannot be a reparse point");
auto user = CurrentUser();
auto sid = reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid;
PSID owner = nullptr;
PACL acl = nullptr;
PSECURITY_DESCRIPTOR descriptor = nullptr;
Require(GetSecurityInfo(
file.get(), SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, &owner,
nullptr, &acl, nullptr, &descriptor) == ERROR_SUCCESS,
"Cannot read LAN file permissions");
std::unique_ptr<void, decltype(&LocalFree)> security(descriptor, LocalFree);
bool ok = acl && acl->AceCount > 0;
// Only the exact protected, full-access DACL written by older versions is
// eligible for owner repair. Never adopt a shared or foreign-user profile.
SECURITY_DESCRIPTOR_CONTROL control{};
DWORD revision = 0;
bool legacy = owner && IsWellKnownSid(owner, WinBuiltinAdministratorsSid) &&
GetSecurityDescriptorControl(descriptor, &control, &revision) &&
(control & SE_DACL_PROTECTED) && acl && acl->AceCount == 1;
for (DWORD i = 0; ok && i < acl->AceCount; ++i) {
void* raw = nullptr;
if (!GetAce(acl, i, &raw)) {
ok = false;
break;
}
const auto* ace = static_cast<ACCESS_ALLOWED_ACE*>(raw);
ok = ace->Header.AceType == ACCESS_ALLOWED_ACE_TYPE &&
EqualSid(const_cast<DWORD*>(&ace->SidStart), sid);
const BYTE flags = (info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
? OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE
: 0;
legacy =
legacy && ace->Mask == FILE_ALL_ACCESS && ace->Header.AceFlags == flags;
}
Require(ok && owner && (EqualSid(owner, sid) || legacy),
"LAN files must be accessible only by the current user");
if (legacy) {
auto writable = OpenLanPath(path, WRITE_OWNER);
BY_HANDLE_FILE_INFORMATION current{};
Require(GetFileInformationByHandle(writable.get(), &current) &&
current.dwVolumeSerialNumber == info.dwVolumeSerialNumber &&
current.nFileIndexHigh == info.nFileIndexHigh &&
current.nFileIndexLow == info.nFileIndexLow &&
SetSecurityInfo(writable.get(), SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION, sid, nullptr,
nullptr, nullptr) == ERROR_SUCCESS,
"Cannot repair LAN file owner");
}
}
void CreatePrivateDirectory(const std::filesystem::path& path) {
if (!std::filesystem::exists(path)) {
auto descriptor = PrivateSecurityDescriptor(true);
SECURITY_ATTRIBUTES security{sizeof(security), descriptor, FALSE};
bool ok = CreateDirectoryW(path.c_str(), &security);
LocalFree(descriptor);
Require(ok, "Cannot create LAN directory");
}
CheckPrivateWindowsPath(path);
}
#endif
std::string Read(const std::filesystem::path& path) {
#ifdef _WIN32
CheckPrivateWindowsPath(path);
#else
struct stat status{};
Require(lstat(path.c_str(), &status) == 0 && S_ISREG(status.st_mode) &&
status.st_uid == getuid() && (status.st_mode & 0077) == 0,
"LAN file must be owned by this user with mode 0600");
#endif
Require(std::filesystem::file_size(path) <= 64 * 1024, "LAN file too large");
std::ifstream input(path, std::ios::binary);
Require(input.good(), "Cannot read LAN file");
return {std::istreambuf_iterator<char>(input), {}};
}
using private_file::Read;
using private_file::Write;
std::string BioText(BIO* bio) {
char* data = nullptr;
long size = BIO_get_mem_data(bio, &data);
@@ -245,7 +82,7 @@ std::string PasswordKey(const std::filesystem::path& directory, bool create) {
Require(
RAND_bytes(reinterpret_cast<unsigned char*>(key.value.data()), 32) == 1,
"Cannot generate LAN password storage key");
PrivateWrite(path, key.value);
Write(path, key.value);
}
auto key = Read(path);
Require(key.size() == 32, "Invalid LAN password storage key");
@@ -358,11 +195,11 @@ void CreateCertificate(const std::filesystem::path& key_path,
Require(PEM_write_bio_PrivateKey(bio.get(), key.get(), nullptr, nullptr, 0,
nullptr, nullptr) == 1,
"Cannot encode private key");
PrivateWrite(key_path, BioText(bio.get()));
Write(key_path, BioText(bio.get()));
(void)BIO_reset(bio.get());
Require(PEM_write_bio_X509(bio.get(), cert.get()) == 1,
"Cannot encode certificate");
PrivateWrite(cert_path, BioText(bio.get()));
Write(cert_path, BioText(bio.get()));
}
} // namespace
unsigned ParsePort(const std::string& value) {
@@ -539,18 +376,7 @@ Identity::Identity(std::filesystem::path directory)
: directory_(std::move(directory)) {}
void Identity::Load() {
DevicePassword::CheckSupport();
std::filesystem::create_directories(directory_.parent_path());
#ifdef _WIN32
CreatePrivateDirectory(directory_);
#else
std::filesystem::create_directories(directory_);
struct stat directory_status{};
Require(lstat(directory_.c_str(), &directory_status) == 0 &&
S_ISDIR(directory_status.st_mode) &&
directory_status.st_uid == getuid(),
"Invalid LAN directory owner or type");
Require(chmod(directory_.c_str(), 0700) == 0, "Cannot protect LAN directory");
#endif
private_file::EnsureDirectory(directory_);
const auto store = directory_ / "identity.json";
if (!std::filesystem::exists(store)) {
Require(!std::filesystem::exists(private_key()) &&
@@ -642,7 +468,7 @@ void Identity::Load() {
state_.erase("password_attempts");
}
void Identity::Save(const nlohmann::json& state) {
PrivateWrite(directory_ / "identity.json", state.dump(2));
Write(directory_ / "identity.json", state.dump(2));
}
std::pair<std::string, nlohmann::json> Identity::GeneratePassword() {
constexpr std::string_view alphabet =
+2 -1
View File
@@ -82,12 +82,13 @@ function setup_targets()
target("desktop_common")
set_kind("object")
add_deps("rd_log", "crossdesk_wire")
add_packages("libyuv")
add_packages("libyuv", "openssl3")
add_packages("concurrentqueue")
add_files("apps/desktop/src/common/*.cpp")
if is_os("windows") then
add_files("apps/desktop/src/platform/windows/system_info.cpp",
"apps/desktop/src/platform/windows/screen_lock.cpp")
add_syslinks("Advapi32", {public = true})
elseif is_os("macosx") then
add_files("apps/desktop/src/platform/macos/system_info.cpp",
"apps/desktop/src/platform/macos/screen_lock.cpp")