diff options
Diffstat (limited to 'src/zencore/crypto.cpp')
| -rw-r--r-- | src/zencore/crypto.cpp | 389 |
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 |