aboutsummaryrefslogtreecommitdiff
path: root/src/zencore/crypto.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zencore/crypto.cpp')
-rw-r--r--src/zencore/crypto.cpp389
1 files changed, 389 insertions, 0 deletions
diff --git a/src/zencore/crypto.cpp b/src/zencore/crypto.cpp
index 049854b42..9984f35ac 100644
--- a/src/zencore/crypto.cpp
+++ b/src/zencore/crypto.cpp
@@ -6,6 +6,7 @@
#include <zencore/scopeguard.h>
#include <zencore/testing.h>
+#include <array>
#include <string>
#include <string_view>
@@ -47,14 +48,27 @@ ZEN_THIRD_PARTY_INCLUDES_START
# include <openssl/conf.h>
# include <openssl/err.h>
# include <openssl/evp.h>
+# include <openssl/rand.h>
#elif ZEN_USE_MBEDTLS
# include <mbedtls/cipher.h>
+# include <mbedtls/ctr_drbg.h>
+# include <mbedtls/entropy.h>
#else
# include <zencore/windows.h>
# include <bcrypt.h>
# define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
# define STATUS_UNSUCCESSFUL ((NTSTATUS)0xC0000001L)
#endif
+
+#if ZEN_PLATFORM_WINDOWS
+# include <zencore/windows.h>
+# include <dpapi.h>
+#elif ZEN_PLATFORM_MAC
+# include <CoreFoundation/CoreFoundation.h>
+# include <Security/Security.h>
+#elif ZEN_PLATFORM_LINUX && ZEN_USE_LIBSECRET
+# include <libsecret/secret.h>
+#endif
ZEN_THIRD_PARTY_INCLUDES_END
namespace zen {
@@ -420,6 +434,291 @@ namespace crypto {
} // namespace crypto
+bool
+SecureRandomBytes(MutableMemoryView Out)
+{
+ if (Out.GetSize() == 0)
+ {
+ return true;
+ }
+
+#if ZEN_USE_BCRYPT
+ NTSTATUS Status =
+ BCryptGenRandom(nullptr, static_cast<PUCHAR>(Out.GetData()), static_cast<ULONG>(Out.GetSize()), BCRYPT_USE_SYSTEM_PREFERRED_RNG);
+ return NT_SUCCESS(Status);
+#elif ZEN_USE_OPENSSL
+ return RAND_bytes(static_cast<unsigned char*>(Out.GetData()), static_cast<int>(Out.GetSize())) == 1;
+#else // ZEN_USE_MBEDTLS
+ mbedtls_entropy_context Entropy;
+ mbedtls_ctr_drbg_context Drbg;
+ mbedtls_entropy_init(&Entropy);
+ mbedtls_ctr_drbg_init(&Drbg);
+ auto _ = MakeGuard([&]() {
+ mbedtls_ctr_drbg_free(&Drbg);
+ mbedtls_entropy_free(&Entropy);
+ });
+ if (mbedtls_ctr_drbg_seed(&Drbg, mbedtls_entropy_func, &Entropy, nullptr, 0) != 0)
+ {
+ return false;
+ }
+ return mbedtls_ctr_drbg_random(&Drbg, static_cast<unsigned char*>(Out.GetData()), Out.GetSize()) == 0;
+#endif
+}
+
+#if ZEN_PLATFORM_MAC || (ZEN_PLATFORM_LINUX && ZEN_USE_LIBSECRET)
+namespace {
+ // Keychain-backed wrap/unwrap. Plaintext is stored in the OS keyring under a
+ // fixed service and a per-call random account id. The "wrapped" blob returned to
+ // the caller is just the account id bytes; the plaintext never lands on disk.
+ constexpr size_t kKeychainAccountBytes = 16;
+ constexpr char kKeychainServiceName[] = "org.unrealengine.zen.auth";
+
+ bool AccountStringFromBytes(const uint8_t* Bytes, size_t Size, char* OutHex, size_t OutHexSize)
+ {
+ if (OutHexSize < Size * 2 + 1)
+ {
+ return false;
+ }
+ for (size_t i = 0; i < Size; ++i)
+ {
+ static const char kHex[] = "0123456789abcdef";
+ OutHex[i * 2] = kHex[Bytes[i] >> 4];
+ OutHex[i * 2 + 1] = kHex[Bytes[i] & 0x0F];
+ }
+ OutHex[Size * 2] = '\0';
+ return true;
+ }
+} // namespace
+#endif
+
+#if ZEN_PLATFORM_LINUX && ZEN_USE_LIBSECRET
+namespace {
+ // Schema name is shared across zen installs on the machine — uniqueness per
+ // install comes from the random `account` attribute.
+ const SecretSchema* ZenAuthSchema()
+ {
+ static const SecretSchema Schema = {"org.unrealengine.zen.AuthMachineKey",
+ SECRET_SCHEMA_NONE,
+ {
+ {"account", SECRET_SCHEMA_ATTRIBUTE_STRING},
+ {nullptr, SecretSchemaAttributeType(0)},
+ },
+ // reserved
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0};
+ return &Schema;
+ }
+} // namespace
+#endif
+
+bool
+TryProtectData(MemoryView Plaintext, std::vector<uint8_t>& OutProtected)
+{
+#if ZEN_PLATFORM_WINDOWS
+ DATA_BLOB In{static_cast<DWORD>(Plaintext.GetSize()), const_cast<BYTE*>(static_cast<const BYTE*>(Plaintext.GetData()))};
+ DATA_BLOB Out{};
+ if (!CryptProtectData(&In, L"zen auth machine key", nullptr, nullptr, nullptr, 0, &Out))
+ {
+ return false;
+ }
+ auto _ = MakeGuard([&Out]() { LocalFree(Out.pbData); });
+ OutProtected.assign(Out.pbData, Out.pbData + Out.cbData);
+ return true;
+#elif ZEN_PLATFORM_MAC
+ uint8_t AccountBytes[kKeychainAccountBytes];
+ if (!SecureRandomBytes(MutableMemoryView(AccountBytes, sizeof(AccountBytes))))
+ {
+ return false;
+ }
+ char AccountHex[sizeof(AccountBytes) * 2 + 1];
+ AccountStringFromBytes(AccountBytes, sizeof(AccountBytes), AccountHex, sizeof(AccountHex));
+
+ CFStringRef Account = CFStringCreateWithCString(nullptr, AccountHex, kCFStringEncodingASCII);
+ if (Account == nullptr)
+ {
+ return false;
+ }
+ auto _A = MakeGuard([&]() { CFRelease(Account); });
+
+ CFDataRef Secret = CFDataCreate(nullptr, static_cast<const UInt8*>(Plaintext.GetData()), static_cast<CFIndex>(Plaintext.GetSize()));
+ if (Secret == nullptr)
+ {
+ return false;
+ }
+ auto _S = MakeGuard([&]() { CFRelease(Secret); });
+
+ const void* Keys[] = {kSecClass, kSecAttrService, kSecAttrAccount, kSecValueData, kSecAttrAccessible};
+ const void* Values[] = {kSecClassGenericPassword,
+ CFSTR("org.unrealengine.zen.auth"),
+ Account,
+ Secret,
+ kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly};
+ CFDictionaryRef Attrs = CFDictionaryCreate(nullptr,
+ Keys,
+ Values,
+ sizeof(Keys) / sizeof(*Keys),
+ &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks);
+ if (Attrs == nullptr)
+ {
+ return false;
+ }
+ auto _D = MakeGuard([&]() { CFRelease(Attrs); });
+
+ OSStatus Status = SecItemAdd(Attrs, nullptr);
+ if (Status != errSecSuccess)
+ {
+ return false;
+ }
+ OutProtected.assign(AccountBytes, AccountBytes + sizeof(AccountBytes));
+ return true;
+#elif ZEN_PLATFORM_LINUX && ZEN_USE_LIBSECRET
+ uint8_t AccountBytes[kKeychainAccountBytes];
+ if (!SecureRandomBytes(MutableMemoryView(AccountBytes, sizeof(AccountBytes))))
+ {
+ return false;
+ }
+ char AccountHex[sizeof(AccountBytes) * 2 + 1];
+ AccountStringFromBytes(AccountBytes, sizeof(AccountBytes), AccountHex, sizeof(AccountHex));
+
+ // libsecret password APIs take UTF-8 strings, so base64 the raw key material
+ // before handing it to the keyring. Decoded back on lookup.
+ gchar* Encoded = g_base64_encode(static_cast<const guchar*>(Plaintext.GetData()), Plaintext.GetSize());
+ if (Encoded == nullptr)
+ {
+ return false;
+ }
+ auto _E = MakeGuard([&]() { g_free(Encoded); });
+
+ GError* Err = nullptr;
+ gboolean Ok = secret_password_store_sync(ZenAuthSchema(),
+ SECRET_COLLECTION_DEFAULT,
+ "zen auth machine key",
+ Encoded,
+ nullptr,
+ &Err,
+ "account",
+ AccountHex,
+ nullptr);
+ if (Err != nullptr)
+ {
+ g_error_free(Err);
+ }
+ if (!Ok)
+ {
+ return false;
+ }
+ OutProtected.assign(AccountBytes, AccountBytes + sizeof(AccountBytes));
+ return true;
+#else
+ (void)Plaintext;
+ (void)OutProtected;
+ return false;
+#endif
+}
+
+bool
+TryUnprotectData(MemoryView Protected, std::vector<uint8_t>& OutPlaintext)
+{
+#if ZEN_PLATFORM_WINDOWS
+ DATA_BLOB In{static_cast<DWORD>(Protected.GetSize()), const_cast<BYTE*>(static_cast<const BYTE*>(Protected.GetData()))};
+ DATA_BLOB Out{};
+ if (!CryptUnprotectData(&In, nullptr, nullptr, nullptr, nullptr, 0, &Out))
+ {
+ return false;
+ }
+ auto _ = MakeGuard([&Out]() { LocalFree(Out.pbData); });
+ OutPlaintext.assign(Out.pbData, Out.pbData + Out.cbData);
+ return true;
+#elif ZEN_PLATFORM_MAC
+ if (Protected.GetSize() != kKeychainAccountBytes)
+ {
+ return false;
+ }
+ char AccountHex[kKeychainAccountBytes * 2 + 1];
+ AccountStringFromBytes(static_cast<const uint8_t*>(Protected.GetData()), kKeychainAccountBytes, AccountHex, sizeof(AccountHex));
+
+ CFStringRef Account = CFStringCreateWithCString(nullptr, AccountHex, kCFStringEncodingASCII);
+ if (Account == nullptr)
+ {
+ return false;
+ }
+ auto _A = MakeGuard([&]() { CFRelease(Account); });
+
+ const void* Keys[] = {kSecClass, kSecAttrService, kSecAttrAccount, kSecReturnData, kSecMatchLimit};
+ const void* Values[] = {kSecClassGenericPassword, CFSTR("org.unrealengine.zen.auth"), Account, kCFBooleanTrue, kSecMatchLimitOne};
+ CFDictionaryRef Query = CFDictionaryCreate(nullptr,
+ Keys,
+ Values,
+ sizeof(Keys) / sizeof(*Keys),
+ &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks);
+ if (Query == nullptr)
+ {
+ return false;
+ }
+ auto _D = MakeGuard([&]() { CFRelease(Query); });
+
+ CFTypeRef Result = nullptr;
+ OSStatus Status = SecItemCopyMatching(Query, &Result);
+ if (Status != errSecSuccess || Result == nullptr)
+ {
+ return false;
+ }
+ auto _R = MakeGuard([&]() { CFRelease(Result); });
+
+ if (CFGetTypeID(Result) != CFDataGetTypeID())
+ {
+ return false;
+ }
+ CFDataRef Data = static_cast<CFDataRef>(Result);
+ const UInt8* DataPtr = CFDataGetBytePtr(Data);
+ const CFIndex DataLen = CFDataGetLength(Data);
+ OutPlaintext.assign(DataPtr, DataPtr + DataLen);
+ return true;
+#elif ZEN_PLATFORM_LINUX && ZEN_USE_LIBSECRET
+ if (Protected.GetSize() != kKeychainAccountBytes)
+ {
+ return false;
+ }
+ char AccountHex[kKeychainAccountBytes * 2 + 1];
+ AccountStringFromBytes(static_cast<const uint8_t*>(Protected.GetData()), kKeychainAccountBytes, AccountHex, sizeof(AccountHex));
+
+ GError* Err = nullptr;
+ gchar* Encoded = secret_password_lookup_sync(ZenAuthSchema(), nullptr, &Err, "account", AccountHex, nullptr);
+ if (Err != nullptr)
+ {
+ g_error_free(Err);
+ }
+ if (Encoded == nullptr)
+ {
+ return false;
+ }
+ // `secret_password_free` zeroes memory before freeing; use it rather than g_free.
+ auto _E = MakeGuard([&]() { secret_password_free(Encoded); });
+
+ gsize DecodedLen = 0;
+ guchar* Decoded = g_base64_decode(Encoded, &DecodedLen);
+ if (Decoded == nullptr)
+ {
+ return false;
+ }
+ auto _D = MakeGuard([&]() { g_free(Decoded); });
+
+ OutPlaintext.assign(Decoded, Decoded + DecodedLen);
+ return true;
+#else
+ (void)Protected;
+ (void)OutPlaintext;
+ return false;
+#endif
+}
+
MemoryView
Aes::Encrypt(const AesKey256Bit& Key, const AesIV128Bit& IV, MemoryView In, MutableMemoryView Out, std::optional<std::string>& Reason)
{
@@ -502,6 +801,96 @@ TEST_CASE("crypto.aes")
}
}
+TEST_CASE("crypto.securerandom")
+{
+ std::array<uint8_t, 64> A{};
+ std::array<uint8_t, 64> B{};
+ CHECK(SecureRandomBytes(MutableMemoryView(A.data(), A.size())));
+ CHECK(SecureRandomBytes(MutableMemoryView(B.data(), B.size())));
+ // Vanishingly small probability two 64-byte draws match.
+ CHECK(A != B);
+}
+
+TEST_CASE("crypto.protectdata")
+{
+ const uint8_t Plain[48] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
+ 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48};
+ std::vector<uint8_t> Wrapped;
+ const bool WrapOk = TryProtectData(MakeMemoryView(Plain), Wrapped);
+# if ZEN_PLATFORM_WINDOWS
+ REQUIRE(WrapOk);
+ CHECK(Wrapped.size() != sizeof(Plain)); // DPAPI envelope adds overhead
+ CHECK(memcmp(Wrapped.data(), Plain, std::min(Wrapped.size(), sizeof(Plain))) != 0);
+
+ std::vector<uint8_t> Unwrapped;
+ REQUIRE(TryUnprotectData(MakeMemoryView(Wrapped), Unwrapped));
+ CHECK(Unwrapped.size() == sizeof(Plain));
+ CHECK(memcmp(Unwrapped.data(), Plain, sizeof(Plain)) == 0);
+# elif ZEN_PLATFORM_MAC
+ // Keychain may not be accessible in headless / CI contexts. Round-trip is
+ // asserted only when Wrap succeeds; otherwise the test is a no-op.
+ if (WrapOk)
+ {
+ CHECK(Wrapped.size() == 16); // Keychain account id
+ std::vector<uint8_t> Unwrapped;
+ REQUIRE(TryUnprotectData(MakeMemoryView(Wrapped), Unwrapped));
+ CHECK(Unwrapped.size() == sizeof(Plain));
+ CHECK(memcmp(Unwrapped.data(), Plain, sizeof(Plain)) == 0);
+
+ // Delete the Keychain entry we just added so tests don't accumulate residue.
+ static const char kHex[] = "0123456789abcdef";
+ char AccountHex[33];
+ for (size_t i = 0; i < 16; ++i)
+ {
+ AccountHex[i * 2] = kHex[Wrapped[i] >> 4];
+ AccountHex[i * 2 + 1] = kHex[Wrapped[i] & 0x0F];
+ }
+ AccountHex[32] = '\0';
+ CFStringRef Account = CFStringCreateWithCString(nullptr, AccountHex, kCFStringEncodingASCII);
+ const void* Keys[] = {kSecClass, kSecAttrService, kSecAttrAccount};
+ const void* Values[] = {kSecClassGenericPassword, CFSTR("org.unrealengine.zen.auth"), Account};
+ CFDictionaryRef Query =
+ CFDictionaryCreate(nullptr, Keys, Values, 3, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+ SecItemDelete(Query);
+ CFRelease(Query);
+ CFRelease(Account);
+ }
+# elif ZEN_PLATFORM_LINUX && ZEN_USE_LIBSECRET
+ // libsecret round-trip only when a Secret Service daemon is reachable. On
+ // headless / container CI with no D-Bus session, WrapOk is false and we fall
+ // back to the raw-bytes path at the authutils level, so the test is a no-op.
+ if (WrapOk)
+ {
+ CHECK(Wrapped.size() == 16);
+ std::vector<uint8_t> Unwrapped;
+ REQUIRE(TryUnprotectData(MakeMemoryView(Wrapped), Unwrapped));
+ CHECK(Unwrapped.size() == sizeof(Plain));
+ CHECK(memcmp(Unwrapped.data(), Plain, sizeof(Plain)) == 0);
+
+ // Clean up the keyring entry we just created.
+ static const char kHex[] = "0123456789abcdef";
+ char AccountHex[33];
+ for (size_t i = 0; i < 16; ++i)
+ {
+ AccountHex[i * 2] = kHex[Wrapped[i] >> 4];
+ AccountHex[i * 2 + 1] = kHex[Wrapped[i] & 0x0F];
+ }
+ AccountHex[32] = '\0';
+ GError* Err = nullptr;
+ secret_password_clear_sync(ZenAuthSchema(), nullptr, &Err, "account", AccountHex, nullptr);
+ if (Err != nullptr)
+ {
+ g_error_free(Err);
+ }
+ }
+# else
+ // No OS-level implementation compiled in; both calls must report failure.
+ CHECK(WrapOk == false);
+ std::vector<uint8_t> Unwrapped;
+ CHECK(TryUnprotectData(MakeMemoryView(Plain), Unwrapped) == false);
+# endif
+}
+
TEST_SUITE_END();
#endif