// Copyright Epic Games, Inc. All Rights Reserved. #include "AESGCMHandlerComponent.h" #include "PlatformCryptoTypes.h" IMPLEMENT_MODULE( FAESGCMHandlerComponentModule, AESGCMHandlerComponent ) const int32 FAESGCMHandlerComponent::KeySizeInBytes; const int32 FAESGCMHandlerComponent::BlockSizeInBytes; const int32 FAESGCMHandlerComponent::IVSizeInBytes; const int32 FAESGCMHandlerComponent::AuthTagSizeInBytes; TSharedPtr FAESGCMHandlerComponentModule::CreateComponentInstance(FString& Options) { return MakeShared(); } FAESGCMHandlerComponent::FAESGCMHandlerComponent() : FEncryptionComponent(FName(TEXT("AESGCMHandlerComponent"))) , bEncryptionEnabled(false) { EncryptionContext = IPlatformCrypto::Get().CreateContext(); } void FAESGCMHandlerComponent::SetEncryptionData(const FEncryptionData& EncryptionData) { Decryptor.Reset(nullptr); Encryptor.Reset(nullptr); if (EncryptionData.Key.Num() != KeySizeInBytes) { UE_LOG(PacketHandlerLog, Log, TEXT("FAESGCMHandlerComponent::SetEncryptionData. NewKey is not %d bytes long, ignoring."), KeySizeInBytes); return; } // Generate random bytes used for encryption packets, make sure first IV byte is non-zero value do { EPlatformCryptoResult RandResult = EncryptionContext->CreateRandomBytes(OutIV); if (RandResult == EPlatformCryptoResult::Failure) { UE_LOG(PacketHandlerLog, Log, TEXT("FAESGCMHandlerComponent::SetEncryptionData: failed to generate IV.")); return; } } while (OutIV[0] == 0); // Dummy IV and AuthTag values, Decrytor/Encryptor will be reset with actual values before each use uint8 DummyIV[IVSizeInBytes] = { 0 }; uint8 DummyAuth[AuthTagSizeInBytes] = { 0 }; Decryptor = EncryptionContext->CreateDecryptor_AES_256_GCM(EncryptionData.Key, DummyIV, DummyAuth); Encryptor = EncryptionContext->CreateEncryptor_AES_256_GCM(EncryptionData.Key, DummyIV); } void FAESGCMHandlerComponent::EnableEncryption() { bEncryptionEnabled = true; } void FAESGCMHandlerComponent::DisableEncryption() { bEncryptionEnabled = false; } bool FAESGCMHandlerComponent::IsEncryptionEnabled() const { return bEncryptionEnabled; } void FAESGCMHandlerComponent::Initialize() { SetActive(true); SetState(UE::Handler::Component::State::Initialized); Initialized(); } void FAESGCMHandlerComponent::InitFaultRecovery(UE::Net::FNetConnectionFaultRecoveryBase* InFaultRecovery) { AESGCMFaultHandler.InitFaultRecovery(InFaultRecovery); } bool FAESGCMHandlerComponent::IsValid() const { return true; } // Incoming & Outgoing pakcets will always operate on byte level. No bit-aligned stuff is supported here for efficiency reasons. // Encrypted packed layout: // [iv:12] [auth:16] [ciphertext:N] // where low byte of first IV byte is non-zero value // Unencrypted packed layout: // [0] [plaintext:N] // where first byte is 0 to signal unencrypted packet // Any further packet handler is expected to encode their bit-length explicitly if they operate on non-byte aligned packet sizes // In typical use case there will be OodleNetworkHandler that is byte aligned and it encodes uncompressed length void FAESGCMHandlerComponent::Incoming(FIncomingPacketRef PacketRef) { using namespace UE::Net; DECLARE_SCOPE_CYCLE_COUNTER(TEXT("PacketHandler AESGCM Decrypt"), STAT_PacketHandler_AESGCM_Decrypt, STATGROUP_Net); FBitReader& Packet = PacketRef.Packet; FInPacketTraits& Traits = PacketRef.Traits; int64 PacketBytes = Packet.GetNumBytes(); // Handle this packet if (IsValid() && PacketBytes > 0) { uint8* PacketData = Packet.GetData(); // If first byte is nonzero, then payload is encrypted if (PacketData[0] != 0) { // If the key hasn't been set yet, we can't decrypt, so ignore this packet. We don't set an error in this case because it may just be an out-of-order packet. if (!Decryptor.IsValid()) { UE_LOG(PacketHandlerLog, Log, TEXT("FAESGCMHandlerComponent::Incoming: received encrypted packet before key was set, ignoring.")); Packet.SetData(nullptr, 0); return; } // First 12 bytes is IV TArrayView IV { PacketData, IVSizeInBytes }; if (PacketBytes >= IV.Num()) { PacketData += IV.Num(); PacketBytes -= IV.Num(); } else { UE_LOG(PacketHandlerLog, Log, TEXT("FAESGCMHandlerComponent::Incoming: missing IV")); Packet.SetError(); AddToChainResultPtr(Traits.ExtendedError, EAESGCMNetResult::AESMissingIV); return; } // Then there are 16 bytes of AuthTag TArrayView AuthTag { PacketData, AuthTagSizeInBytes }; if (PacketBytes >= AuthTag.Num()) { PacketData += AuthTag.Num(); PacketBytes -= AuthTag.Num(); } else { UE_LOG(PacketHandlerLog, Log, TEXT("FAESGCMHandlerComponent::Incoming: missing auth tag")); Packet.SetError(); AddToChainResultPtr(Traits.ExtendedError, EAESGCMNetResult::AESMissingAuthTag); return; } if (PacketBytes == 0) { UE_LOG(PacketHandlerLog, Log, TEXT("FAESGCMHandlerComponent::Incoming: missing ciphertext")); Packet.SetError(); AddToChainResultPtr(Traits.ExtendedError, EAESGCMNetResult::AESMissingPayload); return; } // Rest of bytes is ciphertext TArrayView CipherText { PacketData, static_cast(PacketBytes) }; UE_LOG(PacketHandlerLog, VeryVerbose, TEXT("AESGCM packet handler received %d bytes before decryption."), CipherText.Num()); uint8 PlainText[MAX_PACKET_SIZE]; // Decrypt payload and verify AuthTag EPlatformCryptoResult DecryptResult = Decrypt(PlainText, CipherText, IV, AuthTag); if (DecryptResult == EPlatformCryptoResult::Failure) { UE_LOG(PacketHandlerLog, Log, TEXT("FAESGCMHandlerComponent::Incoming: failed to decrypt packet.")); Packet.SetError(); AddToChainResultPtr(Traits.ExtendedError, EAESGCMNetResult::AESDecryptionFailed); return; } Packet.SetData(PlainText, CipherText.Num() * 8); } else { // Skip first zero byte, rest of bytes contains unencrypted data Packet.Skip(8); } } } void FAESGCMHandlerComponent::Outgoing(FBitWriter& Packet, FOutPacketTraits& Traits) { DECLARE_SCOPE_CYCLE_COUNTER(TEXT("PacketHandler AESGCM Encrypt"), STAT_PacketHandler_AESGCM_Encrypt, STATGROUP_Net); // Handle this packet if (IsValid() && Packet.GetNumBytes() > 0) { if (bEncryptionEnabled) { UE_LOG(PacketHandlerLog, VeryVerbose, TEXT("AESGCM packet handler sending %ld bits before encryption."), Packet.GetNumBits()); TStaticArray AuthTag; TArrayView Payload { Packet.GetData(), static_cast(Packet.GetNumBytes()) }; // Prepare new IV for encryption { // Use 64-bit counter in bytes [1,9], leave byte [0] unchanged as that is non-zero indication that packet is encrypted uint8* CounterLocation = OutIV.GetData() + 1; // This place does not need completely new random value every time, just a unique value for each packet. // Incrementing IV bytes as 64-bit integer with wrap-around is enough for this use case. uint64 Counter = INTEL_ORDER64(FPlatformMemory::ReadUnaligned(CounterLocation)); FPlatformMemory::WriteUnaligned(CounterLocation, INTEL_ORDER64(Counter + 1)); } TArrayView PlainText = { Packet.GetData(), static_cast(Packet.GetNumBytes()) }; uint8 CipherText[MAX_PACKET_SIZE]; // Encrypt payload & write AuthTag EPlatformCryptoResult EncryptResult = Encrypt(CipherText, PlainText, OutIV, AuthTag); if (EncryptResult == EPlatformCryptoResult::Failure) { UE_LOG(PacketHandlerLog, Log, TEXT("FAESGCMHandlerComponent::Outgoing: failed to encrypt packet.")); Packet.SetError(); return; } // Make sure there is enough space allocated for outgoing packet memory int64 NewPacketByteCount = IVSizeInBytes + AuthTagSizeInBytes + PlainText.Num(); if (NewPacketByteCount * 8 > Packet.GetMaxBits()) { // Allocate max MAX_PACKET_SIZE bytes, just like PacketHandler does, so packet memory can be reused later check(NewPacketByteCount <= MAX_PACKET_SIZE); Packet = FBitWriter(MAX_PACKET_SIZE * 8); } uint8* NewPacketData = Packet.GetData(); // Copy the IV, AuthTag and encrypted payload to new packet FPlatformMemory::Memcpy(NewPacketData, OutIV.GetData(), OutIV.Num()); NewPacketData += OutIV.Num(); FPlatformMemory::Memcpy(NewPacketData, AuthTag.GetData(), AuthTag.Num()); NewPacketData += AuthTag.Num(); FPlatformMemory::Memcpy(NewPacketData, CipherText, PlainText.Num()); // Set how many valid bits there are in the new packet Packet.SetNumBits(NewPacketByteCount * 8); UE_LOG(PacketHandlerLog, VeryVerbose, TEXT(" AESGCM packet handler sending %" INT64_FMT " bytes after encryption."), Packet.GetNumBytes()); } else { // Make sure packet has space available for extra 8 bits if (Packet.AllowAppend(8)) { // Reserve one byte in packet data in the beginning uint8* PacketData = Packet.GetData(); FPlatformMemory::Memmove(PacketData + 1, PacketData, Packet.GetNumBytes()); // First byte 0 means that packet contains unencrypted payload PacketData[0] = 0; // Include first 8 bits in new packet data Packet.SetNumBits(8 + Packet.GetNumBits()); } else { Packet.SetOverflowed(Packet.GetNumBits()); } } } } EPlatformCryptoResult FAESGCMHandlerComponent::Decrypt(TArrayView OutPlaintext, const TArrayView InCiphertext, const TArrayView IV, const TArrayView AuthTag) { EPlatformCryptoResult Result = Decryptor->Reset(IV); if (Result != EPlatformCryptoResult::Success) { return Result; } Result = Decryptor->SetAuthTag(AuthTag); if (Result != EPlatformCryptoResult::Success) { return Result; } if (OutPlaintext.Num() < Decryptor->GetUpdateBufferSizeBytes(InCiphertext) + Decryptor->GetFinalizeBufferSizeBytes()) { // not enough space in plaintext output array return EPlatformCryptoResult::Failure; } int32 UpdateBytesWritten = 0; Result = Decryptor->Update(InCiphertext, OutPlaintext, UpdateBytesWritten); if (Result != EPlatformCryptoResult::Success) { return Result; } int32 FinalizeBytesWritten = 0; Result = Decryptor->Finalize(TArrayView(OutPlaintext.GetData() + UpdateBytesWritten, OutPlaintext.Num() - UpdateBytesWritten), FinalizeBytesWritten); if (Result != EPlatformCryptoResult::Success) { return Result; } if (UpdateBytesWritten + FinalizeBytesWritten != InCiphertext.Num()) { // AES GCM mode always decrypts to same amount of bytes as ciphertext return EPlatformCryptoResult::Failure; } return EPlatformCryptoResult::Success; } EPlatformCryptoResult FAESGCMHandlerComponent::Encrypt(TArrayView OutCipherText, const TArrayView InPlaintext, const TArrayView IV, TArrayView OutAuthTag) { EPlatformCryptoResult Result = Encryptor->Reset(IV); if (Result != EPlatformCryptoResult::Success) { return Result; } if (OutCipherText.Num() < Encryptor->GetUpdateBufferSizeBytes(InPlaintext) + Encryptor->GetFinalizeBufferSizeBytes()) { // not enough space in ciphertext output array return EPlatformCryptoResult::Failure; } int32 UpdateBytesWritten = 0; Result = Encryptor->Update(InPlaintext, OutCipherText, UpdateBytesWritten); if (Result != EPlatformCryptoResult::Success) { return Result; } int32 FinalizeBytesWritten = 0; Result = Encryptor->Finalize(TArrayView(OutCipherText.GetData() + UpdateBytesWritten, OutCipherText.Num() - UpdateBytesWritten), FinalizeBytesWritten); if (Result != EPlatformCryptoResult::Success) { return Result; } int32 AuthTagBytesWritten = 0; Result = Encryptor->GenerateAuthTag(OutAuthTag, AuthTagBytesWritten); if (Result != EPlatformCryptoResult::Success) { return Result; } if (UpdateBytesWritten + FinalizeBytesWritten != InPlaintext.Num()) { // AES GCM mode always encrypts to same amount of bytes as plaintext return EPlatformCryptoResult::Failure; } return EPlatformCryptoResult::Success; } int32 FAESGCMHandlerComponent::GetReservedPacketBits() const { // Worst case includes the IV and AuthTag // For unencrypted packets it is just an extra one byte, which is smaller return (IVSizeInBytes + AuthTagSizeInBytes) * 8; } void FAESGCMHandlerComponent::CountBytes(FArchive& Ar) const { FEncryptionComponent::CountBytes(Ar); const SIZE_T SizeOfThis = sizeof(*this) - sizeof(FEncryptionComponent); Ar.CountBytes(SizeOfThis, SizeOfThis); /* Note, as of now, EncryptionContext is just typedef'd, but none of the base types actually allocated memory directly in their classes (although there may be global state). if (FEncryptionContext const * const LocalContext = EncrpytionContext.Get()) { LocalContext->CountBytes(Ar); } */ }