diff options
Diffstat (limited to 'src/zen/authutils.cpp')
| -rw-r--r-- | src/zen/authutils.cpp | 238 |
1 files changed, 184 insertions, 54 deletions
diff --git a/src/zen/authutils.cpp b/src/zen/authutils.cpp index 922007ac8..a2af2b63e 100644 --- a/src/zen/authutils.cpp +++ b/src/zen/authutils.cpp @@ -2,13 +2,16 @@ #include "authutils.h" +#include <zencore/crypto.h> #include <zencore/filesystem.h> #include <zencore/fmtutils.h> +#include <zencore/iobuffer.h> #include <zencore/logging.h> #include <zenhttp/auth/authmgr.h> #include <zenhttp/httpclient.h> #include <zenhttp/httpclientauth.h> +#include <zenutil/authutils.h> ZEN_THIRD_PARTY_INCLUDES_START #include <json11.hpp> @@ -112,77 +115,204 @@ AuthCommandLineOptions::AddOptions(cxxopts::Options& Ops) ""); }; +// Load or generate a per-install machine AES key+IV under AuthDir/machinekey.dat +// so the auth-state file is encrypted with bytes unique to this machine rather +// than a hardcoded constant. +// +// When per-user OS-protected storage is available (DPAPI on Windows) the key +// material is wrapped before it lands on disk, so a copy of the file off-machine +// or out of a backup cannot be unwrapped without also stealing the user's OS +// master key. On platforms without OS-level wrapping we fall back to persisting +// the raw bytes with restrictive file permissions (0600 on POSIX; user-only on +// Windows via inheritance from the profile dir). +// +// File format: +// [4-byte magic 'Z','E','N','\x01'] [1-byte flags] [payload] +// flags bit 0 set -> payload is OS-protected (DPAPI blob) +// flags bit 0 clear -> payload is raw KeyBytes+IvBytes bytes +// Legacy files without the magic are interpreted as raw bytes. void -AuthCommandLineOptions::ParseOptions(cxxopts::Options& Ops, - const std::filesystem::path& SystemRootDir, - HttpClientSettings& ClientSettings, - std::string_view HostUrl, - std::unique_ptr<AuthMgr>& Auth, - bool Quiet, - bool Hidden, - bool Verbose) +AuthCommandLineOptions::LoadOrCreateMachineKey(const std::filesystem::path& AuthDir, bool Quiet) { - auto CreateAuthMgr = [&]() { - ZEN_ASSERT(!SystemRootDir.empty()); - if (!Auth) + constexpr size_t KeyBytes = AesKey256Bit::ByteCount; + constexpr size_t IvBytes = AesIV128Bit::ByteCount; + static constexpr std::array<uint8_t, 4> FileMagic = {'Z', 'E', 'N', 0x01}; + static constexpr uint8_t FlagProtected = 0x01; + const std::filesystem::path KeyFile = AuthDir / "machinekey.dat"; + std::array<uint8_t, KeyBytes + IvBytes> KeyMaterial{}; + bool Loaded = false; + + auto ParseFile = [&](MemoryView FileBytes) -> bool { + // Legacy: raw KeyBytes+IvBytes payload. + if (FileBytes.GetSize() == KeyMaterial.size()) + { + memcpy(KeyMaterial.data(), FileBytes.GetData(), KeyMaterial.size()); + return true; + } + if (FileBytes.GetSize() < FileMagic.size() + 1) + { + return false; + } + if (memcmp(FileBytes.GetData(), FileMagic.data(), FileMagic.size()) != 0) + { + return false; + } + const uint8_t Flags = static_cast<const uint8_t*>(FileBytes.GetData())[FileMagic.size()]; + const MemoryView Payload = FileBytes.Mid(FileMagic.size() + 1); + if (Flags & FlagProtected) { - static const std::string_view DefaultEncryptionKey("abcdefghijklmnopqrstuvxyz0123456"); - static const std::string_view DefaultEncryptionIV("0123456789abcdef"); - if (m_EncryptionKey.empty() && m_EncryptionIV.empty()) + std::vector<uint8_t> Plaintext; + if (!TryUnprotectData(Payload, Plaintext)) { - m_EncryptionKey = DefaultEncryptionKey; - m_EncryptionIV = DefaultEncryptionIV; if (!Quiet) { - ZEN_CONSOLE_WARN("Auth: Using default encryption key and initialization vector for auth storage"); + ZEN_CONSOLE_WARN("Auth: failed to unwrap OS-protected machine key at '{}', regenerating", KeyFile); } + return false; } - else + if (Plaintext.size() != KeyMaterial.size()) { - if (m_EncryptionKey.empty()) - { - m_EncryptionKey = DefaultEncryptionKey; - if (!Quiet) - { - ZEN_CONSOLE_WARN("Auth: Using default encryption key for auth storage"); - } - } - if (m_EncryptionIV.empty()) - { - m_EncryptionIV = DefaultEncryptionIV; - if (!Quiet) - { - ZEN_CONSOLE_WARN("Auth: Using default encryption initialization vector for auth storage"); - } - } + return false; } + memcpy(KeyMaterial.data(), Plaintext.data(), KeyMaterial.size()); + return true; + } + if (Payload.GetSize() != KeyMaterial.size()) + { + return false; + } + memcpy(KeyMaterial.data(), Payload.GetData(), KeyMaterial.size()); + return true; + }; - AuthConfig AuthMgrConfig = {.RootDirectory = SystemRootDir / "auth", - .EncryptionKey = AesKey256Bit::FromString(m_EncryptionKey), - .EncryptionIV = AesIV128Bit::FromString(m_EncryptionIV)}; - if (!AuthMgrConfig.EncryptionKey.IsValid()) - { - throw OptionParseException(fmt::format("'--encryption-aes-key' ('{}') is malformed", m_EncryptionKey), Ops.help()); - } - if (!AuthMgrConfig.EncryptionIV.IsValid()) + std::error_code Ec; + if (std::filesystem::exists(KeyFile, Ec)) + { + IoBuffer Data = ReadFile(KeyFile).Flatten(); + if (ParseFile(Data.GetView())) + { + Loaded = true; + } + else if (!Quiet) + { + ZEN_CONSOLE_WARN("Auth: machine key file '{}' is unreadable (size {}), regenerating", KeyFile, Data.GetSize()); + } + } + + if (!Loaded) + { + CreateDirectories(AuthDir); + if (!SecureRandomBytes(MutableMemoryView(KeyMaterial.data(), KeyMaterial.size()))) + { + throw std::runtime_error("failed to obtain secure random bytes for auth machine key"); + } + + std::vector<uint8_t> FileBytes; + FileBytes.reserve(FileMagic.size() + 1 + KeyMaterial.size()); + FileBytes.insert(FileBytes.end(), FileMagic.begin(), FileMagic.end()); + + std::vector<uint8_t> Wrapped; + if (TryProtectData(MemoryView(KeyMaterial.data(), KeyMaterial.size()), Wrapped)) + { + FileBytes.push_back(FlagProtected); + FileBytes.insert(FileBytes.end(), Wrapped.begin(), Wrapped.end()); + if (!Quiet) { - throw OptionParseException(fmt::format("'--encryption-aes-iv' ('{}') is malformed", m_EncryptionIV), Ops.help()); + ZEN_CONSOLE_WARN("Auth: generated OS-protected machine-specific auth encryption key at '{}'", KeyFile); } - if (Verbose) + } + else + { + FileBytes.push_back(0); + FileBytes.insert(FileBytes.end(), KeyMaterial.begin(), KeyMaterial.end()); + if (!Quiet) { - ExtendableStringBuilder<128> SB; - SB << "\n RootDirectory: " << AuthMgrConfig.RootDirectory.string(); - SB << "\n EncryptionKey: " << HideSensitiveString(m_EncryptionKey); - SB << "\n EncryptionIV: " << HideSensitiveString(m_EncryptionIV); - ZEN_CONSOLE("Auth: Creating auth manager with:{}", SB.ToString()); + ZEN_CONSOLE_WARN("Auth: generated machine-specific auth encryption key at '{}' (no OS wrapping available)", KeyFile); } - Auth = AuthMgr::Create(AuthMgrConfig); } - }; + WriteFile(KeyFile, IoBufferBuilder::MakeCloneFromMemory(FileBytes.data(), FileBytes.size())); + + // Belt and suspenders: restrict access on POSIX. On Windows the + // default DACL inherited from a per-user profile dir is already + // user-only in the common case; an explicit tighten there would + // require touching the DACL which is more code than it's worth + // while DPAPI wrapping is the primary defense. +#if !ZEN_PLATFORM_WINDOWS + std::error_code PermEc; + std::filesystem::permissions(KeyFile, + std::filesystem::perms::owner_read | std::filesystem::perms::owner_write, + std::filesystem::perm_options::replace, + PermEc); +#endif + } + + m_EncryptionKey.assign(reinterpret_cast<const char*>(KeyMaterial.data()), KeyBytes); + m_EncryptionIV.assign(reinterpret_cast<const char*>(KeyMaterial.data() + KeyBytes), IvBytes); +} + +void +AuthCommandLineOptions::CreateAuthMgr(cxxopts::Options& Ops, + const std::filesystem::path& SystemRootDir, + std::unique_ptr<AuthMgr>& InOutAuth, + bool Quiet, + bool Verbose) +{ + ZEN_ASSERT(!SystemRootDir.empty()); + if (InOutAuth) + { + return; + } + + const std::filesystem::path AuthDir = SystemRootDir / "auth"; + + if (m_EncryptionKey.empty() != m_EncryptionIV.empty()) + { + throw OptionParseException( + std::string("'--encryption-aes-key' and '--encryption-aes-iv' must be supplied together or both omitted"), + Ops.help()); + } + + if (m_EncryptionKey.empty() && m_EncryptionIV.empty()) + { + LoadOrCreateMachineKey(AuthDir, Quiet); + } + + AuthConfig AuthMgrConfig = {.RootDirectory = AuthDir, + .EncryptionKey = AesKey256Bit::FromString(m_EncryptionKey), + .EncryptionIV = AesIV128Bit::FromString(m_EncryptionIV)}; + if (!AuthMgrConfig.EncryptionKey.IsValid()) + { + throw OptionParseException(fmt::format("'--encryption-aes-key' ('{}') is malformed", m_EncryptionKey), Ops.help()); + } + if (!AuthMgrConfig.EncryptionIV.IsValid()) + { + throw OptionParseException(fmt::format("'--encryption-aes-iv' ('{}') is malformed", m_EncryptionIV), Ops.help()); + } + if (Verbose) + { + ExtendableStringBuilder<128> SB; + SB << "\n RootDirectory: " << AuthMgrConfig.RootDirectory.string(); + SB << "\n EncryptionKey: " << HideSensitiveString(m_EncryptionKey); + SB << "\n EncryptionIV: " << HideSensitiveString(m_EncryptionIV); + ZEN_CONSOLE("Auth: Creating auth manager with:{}", SB.ToString()); + } + InOutAuth = AuthMgr::Create(AuthMgrConfig); +} + +void +AuthCommandLineOptions::ParseOptions(cxxopts::Options& Ops, + const std::filesystem::path& SystemRootDir, + HttpClientSettings& ClientSettings, + std::string_view HostUrl, + std::unique_ptr<AuthMgr>& Auth, + bool Quiet, + bool Hidden, + bool Verbose) +{ if (!m_OpenIdProviderUrl.empty() && !m_OpenIdClientId.empty()) { - CreateAuthMgr(); + CreateAuthMgr(Ops, SystemRootDir, Auth, Quiet, Verbose); std::string ProviderName = m_OpenIdProviderName.empty() ? "Default" : m_OpenIdProviderName; if (Verbose) { @@ -249,7 +379,7 @@ AuthCommandLineOptions::ParseOptions(cxxopts::Options& Ops, } else if (!m_OpenIdProviderName.empty()) { - CreateAuthMgr(); + CreateAuthMgr(Ops, SystemRootDir, Auth, Quiet, Verbose); if (!Quiet) { ZEN_CONSOLE("Auth: Using OpenId provider: {}", m_OpenIdProviderName); @@ -282,7 +412,7 @@ AuthCommandLineOptions::ParseOptions(cxxopts::Options& Ops, if (!ClientSettings.AccessTokenProvider) { - CreateAuthMgr(); + CreateAuthMgr(Ops, SystemRootDir, Auth, Quiet, Verbose); if (!Quiet) { ZEN_CONSOLE("Auth: Using default Open ID provider"); |