diff options
Diffstat (limited to 'src/core')
26 files changed, 1318 insertions, 62 deletions
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 884c28e20..28de22398 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -12,6 +12,16 @@ add_library(core STATIC core_timing.h core_timing_util.cpp core_timing_util.h + crypto/aes_util.cpp + 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 @@ -327,6 +337,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 @@ -346,7 +358,7 @@ add_library(core STATIC create_target_directory_groups(core) target_link_libraries(core PUBLIC common PRIVATE audio_core video_core) -target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt lz4_static opus unicorn) +target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt lz4_static mbedtls opus unicorn) if (ARCHITECTURE_x86_64) target_sources(core PRIVATE diff --git a/src/core/core.cpp b/src/core/core.cpp index 9824769cf..e51e66550 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -99,8 +99,10 @@ System::ResultStatus System::Load(EmuWindow& emu_window, const std::string& file static_cast<int>(system_mode.second)); switch (system_mode.second) { - case Loader::ResultStatus::ErrorEncrypted: - return ResultStatus::ErrorLoader_ErrorEncrypted; + case Loader::ResultStatus::ErrorMissingKeys: + return ResultStatus::ErrorLoader_ErrorMissingKeys; + case Loader::ResultStatus::ErrorDecrypting: + return ResultStatus::ErrorLoader_ErrorDecrypting; case Loader::ResultStatus::ErrorInvalidFormat: return ResultStatus::ErrorLoader_ErrorInvalidFormat; case Loader::ResultStatus::ErrorUnsupportedArch: @@ -124,8 +126,10 @@ System::ResultStatus System::Load(EmuWindow& emu_window, const std::string& file System::Shutdown(); switch (load_result) { - case Loader::ResultStatus::ErrorEncrypted: - return ResultStatus::ErrorLoader_ErrorEncrypted; + case Loader::ResultStatus::ErrorMissingKeys: + return ResultStatus::ErrorLoader_ErrorMissingKeys; + case Loader::ResultStatus::ErrorDecrypting: + return ResultStatus::ErrorLoader_ErrorDecrypting; case Loader::ResultStatus::ErrorInvalidFormat: return ResultStatus::ErrorLoader_ErrorInvalidFormat; case Loader::ResultStatus::ErrorUnsupportedArch: diff --git a/src/core/core.h b/src/core/core.h index ed475ac4e..4c9967559 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -43,12 +43,14 @@ public: /// Enumeration representing the return values of the System Initialize and Load process. enum class ResultStatus : u32 { - Success, ///< Succeeded - ErrorNotInitialized, ///< Error trying to use core prior to initialization - ErrorGetLoader, ///< Error finding the correct application loader - ErrorSystemMode, ///< Error determining the system mode - ErrorLoader, ///< Error loading the specified application - ErrorLoader_ErrorEncrypted, ///< Error loading the specified application due to encryption + Success, ///< Succeeded + ErrorNotInitialized, ///< Error trying to use core prior to initialization + ErrorGetLoader, ///< Error finding the correct application loader + ErrorSystemMode, ///< Error determining the system mode + ErrorLoader, ///< Error loading the specified application + ErrorLoader_ErrorMissingKeys, ///< Error because the key/keys needed to run could not be + ///< found. + ErrorLoader_ErrorDecrypting, ///< Error loading the specified application due to encryption ErrorLoader_ErrorInvalidFormat, ///< Error loading the specified application due to an /// invalid format ErrorSystemFiles, ///< Error in finding system files diff --git a/src/core/crypto/aes_util.cpp b/src/core/crypto/aes_util.cpp new file mode 100644 index 000000000..4690af5f8 --- /dev/null +++ b/src/core/crypto/aes_util.cpp @@ -0,0 +1,112 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <mbedtls/cipher.h> +#include "core/crypto/aes_util.h" +#include "core/crypto/key_manager.h" + +namespace Core::Crypto { + +static_assert(static_cast<size_t>(Mode::CTR) == static_cast<size_t>(MBEDTLS_CIPHER_AES_128_CTR), + "CTR has incorrect value."); +static_assert(static_cast<size_t>(Mode::ECB) == static_cast<size_t>(MBEDTLS_CIPHER_AES_128_ECB), + "ECB has incorrect value."); +static_assert(static_cast<size_t>(Mode::XTS) == static_cast<size_t>(MBEDTLS_CIPHER_AES_128_XTS), + "XTS has incorrect value."); + +// Structure to hide mbedtls types from header file +struct CipherContext { + mbedtls_cipher_context_t encryption_context; + mbedtls_cipher_context_t decryption_context; +}; + +template <typename Key, size_t KeySize> +Crypto::AESCipher<Key, KeySize>::AESCipher(Key key, Mode mode) + : ctx(std::make_unique<CipherContext>()) { + mbedtls_cipher_init(&ctx->encryption_context); + mbedtls_cipher_init(&ctx->decryption_context); + + ASSERT_MSG((mbedtls_cipher_setup( + &ctx->encryption_context, + mbedtls_cipher_info_from_type(static_cast<mbedtls_cipher_type_t>(mode))) || + mbedtls_cipher_setup( + &ctx->decryption_context, + mbedtls_cipher_info_from_type(static_cast<mbedtls_cipher_type_t>(mode)))) == 0, + "Failed to initialize mbedtls ciphers."); + + ASSERT( + !mbedtls_cipher_setkey(&ctx->encryption_context, key.data(), KeySize * 8, MBEDTLS_ENCRYPT)); + ASSERT( + !mbedtls_cipher_setkey(&ctx->decryption_context, key.data(), KeySize * 8, MBEDTLS_DECRYPT)); + //"Failed to set key on mbedtls ciphers."); +} + +template <typename Key, size_t KeySize> +AESCipher<Key, KeySize>::~AESCipher() { + mbedtls_cipher_free(&ctx->encryption_context); + mbedtls_cipher_free(&ctx->decryption_context); +} + +template <typename Key, size_t KeySize> +void AESCipher<Key, KeySize>::SetIV(std::vector<u8> iv) { + ASSERT_MSG((mbedtls_cipher_set_iv(&ctx->encryption_context, iv.data(), iv.size()) || + mbedtls_cipher_set_iv(&ctx->decryption_context, iv.data(), iv.size())) == 0, + "Failed to set IV on mbedtls ciphers."); +} + +template <typename Key, size_t KeySize> +void AESCipher<Key, KeySize>::Transcode(const u8* src, size_t size, u8* dest, Op op) { + size_t written = 0; + + const auto context = op == Op::Encrypt ? &ctx->encryption_context : &ctx->decryption_context; + + mbedtls_cipher_reset(context); + + if (mbedtls_cipher_get_cipher_mode(context) == MBEDTLS_MODE_XTS) { + mbedtls_cipher_update(context, src, size, 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, src + offset, length, 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 Key, size_t KeySize> +void AESCipher<Key, KeySize>::XTSTranscode(const u8* src, size_t size, u8* 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>(src + i, sector_size, dest + i, op); + } +} + +template <typename Key, size_t KeySize> +std::vector<u8> AESCipher<Key, KeySize>::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; +} + +template class AESCipher<Key128>; +template class AESCipher<Key256>; +} // namespace Core::Crypto
\ No newline at end of file diff --git a/src/core/crypto/aes_util.h b/src/core/crypto/aes_util.h new file mode 100644 index 000000000..5b0b02738 --- /dev/null +++ b/src/core/crypto/aes_util.h @@ -0,0 +1,62 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <type_traits> +#include <vector> +#include "common/assert.h" +#include "core/file_sys/vfs.h" + +namespace Core::Crypto { + +struct CipherContext; + +enum class Mode { + CTR = 11, + ECB = 2, + XTS = 70, +}; + +enum class Op { + Encrypt, + Decrypt, +}; + +template <typename Key, size_t KeySize = sizeof(Key)> +class 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."); + +public: + AESCipher(Key key, Mode mode); + + ~AESCipher(); + + void SetIV(std::vector<u8> iv); + + template <typename Source, typename Dest> + void Transcode(const Source* src, size_t size, Dest* dest, Op op) { + Transcode(reinterpret_cast<const u8*>(src), size, reinterpret_cast<u8*>(dest), op); + } + + void Transcode(const u8* src, size_t size, u8* dest, Op op); + + 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) { + XTSTranscode(reinterpret_cast<const u8*>(src), size, reinterpret_cast<u8*>(dest), sector_id, + sector_size, op); + } + + void XTSTranscode(const u8* src, size_t size, u8* dest, size_t sector_id, size_t sector_size, + Op op); + +private: + std::unique_ptr<CipherContext> ctx; + + static std::vector<u8> CalculateNintendoTweak(size_t sector_id); +}; +} // namespace Core::Crypto diff --git a/src/core/crypto/ctr_encryption_layer.cpp b/src/core/crypto/ctr_encryption_layer.cpp new file mode 100644 index 000000000..106db02b3 --- /dev/null +++ b/src/core/crypto/ctr_encryption_layer.cpp @@ -0,0 +1,56 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <cstring> +#include "common/assert.h" +#include "core/crypto/ctr_encryption_layer.h" + +namespace Core::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) { + std::memcpy(data, block.data() + sector_offset, std::min<u64>(length, read)); + return read; + } + std::memcpy(data, block.data() + sector_offset, read); + return read + Read(data + read, length - read, offset + read); +} + +void CTREncryptionLayer::SetIV(const std::vector<u8>& iv_) { + const auto length = std::min(iv_.size(), iv.size()); + iv.assign(iv_.cbegin(), iv_.cbegin() + length); +} + +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 Core::Crypto diff --git a/src/core/crypto/ctr_encryption_layer.h b/src/core/crypto/ctr_encryption_layer.h new file mode 100644 index 000000000..11b8683c7 --- /dev/null +++ b/src/core/crypto/ctr_encryption_layer.h @@ -0,0 +1,33 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <vector> +#include "core/crypto/aes_util.h" +#include "core/crypto/encryption_layer.h" +#include "core/crypto/key_manager.h" + +namespace Core::Crypto { + +// Sits on top of a VirtualFile and provides CTR-mode AES decription. +class CTREncryptionLayer : public EncryptionLayer { +public: + 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(const 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 Core::Crypto diff --git a/src/core/crypto/encryption_layer.cpp b/src/core/crypto/encryption_layer.cpp new file mode 100644 index 000000000..4204527e3 --- /dev/null +++ b/src/core/crypto/encryption_layer.cpp @@ -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 Core::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 Core::Crypto diff --git a/src/core/crypto/encryption_layer.h b/src/core/crypto/encryption_layer.h new file mode 100644 index 000000000..71bca1f23 --- /dev/null +++ b/src/core/crypto/encryption_layer.h @@ -0,0 +1,32 @@ +// 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 Core::Crypto { + +// Basically non-functional class that implements all of the methods that are irrelevant to an +// EncryptionLayer. Reduces duplicate code. +class EncryptionLayer : public FileSys::VfsFile { +public: + 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 Core::Crypto diff --git a/src/core/crypto/key_manager.cpp b/src/core/crypto/key_manager.cpp new file mode 100644 index 000000000..678ac5752 --- /dev/null +++ b/src/core/crypto/key_manager.cpp @@ -0,0 +1,215 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <array> +#include <fstream> +#include <locale> +#include <sstream> +#include <string_view> +#include <mbedtls/sha256.h> +#include "common/assert.h" +#include "common/common_paths.h" +#include "common/file_util.h" +#include "common/logging/log.h" +#include "core/crypto/key_manager.h" +#include "core/settings.h" +#include "key_manager.h" + +namespace Core::Crypto { + +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); +} + +KeyManager::KeyManager() { + // Initialize keys + const std::string hactool_keys_dir = FileUtil::GetHactoolConfigurationPath(); + const std::string yuzu_keys_dir = FileUtil::GetUserPath(FileUtil::UserPath::KeysDir); + if (Settings::values.use_dev_keys) { + dev_mode = true; + AttemptLoadKeyFile(yuzu_keys_dir, hactool_keys_dir, "dev.keys", false); + } else { + dev_mode = false; + AttemptLoadKeyFile(yuzu_keys_dir, hactool_keys_dir, "prod.keys", false); + } + + AttemptLoadKeyFile(yuzu_keys_dir, hactool_keys_dir, "title.keys", true); +} + +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{}; + std::memcpy(rights_id.data(), rights_id_raw.data(), rights_id_raw.size()); + 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.at(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.at(out[0]); + Key256 key = HexStringToArray<32>(out[1]); + SetKey(index.type, key, index.field1, index.field2); + } + } + } +} + +void KeyManager::AttemptLoadKeyFile(std::string_view dir1_, std::string_view dir2_, + std::string_view filename_, bool title) { + const std::string dir1(dir1_); + const std::string dir2(dir2_); + const std::string filename(filename_); + if (FileUtil::Exists(dir1 + DIR_SEP + filename)) + LoadFromFile(dir1 + DIR_SEP + filename, title); + else if (FileUtil::Exists(dir2 + DIR_SEP + filename)) + LoadFromFile(dir2 + DIR_SEP + filename, title); +} + +bool KeyManager::HasKey(S128KeyType id, u64 field1, u64 field2) const { + return s128_keys.find({id, field1, field2}) != s128_keys.end(); +} + +bool KeyManager::HasKey(S256KeyType id, u64 field1, u64 field2) const { + return s256_keys.find({id, field1, field2}) != s256_keys.end(); +} + +Key128 KeyManager::GetKey(S128KeyType id, u64 field1, u64 field2) const { + if (!HasKey(id, field1, field2)) + return {}; + return s128_keys.at({id, field1, field2}); +} + +Key256 KeyManager::GetKey(S256KeyType id, u64 field1, u64 field2) const { + if (!HasKey(id, field1, field2)) + return {}; + return s256_keys.at({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::KeyFileExists(bool title) { + const std::string hactool_keys_dir = FileUtil::GetHactoolConfigurationPath(); + const std::string yuzu_keys_dir = FileUtil::GetUserPath(FileUtil::UserPath::KeysDir); + if (title) { + return FileUtil::Exists(hactool_keys_dir + DIR_SEP + "title.keys") || + FileUtil::Exists(yuzu_keys_dir + DIR_SEP + "title.keys"); + } + + if (Settings::values.use_dev_keys) { + return FileUtil::Exists(hactool_keys_dir + DIR_SEP + "dev.keys") || + FileUtil::Exists(yuzu_keys_dir + DIR_SEP + "dev.keys"); + } + + return FileUtil::Exists(hactool_keys_dir + DIR_SEP + "prod.keys") || + FileUtil::Exists(yuzu_keys_dir + DIR_SEP + "prod.keys"); +} + +const 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::ETicketRSAKek, 0, 0}}, + {"key_area_key_application_00", + {S128KeyType::KeyArea, 0, static_cast<u64>(KeyAreaKeyType::Application)}}, + {"key_area_key_application_01", + {S128KeyType::KeyArea, 1, static_cast<u64>(KeyAreaKeyType::Application)}}, + {"key_area_key_application_02", + {S128KeyType::KeyArea, 2, static_cast<u64>(KeyAreaKeyType::Application)}}, + {"key_area_key_application_03", + {S128KeyType::KeyArea, 3, static_cast<u64>(KeyAreaKeyType::Application)}}, + {"key_area_key_application_04", + {S128KeyType::KeyArea, 4, static_cast<u64>(KeyAreaKeyType::Application)}}, + {"key_area_key_ocean_00", {S128KeyType::KeyArea, 0, static_cast<u64>(KeyAreaKeyType::Ocean)}}, + {"key_area_key_ocean_01", {S128KeyType::KeyArea, 1, static_cast<u64>(KeyAreaKeyType::Ocean)}}, + {"key_area_key_ocean_02", {S128KeyType::KeyArea, 2, static_cast<u64>(KeyAreaKeyType::Ocean)}}, + {"key_area_key_ocean_03", {S128KeyType::KeyArea, 3, static_cast<u64>(KeyAreaKeyType::Ocean)}}, + {"key_area_key_ocean_04", {S128KeyType::KeyArea, 4, static_cast<u64>(KeyAreaKeyType::Ocean)}}, + {"key_area_key_system_00", {S128KeyType::KeyArea, 0, static_cast<u64>(KeyAreaKeyType::System)}}, + {"key_area_key_system_01", {S128KeyType::KeyArea, 1, static_cast<u64>(KeyAreaKeyType::System)}}, + {"key_area_key_system_02", {S128KeyType::KeyArea, 2, static_cast<u64>(KeyAreaKeyType::System)}}, + {"key_area_key_system_03", {S128KeyType::KeyArea, 3, static_cast<u64>(KeyAreaKeyType::System)}}, + {"key_area_key_system_04", {S128KeyType::KeyArea, 4, static_cast<u64>(KeyAreaKeyType::System)}}, +}; + +const std::unordered_map<std::string, KeyIndex<S256KeyType>> KeyManager::s256_file_id = { + {"header_key", {S256KeyType::Header, 0, 0}}, + {"sd_card_save_key", {S256KeyType::SDSave, 0, 0}}, + {"sd_card_nca_key", {S256KeyType::SDNCA, 0, 0}}, +}; +} // namespace Core::Crypto diff --git a/src/core/crypto/key_manager.h b/src/core/crypto/key_manager.h new file mode 100644 index 000000000..03152a12c --- /dev/null +++ b/src/core/crypto/key_manager.h @@ -0,0 +1,119 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <array> +#include <type_traits> +#include <unordered_map> +#include <vector> +#include <fmt/format.h> +#include "common/common_types.h" + +namespace Core::Crypto { + +using Key128 = std::array<u8, 0x10>; +using Key256 = std::array<u8, 0x20>; +using SHA256Hash = std::array<u8, 0x20>; + +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, // + SDSave, // + SDNCA, // +}; + +enum class S128KeyType : u64 { + Master, // f1=crypto revision + Package1, // f1=crypto revision + Package2, // f1=crypto revision + Titlekek, // f1=crypto revision + ETicketRSAKek, // + KeyArea, // f1=crypto revision f2=type {app, ocean, system} + SDSeed, // + 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() const { + u8 key_size = 16; + if constexpr (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 std::tie(lhs.type, lhs.field1, lhs.field2) == std::tie(rhs.type, rhs.field1, rhs.field2); +} + +template <typename KeyType> +bool operator!=(const KeyIndex<KeyType>& lhs, const KeyIndex<KeyType>& rhs) { + return !operator==(lhs, rhs); +} + +} // namespace Core::Crypto + +namespace std { +template <typename KeyType> +struct hash<Core::Crypto::KeyIndex<KeyType>> { + size_t operator()(const Core::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 Core::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); + +class KeyManager { +public: + KeyManager(); + + bool HasKey(S128KeyType id, u64 field1 = 0, u64 field2 = 0) const; + bool HasKey(S256KeyType id, u64 field1 = 0, u64 field2 = 0) const; + + Key128 GetKey(S128KeyType id, u64 field1 = 0, u64 field2 = 0) const; + Key256 GetKey(S256KeyType id, u64 field1 = 0, u64 field2 = 0) const; + + void SetKey(S128KeyType id, Key128 key, u64 field1 = 0, u64 field2 = 0); + void SetKey(S256KeyType id, Key256 key, u64 field1 = 0, u64 field2 = 0); + + static bool KeyFileExists(bool title); + +private: + std::unordered_map<KeyIndex<S128KeyType>, Key128> s128_keys; + std::unordered_map<KeyIndex<S256KeyType>, Key256> s256_keys; + + bool dev_mode; + void LoadFromFile(std::string_view filename, bool is_title_keys); + void AttemptLoadKeyFile(std::string_view dir1, std::string_view dir2, std::string_view filename, + bool title); + + static const std::unordered_map<std::string, KeyIndex<S128KeyType>> s128_file_id; + static const std::unordered_map<std::string, KeyIndex<S256KeyType>> s256_file_id; +}; +} // namespace Core::Crypto diff --git a/src/core/crypto/sha_util.cpp b/src/core/crypto/sha_util.cpp new file mode 100644 index 000000000..180008a85 --- /dev/null +++ b/src/core/crypto/sha_util.cpp @@ -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 diff --git a/src/core/crypto/sha_util.h b/src/core/crypto/sha_util.h new file mode 100644 index 000000000..fa3fa9d33 --- /dev/null +++ b/src/core/crypto/sha_util.h @@ -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 diff --git a/src/core/file_sys/card_image.cpp b/src/core/file_sys/card_image.cpp new file mode 100644 index 000000000..395eea8ae --- /dev/null +++ b/src/core/file_sys/card_image.cpp @@ -0,0 +1,149 @@ +// 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; + } + + static constexpr std::array<const char*, 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 { + const auto iter = + std::find_if(ncas.begin(), ncas.end(), + [type](const std::shared_ptr<NCA>& nca) { return nca->GetType() == type; }); + return iter == ncas.end() ? nullptr : *iter; +} + +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 (const 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 diff --git a/src/core/file_sys/card_image.h b/src/core/file_sys/card_image.h new file mode 100644 index 000000000..e089d737c --- /dev/null +++ b/src/core/file_sys/card_image.h @@ -0,0 +1,96 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <array> +#include <vector> +#include "common/common_types.h" +#include "common/swap.h" +#include "core/file_sys/content_archive.h" +#include "core/file_sys/vfs.h" +#include "core/loader/loader.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 diff --git a/src/core/file_sys/content_archive.cpp b/src/core/file_sys/content_archive.cpp index 61cb0bbe3..79e70f6ef 100644 --- a/src/core/file_sys/content_archive.cpp +++ b/src/core/file_sys/content_archive.cpp @@ -4,12 +4,14 @@ #include <algorithm> #include <utility> - +#include <boost/optional.hpp> #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/romfs.h" #include "core/file_sys/vfs_offset.h" #include "core/loader/loader.h" -#include "romfs.h" namespace FileSys { @@ -29,11 +31,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,67 +53,170 @@ 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'); +} + +boost::optional<Core::Crypto::Key128> NCA::GetKeyAreaKey(NCASectionCryptoType type) const { + 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; + + if (!keys.HasKey(Core::Crypto::S128KeyType::KeyArea, master_key_id, header.key_index)) + return boost::none; + + std::vector<u8> key_area(header.key_area.begin(), header.key_area.end()); + Core::Crypto::AESCipher<Core::Crypto::Key128> cipher( + keys.GetKey(Core::Crypto::S128KeyType::KeyArea, master_key_id, header.key_index), + Core::Crypto::Mode::ECB); + cipher.Transcode(key_area.data(), key_area.size(), key_area.data(), Core::Crypto::Op::Decrypt); + + Core::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{}; + memcpy(out_128.data(), out.data(), 16); + 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) const { + 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); + { + const auto key = GetKeyAreaKey(NCASectionCryptoType::CTR); + if (key == boost::none) + return nullptr; + auto out = std::make_shared<Core::Crypto::CTREncryptionLayer>( + std::move(in), key.value(), starting_offset); + std::vector<u8> iv(16); + for (u8 i = 0; i < 8; ++i) + iv[i] = header.raw.section_ctr[0x8 - i - 1]; + out->SetIV(iv); + return std::static_pointer_cast<VfsFile>(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{}; + Core::Crypto::AESCipher<Core::Crypto::Key256> cipher( + keys.GetKey(Core::Crypto::S256KeyType::Header), Core::Crypto::Mode::XTS); + cipher.XTSTranscode(&header, sizeof(NCAHeader), &dec_header, 0, 0x200, + Core::Crypto::Op::Decrypt); + if (IsValidNCA(dec_header)) { + header = dec_header; + encrypted = true; + } else { + if (!keys.HasKey(Core::Crypto::S256KeyType::Header)) + status = Loader::ResultStatus::ErrorMissingKeys; + else + status = Loader::ResultStatus::ErrorDecrypting; + 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); + Core::Crypto::AESCipher<Core::Crypto::Key256> cipher( + keys.GetKey(Core::Crypto::S256KeyType::Header), Core::Crypto::Mode::XTS); + cipher.XTSTranscode(raw.data(), length_sections, sections.data(), 2, SECTION_HEADER_SIZE, + Core::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)); - 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."); - + section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset; + const size_t romfs_size = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].size; + auto dec = + Decrypt(section, std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset), + romfs_offset); + if (dec != nullptr) { + files.push_back(std::move(dec)); + romfs = files.back(); + } else { + status = Loader::ResultStatus::ErrorMissingKeys; + return; + } + } 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)); + auto dec = + Decrypt(section, std::make_shared<OffsetVfsFile>(file, size, offset), offset); + if (dec != nullptr) { + auto npfs = std::make_shared<PartitionFilesystem>(std::move(dec)); - if (npfs->GetStatus() == Loader::ResultStatus::Success) { - dirs.emplace_back(npfs); - if (IsDirectoryExeFS(dirs.back())) - exefs = dirs.back(); + if (npfs->GetStatus() == Loader::ResultStatus::Success) { + dirs.push_back(std::move(npfs)); + if (IsDirectoryExeFS(dirs.back())) + exefs = dirs.back(); + } + } else { + status = Loader::ResultStatus::ErrorMissingKeys; + return; } } } @@ -153,6 +266,10 @@ VirtualDir NCA::GetExeFS() const { return exefs; } +VirtualFile NCA::GetBaseFile() const { + return file; +} + bool NCA::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) { return false; } diff --git a/src/core/file_sys/content_archive.h b/src/core/file_sys/content_archive.h index 0b8b9db61..6492163b5 100644 --- a/src/core/file_sys/content_archive.h +++ b/src/core/file_sys/content_archive.h @@ -8,14 +8,18 @@ #include <memory> #include <string> #include <vector> - +#include <boost/optional.hpp> #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" +#include "core/loader/loader.h" namespace FileSys { +union NCASectionHeader; + enum class NCAContentType : u8 { Program = 0, Meta = 1, @@ -24,6 +28,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,7 +59,7 @@ 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."); @@ -58,10 +69,7 @@ inline bool IsDirectoryExeFS(const std::shared_ptr<VfsDirectory>& pfs) { 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,10 +89,15 @@ public: VirtualFile GetRomFS() const; VirtualDir GetExeFS() const; + VirtualFile GetBaseFile() const; + protected: bool ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) override; private: + boost::optional<Core::Crypto::Key128> GetKeyAreaKey(NCASectionCryptoType type) const; + VirtualFile Decrypt(NCASectionHeader header, VirtualFile in, u64 starting_offset) const; + std::vector<VirtualDir> dirs; std::vector<VirtualFile> files; @@ -95,6 +108,10 @@ private: NCAHeader header{}; Loader::ResultStatus status{}; + + bool encrypted; + + Core::Crypto::KeyManager keys; }; } // namespace FileSys diff --git a/src/core/file_sys/vfs.cpp b/src/core/file_sys/vfs.cpp index 84a6a7397..dae1c16ef 100644 --- a/src/core/file_sys/vfs.cpp +++ b/src/core/file_sys/vfs.cpp @@ -285,6 +285,26 @@ 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; + auto iters = std::mismatch(f1_v.begin(), f1_v.end(), f2_v.begin(), f2_v.end()); + if (iters.first != f1_v.end() && iters.second != f2_v.end()) + return false; + } + + return true; +} + bool VfsRawCopy(VirtualFile src, VirtualFile dest) { if (src == nullptr || dest == nullptr) return false; diff --git a/src/core/file_sys/vfs.h b/src/core/file_sys/vfs.h index cf871edd6..fab9e2b45 100644 --- a/src/core/file_sys/vfs.h +++ b/src/core/file_sys/vfs.h @@ -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. diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp index cbc4177c6..57e6c0365 100644 --- a/src/core/loader/loader.cpp +++ b/src/core/loader/loader.cpp @@ -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)); diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index 3ca6bcf8b..e69ab85ef 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -31,6 +31,7 @@ enum class FileType { NSO, NRO, NCA, + XCI, DeconstructedRomDirectory, }; @@ -72,7 +73,8 @@ enum class ResultStatus { ErrorNotUsed, ErrorAlreadyLoaded, ErrorMemoryAllocationFailed, - ErrorEncrypted, + ErrorMissingKeys, + ErrorDecrypting, ErrorUnsupportedArch, }; diff --git a/src/core/loader/nca.cpp b/src/core/loader/nca.cpp index c80df23be..a1f8235d1 100644 --- a/src/core/loader/nca.cpp +++ b/src/core/loader/nca.cpp @@ -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 diff --git a/src/core/loader/nca.h b/src/core/loader/nca.h index 2edd81cb9..e14d618b3 100644 --- a/src/core/loader/nca.h +++ b/src/core/loader/nca.h @@ -33,6 +33,8 @@ public: ResultStatus ReadRomFS(FileSys::VirtualFile& dir) override; + ResultStatus ReadProgramId(u64& out_program_id) override; + ~AppLoader_NCA(); private: diff --git a/src/core/loader/xci.cpp b/src/core/loader/xci.cpp new file mode 100644 index 000000000..eb4dee2c2 --- /dev/null +++ b/src/core/loader/xci.cpp @@ -0,0 +1,74 @@ +// 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))) {} + +AppLoader_XCI::~AppLoader_XCI() = default; + +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; + } + + if (xci->GetNCAFileByType(FileSys::NCAContentType::Program) == nullptr) { + if (!Core::Crypto::KeyManager::KeyFileExists(false)) + return ResultStatus::ErrorMissingKeys; + return ResultStatus::ErrorDecrypting; + } + + 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); +} + +} // namespace Loader diff --git a/src/core/loader/xci.h b/src/core/loader/xci.h new file mode 100644 index 000000000..0dbcfbdf8 --- /dev/null +++ b/src/core/loader/xci.h @@ -0,0 +1,44 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include "common/common_types.h" +#include "core/file_sys/card_image.h" +#include "core/loader/loader.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); + ~AppLoader_XCI(); + + /** + * 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; + +private: + FileSys::ProgramMetadata metadata; + + std::unique_ptr<FileSys::XCI> xci; + std::unique_ptr<AppLoader_NCA> nca_loader; +}; + +} // namespace Loader diff --git a/src/core/settings.h b/src/core/settings.h index e826d4235..73dc3061f 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -139,6 +139,8 @@ struct Values { std::string log_filter; + bool use_dev_keys; + // Audio std::string sink_id; std::string audio_device_id; |