// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include #include #include #include #include #include // On Windows we can use the built-in BCrypt API, on other platforms we currently // support either OpenSSL or mbedTLS. The preference is to use OpenSSL if available // mostly for historical reasons. We should investigate making mbedTLS the default // on non-Windows platforms in future as it is more lightweight. #if ZEN_PLATFORM_WINDOWS # define ZEN_USE_BCRYPT 1 #else # define ZEN_USE_BCRYPT 0 #endif #ifndef ZEN_USE_OPENSSL # if ZEN_USE_BCRYPT # define ZEN_USE_OPENSSL 0 # else # define ZEN_USE_OPENSSL 1 # endif #endif #ifndef ZEN_USE_MBEDTLS # if ZEN_PLATFORM_WINDOWS # define ZEN_USE_MBEDTLS 0 # elif !ZEN_USE_OPENSSL # define ZEN_USE_MBEDTLS 1 # else # define ZEN_USE_MBEDTLS 0 # endif #endif static_assert(ZEN_USE_OPENSSL + ZEN_USE_MBEDTLS + ZEN_USE_BCRYPT <= 1, "Only one crypto backend can be selected"); ZEN_THIRD_PARTY_INCLUDES_START #include #if ZEN_USE_OPENSSL # include # include # include # include #elif ZEN_USE_MBEDTLS # include # include # include #else # include # include # define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) # define STATUS_UNSUCCESSFUL ((NTSTATUS)0xC0000001L) #endif #if ZEN_PLATFORM_WINDOWS # include # include #elif ZEN_PLATFORM_MAC # include # include #elif ZEN_PLATFORM_LINUX && ZEN_USE_LIBSECRET # include #endif ZEN_THIRD_PARTY_INCLUDES_END namespace zen { using namespace std::literals; namespace crypto { enum class TransformMode : uint32_t { Decrypt, Encrypt }; #if ZEN_USE_MBEDTLS class MbedCipherCtx { public: MbedCipherCtx() { mbedtls_cipher_init(&m_Ctx); } ~MbedCipherCtx() { mbedtls_cipher_free(&m_Ctx); } mbedtls_cipher_context_t* operator&() { return &m_Ctx; } mbedtls_cipher_context_t* get() { return &m_Ctx; } private: mbedtls_cipher_context_t m_Ctx; }; MemoryView Transform(TransformMode Mode, MemoryView Key, MemoryView IV, MemoryView In, MutableMemoryView Out, std::optional& Reason) { const mbedtls_cipher_info_t* CipherInfo = mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_AES_256_CBC); if (CipherInfo == nullptr) { Reason = "failed to get mbedTLS cipher info"sv; return MemoryView(); } MbedCipherCtx Ctx; int ret = mbedtls_cipher_setup(Ctx.get(), CipherInfo); if (ret != 0) { Reason = fmt::format("mbedTLS cipher setup failed, ret={}", ret); return MemoryView(); } // key length in bits ret = mbedtls_cipher_setkey(Ctx.get(), reinterpret_cast(Key.GetData()), static_cast(Key.GetSize() * 8), (Mode == TransformMode::Encrypt) ? MBEDTLS_ENCRYPT : MBEDTLS_DECRYPT); if (ret != 0) { Reason = fmt::format("mbedTLS setkey failed, ret={}", ret); return MemoryView(); } ret = mbedtls_cipher_set_iv(Ctx.get(), reinterpret_cast(IV.GetData()), static_cast(IV.GetSize())); if (ret != 0) { Reason = fmt::format("mbedTLS set_iv failed, ret={}", ret); return MemoryView(); } ret = mbedtls_cipher_set_padding_mode(Ctx.get(), MBEDTLS_PADDING_PKCS7); if (ret != 0) { Reason = fmt::format("mbedTLS padding mode configuration failed, ret={}", ret); return MemoryView(); } ret = mbedtls_cipher_reset(Ctx.get()); if (ret != 0) { Reason = fmt::format("mbedTLS reset failed, ret={}", ret); return MemoryView(); } size_t olen = 0; size_t total = 0; ret = mbedtls_cipher_update(Ctx.get(), reinterpret_cast(In.GetData()), static_cast(In.GetSize()), reinterpret_cast(Out.GetData()), &olen); if (ret != 0) { Reason = fmt::format("mbedTLS update failed, ret={}", ret); return MemoryView(); } total = olen; ret = mbedtls_cipher_finish(Ctx.get(), reinterpret_cast(Out.GetData()) + total, &olen); if (ret != 0) { Reason = fmt::format("mbedTLS finish failed, ret={}", ret); return MemoryView(); } total += olen; return Out.Left(static_cast(total)); } #elif ZEN_USE_OPENSSL class EvpContext { public: EvpContext() : m_Ctx(EVP_CIPHER_CTX_new()) {} ~EvpContext() { EVP_CIPHER_CTX_free(m_Ctx); } operator EVP_CIPHER_CTX*() { return m_Ctx; } private: EVP_CIPHER_CTX* m_Ctx; }; MemoryView Transform(TransformMode Mode, MemoryView Key, MemoryView IV, MemoryView In, MutableMemoryView Out, std::optional& Reason) { const EVP_CIPHER* Cipher = EVP_aes_256_cbc(); ZEN_ASSERT(Cipher != nullptr); EvpContext Ctx; int Err = EVP_CipherInit_ex(Ctx, Cipher, nullptr, reinterpret_cast(Key.GetData()), reinterpret_cast(IV.GetData()), static_cast(Mode)); if (Err != 1) { Reason = fmt::format("failed to initialize cipher, error code '{}'", Err); return MemoryView(); } int EncryptedBytes = 0; int TotalEncryptedBytes = 0; Err = EVP_CipherUpdate(Ctx, reinterpret_cast(Out.GetData()), &EncryptedBytes, reinterpret_cast(In.GetData()), static_cast(In.GetSize())); if (Err != 1) { Reason = fmt::format("update crypto transform failed, error code '{}'", Err); return MemoryView(); } TotalEncryptedBytes = EncryptedBytes; MutableMemoryView Remaining = Out.RightChop(EncryptedBytes); EncryptedBytes = static_cast(Remaining.GetSize()); Err = EVP_CipherFinal(Ctx, reinterpret_cast(Remaining.GetData()), &EncryptedBytes); if (Err != 1) { Reason = fmt::format("finalize crypto transform failed, error code '{}'", Err); return MemoryView(); } TotalEncryptedBytes += EncryptedBytes; return Out.Left(TotalEncryptedBytes); } #else MemoryView Transform(TransformMode Mode, MemoryView Key, MemoryView IV, MemoryView In, MutableMemoryView Out, std::optional& Reason) { BCRYPT_ALG_HANDLE hAesAlg = NULL; NTSTATUS Status = STATUS_UNSUCCESSFUL; // Open an algorithm handle. if (!NT_SUCCESS(Status = BCryptOpenAlgorithmProvider(&hAesAlg, BCRYPT_AES_ALGORITHM, NULL, 0))) { Reason = fmt::format("Error 0x{:08x} returned by BCryptGetProperty"sv, Status); return {}; } auto _ = MakeGuard([hAesAlg] { BCryptCloseAlgorithmProvider(hAesAlg, 0); }); DWORD cbData = 0; DWORD cbBlockLen = 0; if (!NT_SUCCESS(Status = BCryptGetProperty(hAesAlg, BCRYPT_BLOCK_LENGTH, (PBYTE)&cbBlockLen, sizeof(DWORD), &cbData, 0))) { Reason = fmt::format("Error 0x{:08x} returned by BCryptGetProperty"sv, Status); return {}; } if (cbBlockLen > IV.GetSize()) { Reason = "block length is longer than the provided IV length"sv; return {}; } AesIV128Bit MutableIV = AesIV128Bit::FromMemoryView(IV); if (!NT_SUCCESS( Status = BCryptSetProperty(hAesAlg, BCRYPT_CHAINING_MODE, (PBYTE)BCRYPT_CHAIN_MODE_CBC, sizeof(BCRYPT_CHAIN_MODE_CBC), 0))) { Reason = fmt::format("Error 0x{:08x} returned by BCryptSetProperty"sv, Status); return {}; } DWORD cbKeyObject = 0; if (!NT_SUCCESS(Status = BCryptGetProperty(hAesAlg, BCRYPT_OBJECT_LENGTH, (PBYTE)&cbKeyObject, sizeof(DWORD), &cbData, 0))) { Reason = fmt::format("Error 0x{:08x} returned by BCryptGetProperty"sv, Status); return {}; } PBYTE pbKeyObject = (PBYTE)Memory::Alloc(cbKeyObject); if (NULL == pbKeyObject) { Reason = fmt::format("memory allocation failed"); return {}; } auto __ = MakeGuard([pbKeyObject] { Memory::Free(pbKeyObject); }); BCRYPT_KEY_HANDLE hKey = NULL; if (!NT_SUCCESS(Status = BCryptGenerateSymmetricKey(hAesAlg, &hKey, pbKeyObject, cbKeyObject, (PBYTE)Key.GetData(), (ULONG)Key.GetSize(), /* flags */ 0))) { Reason = fmt::format("Error 0x{:08x} returned by BCryptGenerateSymmetricKey"sv, Status); return {}; } auto ___ = MakeGuard([hKey] { BCryptDestroyKey(hKey); }); if (Mode == TransformMode::Encrypt) { DWORD CipherTextByteCount = 0; if (NT_SUCCESS(Status = BCryptEncrypt(hKey, (PUCHAR)In.GetData(), (ULONG)In.GetSize(), NULL, (PUCHAR)MutableIV.GetView().GetData(), cbBlockLen, NULL, 0, &CipherTextByteCount, BCRYPT_BLOCK_PADDING))) { if (Out.GetSize() < CipherTextByteCount) { Reason = "invalid output buffer size"; return {}; } if (NT_SUCCESS(Status = BCryptEncrypt(hKey, (PUCHAR)In.GetData(), (ULONG)In.GetSize(), NULL, (PUCHAR)MutableIV.GetView().GetData(), cbBlockLen, (PUCHAR)Out.GetData(), (ULONG)Out.GetSize(), &CipherTextByteCount, BCRYPT_BLOCK_PADDING))) { return Out.Left(CipherTextByteCount); } } Reason = fmt::format("Error 0x{:08x} returned by BCryptEncrypt", Status); return {}; } else { DWORD PlainTextByteCount = 0; // // Get the output buffer size. // if (NT_SUCCESS(Status = BCryptDecrypt(hKey, (PUCHAR)In.GetData(), (ULONG)In.GetSize(), NULL, (PUCHAR)MutableIV.GetView().GetData(), cbBlockLen, NULL, 0, &PlainTextByteCount, BCRYPT_BLOCK_PADDING))) { if (Out.GetSize() < PlainTextByteCount) { Reason = "invalid output buffer size"sv; return {}; } if (NT_SUCCESS(Status = BCryptDecrypt(hKey, (PUCHAR)In.GetData(), (ULONG)In.GetSize(), NULL, (PUCHAR)MutableIV.GetView().GetData(), cbBlockLen, (PUCHAR)Out.GetData(), (ULONG)Out.GetSize(), &PlainTextByteCount, BCRYPT_BLOCK_PADDING))) { return Out.Left(PlainTextByteCount); } } Reason = fmt::format("Error 0x{:08x} returned by BCryptDecrypt"sv, Status); return {}; } } #endif bool ValidateKeyAndIV(const AesKey256Bit& Key, const AesIV128Bit& IV, std::optional& Reason) { if (Key.IsValid() == false) { Reason = "invalid key"sv; return false; } if (IV.IsValid() == false) { Reason = "invalid initialization vector"sv; return false; } return true; } ////////////////////////////////////////////////////////////////////////// // // AES-256-GCM backends // #if ZEN_USE_MBEDTLS MemoryView GcmTransform(TransformMode Mode, const AesKey256Bit& Key, MemoryView Nonce, MemoryView Aad, MemoryView In, MutableMemoryView Out, MutableMemoryView TagOut, // only used for Encrypt MemoryView TagIn, // only used for Decrypt std::optional& Reason) { Reason = "AES-GCM is not implemented on the mbedTLS backend"sv; (void)Mode; (void)Key; (void)Nonce; (void)Aad; (void)In; (void)Out; (void)TagOut; (void)TagIn; return MemoryView(); } #elif ZEN_USE_OPENSSL MemoryView GcmTransform(TransformMode Mode, const AesKey256Bit& Key, MemoryView Nonce, MemoryView Aad, MemoryView In, MutableMemoryView Out, MutableMemoryView TagOut, MemoryView TagIn, std::optional& Reason) { EvpContext Ctx; const EVP_CIPHER* Cipher = EVP_aes_256_gcm(); ZEN_ASSERT(Cipher != nullptr); const bool Encrypting = (Mode == TransformMode::Encrypt); if (EVP_CipherInit_ex(Ctx, Cipher, nullptr, nullptr, nullptr, Encrypting ? 1 : 0) != 1) { Reason = "EVP_CipherInit_ex (algo) failed"sv; return {}; } // Explicitly set IV length to 12; the default is 12 for GCM but pinning // it prevents any surprise from an OpenSSL build that defaults // differently. if (EVP_CIPHER_CTX_ctrl(Ctx, EVP_CTRL_AEAD_SET_IVLEN, (int)AesGcm::NonceSize, nullptr) != 1) { Reason = "EVP_CTRL_AEAD_SET_IVLEN failed"sv; return {}; } if (EVP_CipherInit_ex(Ctx, nullptr, nullptr, reinterpret_cast(Key.GetView().GetData()), reinterpret_cast(Nonce.GetData()), Encrypting ? 1 : 0) != 1) { Reason = "EVP_CipherInit_ex (key+nonce) failed"sv; return {}; } // Feed AAD (if any) before the ciphertext/plaintext. if (!Aad.IsEmpty()) { int AadOutLen = 0; if (EVP_CipherUpdate(Ctx, nullptr, &AadOutLen, reinterpret_cast(Aad.GetData()), static_cast(Aad.GetSize())) != 1) { Reason = "EVP_CipherUpdate (AAD) failed"sv; return {}; } } // For decrypt, set the expected tag before calling Final so the tag // check can fail cleanly. if (!Encrypting) { if (EVP_CIPHER_CTX_ctrl(Ctx, EVP_CTRL_AEAD_SET_TAG, (int)TagIn.GetSize(), (void*)TagIn.GetData()) != 1) { Reason = "EVP_CTRL_AEAD_SET_TAG failed"sv; return {}; } } int BodyLen = 0; if (EVP_CipherUpdate(Ctx, reinterpret_cast(Out.GetData()), &BodyLen, reinterpret_cast(In.GetData()), static_cast(In.GetSize())) != 1) { Reason = "EVP_CipherUpdate (body) failed"sv; return {}; } int FinalLen = 0; if (EVP_CipherFinal_ex(Ctx, reinterpret_cast(Out.GetData()) + BodyLen, &FinalLen) != 1) { // For decrypt, this is the authentication-tag mismatch path. Reason = Encrypting ? std::string("EVP_CipherFinal_ex (encrypt) failed") : std::string("AES-GCM authentication tag mismatch"); return {}; } if (Encrypting) { if (EVP_CIPHER_CTX_ctrl(Ctx, EVP_CTRL_AEAD_GET_TAG, (int)TagOut.GetSize(), TagOut.GetData()) != 1) { Reason = "EVP_CTRL_AEAD_GET_TAG failed"sv; return {}; } } return Out.Left(static_cast(BodyLen + FinalLen)); } #else // ZEN_USE_BCRYPT MemoryView GcmTransform(TransformMode Mode, const AesKey256Bit& Key, MemoryView Nonce, MemoryView Aad, MemoryView In, MutableMemoryView Out, MutableMemoryView TagOut, MemoryView TagIn, std::optional& Reason) { BCRYPT_ALG_HANDLE hAlg = nullptr; NTSTATUS Status = STATUS_UNSUCCESSFUL; if (!NT_SUCCESS(Status = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, nullptr, 0))) { Reason = fmt::format("BCryptOpenAlgorithmProvider failed, 0x{:08x}"sv, Status); return {}; } auto CloseAlg = MakeGuard([hAlg] { BCryptCloseAlgorithmProvider(hAlg, 0); }); if (!NT_SUCCESS(Status = BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE, (PUCHAR)BCRYPT_CHAIN_MODE_GCM, sizeof(BCRYPT_CHAIN_MODE_GCM), 0))) { Reason = fmt::format("BCryptSetProperty(CHAIN_MODE_GCM) failed, 0x{:08x}"sv, Status); return {}; } BCRYPT_KEY_HANDLE hKey = nullptr; if (!NT_SUCCESS(Status = BCryptGenerateSymmetricKey(hAlg, &hKey, nullptr, 0, (PUCHAR)Key.GetView().GetData(), (ULONG)Key.GetView().GetSize(), 0))) { Reason = fmt::format("BCryptGenerateSymmetricKey failed, 0x{:08x}"sv, Status); return {}; } auto CloseKey = MakeGuard([hKey] { BCryptDestroyKey(hKey); }); BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO AuthInfo; BCRYPT_INIT_AUTH_MODE_INFO(AuthInfo); AuthInfo.pbNonce = (PUCHAR)Nonce.GetData(); AuthInfo.cbNonce = (ULONG)Nonce.GetSize(); AuthInfo.pbAuthData = Aad.IsEmpty() ? nullptr : (PUCHAR)Aad.GetData(); AuthInfo.cbAuthData = (ULONG)Aad.GetSize(); ULONG ResultLen = 0; if (Mode == TransformMode::Encrypt) { AuthInfo.pbTag = (PUCHAR)TagOut.GetData(); AuthInfo.cbTag = (ULONG)TagOut.GetSize(); Status = BCryptEncrypt(hKey, (PUCHAR)In.GetData(), (ULONG)In.GetSize(), &AuthInfo, nullptr, 0, (PUCHAR)Out.GetData(), (ULONG)Out.GetSize(), &ResultLen, /*flags=*/0); if (!NT_SUCCESS(Status)) { Reason = fmt::format("BCryptEncrypt (GCM) failed, 0x{:08x}"sv, Status); return {}; } } else { AuthInfo.pbTag = (PUCHAR)TagIn.GetData(); AuthInfo.cbTag = (ULONG)TagIn.GetSize(); Status = BCryptDecrypt(hKey, (PUCHAR)In.GetData(), (ULONG)In.GetSize(), &AuthInfo, nullptr, 0, (PUCHAR)Out.GetData(), (ULONG)Out.GetSize(), &ResultLen, /*flags=*/0); if (!NT_SUCCESS(Status)) { // STATUS_AUTH_TAG_MISMATCH (0xC000A002) is the tag-failure path. Reason = fmt::format("BCryptDecrypt (GCM) failed, 0x{:08x}"sv, Status); return {}; } } return Out.Left(ResultLen); } #endif // backend selection MemoryView Gcm(TransformMode Mode, const AesKey256Bit& Key, MemoryView Nonce, MemoryView Aad, MemoryView In, MutableMemoryView Out, MutableMemoryView TagOut, MemoryView TagIn, std::optional& Reason) { if (!Key.IsValid()) { Reason = "invalid key"sv; return {}; } if (Nonce.GetSize() != AesGcm::NonceSize) { Reason = fmt::format("AES-GCM nonce must be exactly {} bytes"sv, AesGcm::NonceSize); return {}; } if (Out.GetSize() < In.GetSize()) { Reason = "AES-GCM output buffer is too small"sv; return {}; } if (Mode == TransformMode::Encrypt) { if (TagOut.GetSize() != AesGcm::TagSize) { Reason = fmt::format("AES-GCM tag output must be exactly {} bytes"sv, AesGcm::TagSize); return {}; } } else { if (TagIn.GetSize() != AesGcm::TagSize) { Reason = fmt::format("AES-GCM tag input must be exactly {} bytes"sv, AesGcm::TagSize); return {}; } } return GcmTransform(Mode, Key, Nonce, Aad, In, Out, TagOut, TagIn, Reason); } } // namespace crypto bool SecureRandomBytes(MutableMemoryView Out) { if (Out.GetSize() == 0) { return true; } #if ZEN_USE_BCRYPT NTSTATUS Status = BCryptGenRandom(nullptr, static_cast(Out.GetData()), static_cast(Out.GetSize()), BCRYPT_USE_SYSTEM_PREFERRED_RNG); return NT_SUCCESS(Status); #elif ZEN_USE_OPENSSL return RAND_bytes(static_cast(Out.GetData()), static_cast(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(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& OutProtected) { #if ZEN_PLATFORM_WINDOWS DATA_BLOB In{static_cast(Plaintext.GetSize()), const_cast(static_cast(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(Plaintext.GetData()), static_cast(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(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& OutPlaintext) { #if ZEN_PLATFORM_WINDOWS DATA_BLOB In{static_cast(Protected.GetSize()), const_cast(static_cast(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(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(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(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& Reason) { if (crypto::ValidateKeyAndIV(Key, IV, Reason) == false) { return MemoryView(); } return crypto::Transform(crypto::TransformMode::Encrypt, Key.GetView(), IV.GetView(), In, Out, Reason); } MemoryView Aes::Decrypt(const AesKey256Bit& Key, const AesIV128Bit& IV, MemoryView In, MutableMemoryView Out, std::optional& Reason) { if (crypto::ValidateKeyAndIV(Key, IV, Reason) == false) { return MemoryView(); } return crypto::Transform(crypto::TransformMode::Decrypt, Key.GetView(), IV.GetView(), In, Out, Reason); } MemoryView AesGcm::Encrypt(const AesKey256Bit& Key, MemoryView Nonce, MemoryView Aad, MemoryView Plaintext, MutableMemoryView Out, MutableMemoryView OutTag, std::optional& Reason) { return crypto::Gcm(crypto::TransformMode::Encrypt, Key, Nonce, Aad, Plaintext, Out, OutTag, /*TagIn*/ {}, Reason); } MemoryView AesGcm::Decrypt(const AesKey256Bit& Key, MemoryView Nonce, MemoryView Aad, MemoryView Ciphertext, MemoryView Tag, MutableMemoryView Out, std::optional& Reason) { return crypto::Gcm(crypto::TransformMode::Decrypt, Key, Nonce, Aad, Ciphertext, Out, /*TagOut*/ {}, Tag, Reason); } ////////////////////////////////////////////////////////////////////////// // // CryptoRandom // bool CryptoRandom::Fill(MutableMemoryView Buffer, std::optional* Reason) { if (Buffer.GetSize() == 0) { return true; } auto SetReason = [&](std::string Msg) { if (Reason) { *Reason = std::move(Msg); } }; #if ZEN_USE_BCRYPT // BCRYPT_USE_SYSTEM_PREFERRED_RNG draws from the OS CSPRNG without // requiring the caller to manage an algorithm handle. const NTSTATUS Status = BCryptGenRandom(nullptr, (PUCHAR)Buffer.GetData(), (ULONG)Buffer.GetSize(), BCRYPT_USE_SYSTEM_PREFERRED_RNG); if (!NT_SUCCESS(Status)) { SetReason(fmt::format("BCryptGenRandom failed, 0x{:08x}", static_cast(Status))); return false; } return true; #elif ZEN_USE_OPENSSL // RAND_bytes returns 1 on success, 0 on failure, -1 if not supported. const int Rc = RAND_bytes(reinterpret_cast(Buffer.GetData()), static_cast(Buffer.GetSize())); if (Rc != 1) { SetReason(fmt::format("RAND_bytes failed (rc={})", Rc)); return false; } return true; #else // mbedTLS: no CSPRNG wired up here yet. Callers on this backend must // provide their own random source until a proper wiring is added. SetReason("CryptoRandom::Fill is not implemented on the mbedTLS backend"); (void)Buffer; return false; #endif } #if ZEN_WITH_TESTS void crypto_forcelink() { } TEST_SUITE_BEGIN("core.crypto"); TEST_CASE("crypto.bits") { using CryptoBits256Bit = CryptoBits<256>; CryptoBits256Bit Bits; CHECK(Bits.IsNull()); CHECK(Bits.IsValid() == false); CHECK(Bits.GetBitCount() == 256); CHECK(Bits.GetSize() == 32); Bits = CryptoBits256Bit::FromString("Addff"sv); CHECK(Bits.IsValid() == false); Bits = CryptoBits256Bit::FromString("abcdefghijklmnopqrstuvxyz0123456"sv); CHECK(Bits.IsValid()); auto SmallerBits = CryptoBits<128>::FromString("abcdefghijklmnopqrstuvxyz0123456"sv); CHECK(SmallerBits.IsValid() == false); } TEST_CASE("crypto.aes") { SUBCASE("basic") { const uint8_t InitVector[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; const AesKey256Bit Key = AesKey256Bit::FromString("abcdefghijklmnopqrstuvxyz0123456"sv); const AesIV128Bit IV = AesIV128Bit::FromMemoryView(MakeMemoryView(InitVector)); std::string_view PlainText = "The quick brown fox jumps over the lazy dog"sv; std::vector EncryptionBuffer; std::vector DecryptionBuffer; std::optional Reason; EncryptionBuffer.resize(PlainText.size() + Aes::BlockSize); DecryptionBuffer.resize(PlainText.size() + Aes::BlockSize); MemoryView EncryptedView = Aes::Encrypt(Key, IV, MakeMemoryView(PlainText), MakeMutableMemoryView(EncryptionBuffer), Reason); CHECK(Reason.has_value() == false); MemoryView DecryptedView = Aes::Decrypt(Key, IV, EncryptedView, MakeMutableMemoryView(DecryptionBuffer), Reason); CHECK(Reason.has_value() == false); std::string_view EncryptedDecryptedText = std::string_view(reinterpret_cast(DecryptedView.GetData()), DecryptedView.GetSize()); CHECK(EncryptedDecryptedText == PlainText); } } TEST_CASE("crypto.aesgcm") { const AesKey256Bit Key = AesKey256Bit::FromString("abcdefghijklmnopqrstuvxyz0123456"sv); const uint8_t NonceBytes[AesGcm::NonceSize] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; const MemoryView Nonce = MakeMemoryView(NonceBytes); SUBCASE("round trip without AAD") { std::string_view Plain = "The quick brown fox jumps over the lazy dog"sv; std::vector Cipher(Plain.size()); std::vector Tag(AesGcm::TagSize); std::optional Reason; MemoryView CipherView = AesGcm::Encrypt(Key, Nonce, /*Aad*/ {}, MakeMemoryView(Plain), MakeMutableMemoryView(Cipher), MakeMutableMemoryView(Tag), Reason); REQUIRE(!Reason.has_value()); CHECK_EQ(CipherView.GetSize(), Plain.size()); std::vector Decoded(Plain.size()); MemoryView DecodedView = AesGcm::Decrypt(Key, Nonce, /*Aad*/ {}, CipherView, MakeMemoryView(Tag), MakeMutableMemoryView(Decoded), Reason); REQUIRE(!Reason.has_value()); CHECK_EQ(DecodedView.GetSize(), Plain.size()); std::string_view DecodedText(reinterpret_cast(DecodedView.GetData()), DecodedView.GetSize()); CHECK_EQ(DecodedText, Plain); } SUBCASE("round trip with AAD") { std::string_view Plain = "payload"sv; std::string_view Aad = "header bits that are authenticated but not encrypted"sv; std::vector Cipher(Plain.size()); std::vector Tag(AesGcm::TagSize); std::optional Reason; MemoryView CipherView = AesGcm::Encrypt(Key, Nonce, MakeMemoryView(Aad), MakeMemoryView(Plain), MakeMutableMemoryView(Cipher), MakeMutableMemoryView(Tag), Reason); REQUIRE(!Reason.has_value()); std::vector Decoded(Plain.size()); MemoryView DecodedView = AesGcm::Decrypt(Key, Nonce, MakeMemoryView(Aad), CipherView, MakeMemoryView(Tag), MakeMutableMemoryView(Decoded), Reason); REQUIRE(!Reason.has_value()); CHECK_EQ(DecodedView.GetSize(), Plain.size()); } SUBCASE("tampered ciphertext fails authentication") { std::string_view Plain = "important"sv; std::vector Cipher(Plain.size()); std::vector Tag(AesGcm::TagSize); std::optional Reason; MemoryView CipherView = AesGcm::Encrypt(Key, Nonce, /*Aad*/ {}, MakeMemoryView(Plain), MakeMutableMemoryView(Cipher), MakeMutableMemoryView(Tag), Reason); REQUIRE(!Reason.has_value()); // Flip a bit in the ciphertext. Cipher[0] ^= 0x01; std::vector Decoded(Plain.size()); MemoryView DecodedView = AesGcm::Decrypt(Key, Nonce, /*Aad*/ {}, CipherView, MakeMemoryView(Tag), MakeMutableMemoryView(Decoded), Reason); CHECK(Reason.has_value()); CHECK(DecodedView.IsEmpty()); } SUBCASE("tampered tag fails authentication") { std::string_view Plain = "important"sv; std::vector Cipher(Plain.size()); std::vector Tag(AesGcm::TagSize); std::optional Reason; MemoryView CipherView = AesGcm::Encrypt(Key, Nonce, /*Aad*/ {}, MakeMemoryView(Plain), MakeMutableMemoryView(Cipher), MakeMutableMemoryView(Tag), Reason); REQUIRE(!Reason.has_value()); Tag[0] ^= 0x80; std::vector Decoded(Plain.size()); MemoryView DecodedView = AesGcm::Decrypt(Key, Nonce, /*Aad*/ {}, CipherView, MakeMemoryView(Tag), MakeMutableMemoryView(Decoded), Reason); CHECK(Reason.has_value()); CHECK(DecodedView.IsEmpty()); } SUBCASE("AAD mismatch fails authentication") { std::string_view Plain = "payload"sv; std::string_view AadOk = "expected header"sv; std::string_view AadNo = "different header"sv; std::vector Cipher(Plain.size()); std::vector Tag(AesGcm::TagSize); std::optional Reason; MemoryView CipherView = AesGcm::Encrypt(Key, Nonce, MakeMemoryView(AadOk), MakeMemoryView(Plain), MakeMutableMemoryView(Cipher), MakeMutableMemoryView(Tag), Reason); REQUIRE(!Reason.has_value()); std::vector Decoded(Plain.size()); MemoryView DecodedView = AesGcm::Decrypt(Key, Nonce, MakeMemoryView(AadNo), CipherView, MakeMemoryView(Tag), MakeMutableMemoryView(Decoded), Reason); CHECK(Reason.has_value()); CHECK(DecodedView.IsEmpty()); } SUBCASE("wrong nonce size is rejected") { std::string_view Plain = "x"sv; const uint8_t TooShort[8] = {0}; std::vector Cipher(Plain.size()); std::vector Tag(AesGcm::TagSize); std::optional Reason; MemoryView CipherView = AesGcm::Encrypt(Key, MakeMemoryView(TooShort), /*Aad*/ {}, MakeMemoryView(Plain), MakeMutableMemoryView(Cipher), MakeMutableMemoryView(Tag), Reason); CHECK(Reason.has_value()); CHECK(CipherView.IsEmpty()); } } TEST_CASE("crypto.random") { SUBCASE("fills buffer with non-zero bytes") { uint8_t Buffer[32] = {}; MutableMemoryView View = MakeMutableMemoryView(Buffer); const bool Ok = CryptoRandom::Fill(View); REQUIRE(Ok); // Probability of 32 all-zero bytes from a CSPRNG is 2^-256 — we // accept it as "effectively never". bool AnyNonZero = false; for (uint8_t B : Buffer) { if (B != 0) { AnyNonZero = true; break; } } CHECK(AnyNonZero); } SUBCASE("two calls produce different output") { uint8_t A[32] = {}; uint8_t B[32] = {}; CHECK(CryptoRandom::Fill(MakeMutableMemoryView(A))); CHECK(CryptoRandom::Fill(MakeMutableMemoryView(B))); CHECK(memcmp(A, B, 32) != 0); } SUBCASE("zero-size buffer is a no-op success") { uint8_t Dummy = 0xAB; CHECK(CryptoRandom::Fill(MutableMemoryView(&Dummy, size_t{0}))); CHECK_EQ(Dummy, 0xAB); } } TEST_CASE("crypto.securerandom") { std::array A{}; std::array 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 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 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 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 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 Unwrapped; CHECK(TryUnprotectData(MakeMemoryView(Plain), Unwrapped) == false); # endif } TEST_SUITE_END(); #endif } // namespace zen