aboutsummaryrefslogtreecommitdiff
path: root/src/zen/authutils.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zen/authutils.cpp')
-rw-r--r--src/zen/authutils.cpp238
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");