#include "SkyPortalIO.h" #include "Misc/AES.h" #include "HAL/UnrealMemory.h" #include "openssl/aes.h" DEFINE_LOG_CATEGORY(LogHIDApi); DEFINE_LOG_CATEGORY(LogSkyportalIO); //Constructor USkyPortalIO::USkyPortalIO() { bPortalReady = OpenPortalHandle(); } bool USkyPortalIO::OpenPortalHandle() { //reset if (PortalDevice) { hid_close(PortalDevice); bPortalReady = false; } /* Declare two pointers to hold information about HID devices. "list" will point to the head of the linked list of devices, "attributes" will be used to iterate through the list. */ struct hid_device_info* list, * attributes; list = hid_enumerate(0x0, 0x0); // If `list` is NULL, that means no devices were found or there was an error. // In this case, print an error message and terminate the program. if (!list) { UE_LOG(LogHIDApi, Error, TEXT("No devices found")); // Get the error message from the HIDAPI HidError = hid_error(NULL); UE_LOG(LogHIDApi, Error, TEXT("%s"), *HidError); return false; } attributes = list; // Iterate through the linked list of devices int vendorCount = sizeof(VendorIds) / sizeof(VendorIds[0]); int productCount = sizeof(ProductIds) / sizeof(ProductIds[0]); while (attributes) { // Check if the devices match any of vendor_id and product_id for (int i = 0; i < vendorCount; i++) { for (int j = 0; j < productCount; j++) { if (attributes->vendor_id == VendorIds[i] && attributes->product_id == ProductIds[j]) { UE_LOG(LogHIDApi, Display, TEXT("Portal found")); UE_LOG(LogHIDApi, Log, TEXT("Vendor ID: 0x%x, Product ID: 0x%x"), attributes->vendor_id, attributes->product_id); PortalDevice = hid_open(attributes->vendor_id, attributes->product_id, NULL); if (PortalDevice) { UE_LOG(LogHIDApi, Display, TEXT("Successful connection to Portal.")); // Free the device list hid_free_enumeration(list); return true; } break; } } } // Move to the next device in the list attributes = attributes->next; } // Free the device list hid_free_enumeration(list); UE_LOG(LogHIDApi, Error, TEXT("No Portals found")); return false; } // write function need to be different on windows, as hid_write doesn't work with the way windows handle I/O #if PLATFORM_WINDOWS #include #define HID_CTL_CODE(id) \ CTL_CODE(FILE_DEVICE_KEYBOARD, (id), METHOD_NEITHER, FILE_ANY_ACCESS) #define HID_IN_CTL_CODE(id) \ CTL_CODE(FILE_DEVICE_KEYBOARD, (id), METHOD_IN_DIRECT, FILE_ANY_ACCESS) #define IOCTL_HID_SET_OUTPUT_REPORT HID_IN_CTL_CODE(101) struct hid_device_ { HANDLE device_handle; BOOL blocking; USHORT output_report_length; unsigned char* write_buf; size_t input_report_length; USHORT feature_report_length; unsigned char* feature_buf; wchar_t* last_error_str; BOOL read_pending; char* read_buf; OVERLAPPED ol; OVERLAPPED write_ol; struct hid_device_info* device_info; }; bool USkyPortalIO::Write(FWriteBlock* pb) { if (!ensure(PortalDevice)) { UE_LOG(LogSkyportalIO, Error, TEXT("No Portal found")); return false; } BOOL res; OVERLAPPED ol; memset(&ol, 0, sizeof(ol)); DWORD bytes_returned; res = DeviceIoControl(PortalDevice->device_handle, IOCTL_HID_SET_OUTPUT_REPORT, (unsigned char*)pb->data, write_buf_size, (unsigned char*)pb->data, write_buf_size, &bytes_returned, &ol); ensureMsgf(res, TEXT("Unable to write to Portal")); pb->BytesTransferred = bytes_returned; return res; } #else bool USkyPortalIO::Write(FWriteBlock* pb) { if (!ensure(PortalDevice)) { UE_LOG(LogSkyportalIO, Error, TEXT("No Portal found")); return; } pb->data[0] = 0; // Use report 0 res = hid_write(PortalDevice, pb->data, write_buf_size); ensureMsgf(res != -1, TEXT("Unable to write to Portal, %s"), hid_error(PortalDevice)); pb->BytesTransferred = res; return res; } #endif void USkyPortalIO::WriteRaw(const TArray* block) { ; int res = hid_write(PortalDevice, block->GetData(), block->GetAllocatedSize()); //return the number of byte written if (res == -1) { UE_LOG(LogSkyportalIO, Error, TEXT("Unable to write raw data to Portal. error:\n %s"), hid_error(PortalDevice)); return; } } uint8* USkyPortalIO::Read() { uint8* output = new uint8[0x20]; int result = hid_read(PortalDevice, output, 0x20); if (result == -1) { UE_LOG(LogSkyportalIO, Error, TEXT("Unable to read data from Portal. Error:\n%s"), hid_error(PortalDevice)); delete[] output; // Prevent memory leak return nullptr; } if (result == 0) { // No data was read UE_LOG(LogSkyportalIO, Warning, TEXT("No data read from portal")); delete[] output; return nullptr; } return output; } uint8* USkyPortalIO::QueryBlock(uint8 FigureIndex, uint8 BlockIndex) { UE_LOG(LogSkyportalIO, Verbose, TEXT("Querying block %d - Start"), BlockIndex); FWriteBlock command; memset(command.data, 0, write_buf_size); //maybe not needed here command.data[1] = 'Q'; command.data[2] = FigureIndex; // Figure index (0x00-0x0F) command.data[3] = BlockIndex; unsigned char* output = nullptr; int attempt=0; bool write; do { attempt++; UE_LOG(LogSkyportalIO, Verbose, TEXT("Trying to write... attempt:%d"), attempt); write = Write(&command); if (write) { output = Read(); if (output != nullptr && output != 0) { UE_LOG(LogSkyportalIO, Verbose, TEXT("Read success: output[0]=%d, output[1]=%d, output[2]=%d"), output[0], output[1], output[2]); }else{ UE_LOG(LogSkyportalIO, Error, TEXT("Read failed, output is null")); return 0; } } } while (write == false && output && (output[0] != 'Q' || (output[1] % 0x10 != FigureIndex && output[1] != 0x01) || output[2] != BlockIndex) && attempt < 10); if (output == nullptr) { UE_LOG(LogSkyportalIO, Error, TEXT("Query failed after %d attempts"), attempt); return 0; // Return early if the loop failed to get valid output } UE_LOG(LogSkyportalIO, Verbose, TEXT("Querying block %d - success"), BlockIndex); UE_LOG(LogSkyportalIO, VeryVerbose, TEXT("Data block %d = \n %s"), BlockIndex,*OutputToString(output)); return output; } void USkyPortalIO::Close() { if (PortalDevice) { hid_close(PortalDevice); } bPortalReady = false; } /* TODO: Should not be here bool USkyPortalIO::IsFalsePositive() const { int dif = FMath::Abs(CurrentStatusData.Counter - OldStatusData.Counter); return (dif <= 2 || dif >= 254); } */ EPortalCommand GetPortalCommandFromChar(unsigned char Char) { switch (Char) { case 'A': return EPortalCommand::A; case 'C': return EPortalCommand::C; case 'J': return EPortalCommand::J; case 'L': return EPortalCommand::L; case 'M': return EPortalCommand::M; case 'Q': return EPortalCommand::Q; case 'R': return EPortalCommand::R; case 'S': return EPortalCommand::S; default: // Handle the case when the character doesn't match any enum // Return a default or invalid value, or handle the error UE_LOG(LogSkyportalIO, Warning, TEXT("Invalid character for Portal Command: %c"), TCHAR(Char)); return EPortalCommand::S; // 'S' for Status as a default } } FString DataToString(const uint8* data) { FString OutString; for (int32 i = 0; i < 16; i++) { OutString += FString::Printf(TEXT("%02x"), data[i]); } return OutString; } FString OutputToString(const unsigned char* output) { FString OutString; for (int32 i = 0; i < 0x20; i++) { OutString += FString::Printf(TEXT("%02x"), output[i]); } return OutString; } FPortalStatusData ParsePortalStatus(const uint8* StatusResponse) { FPortalStatusData result; result.Counter = StatusResponse[5]; result.bIsReady = (StatusResponse[6] == 0x01); // Parse the figure status array (little-endian 32-bit integer) uint32 FigureStatusArray = 0; // Reading the 32-bit integer (character status array) from the buffer FigureStatusArray |= StatusResponse[1]; // 1st byte FigureStatusArray |= (StatusResponse[2] << 8); // 2nd byte FigureStatusArray |= (StatusResponse[3] << 16); // 3rd byte FigureStatusArray |= (StatusResponse[4] << 24); // 4th byte bool bChangeBitsSet = false; TStaticArray tempArray; // For each of the 16 entries, extract the 2-bit status and map it to EFigureStatus for (int32 i = 0; i < 16; ++i) { uint8 StatusBits = (FigureStatusArray >> (i * 2)) & 0b11; // Extract 2 bits EFigureStatus FigureStatus; switch (StatusBits) { case 0b00: FigureStatus = EFigureStatus::NOT_PRESENT; break; case 0b01: FigureStatus = EFigureStatus::PRESENT; break; case 0b11: FigureStatus = EFigureStatus::ADDED; // When it's a new NUID detected break; case 0b10: FigureStatus = EFigureStatus::REMOVED; // New NUID removed break; default: FigureStatus = EFigureStatus::NOT_PRESENT; // Default case break; } // Add to the array of figure statuses //PortalStatusData.StatusArray.Insert(FigureStatus, i); tempArray[i] = FigureStatus; } result.StatusArray.SetNum(0); result.StatusArray.Append(tempArray); return result; } void DecryptAES128(uint8* OutData, const uint8* InData, const uint8* Key) { UE_LOG(LogSkyportalIO, VeryVerbose, TEXT("DECRYPT -- Starting decryting data %s with %s key"), *DataToString(InData), *DataToString(Key)); AES_KEY AesKey; // Set the decryption key (16 bytes for AES-128) AES_set_decrypt_key(Key, 128, &AesKey); // Perform AES-128 decryption (ECB mode) AES_ecb_encrypt(InData, OutData, &AesKey, AES_DECRYPT); UE_LOG(LogSkyportalIO, VeryVerbose, TEXT("DECRYPT -- data %s with %s key decryted"), *DataToString(InData), *DataToString(Key)); } UFigureData* USkyPortalIO::ReadFigureBlocks(uint8 FigureIndex) { UFigureData* TempFigureData = NewObject(); TempFigureData->dataError = false; // Initialize error flag // Loop over all 64 blocks for (uint8 BlockIndex = 0; BlockIndex < FIGURE_TOTAL_BLOCKS; ++BlockIndex) { UE_LOG(LogSkyportalIO, VeryVerbose, TEXT("Reading block %d"), BlockIndex); // Query the block from the portal uint8* output = QueryBlock(FigureIndex, BlockIndex); if (output == 0) { --BlockIndex; UE_LOG(LogSkyportalIO, Error, TEXT("Query error, reprocessing...")); continue; } // Copy 16 bytes from the output, starting at the third byte FMemory::Memcpy(TempFigureData->data[BlockIndex], output + 3, FIGURE_BLOCK_SIZE); // Block 1 is sometimes a duplicate of block 0 if (BlockIndex == 1) { if (FMemory::Memcmp(TempFigureData->data[1], TempFigureData->data[0], FIGURE_BLOCK_SIZE) == 0) { // Decrement BlockIndex to reprocess block 1 if it's a duplicate of block 0 --BlockIndex; UE_LOG(LogSkyportalIO, Verbose, TEXT("Block 1 is a duplicate of block 0, reprocessing...")); continue; } } if (((BlockIndex + 1) % 4 == 0) || BlockIndex < 8) { // Direct copy from data to decryptedData for certain blocks FMemory::Memcpy(TempFigureData->decryptedData[BlockIndex], TempFigureData->data[BlockIndex], FIGURE_BLOCK_SIZE); UE_LOG(LogSkyportalIO, Verbose, TEXT("Block %d doesnt need decryption"), BlockIndex); } else { /***** MD5 Hash Calculation *****/ // Prepare the hash input buffer UE_LOG(LogSkyportalIO, Verbose, TEXT("DECRYPT -- Starting decryting block %d"), BlockIndex); uint8 hashIn[0x56]; FMemory::Memcpy(hashIn, TempFigureData->data[0], 16); FMemory::Memcpy(hashIn + 0x10, TempFigureData->data[1], 16); hashIn[0x20] = BlockIndex; // Append the 35-byte constant FMemory::Memcpy(hashIn + 0x21, HASH_CONST, sizeof(HASH_CONST)); // Compute the MD5 hash FMD5 MD5; MD5.Update(hashIn, sizeof(hashIn)); uint8 key[16]; // MD5 produces 16 bytes MD5.Final(key); // AES-128 ECB decryption FAES::FAESKey AesKey; FMemory::Memcpy(AesKey.Key, key, 16); //TODO: FAES implementation in Unreal is AES-256. We need AES-128 //FAES::DecryptData(TempFigureData.decryptedData[BlockIndex], TempFigureData.data[BlockIndex], 16, AesKey); DecryptAES128(TempFigureData->decryptedData[BlockIndex], TempFigureData->data[BlockIndex], key); UE_LOG(LogSkyportalIO, Verbose, TEXT("DECRYPT -- block %d decrypted"), BlockIndex); } UE_LOG(LogSkyportalIO, VeryVerbose, TEXT("block %d readed with success"), BlockIndex); } // Some verifications should happen. Like if dataError is set. return TempFigureData; // Return the complete figure data }