IOS: Implement IOSC-like API
This prevents the IOS crypto code and keys from being spread over the codebase. Things only have to be implemented once, and can be used everywhere from the IOS code. Additionally, since ES exposes some IOSC calls directly (DeleteObject and Encrypt/Decrypt), we need this for proper emulation. Currently, this only supports AES key objects.
This commit is contained in:
parent
e01624f64b
commit
f8fb9e2d03
|
@ -10,15 +10,30 @@ namespace Common
|
|||
{
|
||||
namespace AES
|
||||
{
|
||||
std::vector<u8> Decrypt(const u8* key, u8* iv, const u8* src, size_t size)
|
||||
std::vector<u8> DecryptEncrypt(const u8* key, u8* iv, const u8* src, size_t size, Mode mode)
|
||||
{
|
||||
mbedtls_aes_context aes_ctx;
|
||||
std::vector<u8> buffer(size);
|
||||
|
||||
mbedtls_aes_setkey_dec(&aes_ctx, key, 128);
|
||||
mbedtls_aes_crypt_cbc(&aes_ctx, MBEDTLS_AES_DECRYPT, size, iv, src, buffer.data());
|
||||
if (mode == Mode::Encrypt)
|
||||
mbedtls_aes_setkey_enc(&aes_ctx, key, 128);
|
||||
else
|
||||
mbedtls_aes_setkey_dec(&aes_ctx, key, 128);
|
||||
|
||||
mbedtls_aes_crypt_cbc(&aes_ctx, mode == Mode::Encrypt ? MBEDTLS_AES_ENCRYPT : MBEDTLS_AES_DECRYPT,
|
||||
size, iv, src, buffer.data());
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
std::vector<u8> Decrypt(const u8* key, u8* iv, const u8* src, size_t size)
|
||||
{
|
||||
return DecryptEncrypt(key, iv, src, size, Mode::Decrypt);
|
||||
}
|
||||
|
||||
std::vector<u8> Encrypt(const u8* key, u8* iv, const u8* src, size_t size)
|
||||
{
|
||||
return DecryptEncrypt(key, iv, src, size, Mode::Encrypt);
|
||||
}
|
||||
} // namespace AES
|
||||
} // namespace Common
|
||||
|
|
|
@ -13,6 +13,15 @@ namespace Common
|
|||
{
|
||||
namespace AES
|
||||
{
|
||||
enum class Mode
|
||||
{
|
||||
Decrypt,
|
||||
Encrypt,
|
||||
};
|
||||
std::vector<u8> DecryptEncrypt(const u8* key, u8* iv, const u8* src, size_t size, Mode mode);
|
||||
|
||||
// Convenience functions
|
||||
std::vector<u8> Decrypt(const u8* key, u8* iv, const u8* src, size_t size);
|
||||
std::vector<u8> Encrypt(const u8* key, u8* iv, const u8* src, size_t size);
|
||||
} // namespace AES
|
||||
} // namespace Common
|
||||
|
|
|
@ -148,6 +148,7 @@ set(SRCS
|
|||
IOS/Device.cpp
|
||||
IOS/DeviceStub.cpp
|
||||
IOS/IOS.cpp
|
||||
IOS/IOSC.cpp
|
||||
IOS/MemoryValues.cpp
|
||||
IOS/MIOS.cpp
|
||||
IOS/DI/DI.cpp
|
||||
|
|
|
@ -175,6 +175,7 @@
|
|||
<ClCompile Include="IOS\Device.cpp" />
|
||||
<ClCompile Include="IOS\DeviceStub.cpp" />
|
||||
<ClCompile Include="IOS\IOS.cpp" />
|
||||
<ClCompile Include="IOS\IOSC.cpp" />
|
||||
<ClCompile Include="IOS\MemoryValues.cpp" />
|
||||
<ClCompile Include="IOS\MIOS.cpp" />
|
||||
<ClCompile Include="IOS\DI\DI.cpp" />
|
||||
|
@ -432,6 +433,7 @@
|
|||
<ClInclude Include="IOS\Device.h" />
|
||||
<ClInclude Include="IOS\DeviceStub.h" />
|
||||
<ClInclude Include="IOS\IOS.h" />
|
||||
<ClInclude Include="IOS\IOSC.h" />
|
||||
<ClInclude Include="IOS\MemoryValues.h" />
|
||||
<ClInclude Include="IOS\MIOS.h" />
|
||||
<ClInclude Include="IOS\DI\DI.h" />
|
||||
|
|
|
@ -793,6 +793,9 @@
|
|||
<ClCompile Include="IOS\IOS.cpp">
|
||||
<Filter>IOS</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="IOS\IOSC.cpp">
|
||||
<Filter>IOS</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="IOS\MemoryValues.cpp">
|
||||
<Filter>IOS</Filter>
|
||||
</ClCompile>
|
||||
|
@ -1507,6 +1510,9 @@
|
|||
<ClInclude Include="IOS\IOS.h">
|
||||
<Filter>IOS</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="IOS\IOSC.h">
|
||||
<Filter>IOS</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="IOS\MemoryValues.h">
|
||||
<Filter>IOS</Filter>
|
||||
</ClInclude>
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
// Copyright 2017 Dolphin Emulator Project
|
||||
// Licensed under GPLv2+
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include <mbedtls/sha1.h>
|
||||
|
||||
#include "Common/Assert.h"
|
||||
#include "Common/ChunkFile.h"
|
||||
#include "Common/Crypto/AES.h"
|
||||
#include "Common/Crypto/ec.h"
|
||||
#include "Core/IOS/Device.h"
|
||||
#include "Core/IOS/IOSC.h"
|
||||
#include "Core/ec_wii.h"
|
||||
|
||||
namespace IOS
|
||||
{
|
||||
namespace HLE
|
||||
{
|
||||
IOSC::IOSC()
|
||||
{
|
||||
LoadDefaultEntries();
|
||||
}
|
||||
|
||||
IOSC::~IOSC() = default;
|
||||
|
||||
ReturnCode IOSC::CreateObject(Handle* handle, ObjectType type, ObjectSubType subtype, u32 pid)
|
||||
{
|
||||
auto iterator = FindFreeEntry();
|
||||
if (iterator == m_key_entries.end())
|
||||
return IOSC_FAIL_ALLOC;
|
||||
|
||||
iterator->in_use = true;
|
||||
iterator->type = type;
|
||||
iterator->subtype = subtype;
|
||||
iterator->owner_mask = 1 << pid;
|
||||
|
||||
*handle = GetHandleFromIterator(iterator);
|
||||
return IPC_SUCCESS;
|
||||
}
|
||||
|
||||
ReturnCode IOSC::DeleteObject(Handle handle, u32 pid)
|
||||
{
|
||||
if (IsDefaultHandle(handle) || !HasOwnership(handle, pid))
|
||||
return IOSC_EACCES;
|
||||
|
||||
m_key_entries[handle].in_use = false;
|
||||
m_key_entries[handle].data.clear();
|
||||
return IPC_SUCCESS;
|
||||
}
|
||||
|
||||
constexpr size_t AES128_KEY_SIZE = 0x10;
|
||||
ReturnCode IOSC::ImportSecretKey(Handle dest_handle, Handle decrypt_handle, u8* iv,
|
||||
const u8* encrypted_key, u32 pid)
|
||||
{
|
||||
if (!HasOwnership(dest_handle, pid) || !HasOwnership(decrypt_handle, pid) ||
|
||||
IsDefaultHandle(dest_handle))
|
||||
{
|
||||
return IOSC_EACCES;
|
||||
}
|
||||
|
||||
auto* dest_entry = &m_key_entries[dest_handle];
|
||||
// TODO: allow other secret key subtypes
|
||||
if (dest_entry->type != TYPE_SECRET_KEY || dest_entry->subtype != SUBTYPE_AES128)
|
||||
return IOSC_INVALID_OBJTYPE;
|
||||
|
||||
dest_entry->data.resize(AES128_KEY_SIZE);
|
||||
return Decrypt(decrypt_handle, iv, encrypted_key, AES128_KEY_SIZE, dest_entry->data.data(), pid);
|
||||
}
|
||||
|
||||
constexpr size_t ECC233_PUBLIC_KEY_SIZE = 0x3c;
|
||||
ReturnCode IOSC::ImportPublicKey(Handle dest_handle, const u8* public_key, u32 pid)
|
||||
{
|
||||
if (!HasOwnership(dest_handle, pid) || IsDefaultHandle(dest_handle))
|
||||
return IOSC_EACCES;
|
||||
|
||||
auto* dest_entry = &m_key_entries[dest_handle];
|
||||
// TODO: allow other public key subtypes
|
||||
if (dest_entry->type != TYPE_PUBLIC_KEY || dest_entry->subtype != SUBTYPE_ECC233)
|
||||
return IOSC_INVALID_OBJTYPE;
|
||||
|
||||
dest_entry->data.assign(public_key, public_key + ECC233_PUBLIC_KEY_SIZE);
|
||||
return IPC_SUCCESS;
|
||||
}
|
||||
|
||||
ReturnCode IOSC::ComputeSharedKey(Handle dest_handle, Handle private_handle, Handle public_handle,
|
||||
u32 pid)
|
||||
{
|
||||
if (!HasOwnership(dest_handle, pid) || !HasOwnership(private_handle, pid) ||
|
||||
!HasOwnership(public_handle, pid) || IsDefaultHandle(dest_handle))
|
||||
{
|
||||
return IOSC_EACCES;
|
||||
}
|
||||
|
||||
auto* dest_entry = &m_key_entries[dest_handle];
|
||||
const auto* private_entry = &m_key_entries[private_handle];
|
||||
const auto* public_entry = &m_key_entries[public_handle];
|
||||
if (dest_entry->type != TYPE_SECRET_KEY || dest_entry->subtype != SUBTYPE_AES128 ||
|
||||
private_entry->type != TYPE_SECRET_KEY || private_entry->subtype != SUBTYPE_ECC233 ||
|
||||
public_entry->type != TYPE_PUBLIC_KEY || public_entry->subtype != SUBTYPE_ECC233)
|
||||
{
|
||||
return IOSC_INVALID_OBJTYPE;
|
||||
}
|
||||
|
||||
// Calculate the ECC shared secret.
|
||||
std::array<u8, 0x3c> shared_secret;
|
||||
point_mul(shared_secret.data(), private_entry->data.data(), public_entry->data.data());
|
||||
|
||||
std::array<u8, 20> sha1;
|
||||
mbedtls_sha1(shared_secret.data(), shared_secret.size() / 2, sha1.data());
|
||||
|
||||
dest_entry->data.resize(AES128_KEY_SIZE);
|
||||
std::copy_n(sha1.cbegin(), AES128_KEY_SIZE, dest_entry->data.begin());
|
||||
return IPC_SUCCESS;
|
||||
}
|
||||
|
||||
ReturnCode IOSC::DecryptEncrypt(Common::AES::Mode mode, Handle key_handle, u8* iv, const u8* input,
|
||||
size_t size, u8* output, u32 pid) const
|
||||
{
|
||||
if (!HasOwnership(key_handle, pid))
|
||||
return IOSC_EACCES;
|
||||
|
||||
const auto* entry = &m_key_entries[key_handle];
|
||||
if (entry->type != TYPE_SECRET_KEY || entry->subtype != SUBTYPE_AES128)
|
||||
return IOSC_INVALID_OBJTYPE;
|
||||
|
||||
if (entry->data.size() != AES128_KEY_SIZE)
|
||||
return IOSC_FAIL_INTERNAL;
|
||||
|
||||
const std::vector<u8> data =
|
||||
Common::AES::DecryptEncrypt(entry->data.data(), iv, input, size, mode);
|
||||
|
||||
std::memcpy(output, data.data(), data.size());
|
||||
return IPC_SUCCESS;
|
||||
}
|
||||
|
||||
ReturnCode IOSC::Encrypt(Handle key_handle, u8* iv, const u8* input, size_t size, u8* output,
|
||||
u32 pid) const
|
||||
{
|
||||
return DecryptEncrypt(Common::AES::Mode::Encrypt, key_handle, iv, input, size, output, pid);
|
||||
}
|
||||
|
||||
ReturnCode IOSC::Decrypt(Handle key_handle, u8* iv, const u8* input, size_t size, u8* output,
|
||||
u32 pid) const
|
||||
{
|
||||
return DecryptEncrypt(Common::AES::Mode::Decrypt, key_handle, iv, input, size, output, pid);
|
||||
}
|
||||
|
||||
ReturnCode IOSC::GetOwnership(Handle handle, u32* owner) const
|
||||
{
|
||||
if (handle < m_key_entries.size() && m_key_entries[handle].in_use)
|
||||
{
|
||||
*owner = m_key_entries[handle].owner_mask;
|
||||
return IPC_SUCCESS;
|
||||
}
|
||||
return IOSC_EINVAL;
|
||||
}
|
||||
|
||||
ReturnCode IOSC::SetOwnership(Handle handle, u32 new_owner, u32 pid)
|
||||
{
|
||||
if (!HasOwnership(handle, pid))
|
||||
return IOSC_EACCES;
|
||||
|
||||
m_key_entries[handle].owner_mask = new_owner;
|
||||
return IPC_SUCCESS;
|
||||
}
|
||||
|
||||
void IOSC::LoadDefaultEntries()
|
||||
{
|
||||
// TODO: add support for loading and writing to a BootMii / SEEPROM and OTP dump.
|
||||
|
||||
const EcWii& ec = EcWii::GetInstance();
|
||||
|
||||
m_key_entries[HANDLE_CONSOLE_KEY] = {TYPE_SECRET_KEY, SUBTYPE_ECC233,
|
||||
std::vector<u8>(ec.GetNGPriv(), ec.GetNGPriv() + 30), 3};
|
||||
|
||||
// Unimplemented.
|
||||
m_key_entries[HANDLE_CONSOLE_ID] = {TYPE_DATA, SUBTYPE_DATA, std::vector<u8>(4), 0xFFFFFFF};
|
||||
m_key_entries[HANDLE_FS_KEY] = {TYPE_SECRET_KEY, SUBTYPE_AES128, std::vector<u8>(16), 5};
|
||||
m_key_entries[HANDLE_FS_MAC] = {TYPE_SECRET_KEY, SUBTYPE_MAC, std::vector<u8>(20), 5};
|
||||
|
||||
m_key_entries[HANDLE_COMMON_KEY] = {TYPE_SECRET_KEY,
|
||||
SUBTYPE_AES128,
|
||||
{{0xeb, 0xe4, 0x2a, 0x22, 0x5e, 0x85, 0x93, 0xe4, 0x48, 0xd9,
|
||||
0xc5, 0x45, 0x73, 0x81, 0xaa, 0xf7}},
|
||||
3};
|
||||
|
||||
// Unimplemented.
|
||||
m_key_entries[HANDLE_PRNG_KEY] = {TYPE_SECRET_KEY, SUBTYPE_AES128, std::vector<u8>(16), 3};
|
||||
|
||||
m_key_entries[HANDLE_SD_KEY] = {TYPE_SECRET_KEY,
|
||||
SUBTYPE_AES128,
|
||||
{{0xab, 0x01, 0xb9, 0xd8, 0xe1, 0x62, 0x2b, 0x08, 0xaf, 0xba,
|
||||
0xd8, 0x4d, 0xbf, 0xc2, 0xa5, 0x5d}},
|
||||
3};
|
||||
|
||||
// Unimplemented.
|
||||
m_key_entries[HANDLE_BOOT2_VERSION] = {TYPE_DATA, SUBTYPE_VERSION, std::vector<u8>(4), 3};
|
||||
m_key_entries[HANDLE_UNKNOWN_8] = {TYPE_DATA, SUBTYPE_VERSION, std::vector<u8>(4), 3};
|
||||
m_key_entries[HANDLE_UNKNOWN_9] = {TYPE_DATA, SUBTYPE_VERSION, std::vector<u8>(4), 3};
|
||||
m_key_entries[HANDLE_FS_VERSION] = {TYPE_DATA, SUBTYPE_VERSION, std::vector<u8>(4), 3};
|
||||
|
||||
m_key_entries[HANDLE_NEW_COMMON_KEY] = {TYPE_SECRET_KEY,
|
||||
SUBTYPE_AES128,
|
||||
{{0x63, 0xb8, 0x2b, 0xb4, 0xf4, 0x61, 0x4e, 0x2e, 0x13,
|
||||
0xf2, 0xfe, 0xfb, 0xba, 0x4c, 0x9b, 0x7e}},
|
||||
3};
|
||||
}
|
||||
|
||||
IOSC::KeyEntry::KeyEntry() = default;
|
||||
|
||||
IOSC::KeyEntry::KeyEntry(ObjectType type_, ObjectSubType subtype_, std::vector<u8>&& data_,
|
||||
u32 owner_mask_)
|
||||
: in_use(true), type(type_), subtype(subtype_), data(std::move(data_)), owner_mask(owner_mask_)
|
||||
{
|
||||
}
|
||||
|
||||
IOSC::KeyEntries::iterator IOSC::FindFreeEntry()
|
||||
{
|
||||
return std::find_if(m_key_entries.begin(), m_key_entries.end(),
|
||||
[](const auto& entry) { return !entry.in_use; });
|
||||
}
|
||||
|
||||
IOSC::Handle IOSC::GetHandleFromIterator(IOSC::KeyEntries::iterator iterator) const
|
||||
{
|
||||
_assert_(iterator != m_key_entries.end());
|
||||
return static_cast<Handle>(iterator - m_key_entries.begin());
|
||||
}
|
||||
|
||||
bool IOSC::HasOwnership(Handle handle, u32 pid) const
|
||||
{
|
||||
u32 owner_mask;
|
||||
return GetOwnership(handle, &owner_mask) == IPC_SUCCESS && ((1 << pid) & owner_mask) != 0;
|
||||
}
|
||||
|
||||
bool IOSC::IsDefaultHandle(Handle handle) const
|
||||
{
|
||||
constexpr Handle last_default_handle = HANDLE_NEW_COMMON_KEY;
|
||||
return handle <= last_default_handle;
|
||||
}
|
||||
|
||||
void IOSC::DoState(PointerWrap& p)
|
||||
{
|
||||
for (auto& entry : m_key_entries)
|
||||
entry.DoState(p);
|
||||
}
|
||||
|
||||
void IOSC::KeyEntry::DoState(PointerWrap& p)
|
||||
{
|
||||
p.Do(in_use);
|
||||
p.Do(type);
|
||||
p.Do(subtype);
|
||||
p.Do(data);
|
||||
p.Do(owner_mask);
|
||||
}
|
||||
} // namespace HLE
|
||||
} // namespace IOS
|
|
@ -0,0 +1,130 @@
|
|||
// Copyright 2017 Dolphin Emulator Project
|
||||
// Licensed under GPLv2+
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
// Implementation of an IOSC-like API, but much simpler since we only support actual keys.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/Crypto/AES.h"
|
||||
|
||||
class PointerWrap;
|
||||
|
||||
namespace IOS
|
||||
{
|
||||
namespace HLE
|
||||
{
|
||||
enum ReturnCode : s32;
|
||||
|
||||
class IOSC final
|
||||
{
|
||||
public:
|
||||
IOSC();
|
||||
~IOSC();
|
||||
|
||||
using Handle = u32;
|
||||
// We use the same default key handle IDs as the actual IOSC because there are ioctlvs
|
||||
// that accept arbitrary key handles from the PPC, so the IDs must match.
|
||||
// More information on default handles: https://wiibrew.org/wiki/IOS/Syscalls
|
||||
enum DefaultHandle : u32
|
||||
{
|
||||
// ECC-233 private signing key (per-console)
|
||||
HANDLE_CONSOLE_KEY = 0,
|
||||
// Console ID
|
||||
HANDLE_CONSOLE_ID = 1,
|
||||
// NAND FS AES-128 key
|
||||
HANDLE_FS_KEY = 2,
|
||||
// NAND FS HMAC
|
||||
HANDLE_FS_MAC = 3,
|
||||
// Common key
|
||||
HANDLE_COMMON_KEY = 4,
|
||||
// PRNG seed
|
||||
HANDLE_PRNG_KEY = 5,
|
||||
// SD AES-128 key
|
||||
HANDLE_SD_KEY = 6,
|
||||
// boot2 version (writable)
|
||||
HANDLE_BOOT2_VERSION = 7,
|
||||
// Unknown
|
||||
HANDLE_UNKNOWN_8 = 8,
|
||||
// Unknown
|
||||
HANDLE_UNKNOWN_9 = 9,
|
||||
// Filesystem version (writable)
|
||||
HANDLE_FS_VERSION = 10,
|
||||
// New common key (aka Korean common key)
|
||||
HANDLE_NEW_COMMON_KEY = 11,
|
||||
};
|
||||
|
||||
enum ObjectType : u8
|
||||
{
|
||||
TYPE_SECRET_KEY = 0,
|
||||
TYPE_PUBLIC_KEY = 1,
|
||||
TYPE_DATA = 3,
|
||||
};
|
||||
|
||||
enum ObjectSubType : u8
|
||||
{
|
||||
SUBTYPE_AES128 = 0,
|
||||
SUBTYPE_MAC = 1,
|
||||
SUBTYPE_ECC233 = 4,
|
||||
SUBTYPE_DATA = 5,
|
||||
SUBTYPE_VERSION = 6
|
||||
};
|
||||
|
||||
// Create an object for use with the other functions that operate on objects.
|
||||
ReturnCode CreateObject(Handle* handle, ObjectType type, ObjectSubType subtype, u32 pid);
|
||||
// Delete an object. Built-in objects cannot be deleted.
|
||||
ReturnCode DeleteObject(Handle handle, u32 pid);
|
||||
// Import a secret, encrypted key into dest_handle, which will be decrypted using decrypt_handle.
|
||||
ReturnCode ImportSecretKey(Handle dest_handle, Handle decrypt_handle, u8* iv,
|
||||
const u8* encrypted_key, u32 pid);
|
||||
// Import a public key.
|
||||
ReturnCode ImportPublicKey(Handle dest_handle, const u8* public_key, u32 pid);
|
||||
// Compute an AES key from an ECDH shared secret.
|
||||
ReturnCode ComputeSharedKey(Handle dest_handle, Handle private_handle, Handle public_handle,
|
||||
u32 pid);
|
||||
|
||||
// AES encrypt/decrypt.
|
||||
ReturnCode Encrypt(Handle key_handle, u8* iv, const u8* input, size_t size, u8* output,
|
||||
u32 pid) const;
|
||||
ReturnCode Decrypt(Handle key_handle, u8* iv, const u8* input, size_t size, u8* output,
|
||||
u32 pid) const;
|
||||
|
||||
// Ownership
|
||||
ReturnCode GetOwnership(Handle handle, u32* owner) const;
|
||||
ReturnCode SetOwnership(Handle handle, u32 owner, u32 pid);
|
||||
|
||||
void DoState(PointerWrap& p);
|
||||
|
||||
private:
|
||||
struct KeyEntry
|
||||
{
|
||||
KeyEntry();
|
||||
KeyEntry(ObjectType type_, ObjectSubType subtype_, std::vector<u8>&& data_, u32 owner_mask_);
|
||||
void DoState(PointerWrap& p);
|
||||
|
||||
bool in_use = false;
|
||||
ObjectType type;
|
||||
ObjectSubType subtype;
|
||||
std::vector<u8> data;
|
||||
u32 owner_mask = 0;
|
||||
};
|
||||
// The Wii's IOSC is limited to 32 entries, including 12 built-in entries.
|
||||
using KeyEntries = std::array<KeyEntry, 32>;
|
||||
|
||||
void LoadDefaultEntries();
|
||||
KeyEntries::iterator FindFreeEntry();
|
||||
Handle GetHandleFromIterator(KeyEntries::iterator iterator) const;
|
||||
bool HasOwnership(Handle handle, u32 pid) const;
|
||||
bool IsDefaultHandle(Handle handle) const;
|
||||
ReturnCode DecryptEncrypt(Common::AES::Mode mode, Handle key_handle, u8* iv, const u8* input,
|
||||
size_t size, u8* output, u32 pid) const;
|
||||
|
||||
KeyEntries m_key_entries;
|
||||
};
|
||||
} // namespace HLE
|
||||
} // namespace IOS
|
Loading…
Reference in New Issue