mirror of
https://github.com/yuzu-mirror/yuzu.git
synced 2024-11-16 18:07:34 +01:00
Remove files that are not used
This commit is contained in:
parent
d2ad279a32
commit
df5b75694f
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -25,6 +25,9 @@
|
||||
[submodule "unicorn"]
|
||||
path = externals/unicorn
|
||||
url = https://github.com/yuzu-emu/unicorn
|
||||
[submodule "externals/mbedtls"]
|
||||
path = externals/mbedtls
|
||||
url = https://github.com/DarkLordZach/mbedtls
|
||||
[submodule "opus"]
|
||||
path = externals/opus
|
||||
url = https://github.com/ogniK5377/opus.git
|
||||
|
4
externals/CMakeLists.txt
vendored
4
externals/CMakeLists.txt
vendored
@ -35,6 +35,10 @@ set(LZ4_BUNDLED_MODE ON)
|
||||
add_subdirectory(lz4/contrib/cmake_unofficial)
|
||||
target_include_directories(lz4_static INTERFACE ./lz4/lib)
|
||||
|
||||
# mbedtls
|
||||
add_subdirectory(mbedtls)
|
||||
target_include_directories(mbedtls PUBLIC ./mbedtls/include)
|
||||
|
||||
# MicroProfile
|
||||
add_library(microprofile INTERFACE)
|
||||
target_include_directories(microprofile INTERFACE ./microprofile)
|
||||
|
1
externals/mbedtls
vendored
Submodule
1
externals/mbedtls
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit adc11799acb7a688b2715042b81eaf8644442bc4
|
@ -736,6 +736,18 @@ const std::string& GetUserPath(UserPath path, const std::string& new_path) {
|
||||
return paths[path];
|
||||
}
|
||||
|
||||
std::string GetHactoolConfigurationPath() {
|
||||
#ifdef _WIN32
|
||||
char path[MAX_PATH];
|
||||
if (SHGetFolderPathA(NULL, CSIDL_PROFILE, NULL, 0, path) != S_OK)
|
||||
return "";
|
||||
std::string local_path = Common::StringFromFixedZeroTerminatedBuffer(path, MAX_PATH);
|
||||
return local_path + "\\.switch";
|
||||
#else
|
||||
return GetHomeDirectory() + "/.switch";
|
||||
#endif
|
||||
}
|
||||
|
||||
size_t WriteStringToFile(bool text_file, const std::string& str, const char* filename) {
|
||||
return FileUtil::IOFile(filename, text_file ? "w" : "wb").WriteBytes(str.data(), str.size());
|
||||
}
|
||||
|
@ -125,6 +125,8 @@ bool SetCurrentDir(const std::string& directory);
|
||||
// directory. To be used in "multi-user" mode (that is, installed).
|
||||
const std::string& GetUserPath(UserPath path, const std::string& new_path = "");
|
||||
|
||||
std::string GetHactoolConfigurationPath();
|
||||
|
||||
// Returns the path to where the sys file are
|
||||
std::string GetSysDirectory();
|
||||
|
||||
|
@ -212,6 +212,7 @@ void FileBackend::Write(const Entry& entry) {
|
||||
CLS(Input) \
|
||||
CLS(Network) \
|
||||
CLS(Loader) \
|
||||
CLS(Crypto) \
|
||||
CLS(WebService)
|
||||
|
||||
// GetClassName is a macro defined by Windows.h, grrr...
|
||||
|
@ -97,6 +97,7 @@ enum class Class : ClassType {
|
||||
Audio_DSP, ///< The HLE implementation of the DSP
|
||||
Audio_Sink, ///< Emulator audio output backend
|
||||
Loader, ///< ROM loader
|
||||
Crypto, ///< Cryptographic engine/functions
|
||||
Input, ///< Input emulation
|
||||
Network, ///< Network emulation
|
||||
WebService, ///< Interface to yuzu Web Services
|
||||
|
@ -12,6 +12,15 @@ add_library(core STATIC
|
||||
core_timing.h
|
||||
core_timing_util.cpp
|
||||
core_timing_util.h
|
||||
crypto/aes_util.h
|
||||
crypto/encryption_layer.cpp
|
||||
crypto/encryption_layer.h
|
||||
crypto/key_manager.cpp
|
||||
crypto/key_manager.h
|
||||
crypto/ctr_encryption_layer.cpp
|
||||
crypto/ctr_encryption_layer.h
|
||||
file_sys/card_image.cpp
|
||||
file_sys/card_image.h
|
||||
file_sys/content_archive.cpp
|
||||
file_sys/content_archive.h
|
||||
file_sys/control_metadata.cpp
|
||||
@ -317,6 +326,8 @@ add_library(core STATIC
|
||||
loader/nro.h
|
||||
loader/nso.cpp
|
||||
loader/nso.h
|
||||
loader/xci.cpp
|
||||
loader/xci.h
|
||||
memory.cpp
|
||||
memory.h
|
||||
memory_hook.cpp
|
||||
|
6
src/core/crypto/aes_util.cpp
Normal file
6
src/core/crypto/aes_util.cpp
Normal file
@ -0,0 +1,6 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
namespace Crypto {
|
||||
} // namespace Crypto
|
118
src/core/crypto/aes_util.h
Normal file
118
src/core/crypto/aes_util.h
Normal file
@ -0,0 +1,118 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
#include "mbedtls/cipher.h"
|
||||
|
||||
namespace Crypto {
|
||||
|
||||
enum class Mode {
|
||||
CTR = MBEDTLS_CIPHER_AES_128_CTR,
|
||||
ECB = MBEDTLS_CIPHER_AES_128_ECB,
|
||||
XTS = MBEDTLS_CIPHER_AES_128_XTS,
|
||||
};
|
||||
|
||||
enum class Op {
|
||||
ENCRYPT,
|
||||
DECRYPT,
|
||||
};
|
||||
|
||||
template <typename Key, size_t KeySize = sizeof(Key)>
|
||||
struct AESCipher {
|
||||
static_assert(std::is_same_v<Key, std::array<u8, KeySize>>, "Key must be std::array of u8.");
|
||||
static_assert(KeySize == 0x10 || KeySize == 0x20, "KeySize must be 128 or 256.");
|
||||
|
||||
AESCipher(Key key, Mode mode) {
|
||||
mbedtls_cipher_init(&encryption_context);
|
||||
mbedtls_cipher_init(&decryption_context);
|
||||
|
||||
ASSERT_MSG((mbedtls_cipher_setup(
|
||||
&encryption_context,
|
||||
mbedtls_cipher_info_from_type(static_cast<mbedtls_cipher_type_t>(mode))) ||
|
||||
mbedtls_cipher_setup(&decryption_context,
|
||||
mbedtls_cipher_info_from_type(
|
||||
static_cast<mbedtls_cipher_type_t>(mode)))) == 0,
|
||||
"Failed to initialize mbedtls ciphers.");
|
||||
|
||||
ASSERT(
|
||||
!mbedtls_cipher_setkey(&encryption_context, key.data(), KeySize * 8, MBEDTLS_ENCRYPT));
|
||||
ASSERT(
|
||||
!mbedtls_cipher_setkey(&decryption_context, key.data(), KeySize * 8, MBEDTLS_DECRYPT));
|
||||
//"Failed to set key on mbedtls ciphers.");
|
||||
}
|
||||
|
||||
~AESCipher() {
|
||||
mbedtls_cipher_free(&encryption_context);
|
||||
mbedtls_cipher_free(&decryption_context);
|
||||
}
|
||||
|
||||
void SetIV(std::vector<u8> iv) {
|
||||
ASSERT_MSG((mbedtls_cipher_set_iv(&encryption_context, iv.data(), iv.size()) ||
|
||||
mbedtls_cipher_set_iv(&decryption_context, iv.data(), iv.size())) == 0,
|
||||
"Failed to set IV on mbedtls ciphers.");
|
||||
}
|
||||
|
||||
template <typename Source, typename Dest>
|
||||
void Transcode(const Source* src, size_t size, Dest* dest, Op op) {
|
||||
size_t written = 0;
|
||||
|
||||
const auto context = op == Op::ENCRYPT ? &encryption_context : &decryption_context;
|
||||
|
||||
mbedtls_cipher_reset(context);
|
||||
|
||||
if (mbedtls_cipher_get_cipher_mode(context) == MBEDTLS_MODE_XTS) {
|
||||
mbedtls_cipher_update(context, reinterpret_cast<const u8*>(src), size,
|
||||
reinterpret_cast<u8*>(dest), &written);
|
||||
if (written != size)
|
||||
LOG_WARNING(Crypto, "Not all data was decrypted requested={:016X}, actual={:016X}.",
|
||||
size, written);
|
||||
} else {
|
||||
const auto block_size = mbedtls_cipher_get_block_size(context);
|
||||
|
||||
for (size_t offset = 0; offset < size; offset += block_size) {
|
||||
auto length = std::min<size_t>(block_size, size - offset);
|
||||
mbedtls_cipher_update(context, reinterpret_cast<const u8*>(src) + offset, length,
|
||||
reinterpret_cast<u8*>(dest) + offset, &written);
|
||||
if (written != length)
|
||||
LOG_WARNING(Crypto,
|
||||
"Not all data was decrypted requested={:016X}, actual={:016X}.",
|
||||
length, written);
|
||||
}
|
||||
}
|
||||
|
||||
mbedtls_cipher_finish(context, nullptr, nullptr);
|
||||
}
|
||||
|
||||
template <typename Source, typename Dest>
|
||||
void XTSTranscode(const Source* src, size_t size, Dest* dest, size_t sector_id,
|
||||
size_t sector_size, Op op) {
|
||||
if (size % sector_size > 0) {
|
||||
LOG_CRITICAL(Crypto, "Data size must be a multiple of sector size.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < size; i += sector_size) {
|
||||
SetIV(CalculateNintendoTweak(sector_id++));
|
||||
Transcode<u8, u8>(reinterpret_cast<const u8*>(src) + i, sector_size,
|
||||
reinterpret_cast<u8*>(dest) + i, op);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
mbedtls_cipher_context_t encryption_context;
|
||||
mbedtls_cipher_context_t decryption_context;
|
||||
|
||||
static std::vector<u8> CalculateNintendoTweak(size_t sector_id) {
|
||||
std::vector<u8> out(0x10);
|
||||
for (size_t i = 0xF; i <= 0xF; --i) {
|
||||
out[i] = sector_id & 0xFF;
|
||||
sector_id >>= 8;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
} // namespace Crypto
|
56
src/core/crypto/ctr_encryption_layer.cpp
Normal file
56
src/core/crypto/ctr_encryption_layer.cpp
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "core/crypto/ctr_encryption_layer.h"
|
||||
|
||||
namespace Crypto {
|
||||
CTREncryptionLayer::CTREncryptionLayer(FileSys::VirtualFile base_, Key128 key_, size_t base_offset)
|
||||
: EncryptionLayer(std::move(base_)), base_offset(base_offset), cipher(key_, Mode::CTR),
|
||||
iv(16, 0) {}
|
||||
|
||||
size_t CTREncryptionLayer::Read(u8* data, size_t length, size_t offset) const {
|
||||
if (length == 0)
|
||||
return 0;
|
||||
|
||||
const auto sector_offset = offset & 0xF;
|
||||
if (sector_offset == 0) {
|
||||
UpdateIV(base_offset + offset);
|
||||
std::vector<u8> raw = base->ReadBytes(length, offset);
|
||||
if (raw.size() != length)
|
||||
return Read(data, raw.size(), offset);
|
||||
cipher.Transcode(raw.data(), length, data, Op::DECRYPT);
|
||||
return length;
|
||||
}
|
||||
|
||||
// offset does not fall on block boundary (0x10)
|
||||
std::vector<u8> block = base->ReadBytes(0x10, offset - sector_offset);
|
||||
UpdateIV(base_offset + offset - sector_offset);
|
||||
cipher.Transcode(block.data(), block.size(), block.data(), Op::DECRYPT);
|
||||
size_t read = 0x10 - sector_offset;
|
||||
|
||||
if (length + sector_offset < 0x10) {
|
||||
memcpy_s(data, length, block.data() + sector_offset, std::min<u64>(length, read));
|
||||
return read;
|
||||
}
|
||||
|
||||
memcpy_s(data, length, block.data() + sector_offset, read);
|
||||
return read + Read(data + read, length - read, offset + read);
|
||||
}
|
||||
|
||||
void CTREncryptionLayer::SetIV(std::vector<u8> iv_) {
|
||||
const auto length = std::min<size_t>(iv_.size(), iv.size());
|
||||
for (size_t i = 0; i < length; ++i)
|
||||
iv[i] = iv_[i];
|
||||
}
|
||||
|
||||
void CTREncryptionLayer::UpdateIV(size_t offset) const {
|
||||
offset >>= 4;
|
||||
for (size_t i = 0; i < 8; ++i) {
|
||||
iv[16 - i - 1] = offset & 0xFF;
|
||||
offset >>= 8;
|
||||
}
|
||||
cipher.SetIV(iv);
|
||||
}
|
||||
} // namespace Crypto
|
31
src/core/crypto/ctr_encryption_layer.h
Normal file
31
src/core/crypto/ctr_encryption_layer.h
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "aes_util.h"
|
||||
#include "encryption_layer.h"
|
||||
#include "key_manager.h"
|
||||
|
||||
namespace Crypto {
|
||||
|
||||
// Sits on top of a VirtualFile and provides CTR-mode AES decription.
|
||||
struct CTREncryptionLayer : public EncryptionLayer {
|
||||
CTREncryptionLayer(FileSys::VirtualFile base, Key128 key, size_t base_offset);
|
||||
|
||||
size_t Read(u8* data, size_t length, size_t offset) const override;
|
||||
|
||||
void SetIV(std::vector<u8> iv);
|
||||
|
||||
private:
|
||||
size_t base_offset;
|
||||
|
||||
// Must be mutable as operations modify cipher contexts.
|
||||
mutable AESCipher<Key128> cipher;
|
||||
mutable std::vector<u8> iv;
|
||||
|
||||
void UpdateIV(size_t offset) const;
|
||||
};
|
||||
|
||||
} // namespace Crypto
|
42
src/core/crypto/encryption_layer.cpp
Normal file
42
src/core/crypto/encryption_layer.cpp
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "core/crypto/encryption_layer.h"
|
||||
|
||||
namespace Crypto {
|
||||
|
||||
EncryptionLayer::EncryptionLayer(FileSys::VirtualFile base_) : base(std::move(base_)) {}
|
||||
|
||||
std::string EncryptionLayer::GetName() const {
|
||||
return base->GetName();
|
||||
}
|
||||
|
||||
size_t EncryptionLayer::GetSize() const {
|
||||
return base->GetSize();
|
||||
}
|
||||
|
||||
bool EncryptionLayer::Resize(size_t new_size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::shared_ptr<FileSys::VfsDirectory> EncryptionLayer::GetContainingDirectory() const {
|
||||
return base->GetContainingDirectory();
|
||||
}
|
||||
|
||||
bool EncryptionLayer::IsWritable() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EncryptionLayer::IsReadable() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t EncryptionLayer::Write(const u8* data, size_t length, size_t offset) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool EncryptionLayer::Rename(std::string_view name) {
|
||||
return base->Rename(name);
|
||||
}
|
||||
} // namespace Crypto
|
30
src/core/crypto/encryption_layer.h
Normal file
30
src/core/crypto/encryption_layer.h
Normal file
@ -0,0 +1,30 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace Crypto {
|
||||
|
||||
// Basically non-functional class that implements all of the methods that are irrelevant to an
|
||||
// EncryptionLayer. Reduces duplicate code.
|
||||
struct EncryptionLayer : public FileSys::VfsFile {
|
||||
explicit EncryptionLayer(FileSys::VirtualFile base);
|
||||
|
||||
size_t Read(u8* data, size_t length, size_t offset) const override = 0;
|
||||
|
||||
std::string GetName() const override;
|
||||
size_t GetSize() const override;
|
||||
bool Resize(size_t new_size) override;
|
||||
std::shared_ptr<FileSys::VfsDirectory> GetContainingDirectory() const override;
|
||||
bool IsWritable() const override;
|
||||
bool IsReadable() const override;
|
||||
size_t Write(const u8* data, size_t length, size_t offset) override;
|
||||
bool Rename(std::string_view name) override;
|
||||
|
||||
protected:
|
||||
FileSys::VirtualFile base;
|
||||
};
|
||||
|
||||
} // namespace Crypto
|
410
src/core/crypto/key_manager.cpp
Normal file
410
src/core/crypto/key_manager.cpp
Normal file
@ -0,0 +1,410 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <fstream>
|
||||
#include <locale>
|
||||
#include <sstream>
|
||||
#include "common/assert.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "mbedtls/sha256.h"
|
||||
|
||||
namespace Crypto {
|
||||
KeyManager keys = {};
|
||||
|
||||
std::unordered_map<KeyIndex<S128KeyType>, SHA256Hash> KeyManager::s128_hash_prod = {
|
||||
{{S128KeyType::MASTER, 0, 0},
|
||||
"0EE359BE3C864BB0782E1D70A718A0342C551EED28C369754F9C4F691BECF7CA"_array32},
|
||||
{{S128KeyType::MASTER, 1, 0},
|
||||
"4FE707B7E4ABDAF727C894AAF13B1351BFE2AC90D875F73B2E20FA94B9CC661E"_array32},
|
||||
{{S128KeyType::MASTER, 2, 0},
|
||||
"79277C0237A2252EC3DFAC1F7C359C2B3D121E9DB15BB9AB4C2B4408D2F3AE09"_array32},
|
||||
{{S128KeyType::MASTER, 3, 0},
|
||||
"4F36C565D13325F65EE134073C6A578FFCB0008E02D69400836844EAB7432754"_array32},
|
||||
{{S128KeyType::MASTER, 4, 0},
|
||||
"75ff1d95d26113550ee6fcc20acb58e97edeb3a2ff52543ed5aec63bdcc3da50"_array32},
|
||||
{{S128KeyType::PACKAGE1, 0, 0},
|
||||
"4543CD1B7CAD7EE0466A3DE2086A0EF923805DCEA6C741541CDDB14F54F97B40"_array32},
|
||||
{{S128KeyType::PACKAGE1, 1, 0},
|
||||
"4A11DA019D26470C9B805F1721364830DC0096DD66EAC453B0D14455E5AF5CF8"_array32},
|
||||
{{S128KeyType::PACKAGE1, 2, 0},
|
||||
"CCA867360B3318246FBF0B8A86473176ED486DFE229772B941A02E84D50A3155"_array32},
|
||||
{{S128KeyType::PACKAGE1, 3, 0},
|
||||
"E65C383CDF526DFFAA77682868EBFA9535EE60D8075C961BBC1EDE5FBF7E3C5F"_array32},
|
||||
{{S128KeyType::PACKAGE1, 4, 0},
|
||||
"28ae73d6ae8f7206fca549e27097714e599df1208e57099416ff429b71370162"_array32},
|
||||
{{S128KeyType::PACKAGE2, 0, 0},
|
||||
"94D6F38B9D0456644E21DFF4707D092B70179B82D1AA2F5B6A76B8F9ED948264"_array32},
|
||||
{{S128KeyType::PACKAGE2, 1, 0},
|
||||
"7794F24FA879D378FEFDC8776B949B88AD89386410BE9025D463C619F1530509"_array32},
|
||||
{{S128KeyType::PACKAGE2, 2, 0},
|
||||
"5304BDDE6AC8E462961B5DB6E328B1816D245D36D6574BB78938B74D4418AF35"_array32},
|
||||
{{S128KeyType::PACKAGE2, 3, 0},
|
||||
"BE1E52C4345A979DDD4924375B91C902052C2E1CF8FBF2FAA42E8F26D5125B60"_array32},
|
||||
{{S128KeyType::PACKAGE2, 4, 0},
|
||||
"631b45d349ab8f76a050fe59512966fb8dbaf0755ef5b6903048bf036cfa611e"_array32},
|
||||
{{S128KeyType::TITLEKEK, 0, 0},
|
||||
"C2FA30CAC6AE1680466CB54750C24550E8652B3B6F38C30B49DADF067B5935E9"_array32},
|
||||
{{S128KeyType::TITLEKEK, 1, 0},
|
||||
"0D6B8F3746AD910D36438A859C11E8BE4310112425D63751D09B5043B87DE598"_array32},
|
||||
{{S128KeyType::TITLEKEK, 2, 0},
|
||||
"D09E18D3DB6BC7393536896F728528736FBEFCDD15C09D9D612FDE5C7BDCD821"_array32},
|
||||
{{S128KeyType::TITLEKEK, 3, 0},
|
||||
"47C6F9F7E99BB1F56DCDC93CDBD340EA82DCCD74DD8F3535ADA20ECF79D438ED"_array32},
|
||||
{{S128KeyType::TITLEKEK, 4, 0},
|
||||
"128610de8424cb29e08f9ee9a81c9e6ffd3c6662854aad0c8f937e0bcedc4d88"_array32},
|
||||
{{S128KeyType::ETICKET_RSA_KEK, 0, 0},
|
||||
"46cccf288286e31c931379de9efa288c95c9a15e40b00a4c563a8be244ece515"_array32},
|
||||
{{S128KeyType::KEY_AREA, 0, static_cast<u64>(KeyAreaKeyType::Application)},
|
||||
"592957F44FE5DB5EC6B095F568910E31A226D3B7FE42D64CFB9CE4051E90AEB6"_array32},
|
||||
{{S128KeyType::KEY_AREA, 1, static_cast<u64>(KeyAreaKeyType::Application)},
|
||||
"C2252A0FBF9D339ABC3D681351D00452F926E7CA0C6CA85F659078DE3FA647F3"_array32},
|
||||
{{S128KeyType::KEY_AREA, 2, static_cast<u64>(KeyAreaKeyType::Application)},
|
||||
"7C7722824B2F7C4938C40F3EA93E16CB69D3285EB133490EF8ECCD2C4B52DF41"_array32},
|
||||
{{S128KeyType::KEY_AREA, 3, static_cast<u64>(KeyAreaKeyType::Application)},
|
||||
"AFBB8EBFB2094F1CF71E330826AE06D64414FCA128C464618DF30EED92E62BE6"_array32},
|
||||
{{S128KeyType::KEY_AREA, 4, static_cast<u64>(KeyAreaKeyType::Application)},
|
||||
"5dc10eb81918da3f2fa90f69c8542511963656cfb31fb7c779581df8faf1f2f5"_array32},
|
||||
{{S128KeyType::KEY_AREA, 0, static_cast<u64>(KeyAreaKeyType::Ocean)},
|
||||
"AA2C65F0E27F730807A13F2ED5B99BE5183165B87C50B6ED48F5CAC2840687EB"_array32},
|
||||
{{S128KeyType::KEY_AREA, 1, static_cast<u64>(KeyAreaKeyType::Ocean)},
|
||||
"860185F2313A14F7006A029CB21A52750E7718C1E94FFB98C0AE2207D1A60165"_array32},
|
||||
{{S128KeyType::KEY_AREA, 2, static_cast<u64>(KeyAreaKeyType::Ocean)},
|
||||
"7283FB1EFBD42438DADF363FDB776ED355C98737A2AAE75D0E9283CE1C12A2E4"_array32},
|
||||
{{S128KeyType::KEY_AREA, 3, static_cast<u64>(KeyAreaKeyType::Ocean)},
|
||||
"9881C2D3AB70B14C8AA12016FC73ADAD93C6AD9FB59A9ECAD312B6F89E2413EC"_array32},
|
||||
{{S128KeyType::KEY_AREA, 4, static_cast<u64>(KeyAreaKeyType::Ocean)},
|
||||
"eaa6a8d242b89e174928fa9549a0f66ec1562e2576fac896f438a2b3c1fb6005"_array32},
|
||||
{{S128KeyType::KEY_AREA, 0, static_cast<u64>(KeyAreaKeyType::System)},
|
||||
"194CF6BD14554DA8D457E14CBFE04E55C8FB8CA52E0AFB3D7CB7084AE435B801"_array32},
|
||||
{{S128KeyType::KEY_AREA, 1, static_cast<u64>(KeyAreaKeyType::System)},
|
||||
"CE1DB7BB6E5962384889DB7A396AFD614F82F69DC38A33D2DEAF47F3E4B964B7"_array32},
|
||||
{{S128KeyType::KEY_AREA, 2, static_cast<u64>(KeyAreaKeyType::System)},
|
||||
"42238DE5685DEF4FDE7BE42C0097CEB92447006386D6B5D5AAA2C9AFD2E28422"_array32},
|
||||
{{S128KeyType::KEY_AREA, 3, static_cast<u64>(KeyAreaKeyType::System)},
|
||||
"1F6847F268E9D9C5D1AD4D7E226A63B833BF02071446957A962EF065521879C1"_array32},
|
||||
{{S128KeyType::KEY_AREA, 4, static_cast<u64>(KeyAreaKeyType::System)},
|
||||
"644007f9913c3602399d4d75cc34faeb7f1faad18b23e34187b16fdc45f4980f"_array32},
|
||||
};
|
||||
|
||||
std::unordered_map<KeyIndex<S256KeyType>, SHA256Hash> KeyManager::s256_hash_prod = {
|
||||
{{S256KeyType::HEADER, 0, 0},
|
||||
"8E03DE24818D96CE4F2A09B43AF979E679974F7570713A61EED8B314864A11D5"_array32},
|
||||
{{S256KeyType::SD_SAVE, 0, 0},
|
||||
"13020ee72d0f8b8f9112dc738b829fdb017102499a7c2259b52aeefc0a273f5c"_array32},
|
||||
{{S256KeyType::SD_NCA, 0, 0},
|
||||
"8a1c05b4f88bae5b04d77f632e6acfc8893c4a05fd701f53585daafc996b532a"_array32},
|
||||
};
|
||||
|
||||
// TODO(DarkLordZach): Find missing hashes for dev keys.
|
||||
|
||||
std::unordered_map<KeyIndex<S128KeyType>, SHA256Hash> KeyManager::s128_hash_dev = {
|
||||
{{S128KeyType::MASTER, 0, 0},
|
||||
"779dd8b533a2fb670f27b308cb8d0151c4a107568b817429172b7f80aa592c25"_array32},
|
||||
{{S128KeyType::MASTER, 1, 0},
|
||||
"0175c8bc49771576f75527be719098db4ebaf77707206749415663aa3a9ea9cc"_array32},
|
||||
{{S128KeyType::MASTER, 2, 0},
|
||||
"4f0b4d724e5a8787268157c7ce0767c26d2e2021832aa7020f306d6e260eea42"_array32},
|
||||
{{S128KeyType::MASTER, 3, 0},
|
||||
"7b5a29586c1f84f66fbfabb94518fc45408bb8e5445253d063dda7cfef2a818c"_array32},
|
||||
{{S128KeyType::MASTER, 4, 0},
|
||||
"87a61dbb05a8755de7fe069562aab38ebfb266c9eb835f09fa62dacc89c98341"_array32},
|
||||
{{S128KeyType::PACKAGE1, 0, 0},
|
||||
"166510bc63ae50391ebe4ee4ff90ca31cd0e2dd0ff6be839a2f573ec146cc23a"_array32},
|
||||
{{S128KeyType::PACKAGE1, 1, 0},
|
||||
"f74cd01b86743139c920ec54a8116c669eea805a0be1583e13fc5bc8de68645b"_array32},
|
||||
{{S128KeyType::PACKAGE1, 2, 0},
|
||||
"d0cdecd513bb6aa3d9dc6244c977dc8a5a7ea157d0a8747d79e7581146e1f768"_array32},
|
||||
{{S128KeyType::PACKAGE1, 3, 0},
|
||||
"aa39394d626b3b79f5b7ccc07378b5996b6d09bf0eb6771b0b40c9077fbfde8c"_array32},
|
||||
{{S128KeyType::PACKAGE1, 4, 0},
|
||||
"8f4754b8988c0e673fc2bbea0534cdd6075c815c9270754ae980aef3e4f0a508"_array32},
|
||||
{{S128KeyType::PACKAGE2, 0, 0},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::PACKAGE2, 1, 0},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::PACKAGE2, 2, 0},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::PACKAGE2, 3, 0},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::PACKAGE2, 4, 0},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::TITLEKEK, 0, 0},
|
||||
"C2FA30CAC6AE1680466CB54750C24550E8652B3B6F38C30B49DADF067B5935E9"_array32},
|
||||
{{S128KeyType::TITLEKEK, 1, 0},
|
||||
"0D6B8F3746AD910D36438A859C11E8BE4310112425D63751D09B5043B87DE598"_array32},
|
||||
{{S128KeyType::TITLEKEK, 2, 0},
|
||||
"D09E18D3DB6BC7393536896F728528736FBEFCDD15C09D9D612FDE5C7BDCD821"_array32},
|
||||
{{S128KeyType::TITLEKEK, 3, 0},
|
||||
"47C6F9F7E99BB1F56DCDC93CDBD340EA82DCCD74DD8F3535ADA20ECF79D438ED"_array32},
|
||||
{{S128KeyType::TITLEKEK, 4, 0},
|
||||
"128610de8424cb29e08f9ee9a81c9e6ffd3c6662854aad0c8f937e0bcedc4d88"_array32},
|
||||
{{S128KeyType::ETICKET_RSA_KEK, 0, 0},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 0, static_cast<u64>(KeyAreaKeyType::Application)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 1, static_cast<u64>(KeyAreaKeyType::Application)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 2, static_cast<u64>(KeyAreaKeyType::Application)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 3, static_cast<u64>(KeyAreaKeyType::Application)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 4, static_cast<u64>(KeyAreaKeyType::Application)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 0, static_cast<u64>(KeyAreaKeyType::Ocean)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 1, static_cast<u64>(KeyAreaKeyType::Ocean)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 2, static_cast<u64>(KeyAreaKeyType::Ocean)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 3, static_cast<u64>(KeyAreaKeyType::Ocean)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 4, static_cast<u64>(KeyAreaKeyType::Ocean)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 0, static_cast<u64>(KeyAreaKeyType::System)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 1, static_cast<u64>(KeyAreaKeyType::System)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 2, static_cast<u64>(KeyAreaKeyType::System)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 3, static_cast<u64>(KeyAreaKeyType::System)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S128KeyType::KEY_AREA, 4, static_cast<u64>(KeyAreaKeyType::System)},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
};
|
||||
|
||||
std::unordered_map<KeyIndex<S256KeyType>, SHA256Hash> KeyManager::s256_hash_dev = {
|
||||
{{S256KeyType::HEADER, 0, 0},
|
||||
"ecde86a76e37ac4fd7591d3aa55c00cc77d8595fc27968052ec18a177d939060"_array32},
|
||||
{{S256KeyType::SD_SAVE, 0, 0},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
{{S256KeyType::SD_NCA, 0, 0},
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"_array32},
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, KeyIndex<S128KeyType>> KeyManager::s128_file_id = {
|
||||
{"master_key_00", {S128KeyType::MASTER, 0, 0}},
|
||||
{"master_key_01", {S128KeyType::MASTER, 1, 0}},
|
||||
{"master_key_02", {S128KeyType::MASTER, 2, 0}},
|
||||
{"master_key_03", {S128KeyType::MASTER, 3, 0}},
|
||||
{"master_key_04", {S128KeyType::MASTER, 4, 0}},
|
||||
{"package1_key_00", {S128KeyType::PACKAGE1, 0, 0}},
|
||||
{"package1_key_01", {S128KeyType::PACKAGE1, 1, 0}},
|
||||
{"package1_key_02", {S128KeyType::PACKAGE1, 2, 0}},
|
||||
{"package1_key_03", {S128KeyType::PACKAGE1, 3, 0}},
|
||||
{"package1_key_04", {S128KeyType::PACKAGE1, 4, 0}},
|
||||
{"package2_key_00", {S128KeyType::PACKAGE2, 0, 0}},
|
||||
{"package2_key_01", {S128KeyType::PACKAGE2, 1, 0}},
|
||||
{"package2_key_02", {S128KeyType::PACKAGE2, 2, 0}},
|
||||
{"package2_key_03", {S128KeyType::PACKAGE2, 3, 0}},
|
||||
{"package2_key_04", {S128KeyType::PACKAGE2, 4, 0}},
|
||||
{"titlekek_00", {S128KeyType::TITLEKEK, 0, 0}},
|
||||
{"titlekek_01", {S128KeyType::TITLEKEK, 1, 0}},
|
||||
{"titlekek_02", {S128KeyType::TITLEKEK, 2, 0}},
|
||||
{"titlekek_03", {S128KeyType::TITLEKEK, 3, 0}},
|
||||
{"titlekek_04", {S128KeyType::TITLEKEK, 4, 0}},
|
||||
{"eticket_rsa_kek", {S128KeyType::ETICKET_RSA_KEK, 0, 0}},
|
||||
{"key_area_key_application_00",
|
||||
{S128KeyType::KEY_AREA, 0, static_cast<u64>(KeyAreaKeyType::Application)}},
|
||||
{"key_area_key_application_01",
|
||||
{S128KeyType::KEY_AREA, 1, static_cast<u64>(KeyAreaKeyType::Application)}},
|
||||
{"key_area_key_application_02",
|
||||
{S128KeyType::KEY_AREA, 2, static_cast<u64>(KeyAreaKeyType::Application)}},
|
||||
{"key_area_key_application_03",
|
||||
{S128KeyType::KEY_AREA, 3, static_cast<u64>(KeyAreaKeyType::Application)}},
|
||||
{"key_area_key_application_04",
|
||||
{S128KeyType::KEY_AREA, 4, static_cast<u64>(KeyAreaKeyType::Application)}},
|
||||
{"key_area_key_ocean_00", {S128KeyType::KEY_AREA, 0, static_cast<u64>(KeyAreaKeyType::Ocean)}},
|
||||
{"key_area_key_ocean_01", {S128KeyType::KEY_AREA, 1, static_cast<u64>(KeyAreaKeyType::Ocean)}},
|
||||
{"key_area_key_ocean_02", {S128KeyType::KEY_AREA, 2, static_cast<u64>(KeyAreaKeyType::Ocean)}},
|
||||
{"key_area_key_ocean_03", {S128KeyType::KEY_AREA, 3, static_cast<u64>(KeyAreaKeyType::Ocean)}},
|
||||
{"key_area_key_ocean_04", {S128KeyType::KEY_AREA, 4, static_cast<u64>(KeyAreaKeyType::Ocean)}},
|
||||
{"key_area_key_system_00",
|
||||
{S128KeyType::KEY_AREA, 0, static_cast<u64>(KeyAreaKeyType::System)}},
|
||||
{"key_area_key_system_01",
|
||||
{S128KeyType::KEY_AREA, 1, static_cast<u64>(KeyAreaKeyType::System)}},
|
||||
{"key_area_key_system_02",
|
||||
{S128KeyType::KEY_AREA, 2, static_cast<u64>(KeyAreaKeyType::System)}},
|
||||
{"key_area_key_system_03",
|
||||
{S128KeyType::KEY_AREA, 3, static_cast<u64>(KeyAreaKeyType::System)}},
|
||||
{"key_area_key_system_04",
|
||||
{S128KeyType::KEY_AREA, 4, static_cast<u64>(KeyAreaKeyType::System)}},
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, KeyIndex<S256KeyType>> KeyManager::s256_file_id = {
|
||||
{"header_key", {S256KeyType::HEADER, 0, 0}},
|
||||
{"sd_card_save_key", {S256KeyType::SD_SAVE, 0, 0}},
|
||||
{"sd_card_nca_key", {S256KeyType::SD_NCA, 0, 0}},
|
||||
};
|
||||
|
||||
static u8 ToHexNibble(char c1) {
|
||||
if (c1 >= 65 && c1 <= 70)
|
||||
return c1 - 55;
|
||||
if (c1 >= 97 && c1 <= 102)
|
||||
return c1 - 87;
|
||||
if (c1 >= 48 && c1 <= 57)
|
||||
return c1 - 48;
|
||||
throw std::logic_error("Invalid hex digit");
|
||||
}
|
||||
|
||||
template <size_t Size>
|
||||
static std::array<u8, Size> HexStringToArray(std::string_view str) {
|
||||
std::array<u8, Size> out{};
|
||||
for (size_t i = 0; i < 2 * Size; i += 2) {
|
||||
auto d1 = str[i];
|
||||
auto d2 = str[i + 1];
|
||||
out[i / 2] = (ToHexNibble(d1) << 4) | ToHexNibble(d2);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::array<u8, 16> operator""_array16(const char* str, size_t len) {
|
||||
if (len != 32)
|
||||
throw std::logic_error("Not of correct size.");
|
||||
return HexStringToArray<16>(str);
|
||||
}
|
||||
|
||||
std::array<u8, 32> operator""_array32(const char* str, size_t len) {
|
||||
if (len != 64)
|
||||
throw std::logic_error("Not of correct size.");
|
||||
return HexStringToArray<32>(str);
|
||||
}
|
||||
|
||||
void KeyManager::SetValidationMode(bool dev) {
|
||||
dev_mode = dev;
|
||||
}
|
||||
|
||||
void KeyManager::LoadFromFile(std::string_view filename_, bool is_title_keys) {
|
||||
const auto filename = std::string(filename_);
|
||||
std::ifstream file(filename);
|
||||
if (!file.is_open())
|
||||
return;
|
||||
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
std::vector<std::string> out;
|
||||
std::stringstream stream(line);
|
||||
std::string item;
|
||||
while (std::getline(stream, item, '='))
|
||||
out.push_back(std::move(item));
|
||||
|
||||
if (out.size() != 2)
|
||||
continue;
|
||||
|
||||
out[0].erase(std::remove(out[0].begin(), out[0].end(), ' '), out[0].end());
|
||||
out[1].erase(std::remove(out[1].begin(), out[1].end(), ' '), out[1].end());
|
||||
|
||||
if (is_title_keys) {
|
||||
auto rights_id_raw = HexStringToArray<16>(out[0]);
|
||||
u128 rights_id = *reinterpret_cast<std::array<u64, 2>*>(&rights_id_raw);
|
||||
Key128 key = HexStringToArray<16>(out[1]);
|
||||
SetKey(S128KeyType::TITLEKEY, key, rights_id[1], rights_id[0]);
|
||||
} else {
|
||||
std::transform(out[0].begin(), out[0].end(), out[0].begin(), ::tolower);
|
||||
if (s128_file_id.find(out[0]) != s128_file_id.end()) {
|
||||
const auto index = s128_file_id[out[0]];
|
||||
Key128 key = HexStringToArray<16>(out[1]);
|
||||
SetKey(index.type, key, index.field1, index.field2);
|
||||
} else if (s256_file_id.find(out[0]) != s256_file_id.end()) {
|
||||
const auto index = s256_file_id[out[0]];
|
||||
Key256 key = HexStringToArray<32>(out[1]);
|
||||
SetKey(index.type, key, index.field1, index.field2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool KeyManager::HasKey(S128KeyType id, u64 field1, u64 field2) {
|
||||
return s128_keys.find({id, field1, field2}) != s128_keys.end();
|
||||
}
|
||||
|
||||
bool KeyManager::HasKey(S256KeyType id, u64 field1, u64 field2) {
|
||||
return s256_keys.find({id, field1, field2}) != s256_keys.end();
|
||||
}
|
||||
|
||||
Key128 KeyManager::GetKey(S128KeyType id, u64 field1, u64 field2) {
|
||||
if (!HasKey(id, field1, field2))
|
||||
return {};
|
||||
return s128_keys[{id, field1, field2}];
|
||||
}
|
||||
|
||||
Key256 KeyManager::GetKey(S256KeyType id, u64 field1, u64 field2) {
|
||||
if (!HasKey(id, field1, field2))
|
||||
return {};
|
||||
return s256_keys[{id, field1, field2}];
|
||||
}
|
||||
|
||||
void KeyManager::SetKey(S128KeyType id, Key128 key, u64 field1, u64 field2) {
|
||||
s128_keys[{id, field1, field2}] = key;
|
||||
}
|
||||
|
||||
void KeyManager::SetKey(S256KeyType id, Key256 key, u64 field1, u64 field2) {
|
||||
s256_keys[{id, field1, field2}] = key;
|
||||
}
|
||||
|
||||
bool KeyManager::ValidateKey(S128KeyType key, u64 field1, u64 field2) {
|
||||
auto& hash = dev_mode ? s128_hash_dev : s128_hash_prod;
|
||||
|
||||
KeyIndex<S128KeyType> id = {key, field1, field2};
|
||||
if (key == S128KeyType::SD_SEED || key == S128KeyType::TITLEKEY ||
|
||||
hash.find(id) == hash.end()) {
|
||||
LOG_WARNING(Crypto, "Could not validate [{}]", id.DebugInfo());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!HasKey(key, field1, field2)) {
|
||||
LOG_CRITICAL(Crypto,
|
||||
"System has requested validation of [{}], but user has not added it. Add this "
|
||||
"key to use functionality.",
|
||||
id.DebugInfo());
|
||||
return false;
|
||||
}
|
||||
|
||||
SHA256Hash key_hash{};
|
||||
const auto a_key = GetKey(key, field1, field2);
|
||||
mbedtls_sha256(a_key.data(), a_key.size(), key_hash.data(), 0);
|
||||
if (key_hash != hash[id]) {
|
||||
LOG_CRITICAL(Crypto,
|
||||
"The hash of the provided key for [{}] does not match the one on file. This "
|
||||
"means you probably have an incorrect key. If you believe this to be in "
|
||||
"error, contact the yuzu devs.",
|
||||
id.DebugInfo());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool KeyManager::ValidateKey(S256KeyType key, u64 field1, u64 field2) {
|
||||
auto& hash = dev_mode ? s256_hash_dev : s256_hash_prod;
|
||||
|
||||
KeyIndex<S256KeyType> id = {key, field1, field2};
|
||||
if (hash.find(id) == hash.end()) {
|
||||
LOG_ERROR(Crypto, "Could not validate [{}]", id.DebugInfo());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!HasKey(key, field1, field2)) {
|
||||
LOG_ERROR(Crypto,
|
||||
"System has requested validation of [{}], but user has not added it. Add this "
|
||||
"key to use functionality.",
|
||||
id.DebugInfo());
|
||||
return false;
|
||||
}
|
||||
|
||||
SHA256Hash key_hash{};
|
||||
const auto a_key = GetKey(key, field1, field2);
|
||||
mbedtls_sha256(a_key.data(), a_key.size(), key_hash.data(), 0);
|
||||
if (key_hash != hash[id]) {
|
||||
LOG_CRITICAL(Crypto,
|
||||
"The hash of the provided key for [{}] does not match the one on file. This "
|
||||
"means you probably have an incorrect key. If you believe this to be in "
|
||||
"error, contact the yuzu devs.",
|
||||
id.DebugInfo());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace Crypto
|
116
src/core/crypto/key_manager.h
Normal file
116
src/core/crypto/key_manager.h
Normal file
@ -0,0 +1,116 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Crypto {
|
||||
|
||||
typedef std::array<u8, 0x10> Key128;
|
||||
typedef std::array<u8, 0x20> Key256;
|
||||
typedef std::array<u8, 0x20> SHA256Hash;
|
||||
|
||||
static_assert(sizeof(Key128) == 16, "Key128 must be 128 bytes big.");
|
||||
static_assert(sizeof(Key256) == 32, "Key128 must be 128 bytes big.");
|
||||
|
||||
enum class S256KeyType : u64 {
|
||||
HEADER, //
|
||||
SD_SAVE, //
|
||||
SD_NCA, //
|
||||
};
|
||||
|
||||
enum class S128KeyType : u64 {
|
||||
MASTER, // f1=crypto revision
|
||||
PACKAGE1, // f1=crypto revision
|
||||
PACKAGE2, // f1=crypto revision
|
||||
TITLEKEK, // f1=crypto revision
|
||||
ETICKET_RSA_KEK, //
|
||||
KEY_AREA, // f1=crypto revision f2=type {app, ocean, system}
|
||||
SD_SEED, //
|
||||
TITLEKEY, // f1=rights id LSB f2=rights id MSB
|
||||
};
|
||||
|
||||
enum class KeyAreaKeyType : u8 {
|
||||
Application,
|
||||
Ocean,
|
||||
System,
|
||||
};
|
||||
|
||||
template <typename KeyType>
|
||||
struct KeyIndex {
|
||||
KeyType type;
|
||||
u64 field1;
|
||||
u64 field2;
|
||||
|
||||
std::string DebugInfo() {
|
||||
u8 key_size = 16;
|
||||
if (std::is_same_v<KeyType, S256KeyType>)
|
||||
key_size = 32;
|
||||
return fmt::format("key_size={:02X}, key={:02X}, field1={:016X}, field2={:016X}", key_size,
|
||||
static_cast<u8>(type), field1, field2);
|
||||
}
|
||||
};
|
||||
|
||||
// The following two (== and hash) are so KeyIndex can be a key in unordered_map
|
||||
|
||||
template <typename KeyType>
|
||||
bool operator==(const KeyIndex<KeyType>& lhs, const KeyIndex<KeyType>& rhs) {
|
||||
return lhs.type == rhs.type && lhs.field1 == rhs.field1 && lhs.field2 == rhs.field2;
|
||||
}
|
||||
|
||||
} // namespace Crypto
|
||||
|
||||
namespace std {
|
||||
template <typename KeyType>
|
||||
struct hash<Crypto::KeyIndex<KeyType>> {
|
||||
size_t operator()(const Crypto::KeyIndex<KeyType>& k) const {
|
||||
using std::hash;
|
||||
|
||||
return ((hash<u64>()(static_cast<u64>(k.type)) ^ (hash<u64>()(k.field1) << 1)) >> 1) ^
|
||||
(hash<u64>()(k.field2) << 1);
|
||||
}
|
||||
};
|
||||
} // namespace std
|
||||
|
||||
namespace Crypto {
|
||||
|
||||
std::array<u8, 0x10> operator"" _array16(const char* str, size_t len);
|
||||
std::array<u8, 0x20> operator"" _array32(const char* str, size_t len);
|
||||
|
||||
struct KeyManager {
|
||||
void SetValidationMode(bool dev);
|
||||
void LoadFromFile(std::string_view filename, bool is_title_keys);
|
||||
|
||||
bool HasKey(S128KeyType id, u64 field1 = 0, u64 field2 = 0);
|
||||
bool HasKey(S256KeyType id, u64 field1 = 0, u64 field2 = 0);
|
||||
|
||||
Key128 GetKey(S128KeyType id, u64 field1 = 0, u64 field2 = 0);
|
||||
Key256 GetKey(S256KeyType id, u64 field1 = 0, u64 field2 = 0);
|
||||
|
||||
void SetKey(S128KeyType id, Key128 key, u64 field1 = 0, u64 field2 = 0);
|
||||
void SetKey(S256KeyType id, Key256 key, u64 field1 = 0, u64 field2 = 0);
|
||||
|
||||
bool ValidateKey(S128KeyType key, u64 field1 = 0, u64 field2 = 0);
|
||||
bool ValidateKey(S256KeyType key, u64 field1 = 0, u64 field2 = 0);
|
||||
|
||||
private:
|
||||
std::unordered_map<KeyIndex<S128KeyType>, Key128> s128_keys;
|
||||
std::unordered_map<KeyIndex<S256KeyType>, Key256> s256_keys;
|
||||
|
||||
bool dev_mode = false;
|
||||
|
||||
static std::unordered_map<KeyIndex<S128KeyType>, SHA256Hash> s128_hash_prod;
|
||||
static std::unordered_map<KeyIndex<S256KeyType>, SHA256Hash> s256_hash_prod;
|
||||
static std::unordered_map<KeyIndex<S128KeyType>, SHA256Hash> s128_hash_dev;
|
||||
static std::unordered_map<KeyIndex<S256KeyType>, SHA256Hash> s256_hash_dev;
|
||||
static std::unordered_map<std::string, KeyIndex<S128KeyType>> s128_file_id;
|
||||
static std::unordered_map<std::string, KeyIndex<S256KeyType>> s256_file_id;
|
||||
};
|
||||
|
||||
extern KeyManager keys;
|
||||
|
||||
} // namespace Crypto
|
5
src/core/crypto/sha_util.cpp
Normal file
5
src/core/crypto/sha_util.cpp
Normal file
@ -0,0 +1,5 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
namespace Crypto {} // namespace Crypto
|
20
src/core/crypto/sha_util.h
Normal file
20
src/core/crypto/sha_util.h
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
#include "key_manager.h"
|
||||
#include "mbedtls/cipher.h"
|
||||
|
||||
namespace Crypto {
|
||||
typedef std::array<u8, 0x20> SHA256Hash;
|
||||
|
||||
inline SHA256Hash operator"" _HASH(const char* data, size_t len) {
|
||||
if (len != 0x40)
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace Crypto
|
150
src/core/file_sys/card_image.cpp
Normal file
150
src/core/file_sys/card_image.cpp
Normal file
@ -0,0 +1,150 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <core/loader/loader.h>
|
||||
#include "core/file_sys/card_image.h"
|
||||
#include "core/file_sys/partition_filesystem.h"
|
||||
#include "core/file_sys/vfs_offset.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
XCI::XCI(VirtualFile file_) : file(std::move(file_)), partitions(0x4) {
|
||||
if (file->ReadObject(&header) != sizeof(GamecardHeader)) {
|
||||
status = Loader::ResultStatus::ErrorInvalidFormat;
|
||||
return;
|
||||
}
|
||||
|
||||
if (header.magic != Common::MakeMagic('H', 'E', 'A', 'D')) {
|
||||
status = Loader::ResultStatus::ErrorInvalidFormat;
|
||||
return;
|
||||
}
|
||||
|
||||
PartitionFilesystem main_hfs(
|
||||
std::make_shared<OffsetVfsFile>(file, header.hfs_size, header.hfs_offset));
|
||||
|
||||
if (main_hfs.GetStatus() != Loader::ResultStatus::Success) {
|
||||
status = main_hfs.GetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
const static std::array<std::string, 0x4> partition_names = {"update", "normal", "secure",
|
||||
"logo"};
|
||||
|
||||
for (XCIPartition partition :
|
||||
{XCIPartition::Update, XCIPartition::Normal, XCIPartition::Secure, XCIPartition::Logo}) {
|
||||
auto raw = main_hfs.GetFile(partition_names[static_cast<size_t>(partition)]);
|
||||
if (raw != nullptr)
|
||||
partitions[static_cast<size_t>(partition)] = std::make_shared<PartitionFilesystem>(raw);
|
||||
}
|
||||
|
||||
auto result = AddNCAFromPartition(XCIPartition::Secure);
|
||||
if (result != Loader::ResultStatus::Success) {
|
||||
status = result;
|
||||
return;
|
||||
}
|
||||
result = AddNCAFromPartition(XCIPartition::Update);
|
||||
if (result != Loader::ResultStatus::Success) {
|
||||
status = result;
|
||||
return;
|
||||
}
|
||||
|
||||
result = AddNCAFromPartition(XCIPartition::Normal);
|
||||
if (result != Loader::ResultStatus::Success) {
|
||||
status = result;
|
||||
return;
|
||||
}
|
||||
|
||||
if (GetFormatVersion() >= 0x2) {
|
||||
result = AddNCAFromPartition(XCIPartition::Logo);
|
||||
if (result != Loader::ResultStatus::Success) {
|
||||
status = result;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
status = Loader::ResultStatus::Success;
|
||||
}
|
||||
|
||||
Loader::ResultStatus XCI::GetStatus() const {
|
||||
return status;
|
||||
}
|
||||
|
||||
VirtualDir XCI::GetPartition(XCIPartition partition) const {
|
||||
return partitions[static_cast<size_t>(partition)];
|
||||
}
|
||||
|
||||
VirtualDir XCI::GetSecurePartition() const {
|
||||
return GetPartition(XCIPartition::Secure);
|
||||
}
|
||||
|
||||
VirtualDir XCI::GetNormalPartition() const {
|
||||
return GetPartition(XCIPartition::Normal);
|
||||
}
|
||||
|
||||
VirtualDir XCI::GetUpdatePartition() const {
|
||||
return GetPartition(XCIPartition::Update);
|
||||
}
|
||||
|
||||
VirtualDir XCI::GetLogoPartition() const {
|
||||
return GetPartition(XCIPartition::Logo);
|
||||
}
|
||||
|
||||
std::shared_ptr<NCA> XCI::GetNCAByType(NCAContentType type) const {
|
||||
for (const auto& nca : ncas) {
|
||||
if (nca->GetType() == type)
|
||||
return nca;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
VirtualFile XCI::GetNCAFileByType(NCAContentType type) const {
|
||||
auto nca = GetNCAByType(type);
|
||||
if (nca != nullptr)
|
||||
return nca->GetBaseFile();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<VfsFile>> XCI::GetFiles() const {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<VfsDirectory>> XCI::GetSubdirectories() const {
|
||||
return std::vector<std::shared_ptr<VfsDirectory>>();
|
||||
}
|
||||
|
||||
std::string XCI::GetName() const {
|
||||
return file->GetName();
|
||||
}
|
||||
|
||||
std::shared_ptr<VfsDirectory> XCI::GetParentDirectory() const {
|
||||
return file->GetContainingDirectory();
|
||||
}
|
||||
|
||||
bool XCI::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Loader::ResultStatus XCI::AddNCAFromPartition(XCIPartition part) {
|
||||
if (partitions[static_cast<size_t>(part)] == nullptr) {
|
||||
return Loader::ResultStatus::ErrorInvalidFormat;
|
||||
}
|
||||
|
||||
for (VirtualFile file : partitions[static_cast<size_t>(part)]->GetFiles()) {
|
||||
if (file->GetExtension() != "nca")
|
||||
continue;
|
||||
auto nca = std::make_shared<NCA>(file);
|
||||
if (nca->GetStatus() == Loader::ResultStatus::Success)
|
||||
ncas.push_back(std::move(nca));
|
||||
}
|
||||
|
||||
return Loader::ResultStatus::Success;
|
||||
}
|
||||
|
||||
u8 XCI::GetFormatVersion() const {
|
||||
return GetLogoPartition() == nullptr ? 0x1 : 0x2;
|
||||
}
|
||||
} // namespace FileSys
|
93
src/core/file_sys/card_image.h
Normal file
93
src/core/file_sys/card_image.h
Normal file
@ -0,0 +1,93 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include "common/swap.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
enum class GamecardSize : u8 {
|
||||
S_1GB = 0xFA,
|
||||
S_2GB = 0xF8,
|
||||
S_4GB = 0xF0,
|
||||
S_8GB = 0xE0,
|
||||
S_16GB = 0xE1,
|
||||
S_32GB = 0xE2,
|
||||
};
|
||||
|
||||
struct GamecardInfo {
|
||||
std::array<u8, 0x70> data;
|
||||
};
|
||||
static_assert(sizeof(GamecardInfo) == 0x70, "GamecardInfo has incorrect size.");
|
||||
|
||||
struct GamecardHeader {
|
||||
std::array<u8, 0x100> signature;
|
||||
u32_le magic;
|
||||
u32_le secure_area_start;
|
||||
u32_le backup_area_start;
|
||||
u8 kek_index;
|
||||
GamecardSize size;
|
||||
u8 header_version;
|
||||
u8 flags;
|
||||
u64_le package_id;
|
||||
u64_le valid_data_end;
|
||||
u128 info_iv;
|
||||
u64_le hfs_offset;
|
||||
u64_le hfs_size;
|
||||
std::array<u8, 0x20> hfs_header_hash;
|
||||
std::array<u8, 0x20> initial_data_hash;
|
||||
u32_le secure_mode_flag;
|
||||
u32_le title_key_flag;
|
||||
u32_le key_flag;
|
||||
u32_le normal_area_end;
|
||||
GamecardInfo info;
|
||||
};
|
||||
static_assert(sizeof(GamecardHeader) == 0x200, "GamecardHeader has incorrect size.");
|
||||
|
||||
enum class XCIPartition : u8 { Update, Normal, Secure, Logo };
|
||||
|
||||
class XCI : public ReadOnlyVfsDirectory {
|
||||
public:
|
||||
explicit XCI(VirtualFile file);
|
||||
|
||||
Loader::ResultStatus GetStatus() const;
|
||||
|
||||
u8 GetFormatVersion() const;
|
||||
|
||||
VirtualDir GetPartition(XCIPartition partition) const;
|
||||
VirtualDir GetSecurePartition() const;
|
||||
VirtualDir GetNormalPartition() const;
|
||||
VirtualDir GetUpdatePartition() const;
|
||||
VirtualDir GetLogoPartition() const;
|
||||
|
||||
std::shared_ptr<NCA> GetNCAByType(NCAContentType type) const;
|
||||
VirtualFile GetNCAFileByType(NCAContentType type) const;
|
||||
|
||||
std::vector<std::shared_ptr<VfsFile>> GetFiles() const override;
|
||||
|
||||
std::vector<std::shared_ptr<VfsDirectory>> GetSubdirectories() const override;
|
||||
|
||||
std::string GetName() const override;
|
||||
|
||||
std::shared_ptr<VfsDirectory> GetParentDirectory() const override;
|
||||
|
||||
protected:
|
||||
bool ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) override;
|
||||
|
||||
private:
|
||||
Loader::ResultStatus AddNCAFromPartition(XCIPartition part);
|
||||
|
||||
VirtualFile file;
|
||||
GamecardHeader header{};
|
||||
|
||||
Loader::ResultStatus status;
|
||||
|
||||
std::vector<VirtualDir> partitions;
|
||||
std::vector<std::shared_ptr<NCA>> ncas;
|
||||
};
|
||||
} // namespace FileSys
|
@ -6,9 +6,12 @@
|
||||
#include <utility>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "core/crypto/aes_util.h"
|
||||
#include "core/crypto/ctr_encryption_layer.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/vfs_offset.h"
|
||||
#include "core/loader/loader.h"
|
||||
#include "mbedtls/cipher.h"
|
||||
#include "romfs.h"
|
||||
|
||||
namespace FileSys {
|
||||
@ -29,11 +32,19 @@ enum class NCASectionFilesystemType : u8 {
|
||||
struct NCASectionHeaderBlock {
|
||||
INSERT_PADDING_BYTES(3);
|
||||
NCASectionFilesystemType filesystem_type;
|
||||
u8 crypto_type;
|
||||
NCASectionCryptoType crypto_type;
|
||||
INSERT_PADDING_BYTES(3);
|
||||
};
|
||||
static_assert(sizeof(NCASectionHeaderBlock) == 0x8, "NCASectionHeaderBlock has incorrect size.");
|
||||
|
||||
struct NCASectionRaw {
|
||||
NCASectionHeaderBlock header;
|
||||
std::array<u8, 0x138> block_data;
|
||||
std::array<u8, 0x8> section_ctr;
|
||||
INSERT_PADDING_BYTES(0xB8);
|
||||
};
|
||||
static_assert(sizeof(NCASectionRaw) == 0x200, "NCASectionRaw has incorrect size.");
|
||||
|
||||
struct PFS0Superblock {
|
||||
NCASectionHeaderBlock header_block;
|
||||
std::array<u8, 0x20> hash;
|
||||
@ -43,62 +54,155 @@ struct PFS0Superblock {
|
||||
u64_le hash_table_size;
|
||||
u64_le pfs0_header_offset;
|
||||
u64_le pfs0_size;
|
||||
INSERT_PADDING_BYTES(432);
|
||||
INSERT_PADDING_BYTES(0x1B0);
|
||||
};
|
||||
static_assert(sizeof(PFS0Superblock) == 0x200, "PFS0Superblock has incorrect size.");
|
||||
|
||||
struct RomFSSuperblock {
|
||||
NCASectionHeaderBlock header_block;
|
||||
IVFCHeader ivfc;
|
||||
INSERT_PADDING_BYTES(0x118);
|
||||
};
|
||||
static_assert(sizeof(RomFSSuperblock) == 0xE8, "RomFSSuperblock has incorrect size.");
|
||||
static_assert(sizeof(RomFSSuperblock) == 0x200, "RomFSSuperblock has incorrect size.");
|
||||
|
||||
union NCASectionHeader {
|
||||
NCASectionRaw raw;
|
||||
PFS0Superblock pfs0;
|
||||
RomFSSuperblock romfs;
|
||||
};
|
||||
static_assert(sizeof(NCASectionHeader) == 0x200, "NCASectionHeader has incorrect size.");
|
||||
|
||||
bool IsValidNCA(const NCAHeader& header) {
|
||||
// TODO(DarkLordZach): Add NCA2/NCA0 support.
|
||||
return header.magic == Common::MakeMagic('N', 'C', 'A', '3');
|
||||
}
|
||||
|
||||
Crypto::Key128 NCA::GetKeyAreaKey(NCASectionCryptoType type) {
|
||||
u8 master_key_id = header.crypto_type;
|
||||
if (header.crypto_type_2 > master_key_id)
|
||||
master_key_id = header.crypto_type_2;
|
||||
if (master_key_id > 0)
|
||||
--master_key_id;
|
||||
|
||||
std::vector<u8> key_area(header.key_area.begin(), header.key_area.end());
|
||||
if (!Crypto::keys.ValidateKey(Crypto::S128KeyType::KEY_AREA, master_key_id, header.key_index)) {
|
||||
status = Loader::ResultStatus::ErrorEncrypted;
|
||||
return {};
|
||||
}
|
||||
Crypto::AESCipher<Crypto::Key128> cipher(
|
||||
Crypto::keys.GetKey(Crypto::S128KeyType::KEY_AREA, master_key_id, header.key_index),
|
||||
Crypto::Mode::ECB);
|
||||
cipher.Transcode(key_area.data(), key_area.size(), key_area.data(), Crypto::Op::DECRYPT);
|
||||
|
||||
Crypto::Key128 out;
|
||||
if (type == NCASectionCryptoType::XTS)
|
||||
std::copy(key_area.begin(), key_area.begin() + 0x10, out.begin());
|
||||
else if (type == NCASectionCryptoType::CTR)
|
||||
std::copy(key_area.begin() + 0x20, key_area.begin() + 0x30, out.begin());
|
||||
else
|
||||
LOG_CRITICAL(Crypto, "Called GetKeyAreaKey on invalid NCASectionCryptoType type={:02X}",
|
||||
static_cast<u8>(type));
|
||||
|
||||
u128 out_128 = *reinterpret_cast<u128*>(&out);
|
||||
LOG_DEBUG(Crypto, "called with crypto_rev={:02X}, kak_index={:02X}, key={:016X}{:016X}",
|
||||
master_key_id, header.key_index, out_128[1], out_128[0]);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
VirtualFile NCA::Decrypt(NCASectionHeader header, VirtualFile in, u64 starting_offset) {
|
||||
if (!encrypted)
|
||||
return in;
|
||||
|
||||
switch (header.raw.header.crypto_type) {
|
||||
case NCASectionCryptoType::NONE:
|
||||
LOG_DEBUG(Crypto, "called with mode=NONE");
|
||||
return in;
|
||||
case NCASectionCryptoType::CTR:
|
||||
LOG_DEBUG(Crypto, "called with mode=CTR, starting_offset={:016X}", starting_offset);
|
||||
{
|
||||
auto out = std::make_shared<Crypto::CTREncryptionLayer>(
|
||||
std::move(in), GetKeyAreaKey(NCASectionCryptoType::CTR), starting_offset);
|
||||
std::vector<u8> iv(16, 0);
|
||||
for (u8 i = 0; i < 8; ++i)
|
||||
iv[i] = header.raw.section_ctr[0x8 - i - 1];
|
||||
out->SetIV(iv);
|
||||
return out;
|
||||
}
|
||||
case NCASectionCryptoType::XTS:
|
||||
// TODO(DarkLordZach): Implement XTSEncryptionLayer and title key encryption.
|
||||
default:
|
||||
LOG_ERROR(Crypto, "called with unhandled crypto type={:02X}",
|
||||
static_cast<u8>(header.raw.header.crypto_type));
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
NCA::NCA(VirtualFile file_) : file(std::move(file_)) {
|
||||
if (sizeof(NCAHeader) != file->ReadObject(&header))
|
||||
LOG_CRITICAL(Loader, "File reader errored out during header read.");
|
||||
LOG_ERROR(Loader, "File reader errored out during header read.");
|
||||
|
||||
encrypted = false;
|
||||
|
||||
if (!IsValidNCA(header)) {
|
||||
status = Loader::ResultStatus::ErrorInvalidFormat;
|
||||
return;
|
||||
NCAHeader dec_header{};
|
||||
if (!Crypto::keys.ValidateKey(Crypto::S256KeyType::HEADER)) {
|
||||
status = Loader::ResultStatus::ErrorEncrypted;
|
||||
return;
|
||||
}
|
||||
Crypto::AESCipher<Crypto::Key256> cipher(Crypto::keys.GetKey(Crypto::S256KeyType::HEADER),
|
||||
Crypto::Mode::XTS);
|
||||
cipher.XTSTranscode(&header, sizeof(NCAHeader), &dec_header, 0, 0x200, Crypto::Op::DECRYPT);
|
||||
if (IsValidNCA(dec_header)) {
|
||||
header = dec_header;
|
||||
encrypted = true;
|
||||
} else {
|
||||
status = Loader::ResultStatus::ErrorInvalidFormat;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::ptrdiff_t number_sections =
|
||||
const std::ptrdiff_t number_sections =
|
||||
std::count_if(std::begin(header.section_tables), std::end(header.section_tables),
|
||||
[](NCASectionTableEntry entry) { return entry.media_offset > 0; });
|
||||
|
||||
std::vector<NCASectionHeader> sections(number_sections);
|
||||
const auto length_sections = SECTION_HEADER_SIZE * number_sections;
|
||||
|
||||
if (encrypted) {
|
||||
auto raw = file->ReadBytes(length_sections, SECTION_HEADER_OFFSET);
|
||||
if (!Crypto::keys.ValidateKey(Crypto::S256KeyType::HEADER)) {
|
||||
status = Loader::ResultStatus::ErrorEncrypted;
|
||||
return;
|
||||
}
|
||||
Crypto::AESCipher<Crypto::Key256> cipher(Crypto::keys.GetKey(Crypto::S256KeyType::HEADER),
|
||||
Crypto::Mode::XTS);
|
||||
cipher.XTSTranscode(raw.data(), length_sections, sections.data(), 2, SECTION_HEADER_SIZE,
|
||||
Crypto::Op::DECRYPT);
|
||||
} else {
|
||||
file->ReadBytes(sections.data(), length_sections, SECTION_HEADER_OFFSET);
|
||||
}
|
||||
|
||||
for (std::ptrdiff_t i = 0; i < number_sections; ++i) {
|
||||
// Seek to beginning of this section.
|
||||
NCASectionHeaderBlock block{};
|
||||
if (sizeof(NCASectionHeaderBlock) !=
|
||||
file->ReadObject(&block, SECTION_HEADER_OFFSET + i * SECTION_HEADER_SIZE))
|
||||
LOG_CRITICAL(Loader, "File reader errored out during header read.");
|
||||
|
||||
if (block.filesystem_type == NCASectionFilesystemType::ROMFS) {
|
||||
RomFSSuperblock sb{};
|
||||
if (sizeof(RomFSSuperblock) !=
|
||||
file->ReadObject(&sb, SECTION_HEADER_OFFSET + i * SECTION_HEADER_SIZE))
|
||||
LOG_CRITICAL(Loader, "File reader errored out during header read.");
|
||||
auto section = sections[i];
|
||||
|
||||
if (section.raw.header.filesystem_type == NCASectionFilesystemType::ROMFS) {
|
||||
const size_t romfs_offset =
|
||||
header.section_tables[i].media_offset * MEDIA_OFFSET_MULTIPLIER +
|
||||
sb.ivfc.levels[IVFC_MAX_LEVEL - 1].offset;
|
||||
const size_t romfs_size = sb.ivfc.levels[IVFC_MAX_LEVEL - 1].size;
|
||||
files.emplace_back(std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset));
|
||||
section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset;
|
||||
const size_t romfs_size = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].size;
|
||||
files.emplace_back(
|
||||
Decrypt(section, std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset),
|
||||
romfs_offset));
|
||||
romfs = files.back();
|
||||
} else if (block.filesystem_type == NCASectionFilesystemType::PFS0) {
|
||||
PFS0Superblock sb{};
|
||||
// Seek back to beginning of this section.
|
||||
if (sizeof(PFS0Superblock) !=
|
||||
file->ReadObject(&sb, SECTION_HEADER_OFFSET + i * SECTION_HEADER_SIZE))
|
||||
LOG_CRITICAL(Loader, "File reader errored out during header read.");
|
||||
|
||||
} else if (section.raw.header.filesystem_type == NCASectionFilesystemType::PFS0) {
|
||||
u64 offset = (static_cast<u64>(header.section_tables[i].media_offset) *
|
||||
MEDIA_OFFSET_MULTIPLIER) +
|
||||
sb.pfs0_header_offset;
|
||||
section.pfs0.pfs0_header_offset;
|
||||
u64 size = MEDIA_OFFSET_MULTIPLIER * (header.section_tables[i].media_end_offset -
|
||||
header.section_tables[i].media_offset);
|
||||
auto npfs = std::make_shared<PartitionFilesystem>(
|
||||
std::make_shared<OffsetVfsFile>(file, size, offset));
|
||||
Decrypt(section, std::make_shared<OffsetVfsFile>(file, size, offset), offset));
|
||||
|
||||
if (npfs->GetStatus() == Loader::ResultStatus::Success) {
|
||||
dirs.emplace_back(npfs);
|
||||
@ -153,6 +257,10 @@ VirtualDir NCA::GetExeFS() const {
|
||||
return exefs;
|
||||
}
|
||||
|
||||
VirtualFile NCA::GetBaseFile() const {
|
||||
return file;
|
||||
}
|
||||
|
||||
bool NCA::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) {
|
||||
return false;
|
||||
}
|
||||
|
@ -12,10 +12,10 @@
|
||||
#include "common/common_funcs.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/swap.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/file_sys/partition_filesystem.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
enum class NCAContentType : u8 {
|
||||
Program = 0,
|
||||
Meta = 1,
|
||||
@ -24,6 +24,13 @@ enum class NCAContentType : u8 {
|
||||
Data = 4,
|
||||
};
|
||||
|
||||
enum class NCASectionCryptoType : u8 {
|
||||
NONE = 1,
|
||||
XTS = 2,
|
||||
CTR = 3,
|
||||
BKTR = 4,
|
||||
};
|
||||
|
||||
struct NCASectionTableEntry {
|
||||
u32_le media_offset;
|
||||
u32_le media_end_offset;
|
||||
@ -48,20 +55,19 @@ struct NCAHeader {
|
||||
std::array<u8, 0x10> rights_id;
|
||||
std::array<NCASectionTableEntry, 0x4> section_tables;
|
||||
std::array<std::array<u8, 0x20>, 0x4> hash_tables;
|
||||
std::array<std::array<u8, 0x10>, 0x4> key_area;
|
||||
std::array<u8, 0x40> key_area;
|
||||
INSERT_PADDING_BYTES(0xC0);
|
||||
};
|
||||
static_assert(sizeof(NCAHeader) == 0x400, "NCAHeader has incorrect size.");
|
||||
|
||||
union NCASectionHeader;
|
||||
|
||||
inline bool IsDirectoryExeFS(const std::shared_ptr<VfsDirectory>& pfs) {
|
||||
// According to switchbrew, an exefs must only contain these two files:
|
||||
return pfs->GetFile("main") != nullptr && pfs->GetFile("main.npdm") != nullptr;
|
||||
}
|
||||
|
||||
inline bool IsValidNCA(const NCAHeader& header) {
|
||||
return header.magic == Common::MakeMagic('N', 'C', 'A', '2') ||
|
||||
header.magic == Common::MakeMagic('N', 'C', 'A', '3');
|
||||
}
|
||||
bool IsValidNCA(const NCAHeader& header);
|
||||
|
||||
// An implementation of VfsDirectory that represents a Nintendo Content Archive (NCA) conatiner.
|
||||
// After construction, use GetStatus to determine if the file is valid and ready to be used.
|
||||
@ -81,6 +87,8 @@ public:
|
||||
VirtualFile GetRomFS() const;
|
||||
VirtualDir GetExeFS() const;
|
||||
|
||||
VirtualFile GetBaseFile() const;
|
||||
|
||||
protected:
|
||||
bool ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) override;
|
||||
|
||||
@ -95,6 +103,12 @@ private:
|
||||
NCAHeader header{};
|
||||
|
||||
Loader::ResultStatus status{};
|
||||
|
||||
bool encrypted;
|
||||
|
||||
Crypto::Key128 GetKeyAreaKey(NCASectionCryptoType type);
|
||||
|
||||
VirtualFile Decrypt(NCASectionHeader header, VirtualFile in, size_t starting_offset);
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
|
@ -285,6 +285,27 @@ bool ReadOnlyVfsDirectory::Rename(std::string_view name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DeepEquals(const VirtualFile& file1, const VirtualFile& file2, size_t block_size) {
|
||||
if (file1->GetSize() != file2->GetSize())
|
||||
return false;
|
||||
|
||||
std::vector<u8> f1_v(block_size);
|
||||
std::vector<u8> f2_v(block_size);
|
||||
for (size_t i = 0; i < file1->GetSize(); i += block_size) {
|
||||
auto f1_vs = file1->Read(f1_v.data(), block_size, i);
|
||||
auto f2_vs = file2->Read(f2_v.data(), block_size, i);
|
||||
|
||||
if (f1_vs != f2_vs)
|
||||
return false;
|
||||
for (size_t j = 0; j < f1_vs; ++j) {
|
||||
if (f1_v[j] != f2_v[j])
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VfsRawCopy(VirtualFile src, VirtualFile dest) {
|
||||
if (src == nullptr || dest == nullptr)
|
||||
return false;
|
||||
|
@ -245,6 +245,9 @@ struct ReadOnlyVfsDirectory : public VfsDirectory {
|
||||
bool Rename(std::string_view name) override;
|
||||
};
|
||||
|
||||
// Compare the two files, byte-for-byte, in increments specificed by block_size
|
||||
bool DeepEquals(const VirtualFile& file1, const VirtualFile& file2, size_t block_size = 0x200);
|
||||
|
||||
// A method that copies the raw data between two different implementations of VirtualFile. If you
|
||||
// are using the same implementation, it is probably better to use the Copy method in the parent
|
||||
// directory of src/dest.
|
||||
|
@ -13,6 +13,7 @@
|
||||
#include "core/loader/nca.h"
|
||||
#include "core/loader/nro.h"
|
||||
#include "core/loader/nso.h"
|
||||
#include "core/loader/xci.h"
|
||||
|
||||
namespace Loader {
|
||||
|
||||
@ -35,6 +36,7 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
|
||||
CHECK_TYPE(NSO)
|
||||
CHECK_TYPE(NRO)
|
||||
CHECK_TYPE(NCA)
|
||||
CHECK_TYPE(XCI)
|
||||
|
||||
#undef CHECK_TYPE
|
||||
|
||||
@ -60,6 +62,8 @@ FileType GuessFromFilename(const std::string& name) {
|
||||
return FileType::NSO;
|
||||
if (extension == "nca")
|
||||
return FileType::NCA;
|
||||
if (extension == "xci")
|
||||
return FileType::XCI;
|
||||
|
||||
return FileType::Unknown;
|
||||
}
|
||||
@ -74,6 +78,8 @@ const char* GetFileTypeString(FileType type) {
|
||||
return "NSO";
|
||||
case FileType::NCA:
|
||||
return "NCA";
|
||||
case FileType::XCI:
|
||||
return "XCI";
|
||||
case FileType::DeconstructedRomDirectory:
|
||||
return "Directory";
|
||||
case FileType::Error:
|
||||
@ -111,6 +117,9 @@ static std::unique_ptr<AppLoader> GetFileLoader(FileSys::VirtualFile file, FileT
|
||||
case FileType::NCA:
|
||||
return std::make_unique<AppLoader_NCA>(std::move(file));
|
||||
|
||||
case FileType::XCI:
|
||||
return std::make_unique<AppLoader_XCI>(std::move(file));
|
||||
|
||||
// NX deconstructed ROM directory.
|
||||
case FileType::DeconstructedRomDirectory:
|
||||
return std::make_unique<AppLoader_DeconstructedRomDirectory>(std::move(file));
|
||||
|
@ -31,6 +31,7 @@ enum class FileType {
|
||||
NSO,
|
||||
NRO,
|
||||
NCA,
|
||||
XCI,
|
||||
DeconstructedRomDirectory,
|
||||
};
|
||||
|
||||
|
@ -25,12 +25,10 @@ namespace Loader {
|
||||
AppLoader_NCA::AppLoader_NCA(FileSys::VirtualFile file) : AppLoader(std::move(file)) {}
|
||||
|
||||
FileType AppLoader_NCA::IdentifyType(const FileSys::VirtualFile& file) {
|
||||
// TODO(DarkLordZach): Assuming everything is decrypted. Add crypto support.
|
||||
FileSys::NCAHeader header{};
|
||||
if (sizeof(FileSys::NCAHeader) != file->ReadObject(&header))
|
||||
return FileType::Error;
|
||||
FileSys::NCA nca(file);
|
||||
|
||||
if (IsValidNCA(header) && header.content_type == FileSys::NCAContentType::Program)
|
||||
if (nca.GetStatus() == ResultStatus::Success &&
|
||||
nca.GetType() == FileSys::NCAContentType::Program)
|
||||
return FileType::NCA;
|
||||
|
||||
return FileType::Error;
|
||||
@ -98,12 +96,21 @@ ResultStatus AppLoader_NCA::Load(Kernel::SharedPtr<Kernel::Process>& process) {
|
||||
}
|
||||
|
||||
ResultStatus AppLoader_NCA::ReadRomFS(FileSys::VirtualFile& dir) {
|
||||
if (nca == nullptr || nca->GetRomFS() == nullptr || nca->GetRomFS()->GetSize() == 0)
|
||||
if (nca == nullptr)
|
||||
return ResultStatus::ErrorNotLoaded;
|
||||
if (nca->GetRomFS() == nullptr || nca->GetRomFS()->GetSize() == 0)
|
||||
return ResultStatus::ErrorNotUsed;
|
||||
dir = nca->GetRomFS();
|
||||
return ResultStatus::Success;
|
||||
}
|
||||
|
||||
ResultStatus AppLoader_NCA::ReadProgramId(u64& out_program_id) {
|
||||
if (nca == nullptr)
|
||||
return ResultStatus::ErrorNotLoaded;
|
||||
out_program_id = nca->GetTitleId();
|
||||
return ResultStatus::Success;
|
||||
}
|
||||
|
||||
AppLoader_NCA::~AppLoader_NCA() = default;
|
||||
|
||||
} // namespace Loader
|
||||
|
@ -33,6 +33,8 @@ public:
|
||||
|
||||
ResultStatus ReadRomFS(FileSys::VirtualFile& dir) override;
|
||||
|
||||
ResultStatus ReadProgramId(u64& out_program_id) override;
|
||||
|
||||
~AppLoader_NCA();
|
||||
|
||||
private:
|
||||
|
67
src/core/loader/xci.cpp
Normal file
67
src/core/loader/xci.cpp
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "common/file_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common/swap.h"
|
||||
#include "core/core.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/control_metadata.h"
|
||||
#include "core/file_sys/program_metadata.h"
|
||||
#include "core/file_sys/romfs.h"
|
||||
#include "core/gdbstub/gdbstub.h"
|
||||
#include "core/hle/kernel/process.h"
|
||||
#include "core/hle/kernel/resource_limit.h"
|
||||
#include "core/hle/service/filesystem/filesystem.h"
|
||||
#include "core/loader/nso.h"
|
||||
#include "core/loader/xci.h"
|
||||
#include "core/memory.h"
|
||||
|
||||
namespace Loader {
|
||||
|
||||
AppLoader_XCI::AppLoader_XCI(FileSys::VirtualFile file)
|
||||
: AppLoader(file), xci(std::make_unique<FileSys::XCI>(file)),
|
||||
nca_loader(std::make_unique<AppLoader_NCA>(
|
||||
xci->GetNCAFileByType(FileSys::NCAContentType::Program))) {}
|
||||
|
||||
FileType AppLoader_XCI::IdentifyType(const FileSys::VirtualFile& file) {
|
||||
FileSys::XCI xci(file);
|
||||
|
||||
if (xci.GetStatus() == ResultStatus::Success &&
|
||||
xci.GetNCAByType(FileSys::NCAContentType::Program) != nullptr &&
|
||||
AppLoader_NCA::IdentifyType(xci.GetNCAFileByType(FileSys::NCAContentType::Program)) ==
|
||||
FileType::NCA)
|
||||
return FileType::XCI;
|
||||
|
||||
return FileType::Error;
|
||||
}
|
||||
|
||||
ResultStatus AppLoader_XCI::Load(Kernel::SharedPtr<Kernel::Process>& process) {
|
||||
if (is_loaded) {
|
||||
return ResultStatus::ErrorAlreadyLoaded;
|
||||
}
|
||||
|
||||
auto result = nca_loader->Load(process);
|
||||
if (result != ResultStatus::Success)
|
||||
return result;
|
||||
|
||||
is_loaded = true;
|
||||
|
||||
return ResultStatus::Success;
|
||||
}
|
||||
|
||||
ResultStatus AppLoader_XCI::ReadRomFS(FileSys::VirtualFile& dir) {
|
||||
return nca_loader->ReadRomFS(dir);
|
||||
}
|
||||
|
||||
ResultStatus AppLoader_XCI::ReadProgramId(u64& out_program_id) {
|
||||
return nca_loader->ReadProgramId(out_program_id);
|
||||
}
|
||||
|
||||
AppLoader_XCI::~AppLoader_XCI() = default;
|
||||
|
||||
} // namespace Loader
|
42
src/core/loader/xci.h
Normal file
42
src/core/loader/xci.h
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/file_sys/card_image.h"
|
||||
#include "core/loader/nca.h"
|
||||
|
||||
namespace Loader {
|
||||
|
||||
/// Loads an XCI file
|
||||
class AppLoader_XCI final : public AppLoader {
|
||||
public:
|
||||
explicit AppLoader_XCI(FileSys::VirtualFile file);
|
||||
|
||||
/**
|
||||
* Returns the type of the file
|
||||
* @param file std::shared_ptr<VfsFile> open file
|
||||
* @return FileType found, or FileType::Error if this loader doesn't know it
|
||||
*/
|
||||
static FileType IdentifyType(const FileSys::VirtualFile& file);
|
||||
|
||||
FileType GetFileType() override {
|
||||
return IdentifyType(file);
|
||||
}
|
||||
|
||||
ResultStatus Load(Kernel::SharedPtr<Kernel::Process>& process) override;
|
||||
|
||||
ResultStatus ReadRomFS(FileSys::VirtualFile& dir) override;
|
||||
ResultStatus ReadProgramId(u64& out_program_id) override;
|
||||
|
||||
~AppLoader_XCI();
|
||||
|
||||
private:
|
||||
FileSys::ProgramMetadata metadata;
|
||||
|
||||
std::unique_ptr<FileSys::XCI> xci;
|
||||
std::unique_ptr<AppLoader_NCA> nca_loader;
|
||||
};
|
||||
|
||||
} // namespace Loader
|
@ -137,6 +137,8 @@ struct Values {
|
||||
|
||||
std::string log_filter;
|
||||
|
||||
bool use_dev_keys;
|
||||
|
||||
// Audio
|
||||
std::string sink_id;
|
||||
std::string audio_device_id;
|
||||
|
@ -109,6 +109,7 @@ void Config::ReadValues() {
|
||||
|
||||
qt_config->beginGroup("Miscellaneous");
|
||||
Settings::values.log_filter = qt_config->value("log_filter", "*:Info").toString().toStdString();
|
||||
Settings::values.use_dev_keys = qt_config->value("use_dev_keys", false).toBool();
|
||||
qt_config->endGroup();
|
||||
|
||||
qt_config->beginGroup("Debugging");
|
||||
@ -218,6 +219,7 @@ void Config::SaveValues() {
|
||||
|
||||
qt_config->beginGroup("Miscellaneous");
|
||||
qt_config->setValue("log_filter", QString::fromStdString(Settings::values.log_filter));
|
||||
qt_config->setValue("use_dev_keys", Settings::values.use_dev_keys);
|
||||
qt_config->endGroup();
|
||||
|
||||
qt_config->beginGroup("Debugging");
|
||||
|
@ -365,7 +365,7 @@ void GameList::LoadInterfaceLayout() {
|
||||
item_model->sort(header->sortIndicatorSection(), header->sortIndicatorOrder());
|
||||
}
|
||||
|
||||
const QStringList GameList::supported_file_extensions = {"nso", "nro", "nca"};
|
||||
const QStringList GameList::supported_file_extensions = {"nso", "nro", "nca", "xci"};
|
||||
|
||||
static bool HasSupportedFileExtension(const std::string& file_name) {
|
||||
QFileInfo file = QFileInfo(file_name.c_str());
|
||||
|
@ -13,6 +13,7 @@
|
||||
#include <QMessageBox>
|
||||
#include <QtGui>
|
||||
#include <QtWidgets>
|
||||
#include <core/crypto/key_manager.h>
|
||||
#include "common/common_paths.h"
|
||||
#include "common/logging/backend.h"
|
||||
#include "common/logging/filter.h"
|
||||
@ -88,6 +89,19 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
|
||||
ui.setupUi(this);
|
||||
statusBar()->hide();
|
||||
|
||||
// Initialize keys
|
||||
std::string keys_dir = FileUtil::GetHactoolConfigurationPath();
|
||||
if (Settings::values.use_dev_keys) {
|
||||
Crypto::keys.SetValidationMode(true);
|
||||
if (FileUtil::Exists(keys_dir + DIR_SEP + "dev.keys"))
|
||||
Crypto::keys.LoadFromFile(keys_dir + DIR_SEP + "dev.keys", false);
|
||||
} else {
|
||||
if (FileUtil::Exists(keys_dir + DIR_SEP + "prod.keys"))
|
||||
Crypto::keys.LoadFromFile(keys_dir + DIR_SEP + "prod.keys", false);
|
||||
}
|
||||
if (FileUtil::Exists(keys_dir + DIR_SEP + "title.keys"))
|
||||
Crypto::keys.LoadFromFile(keys_dir + DIR_SEP + "title.keys", true);
|
||||
|
||||
default_theme_paths = QIcon::themeSearchPaths();
|
||||
UpdateUITheme();
|
||||
|
||||
|
@ -119,6 +119,7 @@ void Config::ReadValues() {
|
||||
|
||||
// Miscellaneous
|
||||
Settings::values.log_filter = sdl2_config->Get("Miscellaneous", "log_filter", "*:Trace");
|
||||
Settings::values.use_dev_keys = sdl2_config->GetBoolean("Miscellaneous", "use_dev_keys", false);
|
||||
|
||||
// Debugging
|
||||
Settings::values.use_gdbstub = sdl2_config->GetBoolean("Debugging", "use_gdbstub", false);
|
||||
|
@ -23,6 +23,7 @@
|
||||
#include "yuzu_cmd/emu_window/emu_window_sdl2.h"
|
||||
|
||||
#include <getopt.h>
|
||||
#include "core/crypto/key_manager.h"
|
||||
#ifndef _MSC_VER
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
@ -71,6 +72,20 @@ static void InitializeLogging() {
|
||||
/// Application entry point
|
||||
int main(int argc, char** argv) {
|
||||
Config config;
|
||||
|
||||
// Initialize keys
|
||||
std::string keys_dir = FileUtil::GetHactoolConfigurationPath();
|
||||
if (Settings::values.use_dev_keys) {
|
||||
Crypto::keys.SetValidationMode(true);
|
||||
if (FileUtil::Exists(keys_dir + DIR_SEP + "dev.keys"))
|
||||
Crypto::keys.LoadFromFile(keys_dir + DIR_SEP + "dev.keys", false);
|
||||
} else {
|
||||
if (FileUtil::Exists(keys_dir + DIR_SEP + "prod.keys"))
|
||||
Crypto::keys.LoadFromFile(keys_dir + DIR_SEP + "prod.keys", false);
|
||||
}
|
||||
if (FileUtil::Exists(keys_dir + DIR_SEP + "title.keys"))
|
||||
Crypto::keys.LoadFromFile(keys_dir + DIR_SEP + "title.keys", true);
|
||||
|
||||
int option_index = 0;
|
||||
bool use_gdbstub = Settings::values.use_gdbstub;
|
||||
u32 gdb_port = static_cast<u32>(Settings::values.gdbstub_port);
|
||||
|
Loading…
Reference in New Issue
Block a user