Pointless huge refactor

This commit is contained in:
Pengfei
2021-08-04 11:11:52 +08:00
parent 22ebf68b90
commit c7b66f6308
34 changed files with 84 additions and 84 deletions
+164
View File
@@ -0,0 +1,164 @@
// Copyright 2021 Pengfei Zhu
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <cstring>
#include <unordered_map>
#include <cryptopp/integer.h>
#include "common/alignment.h"
#include "common/assert.h"
#include "common/common_funcs.h"
#include "common/file_util.h"
#include "common/logging/log.h"
#include "common/string_util.h"
#include "core/file_sys/certificate.h"
#include "core/file_sys/cia_common.h"
#include "core/file_sys/data/data_container.h"
namespace Core {
// Sizes include padding (0x34 for RSA, 0x3C for ECC)
inline std::size_t GetPublicKeySize(u32 public_key_type) {
switch (public_key_type) {
case PublicKeyType::RSA_4096:
return 0x238;
case PublicKeyType::RSA_2048:
return 0x138;
case PublicKeyType::ECC:
return 0x78;
}
LOG_ERROR(Common_Filesystem, "Tried to read cert with bad public key {}", public_key_type);
return 0;
}
bool Certificate::Load(std::vector<u8> file_data, std::size_t offset) {
const auto total_size = static_cast<std::size_t>(file_data.size() - offset);
if (!signature.Load(file_data, offset)) {
return false;
}
// certificate body
const auto signature_size = signature.GetSize();
TRY_MEMCPY(&body, file_data, offset + signature_size, sizeof(Body));
// Public key lengths are variable
const auto public_key_size = GetPublicKeySize(body.key_type);
if (public_key_size == 0) {
return false;
}
public_key.resize(public_key_size);
const auto public_key_offset = offset + signature_size + sizeof(Body);
TRY_MEMCPY(public_key.data(), file_data, public_key_offset, public_key.size());
return true;
}
bool Certificate::Save(FileUtil::IOFile& file) const {
// signature
if (!signature.Save(file)) {
return false;
}
// body
if (file.WriteBytes(&body, sizeof(body)) != sizeof(body)) {
LOG_ERROR(Core, "Failed to write body");
return false;
}
// public key
if (file.WriteBytes(public_key.data(), public_key.size()) != public_key.size()) {
LOG_ERROR(Core, "Failed to write public key");
return false;
}
return true;
}
std::size_t Certificate::GetSize() const {
return signature.GetSize() + sizeof(Body) + public_key.size();
}
std::pair<CryptoPP::Integer, CryptoPP::Integer> Certificate::GetRSAPublicKey() const {
if (body.key_type == PublicKeyType::RSA_2048) {
return {CryptoPP::Integer(public_key.data(), 0x100),
CryptoPP::Integer(public_key.data() + 0x100, 0x4)};
} else if (body.key_type == PublicKeyType::RSA_4096) {
return {CryptoPP::Integer(public_key.data(), 0x200),
CryptoPP::Integer(public_key.data() + 0x200, 0x4)};
} else {
UNREACHABLE_MSG("Certificate is not RSA");
}
}
namespace Certs {
static std::unordered_map<std::string, Certificate> g_certs;
static bool g_is_loaded = false;
bool Load(const std::string& path) {
g_certs.clear();
FileUtil::IOFile file(path, "rb");
DataContainer container(file.GetData());
std::vector<std::vector<u8>> data;
if (!container.IsGood() || !container.GetIVFCLevel4Data(data)) {
return false;
}
CertsDBHeader header;
TRY_MEMCPY(&header, data[0], 0, sizeof(header));
if (header.magic != MakeMagic('C', 'E', 'R', 'T')) {
LOG_ERROR(Core, "File is invalid {}", path);
return false;
}
const auto total_size = header.size + sizeof(header);
if (data[0].size() < total_size) {
LOG_ERROR(Core, "File {} header reports invalid size, may be corrupted", path);
return false;
}
std::size_t pos = sizeof(header);
while (pos < total_size) {
Certificate cert;
if (!cert.Load(data[0], pos)) { // Failed to load
return false;
}
pos += cert.GetSize();
const auto issuer = Common::StringFromFixedZeroTerminatedBuffer(cert.body.issuer.data(),
cert.body.issuer.size());
const auto name = Common::StringFromFixedZeroTerminatedBuffer(cert.body.name.data(),
cert.body.name.size());
const auto full_name = issuer + "-" + name;
g_certs.emplace(full_name, std::move(cert));
}
for (const auto& cert : CIACertNames) {
if (!g_certs.count(cert)) {
LOG_ERROR(Core, "Cert {} required for CIA building but does not exist", cert);
return false;
}
}
g_is_loaded = true;
return true;
}
bool IsLoaded() {
return g_is_loaded;
}
const Certificate& Get(const std::string& name) {
return g_certs.at(name);
}
bool Exists(const std::string& name) {
return g_certs.count(name);
}
} // namespace Certs
} // namespace Core
+69
View File
@@ -0,0 +1,69 @@
// Copyright 2021 Pengfei Zhu
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <array>
#include <string>
#include <vector>
#include "common/common_funcs.h"
#include "common/swap.h"
#include "core/file_sys/signature.h"
namespace CryptoPP {
class Integer;
}
namespace FileUtil {
class IOFile;
}
namespace Core {
enum PublicKeyType : u32 {
RSA_4096 = 0,
RSA_2048 = 1,
ECC = 2,
};
class Certificate {
public:
struct Body {
std::array<char, 0x40> issuer;
u32_be key_type;
std::array<char, 0x40> name;
u32_be expiration_time;
};
static_assert(sizeof(Body) == 0x88);
bool Load(std::vector<u8> file_data, std::size_t offset = 0);
bool Save(FileUtil::IOFile& file) const;
std::size_t GetSize() const;
/// (modulus, exponent)
std::pair<CryptoPP::Integer, CryptoPP::Integer> GetRSAPublicKey() const;
Signature signature;
Body body;
std::vector<u8> public_key;
};
struct CertsDBHeader {
u32_le magic;
INSERT_PADDING_BYTES(4);
u32_le size;
INSERT_PADDING_BYTES(4);
};
static_assert(sizeof(CertsDBHeader) == 0x10);
namespace Certs {
bool Load(const std::string& path);
bool IsLoaded();
const Certificate& Get(const std::string& name);
bool Exists(const std::string& name);
} // namespace Certs
} // namespace Core
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2018 Citra Emulator Project / 2021 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <array>
#include "common/common_types.h"
namespace Core {
/// Full names of the certificates contained in a CIA.
constexpr std::array<const char*, 3> CIACertNames{{
"Root-CA00000003",
"Root-CA00000003-XS0000000c",
"Root-CA00000003-CP0000000b",
}};
enum class CIABuildType {
Standard, /// Decrypted CIA with generalized ticket
PirateLegit, /// Uses legit TMD and encryption, but with generalized ticket
Legit, /// Fully legit, with personal ticket containing console ID and eshop account
};
} // namespace Core
+212
View File
@@ -0,0 +1,212 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <cmath>
#include "common/assert.h"
#include "common/common_funcs.h"
#include "core/file_sys/data/data_container.h"
namespace Core {
DPFSContainer::DPFSContainer(DPFSDescriptor descriptor_, u8 level1_selector_,
std::vector<u32_le> data_)
: descriptor(std::move(descriptor_)), level1_selector(level1_selector_),
data(std::move(data_)) {
ASSERT_MSG(descriptor.magic == MakeMagic('D', 'P', 'F', 'S'), "DPFS Magic is not correct");
ASSERT_MSG(descriptor.version == 0x10000, "DPFS Version is not correct");
}
bool DPFSContainer::GetBit(u8& out, u8 level, u8 selector, u64 index) const {
ASSERT_MSG(level <= 2 && selector <= 1, "Level or selector invalid");
const auto word =
(descriptor.levels[level].offset + selector * descriptor.levels[level].size) / 4 +
index / 32;
if (data.size() <= word) {
LOG_ERROR(Core, "Out of bound: level {} selector {} index {}", level, selector, index);
return false;
}
out = (data[word] >> (31 - (index % 32))) & static_cast<u32_le>(1);
return true;
}
bool DPFSContainer::GetByte(u8& out, u8 level, u8 selector, u64 index) const {
ASSERT_MSG(level <= 2 && selector <= 1, "Level or selector invalid");
const auto byte =
descriptor.levels[level].offset + selector * descriptor.levels[level].size + index;
if (data.size() * 4 <= byte) {
LOG_ERROR(Core, "Out of bound: level {} selector {} index {}", level, selector, index);
return false;
}
out =
reinterpret_cast<const u8*>(data.data())[descriptor.levels[level].offset +
selector * descriptor.levels[level].size + index];
return true;
}
bool DPFSContainer::GetLevel3Data(std::vector<u8>& out) const {
std::vector<u8> level3_data(descriptor.levels[2].size);
for (std::size_t i = 0; i < level3_data.size(); i++) {
auto level2_bit_index = i / std::pow(2, descriptor.levels[2].block_size);
auto level1_bit_index =
(level2_bit_index / 8) / std::pow(2, descriptor.levels[1].block_size);
u8 level2_selector, level3_selector;
if (!GetBit(level2_selector, 0, level1_selector, level1_bit_index) ||
!GetBit(level3_selector, 1, level2_selector, level2_bit_index) ||
!GetByte(level3_data[i], 2, level3_selector, i)) {
return false;
}
}
out = std::move(level3_data);
return true;
}
DataContainer::DataContainer(std::vector<u8> data_) : data(std::move(data_)) {
if (data.size() < 0x200) {
LOG_ERROR(Core, "Data size {:X} is too small", data.size());
is_good = false;
return;
}
u32_le magic;
std::memcpy(&magic, data.data() + 0x100, sizeof(u32_le));
if (magic == MakeMagic('D', 'I', 'S', 'A')) {
is_good = InitAsDISA();
} else if (magic == MakeMagic('D', 'I', 'F', 'F')) {
is_good = InitAsDIFF();
} else {
LOG_ERROR(Core, "Unknown magic 0x{:08x}", magic);
is_good = false;
}
}
DataContainer::~DataContainer() = default;
bool DataContainer::IsGood() const {
return is_good;
}
bool DataContainer::InitAsDISA() {
DISAHeader header;
TRY_MEMCPY(&header, data, 0x100, sizeof(header));
if (header.version != 0x40000) {
LOG_ERROR(Core, "DISA Version {:x} is not correct", header.version);
return false;
}
if (header.active_partition_table == 0) { // primary
partition_table_offset = header.primary_partition_table_offset;
} else {
partition_table_offset = header.secondary_partition_table_offset;
}
partition_count = header.partition_count;
if (header.partition_count == 2) {
partition_descriptors = {header.partition_descriptors[0], header.partition_descriptors[1]};
partitions = {header.partitions[0], header.partitions[1]};
} else {
partition_descriptors = {header.partition_descriptors[0]};
partitions = {header.partitions[0]};
}
return true;
}
bool DataContainer::InitAsDIFF() {
DIFFHeader header;
TRY_MEMCPY(&header, data, 0x100, sizeof(header));
if (header.version != 0x30000) {
LOG_ERROR(Core, "DIFF Version {:x} is not correct", header.version);
return false;
}
if (header.active_partition_table == 0) { // primary
partition_table_offset = header.primary_partition_table_offset;
} else {
partition_table_offset = header.secondary_partition_table_offset;
}
partition_count = 1;
partition_descriptors = {{/* offset */ 0, /* size */ header.partition_table_size}};
partitions = {header.partition_A};
return true;
}
bool DataContainer::GetPartitionData(std::vector<u8>& out, u8 index) const {
auto partition_descriptor_offset = partition_table_offset + partition_descriptors[index].offset;
DIFIHeader difi;
TRY_MEMCPY(&difi, data, partition_descriptor_offset, sizeof(difi));
if (difi.magic != MakeMagic('D', 'I', 'F', 'I') || difi.version != 0x10000) {
LOG_ERROR(Core, "Invalid magic {:08x} or version {}", difi.magic, difi.version);
return false;
}
ASSERT_MSG(difi.ivfc.size >= sizeof(IVFCDescriptor), "IVFC descriptor size is too small");
IVFCDescriptor ivfc_descriptor;
TRY_MEMCPY(&ivfc_descriptor, data, partition_descriptor_offset + difi.ivfc.offset,
sizeof(ivfc_descriptor));
if (difi.enable_external_IVFC_level_4) {
if (data.size() < partitions[index].offset + difi.external_IVFC_level_4_offset +
ivfc_descriptor.levels[3].size) {
LOG_ERROR(Core, "File size is too small");
return false;
}
out = std::vector<u8>(
data.data() + partitions[index].offset + difi.external_IVFC_level_4_offset,
data.data() + partitions[index].offset + difi.external_IVFC_level_4_offset +
ivfc_descriptor.levels[3].size);
return true;
}
// Unwrap DPFS Tree
ASSERT_MSG(difi.dpfs.size >= sizeof(DPFSDescriptor), "DPFS descriptor size is too small");
DPFSDescriptor dpfs_descriptor;
TRY_MEMCPY(&dpfs_descriptor, data, partition_descriptor_offset + difi.dpfs.offset,
sizeof(dpfs_descriptor));
std::vector<u32_le> partition_data(partitions[index].size / 4);
TRY_MEMCPY(partition_data.data(), data, partitions[index].offset, partitions[index].size);
DPFSContainer dpfs_container(std::move(dpfs_descriptor), difi.dpfs_level1_selector,
std::move(partition_data));
std::vector<u8> ivfc_data;
if (!dpfs_container.GetLevel3Data(ivfc_data)) {
return false;
}
if (ivfc_data.size() < ivfc_descriptor.levels[3].offset + ivfc_descriptor.levels[3].size) {
LOG_ERROR(Core, "IVFC data size is too small");
return false;
}
out = std::vector<u8>(ivfc_data.data() + ivfc_descriptor.levels[3].offset,
ivfc_data.data() + ivfc_descriptor.levels[3].offset +
ivfc_descriptor.levels[3].size);
return true;
}
bool DataContainer::GetIVFCLevel4Data(std::vector<std::vector<u8>>& out) const {
if (partition_count == 1) {
out.resize(1);
return GetPartitionData(out[0], 0);
} else {
out.resize(2);
return GetPartitionData(out[0], 0) && GetPartitionData(out[1], 1);
}
}
} // namespace Core
+136
View File
@@ -0,0 +1,136 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <array>
#include <vector>
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/swap.h"
namespace Core {
#pragma pack(push, 1)
struct DataDescriptor {
u64_le offset;
u64_le size;
};
struct DISAHeader {
u32_le magic;
u32_le version;
u32_le partition_count;
INSERT_PADDING_BYTES(4);
u64_le secondary_partition_table_offset;
u64_le primary_partition_table_offset;
u64_le partition_table_size;
std::array<DataDescriptor, 2> partition_descriptors;
std::array<DataDescriptor, 2> partitions;
u8 active_partition_table;
INSERT_PADDING_BYTES(3);
std::array<u8, 0x20> sha_hash;
INSERT_PADDING_BYTES(0x74);
};
static_assert(sizeof(DISAHeader) == 0x100, "Size of DISA header is incorrect");
struct DIFFHeader {
u32_le magic;
u32_le version;
u64_le secondary_partition_table_offset;
u64_le primary_partition_table_offset;
u64_le partition_table_size;
DataDescriptor partition_A;
u8 active_partition_table;
INSERT_PADDING_BYTES(3);
std::array<u8, 0x20> sha_hash;
u64_le unique_identifier;
INSERT_PADDING_BYTES(0xA4);
};
static_assert(sizeof(DIFFHeader) == 0x100, "Size of DIFF header is incorrect");
struct DIFIHeader {
u32_le magic;
u32_le version;
DataDescriptor ivfc;
DataDescriptor dpfs;
DataDescriptor partition_hash;
u8 enable_external_IVFC_level_4;
u8 dpfs_level1_selector;
INSERT_PADDING_BYTES(2);
u64_le external_IVFC_level_4_offset;
};
static_assert(sizeof(DIFIHeader) == 0x44, "Size of DIFI header is incorrect");
/// Descriptor for both IVFC and DPFS levels
struct LevelDescriptor {
u64_le offset;
u64_le size;
u32_le block_size; // In log2
INSERT_PADDING_BYTES(4);
};
static_assert(sizeof(LevelDescriptor) == 0x18, "Size of level descriptor is incorrect");
struct IVFCDescriptor {
u32_le magic;
u32_le version;
u64_le master_hash_size;
std::array<LevelDescriptor, 4> levels;
u64_le descriptor_size;
};
static_assert(sizeof(IVFCDescriptor) == 0x78, "Size of IVFC descriptor is incorrect");
struct DPFSDescriptor {
u32_le magic;
u32_le version;
std::array<LevelDescriptor, 3> levels;
};
static_assert(sizeof(DPFSDescriptor) == 0x50, "Size of DPFS descriptor is incorrect");
#pragma pack(pop)
class DPFSContainer {
public:
explicit DPFSContainer(DPFSDescriptor descriptor, u8 level1_selector, std::vector<u32_le> data);
/// Unwraps the DPFS Tree, returning actual data in Level3.
bool GetLevel3Data(std::vector<u8>& out) const;
private:
bool GetBit(u8& out, u8 level, u8 selector, u64 index) const;
bool GetByte(u8& out, u8 level, u8 selector, u64 index) const;
DPFSDescriptor descriptor;
u8 level1_selector;
std::vector<u32_le> data;
};
/**
* DISA/DIFF Container.
*/
class DataContainer {
public:
explicit DataContainer(std::vector<u8> data);
~DataContainer();
/// Unwraps the whole container, returning the data in IVFC Level 4 of all partitions.
bool GetIVFCLevel4Data(std::vector<std::vector<u8>>& out) const;
bool IsGood() const;
private:
bool InitAsDISA();
bool InitAsDIFF();
/// Unwraps the whole container, returning the data in IVFC Level 4 of a partition.
bool GetPartitionData(std::vector<u8>& out, u8 index) const;
bool is_good = false;
std::vector<u8> data;
u32 partition_count;
u64_le partition_table_offset;
std::vector<DataDescriptor> partition_descriptors;
std::vector<DataDescriptor> partitions;
};
} // namespace Core
+130
View File
@@ -0,0 +1,130 @@
// Copyright 2021 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "core/decryptor.h"
#include "core/file_sys/data/data_container.h"
#include "core/file_sys/data/extdata.h"
namespace Core {
Extdata::Extdata(std::string data_path_, const SDMCDecryptor& decryptor_)
: data_path(std::move(data_path_)), decryptor(&decryptor_) {
if (data_path.back() != '/' && data_path.back() != '\\') {
data_path += '/';
}
use_decryptor = true;
is_good = Init();
}
Extdata::Extdata(std::string data_path_) : data_path(std::move(data_path_)) {
if (data_path.back() != '/' && data_path.back() != '\\') {
data_path += '/';
}
use_decryptor = false;
is_good = Init();
}
Extdata::~Extdata() = default;
bool Extdata::CheckMagic() const {
if (header.fat_header.magic != MakeMagic('V', 'S', 'X', 'E') ||
header.fat_header.version != 0x30000) {
LOG_ERROR(Core, "File is invalid, decryption errors may have happened.");
return false;
}
return true;
}
bool Extdata::IsGood() const {
return is_good;
}
bool Extdata::Extract(std::string path) const {
if (path.back() != '/' && path.back() != '\\') {
path += '/';
}
if (!ExtractDirectory(path, 1)) {
return false;
}
// Write format info
const auto format_info = GetFormatInfo();
return FileUtil::WriteBytesToFile(path + "metadata", &format_info, sizeof(format_info));
}
std::vector<u8> Extdata::ReadFile(const std::string& path) const {
if (use_decryptor) {
return decryptor->DecryptFile(path);
} else {
FileUtil::IOFile file(path, "rb");
return file.GetData();
}
}
bool Extdata::Init() {
// Read VSXE file
auto vsxe_raw = ReadFile(data_path + "00000000/00000001");
if (vsxe_raw.empty()) {
LOG_ERROR(Core, "Failed to load or decrypt VSXE");
return false;
}
DataContainer vsxe_container(std::move(vsxe_raw));
if (!vsxe_container.IsGood()) {
return false;
}
std::vector<std::vector<u8>> data;
if (!vsxe_container.GetIVFCLevel4Data(data)) {
return false;
}
return Archive<Extdata>::Init(std::move(data));
}
bool Extdata::ExtractFile(const std::string& path, std::size_t index) const {
/// Maximum amount of device files a device directory can hold.
constexpr u32 DeviceDirCapacity = 126;
u32 file_index = index + 1;
u32 sub_directory_id = file_index / DeviceDirCapacity;
u32 sub_file_id = file_index % DeviceDirCapacity;
std::string device_file_path =
fmt::format("{}{:08x}/{:08x}", data_path, sub_directory_id, sub_file_id);
auto container_data = ReadFile(device_file_path);
if (container_data.empty()) { // File does not exist?
LOG_WARNING(Core, "Ignoring file {}", device_file_path);
return true;
}
DataContainer container(std::move(container_data));
if (!container.IsGood()) {
return false;
}
std::vector<std::vector<u8>> data;
if (!container.GetIVFCLevel4Data(data)) {
return false;
}
return FileUtil::WriteBytesToFile(path, data[0].data(), data[0].size());
}
ArchiveFormatInfo Extdata::GetFormatInfo() const {
// This information is based on how Citra created the metadata in FS
ArchiveFormatInfo format_info = {/* total_size */ 0,
/* number_directories */ fs_info.maximum_directory_count,
/* number_files */ fs_info.maximum_file_count,
/* duplicate_data */ false};
return format_info;
}
} // namespace Core
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2021 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include "core/file_sys/data/inner_fat.hpp"
namespace Core {
class SDMCDecryptor;
class Extdata final : public Archive<Extdata> {
public:
/**
* Loads an SD extdata folder.
* @param data_path Path to the ENCRYPTED SD extdata folder, relative to decryptor root
* @param decryptor Const reference to the SDMCDecryptor.
*/
explicit Extdata(std::string data_path, const SDMCDecryptor& decryptor);
/**
* Loads an Extdata folder without encryption.
* @param data_path Path to the DECRYPTED extdata folder
*/
explicit Extdata(std::string data_path);
~Extdata();
bool IsGood() const;
bool Extract(std::string path) const;
private:
bool Init();
bool CheckMagic() const;
std::vector<u8> ReadFile(const std::string& path) const;
bool ExtractFile(const std::string& path, std::size_t index) const;
ArchiveFormatInfo GetFormatInfo() const;
bool is_good = false;
std::string data_path;
const SDMCDecryptor* decryptor = nullptr;
bool use_decryptor = true;
friend class Archive<Extdata>;
friend class InnerFAT<Extdata>;
};
} // namespace Core
+334
View File
@@ -0,0 +1,334 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <array>
#include <type_traits>
#include <vector>
#include "common/assert.h"
#include "common/bit_field.h"
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/file_util.h"
#include "common/logging/log.h"
#include "common/string_util.h"
#include "common/swap.h"
namespace Core {
union TableOffset {
// This has different meanings for different savegame layouts
struct { // duplicate data = true
u32_le block_index;
u32_le block_count;
} duplicate;
u64_le non_duplicate; // duplicate data = false
};
struct FATHeader {
u32_le magic;
u32_le version;
u64_le filesystem_information_offset;
u64_le image_size;
u32_le image_block_size;
INSERT_PADDING_BYTES(4);
};
static_assert(sizeof(FATHeader) == 0x20, "FATHeader has incorrect size");
struct FileSystemInformation {
INSERT_PADDING_BYTES(4);
u32_le data_region_block_size;
u64_le directory_hash_table_offset;
u32_le directory_hash_table_bucket_count;
INSERT_PADDING_BYTES(4);
u64_le file_hash_table_offset;
u32_le file_hash_table_bucket_count;
INSERT_PADDING_BYTES(4);
u64_le file_allocation_table_offset;
u32_le file_allocation_table_entry_count;
INSERT_PADDING_BYTES(4);
u64_le data_region_offset;
u32_le data_region_block_count;
INSERT_PADDING_BYTES(4);
TableOffset directory_entry_table;
u32_le maximum_directory_count;
INSERT_PADDING_BYTES(4);
TableOffset file_entry_table;
u32_le maximum_file_count;
INSERT_PADDING_BYTES(4);
};
static_assert(sizeof(FileSystemInformation) == 0x68, "FileSystemInformation has incorrect size");
struct DirectoryEntryTableEntry {
u32_le parent_directory_index;
std::array<char, 16> name;
u32_le next_sibling_index;
u32_le first_subdirectory_index;
u32_le first_file_index;
INSERT_PADDING_BYTES(4);
u32_le next_hash_bucket_entry;
};
static_assert(sizeof(DirectoryEntryTableEntry) == 0x28,
"DirectoryEntryTableEntry has incorrect size");
struct FileEntryTableEntry {
u32_le parent_directory_index;
std::array<char, 16> name;
u32_le next_sibling_index;
INSERT_PADDING_BYTES(4);
u32_le data_block_index;
u64_le file_size;
INSERT_PADDING_BYTES(4);
u32_le next_hash_bucket_entry;
};
static_assert(sizeof(FileEntryTableEntry) == 0x30, "FileEntryTableEntry has incorrect size");
struct FATNode {
union {
BitField<0, 31, u32> index;
BitField<31, 1, u32> flag;
u32_le raw;
} u, v;
};
namespace detail {
#pragma pack(push, 1)
template <typename Preheader>
struct FullHeaderInternal {
static constexpr std::size_t PreheaderSize = sizeof(Preheader);
Preheader pre_header;
FATHeader fat_header;
};
template <>
struct FullHeaderInternal<void> {
static constexpr std::size_t PreheaderSize = 0;
FATHeader fat_header;
};
#pragma pack(pop)
template <typename Preheader>
struct FullHeaderInternal2 {
using Type = FullHeaderInternal<Preheader>;
static_assert(sizeof(Type) == sizeof(Preheader) + sizeof(FATHeader));
static_assert(std::is_standard_layout_v<Type>);
};
template <>
struct FullHeaderInternal2<void> {
using Type = FullHeaderInternal<void>;
static_assert(sizeof(Type) == sizeof(FATHeader));
static_assert(std::is_standard_layout_v<Type>);
};
} // namespace detail
template <typename Preheader = void>
using FullHeader = typename detail::FullHeaderInternal2<Preheader>::Type;
template <typename T, typename Preheader = void,
typename DirectoryEntryType = DirectoryEntryTableEntry,
typename FileEntryType = FileEntryTableEntry>
class InnerFAT {
protected:
bool Init(std::vector<std::vector<u8>> partitions) {
duplicate_data = partitions.size() == 1;
const auto& header_vector = partitions[0];
// Read header
TRY_MEMCPY(&header, header_vector, 0, sizeof(header));
if (!static_cast<const T*>(this)->CheckMagic()) {
LOG_ERROR(Core, "File is invalid, decryption errors may have happened.");
return false;
}
static constexpr std::size_t PreheaderSize = FullHeader<Preheader>::PreheaderSize;
// Read filesystem information
TRY_MEMCPY(&fs_info, header_vector,
PreheaderSize + header.fat_header.filesystem_information_offset,
sizeof(fs_info));
// Read data region
if (duplicate_data) {
data_region.resize(fs_info.data_region_block_count *
static_cast<std::size_t>(fs_info.data_region_block_size));
// This check is relaxed (not counting in PreheaderSize) for title.db
if (partitions[0].size() < fs_info.data_region_offset + data_region.size()) {
LOG_ERROR(Core, "File size is too small");
return false;
}
const auto offset = PreheaderSize + fs_info.data_region_offset;
ASSERT(partitions[0].size() > offset);
const auto to_copy =
std::min<std::size_t>(data_region.size(), partitions[0].size() - offset);
std::memcpy(data_region.data(), partitions[0].data() + offset, to_copy);
} else {
data_region = std::move(partitions[1]);
}
// Directory & file entry tables are allocated in the data region as if they were normal
// files. However, only continuous allocation has been observed so far according to 3DBrew,
// so it should be safe to directly read the bytes.
// Read directory entry table, +2 to include head and root
directory_entry_table.resize(fs_info.maximum_directory_count + 2);
auto directory_entry_table_pos =
duplicate_data ? PreheaderSize + fs_info.data_region_offset +
fs_info.directory_entry_table.duplicate.block_index *
static_cast<std::size_t>(fs_info.data_region_block_size)
: PreheaderSize + fs_info.directory_entry_table.non_duplicate;
TRY_MEMCPY(directory_entry_table.data(), header_vector, directory_entry_table_pos,
directory_entry_table.size() * sizeof(DirectoryEntryType));
// Read file entry table
file_entry_table.resize(fs_info.maximum_file_count + 1); // including head
auto file_entry_table_pos =
duplicate_data ? PreheaderSize + fs_info.data_region_offset +
fs_info.file_entry_table.duplicate.block_index *
static_cast<std::size_t>(fs_info.data_region_block_size)
: PreheaderSize + fs_info.file_entry_table.non_duplicate;
TRY_MEMCPY(file_entry_table.data(), header_vector, file_entry_table_pos,
file_entry_table.size() * sizeof(FileEntryType));
// Read file allocation table
fat.resize(fs_info.file_allocation_table_entry_count);
TRY_MEMCPY(fat.data(), header_vector, PreheaderSize + fs_info.file_allocation_table_offset,
fat.size() * sizeof(FATNode));
return true;
}
bool GetFileData(std::vector<u8>& out, std::size_t index) const {
if (index >= file_entry_table.size()) {
LOG_ERROR(Core, "Index out of bound {}", index);
return false;
}
auto entry = file_entry_table[index];
u32 block = entry.data_block_index;
if (block == 0x80000000) { // empty file
return true;
}
u64 file_size = entry.file_size;
if (file_size >= 64 * 1024 * 1024) {
LOG_ERROR(Core, "File size too large");
return false;
}
out.resize(file_size);
std::size_t written = 0;
while (true) {
// Entry index is block index + 1
auto block_data = fat[block + 1];
u32 last_block = block;
if (block_data.v.flag) { // This node has multiple entries
last_block = fat[block + 2].v.index - 1;
}
// offset & size of the data chunk represented by the FAT node
const auto offset = static_cast<std::ptrdiff_t>(fs_info.data_region_block_size) * block;
const auto size =
static_cast<std::size_t>(fs_info.data_region_block_size) * (last_block - block + 1);
const auto to_write = std::min<std::size_t>(file_size, size);
TRY_MEMCPY(out.data() + written, data_region, offset, to_write);
file_size -= to_write;
written += to_write;
if (block_data.v.index == 0 || file_size == 0) // last node
break;
block = block_data.v.index - 1;
}
return true;
}
bool duplicate_data;
FullHeader<Preheader> header;
FileSystemInformation fs_info;
std::vector<DirectoryEntryType> directory_entry_table;
std::vector<FileEntryType> file_entry_table;
std::vector<FATNode> fat;
std::vector<u8> data_region;
};
/// Parameters of the archive, as specified in the Create or Format call.
struct ArchiveFormatInfo {
u32_le total_size; ///< The pre-defined size of the archive.
u32_le number_directories; ///< The pre-defined number of directories in the archive.
u32_le number_files; ///< The pre-defined number of files in the archive.
u8 duplicate_data; ///< Whether the archive should duplicate the data.
};
static_assert(std::is_standard_layout_v<ArchiveFormatInfo> && std::is_trivial_v<ArchiveFormatInfo>,
"ArchiveFormatInfo is not POD");
/**
* Represents an Archive-like pack where there are directory structures.
* Has an ExtractDirectory function that recursively extracts directories.
* Expects implementor to have ExtractFile.
*/
template <typename T>
class Archive : protected InnerFAT<T> {
public:
bool ExtractDirectory(const std::string& path, std::size_t index) const {
if (index >= this->directory_entry_table.size()) {
LOG_ERROR(Core, "Index out of bound {}", index);
return false;
}
const auto& entry = this->directory_entry_table[index];
const std::string name =
Common::StringFromFixedZeroTerminatedBuffer(entry.name.data(), entry.name.size());
std::string new_path = name.empty() ? path : path + name + "/"; // Name is empty for root
if (!FileUtil::CreateFullPath(new_path)) {
LOG_ERROR(Core, "Could not create path {}", new_path);
return false;
}
// Files
u32 cur = entry.first_file_index;
while (cur != 0) {
if (cur >= this->file_entry_table.size()) {
LOG_ERROR(Core, "Index out of bound {}", cur);
return false;
}
const auto& file_entry = this->file_entry_table[cur];
const std::string file_name = Common::StringFromFixedZeroTerminatedBuffer(
file_entry.name.data(), file_entry.name.size());
if (!static_cast<const T*>(this)->ExtractFile(new_path + file_name, cur)) {
return false;
}
cur = this->file_entry_table[cur].next_sibling_index;
}
// Subdirectories
cur = entry.first_subdirectory_index;
while (cur != 0) {
if (!ExtractDirectory(new_path, cur))
return false;
cur = this->directory_entry_table[cur].next_sibling_index;
}
return true;
}
};
} // namespace Core
+66
View File
@@ -0,0 +1,66 @@
// Copyright 2021 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "core/file_sys/data/savegame.h"
namespace Core {
Savegame::Savegame(std::vector<std::vector<u8>> partitions) {
is_good = Archive<Savegame>::Init(std::move(partitions));
}
Savegame::~Savegame() = default;
bool Savegame::CheckMagic() const {
if (header.fat_header.magic != MakeMagic('S', 'A', 'V', 'E') ||
header.fat_header.version != 0x40000) {
LOG_ERROR(Core, "File is invalid, decryption errors may have happened.");
return false;
}
return true;
}
bool Savegame::IsGood() const {
return is_good;
}
bool Savegame::ExtractFile(const std::string& path, std::size_t index) const {
std::vector<u8> data;
if (!GetFileData(data, index)) {
LOG_ERROR(Core, "Could not get file data for index {}", index);
return false;
}
return FileUtil::WriteBytesToFile(path, data.data(), data.size());
}
bool Savegame::Extract(std::string path) const {
if (path.back() != '/' && path.back() != '\\') {
path += '/';
}
// All saves on a physical 3DS are called 00000001.sav
if (!ExtractDirectory(path + "00000001/", 1)) { // Directory 1 = root
return false;
}
// Write format info
const auto format_info = GetFormatInfo();
return FileUtil::WriteBytesToFile(path + "00000001.metadata", &format_info,
sizeof(format_info));
}
ArchiveFormatInfo Savegame::GetFormatInfo() const {
// Tests on a physical 3DS shows that the `total_size` field seems to always be 0
// when requested with the UserSaveData archive, and 134328448 when requested with
// the SaveData archive. More investigation is required to tell whether this is a fixed value.
ArchiveFormatInfo format_info = {/* total_size */ 0x40000,
/* number_directories */ fs_info.maximum_directory_count,
/* number_files */ fs_info.maximum_file_count,
/* duplicate_data */ duplicate_data};
return format_info;
}
} // namespace Core
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2021 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include "core/file_sys/data/inner_fat.hpp"
namespace Core {
class Savegame final : public Archive<Savegame> {
public:
explicit Savegame(std::vector<std::vector<u8>> partitions);
~Savegame();
bool IsGood() const;
bool Extract(std::string path) const;
private:
bool CheckMagic() const;
bool ExtractFile(const std::string& path, std::size_t index) const;
ArchiveFormatInfo GetFormatInfo() const;
bool is_good = false;
friend class Archive<Savegame>;
friend class InnerFAT<Savegame>;
};
} // namespace Core
+623
View File
@@ -0,0 +1,623 @@
// Copyright 2017 Citra Emulator Project / 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <cinttypes>
#include <cmath>
#include <cstring>
#include <memory>
#include <cryptopp/aes.h>
#include <cryptopp/modes.h>
#include <cryptopp/sha.h>
#include "common/alignment.h"
#include "common/assert.h"
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/logging/log.h"
#include "core/db/seed_db.h"
#include "core/file_sys/data/data_container.h"
#include "core/file_sys/ncch_container.h"
#include "core/importer.h"
#include "core/key/key.h"
namespace Core {
static const int kMaxSections = 8; ///< Maximum number of sections (files) in an ExeFs
static const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes)
NCCHContainer::NCCHContainer(std::shared_ptr<FileUtil::IOFile> file_) : file(std::move(file_)) {}
bool NCCHContainer::OpenFile(std::shared_ptr<FileUtil::IOFile> file_) {
file = std::move(file_);
if (!file->IsOpen()) {
LOG_WARNING(Service_FS, "Failed to open");
return false;
}
LOG_DEBUG(Service_FS, "Opened");
return true;
}
bool NCCHContainer::Load() {
if (is_loaded)
return true;
if (file->IsOpen()) {
// Reset read pointer in case this file has been read before.
file->Seek(0, SEEK_SET);
if (file->ReadBytes(&ncch_header, sizeof(NCCH_Header)) != sizeof(NCCH_Header)) {
LOG_ERROR(Service_FS, "Could not read from file");
return false;
}
// Verify we are loading the correct file type...
if (MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
LOG_ERROR(Service_FS, "Invalid magic, file may be corrupted");
return false;
}
bool failed_to_decrypt = false;
if (!ncch_header.no_crypto) {
is_encrypted = true;
// Find primary and secondary keys
if (ncch_header.fixed_key) {
LOG_DEBUG(Service_FS, "Fixed-key crypto");
primary_key.fill(0);
secondary_key.fill(0);
} else {
std::array<u8, 16> key_y_primary, key_y_secondary;
std::copy(ncch_header.signature, ncch_header.signature + key_y_primary.size(),
key_y_primary.begin());
if (!ncch_header.seed_crypto) {
key_y_secondary = key_y_primary;
} else {
auto opt{Seeds::GetSeed(ncch_header.program_id)};
if (!opt.has_value()) {
LOG_ERROR(Service_FS, "Seed for program {:016X} not found",
ncch_header.program_id);
failed_to_decrypt = true;
} else {
auto seed{*opt};
std::array<u8, 32> input;
std::memcpy(input.data(), key_y_primary.data(), key_y_primary.size());
std::memcpy(input.data() + key_y_primary.size(), seed.data(), seed.size());
CryptoPP::SHA256 sha;
std::array<u8, CryptoPP::SHA256::DIGESTSIZE> hash;
sha.CalculateDigest(hash.data(), input.data(), input.size());
std::memcpy(key_y_secondary.data(), hash.data(), key_y_secondary.size());
}
}
Key::SetKeyY(Key::NCCHSecure1, key_y_primary);
if (!Key::IsNormalKeyAvailable(Key::NCCHSecure1)) {
LOG_ERROR(Service_FS, "Secure1 KeyX missing");
failed_to_decrypt = true;
}
primary_key = Key::GetNormalKey(Key::NCCHSecure1);
const auto SetSecondaryKey = [this, &failed_to_decrypt,
&key_y_secondary](Key::KeySlotID slot) {
Key::SetKeyY(slot, key_y_secondary);
if (!Key::IsNormalKeyAvailable(slot)) {
LOG_ERROR(Service_FS, "{:#04X} KeyX missing", slot);
failed_to_decrypt = true;
}
secondary_key = Key::GetNormalKey(slot);
};
switch (ncch_header.secondary_key_slot) {
case 0:
LOG_DEBUG(Service_FS, "Secure1 crypto");
SetSecondaryKey(Key::NCCHSecure1);
break;
case 1:
LOG_DEBUG(Service_FS, "Secure2 crypto");
SetSecondaryKey(Key::NCCHSecure2);
break;
case 10:
LOG_DEBUG(Service_FS, "Secure3 crypto");
SetSecondaryKey(Key::NCCHSecure3);
break;
case 11:
LOG_DEBUG(Service_FS, "Secure4 crypto");
SetSecondaryKey(Key::NCCHSecure4);
break;
}
}
// Find CTR for each section
// Written with reference to
// https://github.com/d0k3/GodMode9/blob/99af6a73be48fa7872649aaa7456136da0df7938/arm9/source/game/ncch.c#L34-L52
if (ncch_header.version == 0 || ncch_header.version == 2) {
LOG_DEBUG(Loader, "NCCH version 0/2");
// In this version, CTR for each section is a magic number prefixed by partition ID
// (reverse order)
std::reverse_copy(ncch_header.partition_id, ncch_header.partition_id + 8,
exheader_ctr.begin());
exefs_ctr = romfs_ctr = exheader_ctr;
exheader_ctr[8] = 1;
exefs_ctr[8] = 2;
romfs_ctr[8] = 3;
} else if (ncch_header.version == 1) {
LOG_DEBUG(Loader, "NCCH version 1");
// In this version, CTR for each section is the section offset prefixed by partition
// ID, as if the entire NCCH image is encrypted using a single CTR stream.
std::copy(ncch_header.partition_id, ncch_header.partition_id + 8,
exheader_ctr.begin());
exefs_ctr = romfs_ctr = exheader_ctr;
auto u32ToBEArray = [](u32 value) -> std::array<u8, 4> {
return std::array<u8, 4>{
static_cast<u8>(value >> 24),
static_cast<u8>((value >> 16) & 0xFF),
static_cast<u8>((value >> 8) & 0xFF),
static_cast<u8>(value & 0xFF),
};
};
auto offset_exheader = u32ToBEArray(0x200); // exheader offset
auto offset_exefs = u32ToBEArray(ncch_header.exefs_offset * kBlockSize);
auto offset_romfs = u32ToBEArray(ncch_header.romfs_offset * kBlockSize);
std::copy(offset_exheader.begin(), offset_exheader.end(),
exheader_ctr.begin() + 12);
std::copy(offset_exefs.begin(), offset_exefs.end(), exefs_ctr.begin() + 12);
std::copy(offset_romfs.begin(), offset_romfs.end(), romfs_ctr.begin() + 12);
} else {
LOG_ERROR(Service_FS, "Unknown NCCH version {}", ncch_header.version);
failed_to_decrypt = true;
}
} else {
LOG_DEBUG(Service_FS, "No crypto");
is_encrypted = false;
}
// System archives and DLC don't have an extended header but have RomFS
if (ncch_header.extended_header_size) {
if (file->ReadBytes(&exheader_header,
sizeof(exheader_header) != sizeof(exheader_header))) {
LOG_ERROR(Service_FS, "Could not read exheader from file");
return false;
}
if (is_encrypted) {
// This ID check is masked to low 32-bit as a toleration to ill-formed ROM created
// by merging games and its updates.
if ((exheader_header.system_info.jump_id & 0xFFFFFFFF) ==
(ncch_header.program_id & 0xFFFFFFFF)) {
LOG_WARNING(Service_FS, "NCCH is marked as encrypted but with decrypted "
"exheader. Force no crypto scheme.");
is_encrypted = false;
} else {
if (failed_to_decrypt) {
LOG_ERROR(Service_FS, "Failed to decrypt");
return false;
}
CryptoPP::byte* data = reinterpret_cast<CryptoPP::byte*>(&exheader_header);
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption(
primary_key.data(), primary_key.size(), exheader_ctr.data())
.ProcessData(data, data, sizeof(exheader_header));
}
}
u32 entry_point = exheader_header.codeset_info.text.address;
u32 code_size = exheader_header.codeset_info.text.code_size;
u32 stack_size = exheader_header.codeset_info.stack_size;
u32 bss_size = exheader_header.codeset_info.bss_size;
u32 core_version = exheader_header.arm11_system_local_caps.core_version;
u8 priority = exheader_header.arm11_system_local_caps.priority;
u8 resource_limit_category =
exheader_header.arm11_system_local_caps.resource_limit_category;
LOG_DEBUG(Service_FS, "Name: {}",
exheader_header.codeset_info.name);
LOG_DEBUG(Service_FS, "Program ID: {:016X}", ncch_header.program_id);
LOG_DEBUG(Service_FS, "Entry point: 0x{:08X}", entry_point);
LOG_DEBUG(Service_FS, "Code size: 0x{:08X}", code_size);
LOG_DEBUG(Service_FS, "Stack size: 0x{:08X}", stack_size);
LOG_DEBUG(Service_FS, "Bss size: 0x{:08X}", bss_size);
LOG_DEBUG(Service_FS, "Core version: {}", core_version);
LOG_DEBUG(Service_FS, "Thread priority: 0x{:X}", priority);
LOG_DEBUG(Service_FS, "Resource limit category: {}", resource_limit_category);
LOG_DEBUG(Service_FS, "System Mode: {}",
static_cast<int>(exheader_header.arm11_system_local_caps.system_mode));
has_exheader = true;
}
// DLC can have an ExeFS and a RomFS but no extended header
if (ncch_header.exefs_size) {
exefs_offset = ncch_header.exefs_offset * kBlockSize;
u32 exefs_size = ncch_header.exefs_size * kBlockSize;
LOG_DEBUG(Service_FS, "ExeFS offset: 0x{:08X}", exefs_offset);
LOG_DEBUG(Service_FS, "ExeFS size: 0x{:08X}", exefs_size);
file->Seek(exefs_offset, SEEK_SET);
if (file->ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header)) {
LOG_ERROR(Service_FS, "Could not read ExeFS header from file");
return false;
}
if (is_encrypted) {
CryptoPP::byte* data = reinterpret_cast<CryptoPP::byte*>(&exefs_header);
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption(primary_key.data(),
primary_key.size(), exefs_ctr.data())
.ProcessData(data, data, sizeof(exefs_header));
}
exefs_file = file;
has_exefs = true;
}
if (ncch_header.romfs_offset != 0 && ncch_header.romfs_size != 0)
has_romfs = true;
}
is_loaded = true;
return true;
}
bool NCCHContainer::LoadSectionExeFS(const char* name, std::vector<u8>& buffer) {
if (!Load()) {
return false;
}
if (!exefs_file || !exefs_file->IsOpen()) {
LOG_ERROR(Service_FS, "NCCH does not have ExeFS");
return false;
}
LOG_DEBUG(Service_FS, "{} sections:", kMaxSections);
// Iterate through the ExeFs archive until we find a section with the specified name...
for (unsigned section_number = 0; section_number < kMaxSections; section_number++) {
const auto& section = exefs_header.section[section_number];
if (strcmp(section.name, name) == 0) {
LOG_DEBUG(Service_FS, "{} - offset: 0x{:08X}, size: 0x{:08X}, name: {}", section_number,
section.offset, section.size, section.name);
s64 section_offset = (section.offset + exefs_offset + sizeof(ExeFs_Header));
exefs_file->Seek(section_offset, SEEK_SET);
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption dec(primary_key.data(),
primary_key.size(), exefs_ctr.data());
dec.Seek(section.offset + sizeof(ExeFs_Header));
buffer.resize(section.size);
if (exefs_file->ReadBytes(&buffer[0], section.size) != section.size)
return false;
if (is_encrypted) {
dec.ProcessData(&buffer[0], &buffer[0], section.size);
}
return true;
}
}
LOG_ERROR(Service_FS, "Section {} not found", name);
return false;
}
bool NCCHContainer::ReadProgramId(u64_le& program_id) {
if (!Load()) {
return false;
}
program_id = ncch_header.program_id;
return true;
}
bool NCCHContainer::ReadExtdataId(u64& extdata_id) {
if (!Load()) {
return false;
}
if (!has_exheader) {
LOG_ERROR(Service_FS, "NCCH does not have ExHeader");
return false;
}
if (exheader_header.arm11_system_local_caps.storage_info.other_attributes >> 1) {
// Using extended save data access
// There would be multiple possible extdata IDs in this case. The best we can do for now is
// guessing that the first one would be the main save.
const std::array<u64, 6> extdata_ids{{
exheader_header.arm11_system_local_caps.storage_info.extdata_id0.Value(),
exheader_header.arm11_system_local_caps.storage_info.extdata_id1.Value(),
exheader_header.arm11_system_local_caps.storage_info.extdata_id2.Value(),
exheader_header.arm11_system_local_caps.storage_info.extdata_id3.Value(),
exheader_header.arm11_system_local_caps.storage_info.extdata_id4.Value(),
exheader_header.arm11_system_local_caps.storage_info.extdata_id5.Value(),
}};
for (u64 id : extdata_ids) {
if (id) {
// Found a non-zero ID, use it
extdata_id = id;
return true;
}
}
LOG_INFO(Service_FS, "Title does not have extdata ID");
return false;
}
extdata_id = exheader_header.arm11_system_local_caps.storage_info.ext_save_data_id;
return true;
}
bool NCCHContainer::HasExeFS() {
if (!Load()) {
return false;
}
return has_exefs;
}
bool NCCHContainer::HasExHeader() {
if (!Load()) {
return false;
}
return has_exheader;
}
bool NCCHContainer::ReadCodesetName(std::string& name) {
if (!Load()) {
return false;
}
if (!has_exheader) {
LOG_ERROR(Service_FS, "NCCH does not have ExHeader");
return false;
}
std::array<char, 9> name_data{};
std::memcpy(name_data.data(), exheader_header.codeset_info.name, 8);
name = name_data.data();
return true;
}
bool NCCHContainer::ReadProductCode(std::string& product_code) {
if (!Load()) {
return false;
}
std::array<char, 17> data{};
std::memcpy(data.data(), ncch_header.product_code, 16);
product_code = data.data();
return true;
}
bool NCCHContainer::ReadEncryptionType(EncryptionType& encryption) {
if (!Load()) {
return false;
}
if (!is_encrypted) {
encryption = EncryptionType::None;
} else if (ncch_header.fixed_key) {
encryption = EncryptionType::FixedKey;
} else {
switch (ncch_header.secondary_key_slot) {
case 0:
encryption = EncryptionType::NCCHSecure1;
break;
case 1:
encryption = EncryptionType::NCCHSecure2;
break;
case 10:
encryption = EncryptionType::NCCHSecure3;
break;
case 11:
encryption = EncryptionType::NCCHSecure4;
break;
default:
LOG_ERROR(Service_FS, "Unknown encryption type {:X}!", ncch_header.secondary_key_slot);
return false;
}
}
return true;
}
bool NCCHContainer::ReadSeedCrypto(bool& used) {
if (!Load()) {
return false;
}
used = ncch_header.seed_crypto;
return true;
}
bool NCCHContainer::DecryptToFile(std::shared_ptr<FileUtil::IOFile> dest_file,
const Common::ProgressCallback& callback) {
if (!Load()) {
return false;
}
if (!*dest_file) {
LOG_ERROR(Core, "File is not open");
return false;
}
if (!is_encrypted) {
// Simply copy everything. QuickDecryptor is used for progress reporting
file->Seek(0, SEEK_SET);
const auto size = file->GetSize();
decryptor.Reset(size);
decryptor.SetCrypto(nullptr);
return decryptor.CryptAndWriteFile(file, size, dest_file, callback);
}
const auto total_size = file->GetSize();
decryptor.Reset(total_size); // This is inaccurate but doesn't really matter as we don't use it
std::size_t written{};
const auto decryptor_callback = [total_size, &written, &callback](std::size_t current,
std::size_t /*total*/) {
callback(written + current, total_size);
};
// Write NCCH header
NCCH_Header modified_header = ncch_header;
// Set flags (equivalent to GodMode9 behaviour)
modified_header.secondary_key_slot = 0;
modified_header.fixed_key.Assign(0);
modified_header.no_crypto.Assign(1);
modified_header.seed_crypto.Assign(0);
if (dest_file->WriteBytes(&modified_header, sizeof(modified_header)) !=
sizeof(modified_header)) {
LOG_ERROR(Core, "Could not write NCCH header to file");
return false;
}
written += sizeof(NCCH_Header);
// Write Exheader
if (has_exheader) {
if (dest_file->WriteBytes(&exheader_header, sizeof(exheader_header)) !=
sizeof(exheader_header)) {
LOG_ERROR(Core, "Could not write Exheader to file");
return false;
}
written += sizeof(ExHeader_Header);
}
const auto Write = [&](std::string_view name, std::size_t offset, std::size_t size,
bool decrypt = false, const Key::AESKey& key = {},
const Key::AESKey& ctr = {}, std::size_t aes_seek_pos = 0) {
if (offset == 0 || size == 0) {
return true;
}
if (aborted.exchange(false)) {
return false;
}
ASSERT_MSG(written <= offset, "Offsets are not in increasing order");
// Zero out the gap manually to ensure correct hashes when used with CIAs, etc.
const std::array<u8, 1024> zeroes{};
std::size_t zeroes_left = offset - written;
while (zeroes_left > 0) {
const auto to_write = std::min(zeroes.size(), zeroes_left);
if (dest_file->WriteBytes(zeroes.data(), to_write) != to_write) {
LOG_ERROR(Core, "Could not write zeroes before {}", name);
return false;
}
zeroes_left -= to_write;
}
file->Seek(offset, SEEK_SET);
if (aborted.exchange(false)) {
return false;
}
written = offset;
decryptor.SetCrypto(decrypt ? CreateCTRCrypto(key, ctr, aes_seek_pos) : nullptr);
if (!decryptor.CryptAndWriteFile(file, size, dest_file, decryptor_callback)) {
LOG_ERROR(Core, "Could not write {}", name);
return false;
}
written = offset + size;
return true;
};
if (!Write("logo", ncch_header.logo_region_offset * 0x200,
ncch_header.logo_region_size * 0x200)) {
return false;
}
if (!Write("plain region", ncch_header.plain_region_offset * 0x200,
ncch_header.plain_region_size * 0x200)) {
return false;
}
// Write ExeFS header
if (has_exefs) {
if (dest_file->WriteBytes(&exefs_header, sizeof(exefs_header)) != sizeof(exefs_header)) {
LOG_ERROR(Core, "Could not write ExeFS header to file");
return false;
}
written += sizeof(ExeFs_Header);
for (unsigned section_number = 0; section_number < kMaxSections; section_number++) {
const auto& section = exefs_header.section[section_number];
if (section.offset == 0 && section.size == 0) { // not used
continue;
}
Key::AESKey key;
if (strcmp(section.name, "icon") == 0 || strcmp(section.name, "banner") == 0) {
key = primary_key;
} else {
key = secondary_key;
}
// Plus 1 for the ExeFS header
if (!Write(section.name, section.offset + (ncch_header.exefs_offset + 1) * 0x200,
section.size, true, key, exefs_ctr, section.offset + sizeof(exefs_header))) {
return false;
}
}
}
if (has_romfs && !Write("romfs", ncch_header.romfs_offset * 0x200,
ncch_header.romfs_size * 0x200, true, secondary_key, romfs_ctr)) {
return false;
}
if (written < total_size) {
LOG_WARNING(Core, "Data after {} ignored", written);
}
callback(total_size, total_size);
return true;
}
void NCCHContainer::AbortDecryptToFile() {
aborted = true;
decryptor.Abort();
}
#pragma pack(push, 1)
struct RomFSIVFCHeader {
u32_le magic;
u32_le version;
u32_le master_hash_size;
std::array<LevelDescriptor, 3> levels;
INSERT_PADDING_BYTES(0xC);
};
static_assert(sizeof(RomFSIVFCHeader) == 0x60, "Size of RomFSIVFCHeader is incorrect");
#pragma pack(pop)
std::vector<u8> LoadSharedRomFS(const std::vector<u8>& data) {
NCCH_Header header;
ASSERT_MSG(data.size() >= sizeof(header), "NCCH size is too small");
std::memcpy(&header, data.data(), sizeof(header));
const std::size_t offset = header.romfs_offset * 0x200; // 0x200: Media unit
RomFSIVFCHeader ivfc;
ASSERT_MSG(data.size() >= offset + sizeof(ivfc), "NCCH size is too small");
std::memcpy(&ivfc, data.data() + offset, sizeof(ivfc));
ASSERT_MSG(ivfc.magic == MakeMagic('I', 'V', 'F', 'C'), "IVFC magic is incorrect");
ASSERT_MSG(ivfc.version == 0x10000, "IVFC version is incorrect");
std::vector<u8> result(ivfc.levels[2].size);
// Calculation from ctrtool
const std::size_t data_offset =
offset + Common::AlignUp(sizeof(ivfc) + ivfc.master_hash_size,
std::pow(2, ivfc.levels[2].block_size));
ASSERT_MSG(data.size() >= data_offset + ivfc.levels[2].size);
std::memcpy(result.data(), data.data() + data_offset, ivfc.levels[2].size);
return result;
}
} // namespace Core
+320
View File
@@ -0,0 +1,320 @@
// Copyright 2017 Citra Emulator Project / 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "common/bit_field.h"
#include "common/common_types.h"
#include "common/file_util.h"
#include "common/progress_callback.h"
#include "common/swap.h"
#include "core/decryptor.h"
namespace Core {
////////////////////////////////////////////////////////////////////////////////////////////////////
/// NCCH (Nintendo Content Container Header) header
struct NCCH_Header {
u8 signature[0x100];
u32_le magic;
u32_le content_size;
u8 partition_id[8];
u16_le maker_code;
u16_le version;
u8 reserved_0[4];
u64_le program_id;
u8 reserved_1[0x10];
u8 logo_region_hash[0x20];
u8 product_code[0x10];
u8 extended_header_hash[0x20];
u32_le extended_header_size;
u8 reserved_2[4];
u8 reserved_flag[3];
u8 secondary_key_slot;
u8 platform;
enum class ContentType : u8 {
Application = 0,
SystemUpdate = 1,
Manual = 2,
Child = 3,
Trial = 4,
};
union {
BitField<0, 1, u8> is_data;
BitField<1, 1, u8> is_executable;
BitField<2, 3, ContentType> content_type;
};
u8 content_unit_size;
union {
BitField<0, 1, u8> fixed_key;
BitField<1, 1, u8> no_romfs;
BitField<2, 1, u8> no_crypto;
BitField<5, 1, u8> seed_crypto;
u8 raw_crypto_flags;
};
u32_le plain_region_offset;
u32_le plain_region_size;
u32_le logo_region_offset;
u32_le logo_region_size;
u32_le exefs_offset;
u32_le exefs_size;
u32_le exefs_hash_region_size;
u8 reserved_3[4];
u32_le romfs_offset;
u32_le romfs_size;
u32_le romfs_hash_region_size;
u8 reserved_4[4];
u8 exefs_super_block_hash[0x20];
u8 romfs_super_block_hash[0x20];
};
static_assert(sizeof(NCCH_Header) == 0x200, "NCCH header structure size is wrong");
////////////////////////////////////////////////////////////////////////////////////////////////////
// ExeFS (executable file system) headers
struct ExeFs_SectionHeader {
char name[8];
u32 offset;
u32 size;
};
struct ExeFs_Header {
ExeFs_SectionHeader section[8];
u8 reserved[0x80];
u8 hashes[8][0x20];
};
////////////////////////////////////////////////////////////////////////////////////////////////////
// ExHeader (executable file system header) headers
struct ExHeader_SystemInfoFlags {
u8 reserved[5];
u8 flag;
u8 remaster_version[2];
};
struct ExHeader_CodeSegmentInfo {
u32 address;
u32 num_max_pages;
u32 code_size;
};
struct ExHeader_CodeSetInfo {
u8 name[8];
ExHeader_SystemInfoFlags flags;
ExHeader_CodeSegmentInfo text;
u32 stack_size;
ExHeader_CodeSegmentInfo ro;
u8 reserved[4];
ExHeader_CodeSegmentInfo data;
u32 bss_size;
};
struct ExHeader_DependencyList {
u8 program_id[0x30][8];
};
struct ExHeader_SystemInfo {
u64 save_data_size;
u64_le jump_id;
u8 reserved_2[0x30];
};
struct ExHeader_StorageInfo {
union {
u64_le ext_save_data_id;
// When using extended savedata access
// Prefer the ID specified in the most significant bits
BitField<40, 20, u64> extdata_id3;
BitField<20, 20, u64> extdata_id4;
BitField<0, 20, u64> extdata_id5;
};
u8 system_save_data_id[8];
union {
u64_le storage_accessible_unique_ids;
// When using extended savedata access
// Prefer the ID specified in the most significant bits
BitField<40, 20, u64> extdata_id0;
BitField<20, 20, u64> extdata_id1;
BitField<0, 20, u64> extdata_id2;
};
u8 access_info[7];
u8 other_attributes;
};
struct ExHeader_ARM11_SystemLocalCaps {
u64_le program_id;
u32_le core_version;
u8 reserved_flags[2];
union {
u8 flags0;
BitField<0, 2, u8> ideal_processor;
BitField<2, 2, u8> affinity_mask;
BitField<4, 4, u8> system_mode;
};
u8 priority;
u8 resource_limit_descriptor[0x10][2];
ExHeader_StorageInfo storage_info;
u8 service_access_control[0x20][8];
u8 ex_service_access_control[0x2][8];
u8 reserved[0xf];
u8 resource_limit_category;
};
struct ExHeader_ARM11_KernelCaps {
u32_le descriptors[28];
u8 reserved[0x10];
};
struct ExHeader_ARM9_AccessControl {
u8 descriptors[15];
u8 descversion;
};
struct ExHeader_Header {
ExHeader_CodeSetInfo codeset_info;
ExHeader_DependencyList dependency_list;
ExHeader_SystemInfo system_info;
ExHeader_ARM11_SystemLocalCaps arm11_system_local_caps;
ExHeader_ARM11_KernelCaps arm11_kernel_caps;
ExHeader_ARM9_AccessControl arm9_access_control;
struct {
u8 signature[0x100];
u8 ncch_public_key_modulus[0x100];
ExHeader_ARM11_SystemLocalCaps arm11_system_local_caps;
ExHeader_ARM11_KernelCaps arm11_kernel_caps;
ExHeader_ARM9_AccessControl arm9_access_control;
} access_desc;
};
static_assert(sizeof(ExHeader_Header) == 0x800, "ExHeader structure size is wrong");
enum class EncryptionType;
/**
* Helper which implements an interface to deal with NCCH containers which can
* contain ExeFS archives or RomFS archives for games or other applications.
*
* Note that this is heavily stripped down and can only read (primary-key
* encrypted non-code sections of) ExeFS and ExHeader by design.
*/
class NCCHContainer {
public:
NCCHContainer(std::shared_ptr<FileUtil::IOFile> file);
NCCHContainer() {}
bool OpenFile(std::shared_ptr<FileUtil::IOFile> file);
/**
* Ensure ExeFS and exheader is loaded and ready for reading sections
*/
bool Load();
/**
* Reads an application ExeFS section of an NCCH file (non-compressed, primary key only)
* @param name Name of section to read out of NCCH file
* @param buffer Vector to read data into
*/
bool LoadSectionExeFS(const char* name, std::vector<u8>& buffer);
/**
* Get the Program ID of the NCCH container
*/
bool ReadProgramId(u64_le& program_id);
/**
* Get the Extdata ID of the NCCH container
*/
bool ReadExtdataId(u64& extdata_id);
/**
* Checks whether the NCCH container contains an ExeFS
* @return bool check result
*/
bool HasExeFS();
/**
* Checks whether the NCCH container contains an ExHeader
* @return bool check result
*/
bool HasExHeader();
/**
* Reads the name of the codeset.
*/
bool ReadCodesetName(std::string& name);
/**
* Reads the product code.
*/
bool ReadProductCode(std::string& name);
/**
* Gets encryption type (which key is used).
*/
bool ReadEncryptionType(EncryptionType& encryption);
/**
* Gets whether seed crypto is used.
*/
bool ReadSeedCrypto(bool& used);
/**
* Decrypts this NCCH and write to the destination file.
*/
bool DecryptToFile(
std::shared_ptr<FileUtil::IOFile> dest_file,
const Common::ProgressCallback& callback = [](std::size_t, std::size_t) {});
/**
* Aborts DecryptToFile. Simply aborts the decryptor.
*/
void AbortDecryptToFile();
NCCH_Header ncch_header;
ExHeader_Header exheader_header;
ExeFs_Header exefs_header;
private:
bool has_exheader = false;
bool has_exefs = false;
bool has_romfs = false;
bool is_loaded = false;
bool is_encrypted = false;
// for decrypting exheader, exefs header and icon/banner section
std::array<u8, 16> primary_key{};
std::array<u8, 16> secondary_key{}; // for decrypting romfs and .code section
std::array<u8, 16> exheader_ctr{};
std::array<u8, 16> exefs_ctr{};
std::array<u8, 16> romfs_ctr{};
u32 exefs_offset = 0;
std::string root_folder;
std::string filepath;
std::shared_ptr<FileUtil::IOFile> file;
std::shared_ptr<FileUtil::IOFile> exefs_file;
// Used for DecryptToFile
QuickDecryptor decryptor;
std::atomic_bool aborted{false};
friend class CIABuilder;
};
/**
* Extracts the shared RomFS from a NCCH image.
* Used for handling system archives.
*/
std::vector<u8> LoadSharedRomFS(const std::vector<u8>& data);
} // namespace Core
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2021 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <cryptopp/rsa.h>
#include "common/alignment.h"
#include "common/common_funcs.h"
#include "common/file_util.h"
#include "common/logging/log.h"
#include "core/file_sys/certificate.h"
#include "core/file_sys/signature.h"
namespace Core {
enum SignatureType : u32 {
Rsa4096Sha1 = 0x10000,
Rsa2048Sha1 = 0x10001,
EllipticSha1 = 0x10002,
Rsa4096Sha256 = 0x10003,
Rsa2048Sha256 = 0x10004,
EcdsaSha256 = 0x10005
};
static u32 GetSignatureSize(u32 type) {
switch (type) {
case Rsa4096Sha1:
case Rsa4096Sha256:
return 0x200;
case Rsa2048Sha1:
case Rsa2048Sha256:
return 0x100;
case EllipticSha1:
case EcdsaSha256:
return 0x3C;
}
LOG_ERROR(Common_Filesystem, "Invalid signature type {}", type);
return 0;
}
bool Signature::Load(const std::vector<u8>& file_data, std::size_t offset) {
TRY_MEMCPY(&type, file_data, offset, sizeof(type));
const auto data_size = GetSignatureSize(type);
if (data_size == 0) {
return false;
}
data.resize(data_size);
TRY_MEMCPY(data.data(), file_data, offset + sizeof(u32), data_size);
return true;
}
bool Signature::Save(FileUtil::IOFile& file) const {
if (file.WriteBytes(&type, sizeof(type)) != sizeof(type)) {
LOG_ERROR(Core, "Could not write to file");
return false;
}
if (file.WriteBytes(data.data(), data.size()) != data.size()) {
LOG_ERROR(Core, "Could not write to file");
return false;
}
return file.Seek(GetSize() - data.size() - sizeof(type), SEEK_CUR);
}
std::size_t Signature::GetSize() const {
return Common::AlignUp(data.size() + sizeof(type), 0x40);
}
bool Signature::Verify(const std::string& issuer,
const std::function<void(CryptoPP::PK_MessageAccumulator*)>& func) const {
const auto& cert = Certs::Get(issuer);
if (type != SignatureType::Rsa2048Sha256 || cert.body.key_type != PublicKeyType::RSA_2048) {
LOG_ERROR(Core, "Unsupported signature type or cert public key type");
return false;
}
const auto [modulus, exponent] = cert.GetRSAPublicKey();
CryptoPP::RSASS<CryptoPP::PKCS1v15, CryptoPP::SHA256>::Verifier verifier(modulus, exponent);
auto* message = verifier.NewVerificationAccumulator();
func(message);
verifier.InputSignature(*message, data.data(), data.size());
return verifier.Verify(message);
}
} // namespace Core
+40
View File
@@ -0,0 +1,40 @@
// Copyright 2021 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <functional>
#include <vector>
#include "common/common_types.h"
#include "common/swap.h"
namespace CryptoPP {
class PK_MessageAccumulator;
}
namespace FileUtil {
class IOFile;
}
namespace Core {
/// Consists of a signature type, a signature, and alignment to 0x40.
class Signature {
public:
bool Load(const std::vector<u8>& file_data, std::size_t offset = 0);
/// Writes signature to file. Includes the alignment
bool Save(FileUtil::IOFile& file) const;
std::size_t GetSize() const;
/// Verifies the signature. Accepts a functor which should add the message to the accumulator
bool Verify(const std::string& issuer,
const std::function<void(CryptoPP::PK_MessageAccumulator*)>& func) const;
u32_be type;
std::vector<u8> data;
};
} // namespace Core
+112
View File
@@ -0,0 +1,112 @@
// Copyright 2016 Citra Emulator Project / 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <cstring>
#include <vector>
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "core/file_sys/smdh.h"
namespace Core {
// 8x8 Z-Order coordinate from 2D coordinates
static constexpr u32 MortonInterleave(u32 x, u32 y) {
constexpr u32 xlut[] = {0x00, 0x01, 0x04, 0x05, 0x10, 0x11, 0x14, 0x15};
constexpr u32 ylut[] = {0x00, 0x02, 0x08, 0x0a, 0x20, 0x22, 0x28, 0x2a};
return xlut[x % 8] + ylut[y % 8];
}
/**
* Calculates the offset of the position of the pixel in Morton order
*/
static inline u32 GetMortonOffset(u32 x, u32 y, u32 bytes_per_pixel) {
// Images are split into 8x8 tiles. Each tile is composed of four 4x4 subtiles each
// of which is composed of four 2x2 subtiles each of which is composed of four texels.
// Each structure is embedded into the next-bigger one in a diagonal pattern, e.g.
// texels are laid out in a 2x2 subtile like this:
// 2 3
// 0 1
//
// The full 8x8 tile has the texels arranged like this:
//
// 42 43 46 47 58 59 62 63
// 40 41 44 45 56 57 60 61
// 34 35 38 39 50 51 54 55
// 32 33 36 37 48 49 52 53
// 10 11 14 15 26 27 30 31
// 08 09 12 13 24 25 28 29
// 02 03 06 07 18 19 22 23
// 00 01 04 05 16 17 20 21
//
// This pattern is what's called Z-order curve, or Morton order.
const unsigned int block_height = 8;
const unsigned int coarse_x = x & ~7;
u32 i = MortonInterleave(x, y);
const unsigned int offset = coarse_x * block_height;
return (i + offset) * bytes_per_pixel;
}
bool IsValidSMDH(const std::vector<u8>& smdh_data) {
if (smdh_data.size() < sizeof(Core::SMDH))
return false;
u32 magic;
memcpy(&magic, smdh_data.data(), sizeof(u32));
return MakeMagic('S', 'M', 'D', 'H') == magic;
}
std::vector<u16> SMDH::GetIcon(bool large) const {
u32 size;
const u8* icon_data;
if (large) {
size = 48;
icon_data = large_icon.data();
} else {
size = 24;
icon_data = small_icon.data();
}
std::vector<u16> icon(size * size);
for (u32 x = 0; x < size; ++x) {
for (u32 y = 0; y < size; ++y) {
u32 coarse_y = y & ~7;
const u8* pixel = icon_data + GetMortonOffset(x, y, 2) + coarse_y * size * 2;
icon[x + size * y] = (pixel[1] << 8) + pixel[0];
}
}
return icon;
}
std::array<u16, 0x40> SMDH::GetShortTitle(Core::SMDH::TitleLanguage language) const {
return titles[static_cast<int>(language)].short_title;
}
std::string SMDH::GetRegionString() const {
constexpr u32 REGION_COUNT = 7;
// JPN/USA/EUR/Australia/CHN/KOR/TWN
// Australia does not have a symbol because it's practically the same as Europe
static const std::array<std::string, REGION_COUNT> RegionSymbols{
{"J", "U", "E", "", "C", "K", "T"}};
std::string region_string;
for (u32 region = 0; region < REGION_COUNT; ++region) {
if (region_lockout & (1 << region)) {
region_string.append(RegionSymbols[region]);
}
}
if (region_string == "JUECKT") {
return "W";
}
return region_string;
}
} // namespace Core
+84
View File
@@ -0,0 +1,84 @@
// Copyright 2016 Citra Emulator Project / 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <array>
#include <vector>
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/swap.h"
namespace Core {
/**
* Tests if data is a valid SMDH by its length and magic number.
* @param smdh_data data buffer to test
* @return bool test result
*/
bool IsValidSMDH(const std::vector<u8>& smdh_data);
/// SMDH data structure that contains titles, icons etc. See https://www.3dbrew.org/wiki/SMDH
struct SMDH {
u32_le magic;
u16_le version;
INSERT_PADDING_BYTES(2);
struct Title {
std::array<u16, 0x40> short_title;
std::array<u16, 0x80> long_title;
std::array<u16, 0x40> publisher;
};
std::array<Title, 16> titles;
std::array<u8, 16> ratings;
u32_le region_lockout;
u32_le match_maker_id;
u64_le match_maker_bit_id;
u32_le flags;
u16_le eula_version;
INSERT_PADDING_BYTES(2);
float_le banner_animation_frame;
u32_le cec_id;
INSERT_PADDING_BYTES(8);
std::array<u8, 0x480> small_icon;
std::array<u8, 0x1200> large_icon;
/// indicates the language used for each title entry
enum class TitleLanguage {
Japanese = 0,
English = 1,
French = 2,
German = 3,
Italian = 4,
Spanish = 5,
SimplifiedChinese = 6,
Korean = 7,
Dutch = 8,
Portuguese = 9,
Russian = 10,
TraditionalChinese = 11
};
/**
* Gets game icon from SMDH
* @param large If true, returns large icon (48x48), otherwise returns small icon (24x24)
* @return vector of RGB565 data
*/
std::vector<u16> GetIcon(bool large) const;
/**
* Gets the short game title from SMDH
* @param language title language
* @return UTF-16 array of the short title
*/
std::array<u16, 0x40> GetShortTitle(Core::SMDH::TitleLanguage language) const;
/// Gets a string representing the supported regions.
std::string GetRegionString() const;
};
static_assert(sizeof(SMDH) == 0x36C0, "SMDH structure size is wrong");
} // namespace Core
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2020 Pengfei Zhu
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <cstring>
#include <string_view>
#include <cryptopp/rsa.h>
#include "common/alignment.h"
#include "common/assert.h"
#include "common/common_funcs.h"
#include "common/file_util.h"
#include "common/string_util.h"
#include "core/file_sys/cia_common.h"
#include "core/file_sys/ticket.h"
namespace Core {
bool Ticket::Load(const std::vector<u8> file_data, std::size_t offset) {
if (!signature.Load(file_data, offset)) {
return false;
}
TRY_MEMCPY(&body, file_data, offset + signature.GetSize(), sizeof(Body));
return true;
}
bool Ticket::Save(FileUtil::IOFile& file) const {
// signature
if (!signature.Save(file)) {
return false;
}
// body
if (file.WriteBytes(&body, sizeof(body)) != sizeof(body)) {
LOG_ERROR(Core, "Failed to write body");
return false;
}
return true;
}
bool Ticket::ValidateSignature() const {
const auto issuer =
Common::StringFromFixedZeroTerminatedBuffer(body.issuer.data(), body.issuer.size());
return signature.Verify(issuer, [this](CryptoPP::PK_MessageAccumulator* message) {
message->Update(reinterpret_cast<const u8*>(&body), sizeof(body));
});
}
std::size_t Ticket::GetSize() const {
return signature.GetSize() + sizeof(body);
}
constexpr std::string_view TicketIssuer = "Root-CA00000003-XS0000000c";
// TODO: Make use of this?
constexpr std::string_view TicketIssuerDev = "Root-CA00000004-XS00000009";
// From GodMode9
constexpr std::array<u8, 44> TicketContentIndex{
{0x00, 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0xAC, 0x00, 0x00, 0x00, 0x14, 0x00, 0x01, 0x00,
0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x84, 0x00, 0x00, 0x00, 0x84, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
// Values taken from GodMode9
Ticket BuildFakeTicket(u64 title_id) {
Ticket ticket{};
ticket.signature.type = 0x10004; // RSA_2048 SHA256
ticket.signature.data.resize(0x100); // Size of RSA_2048 signature
std::memset(ticket.signature.data.data(), 0xFF, ticket.signature.data.size());
auto& body = ticket.body;
std::memcpy(body.issuer.data(), TicketIssuer.data(), TicketIssuer.size());
std::memset(body.ecc_public_key.data(), 0xFF, body.ecc_public_key.size());
body.version = 0x01;
std::memset(body.title_key.data(), 0xFF, body.title_key.size());
body.title_id = title_id;
body.common_key_index = 0x00;
body.audit = 0x01;
std::memcpy(body.content_index.data(), TicketContentIndex.data(), TicketContentIndex.size());
// GodMode9 by default sets all remaining 0x80 bytes to 0xFF
std::memset(body.content_index.data() + TicketContentIndex.size(), 0xFF, 0x80);
return ticket;
}
} // namespace Core
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2018 Citra Emulator Project / 2020 Pengfei Zhu
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <array>
#include <vector>
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/swap.h"
#include "core/file_sys/signature.h"
namespace FileUtil {
class IOFile;
}
namespace Core {
class Ticket {
public:
#pragma pack(push, 1)
struct Body {
std::array<char, 0x40> issuer;
std::array<u8, 0x3C> ecc_public_key;
u8 version;
u8 ca_crl_version;
u8 signer_crl_version;
std::array<u8, 0x10> title_key;
INSERT_PADDING_BYTES(1);
u64_be ticket_id;
u32_be console_id;
u64_be title_id;
INSERT_PADDING_BYTES(2);
u16_be ticket_title_version;
INSERT_PADDING_BYTES(8);
u8 license_type;
u8 common_key_index;
INSERT_PADDING_BYTES(0x2A);
u32_be eshop_account_id;
INSERT_PADDING_BYTES(1);
u8 audit;
INSERT_PADDING_BYTES(0x42);
std::array<u8, 0x40> limits;
std::array<u8, 0xAC> content_index;
};
static_assert(sizeof(Body) == 0x210, "Ticket body structure size is wrong");
#pragma pack(pop)
bool Load(const std::vector<u8> file_data, std::size_t offset = 0);
bool Save(FileUtil::IOFile& file) const;
bool ValidateSignature() const;
std::size_t GetSize() const;
Signature signature;
Body body;
};
Ticket BuildFakeTicket(u64 title_id);
} // namespace Core
+283
View File
@@ -0,0 +1,283 @@
// Copyright 2017 Citra Emulator Project / 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <cinttypes>
#include <cryptopp/rsa.h>
#include <cryptopp/sha.h>
#include "common/alignment.h"
#include "common/assert.h"
#include "common/file_util.h"
#include "common/logging/log.h"
#include "common/string_util.h"
#include "core/file_sys/certificate.h"
#include "core/file_sys/cia_common.h"
#include "core/file_sys/title_metadata.h"
namespace Core {
bool TitleMetadata::Load(const std::vector<u8> file_data, std::size_t offset) {
std::size_t total_size = static_cast<std::size_t>(file_data.size() - offset);
if (total_size < sizeof(u32_be))
return false;
if (!signature.Load(file_data, offset)) {
return false;
}
const auto signature_size = signature.GetSize();
std::size_t body_end = signature_size + sizeof(Body);
if (total_size < body_end)
return false;
// Read TMD body, then load the amount of ContentChunks specified
std::memcpy(&tmd_body, &file_data[offset + signature_size], sizeof(TitleMetadata::Body));
std::size_t expected_size = signature_size + sizeof(Body) +
static_cast<u16>(tmd_body.content_count) * sizeof(ContentChunk);
if (total_size < expected_size) {
LOG_ERROR(Service_FS, "Malformed TMD, expected size 0x{:x}, got 0x{:x}!", expected_size,
total_size);
return false;
}
for (u16 i = 0; i < tmd_body.content_count; i++) {
ContentChunk chunk;
std::memcpy(&chunk, &file_data[offset + body_end + (i * sizeof(ContentChunk))],
sizeof(ContentChunk));
tmd_chunks.push_back(chunk);
}
return true;
}
bool TitleMetadata::Save(FileUtil::IOFile& file) {
const std::size_t offset = file.Tell();
if (!file.IsOpen())
return false;
if (!signature.Save(file)) {
return false;
}
// Write our TMD body, then write each of our ContentChunks
if (file.WriteBytes(&tmd_body, sizeof(TitleMetadata::Body)) != sizeof(TitleMetadata::Body))
return false;
for (u16 i = 0; i < tmd_body.content_count; i++) {
ContentChunk chunk = tmd_chunks[i];
if (file.WriteBytes(&chunk, sizeof(ContentChunk)) != sizeof(ContentChunk))
return false;
}
return true;
}
bool TitleMetadata::Save(const std::string& file_path) {
FileUtil::IOFile file(file_path, "wb");
return Save(file);
}
void TitleMetadata::FixHashes() {
// Update our TMD body values and hashes
tmd_body.content_count = static_cast<u16>(tmd_chunks.size());
// TODO(shinyquagsire23): Do TMDs with more than one contentinfo exist?
// For now we'll just adjust the first index to hold all content chunks
// and ensure that no further content info data exists.
tmd_body.contentinfo = {};
tmd_body.contentinfo[0].index = 0;
tmd_body.contentinfo[0].command_count = static_cast<u16>(tmd_chunks.size());
CryptoPP::SHA256 chunk_hash;
for (u16 i = 0; i < tmd_body.content_count; i++) {
chunk_hash.Update(reinterpret_cast<u8*>(&tmd_chunks[i]), sizeof(ContentChunk));
}
chunk_hash.Final(tmd_body.contentinfo[0].hash.data());
CryptoPP::SHA256 contentinfo_hash;
for (std::size_t i = 0; i < tmd_body.contentinfo.size(); i++) {
contentinfo_hash.Update(reinterpret_cast<u8*>(&tmd_body.contentinfo[i]),
sizeof(ContentInfo));
}
contentinfo_hash.Final(tmd_body.contentinfo_hash.data());
}
bool TitleMetadata::VerifyHashes() const {
// This can probably be simplified to just checking contentinfo 0 (as above), but do the full
// thing for completeness.
// TODO: Is this what index and command_count actually mean?
CryptoPP::SHA256 contentinfo_hash;
for (std::size_t i = 0; i < tmd_body.contentinfo.size(); i++) {
contentinfo_hash.Update(reinterpret_cast<const u8*>(&tmd_body.contentinfo[i]),
sizeof(ContentInfo));
const std::size_t offset = tmd_body.contentinfo[i].index;
const std::size_t count = tmd_body.contentinfo[i].command_count;
if (count == 0) {
continue;
}
if (offset + count > tmd_body.content_count) { // sanity check
LOG_INFO(Core, "Index / count out of bound for content info record {}", i);
return false;
}
CryptoPP::SHA256 chunk_hash;
for (std::size_t j = offset; j < offset + count; ++j) {
chunk_hash.Update(reinterpret_cast<const u8*>(&tmd_chunks[j]), sizeof(ContentChunk));
}
if (!chunk_hash.Verify(tmd_body.contentinfo[i].hash.data())) {
LOG_ERROR(Core, "Chunk hash dismatch for content info record {}", i);
return false;
}
}
if (!contentinfo_hash.Verify(tmd_body.contentinfo_hash.data())) {
LOG_ERROR(Core, "Content info hash dismatch");
return false;
}
return true;
}
bool TitleMetadata::ValidateSignature() const {
const auto issuer =
Common::StringFromFixedZeroTerminatedBuffer(tmd_body.issuer.data(), tmd_body.issuer.size());
return signature.Verify(issuer, [this](auto* message) {
static_assert(offsetof(Body, contentinfo) == 0xC4, "Signed data length is not correct");
message->Update(reinterpret_cast<const u8*>(&tmd_body), offsetof(Body, contentinfo));
});
}
std::size_t TitleMetadata::GetSize() const {
return signature.GetSize() + sizeof(TitleMetadata::Body) +
sizeof(ContentChunk) * tmd_chunks.size();
}
u64 TitleMetadata::GetTitleID() const {
return tmd_body.title_id;
}
u32 TitleMetadata::GetTitleType() const {
return tmd_body.title_type;
}
u16 TitleMetadata::GetTitleVersion() const {
return tmd_body.title_version;
}
u64 TitleMetadata::GetSystemVersion() const {
return tmd_body.system_version;
}
size_t TitleMetadata::GetContentCount() const {
return tmd_chunks.size();
}
u32 TitleMetadata::GetBootContentID() const {
return tmd_chunks[TMDContentIndex::Main].id;
}
u32 TitleMetadata::GetManualContentID() const {
return tmd_chunks[TMDContentIndex::Manual].id;
}
u32 TitleMetadata::GetDLPContentID() const {
return tmd_chunks[TMDContentIndex::DLP].id;
}
u32 TitleMetadata::GetContentIDByIndex(u16 index) const {
return tmd_chunks[index].id;
}
u16 TitleMetadata::GetContentTypeByIndex(u16 index) const {
return tmd_chunks[index].type;
}
u64 TitleMetadata::GetContentSizeByIndex(u16 index) const {
return tmd_chunks[index].size;
}
std::array<u8, 16> TitleMetadata::GetContentCTRByIndex(u16 index) const {
std::array<u8, 16> ctr{};
std::memcpy(ctr.data(), &tmd_chunks[index].index, sizeof(u16));
return ctr;
}
void TitleMetadata::SetTitleID(u64 title_id) {
tmd_body.title_id = title_id;
}
void TitleMetadata::SetTitleType(u32 type) {
tmd_body.title_type = type;
}
void TitleMetadata::SetTitleVersion(u16 version) {
tmd_body.title_version = version;
}
void TitleMetadata::SetSystemVersion(u64 version) {
tmd_body.system_version = version;
}
TitleMetadata::ContentChunk& TitleMetadata::GetContentChunkByID(u32 content_id) {
const auto it =
std::find_if(tmd_chunks.begin(), tmd_chunks.end(),
[content_id](const ContentChunk& chunk) { return chunk.id == content_id; });
ASSERT(it != tmd_chunks.end());
return *it;
}
const TitleMetadata::ContentChunk& TitleMetadata::GetContentChunkByID(u32 content_id) const {
const auto it =
std::find_if(tmd_chunks.begin(), tmd_chunks.end(),
[content_id](const ContentChunk& chunk) { return chunk.id == content_id; });
ASSERT(it != tmd_chunks.end());
return *it;
}
bool TitleMetadata::HasContentID(u32 content_id) const {
const auto it =
std::find_if(tmd_chunks.begin(), tmd_chunks.end(),
[content_id](const ContentChunk& chunk) { return chunk.id == content_id; });
return it != tmd_chunks.end();
}
void TitleMetadata::AddContentChunk(const ContentChunk& chunk) {
tmd_chunks.push_back(chunk);
}
void TitleMetadata::Print() const {
LOG_DEBUG(Service_FS, "{} chunks", static_cast<u32>(tmd_body.content_count));
// Content info describes ranges of content chunks
LOG_DEBUG(Service_FS, "Content info:");
for (std::size_t i = 0; i < tmd_body.contentinfo.size(); i++) {
if (tmd_body.contentinfo[i].command_count == 0)
break;
LOG_DEBUG(Service_FS, " Index {:04X}, Command Count {:04X}",
static_cast<u32>(tmd_body.contentinfo[i].index),
static_cast<u32>(tmd_body.contentinfo[i].command_count));
}
// For each content info, print their content chunk range
for (std::size_t i = 0; i < tmd_body.contentinfo.size(); i++) {
u16 index = static_cast<u16>(tmd_body.contentinfo[i].index);
u16 count = static_cast<u16>(tmd_body.contentinfo[i].command_count);
if (count == 0)
continue;
LOG_DEBUG(Service_FS, "Content chunks for content info index {}:", i);
for (u16 j = index; j < static_cast<u16>(index + count); j++) {
// Don't attempt to print content we don't have
if (j > tmd_body.content_count)
break;
const ContentChunk& chunk = tmd_chunks[j];
LOG_DEBUG(Service_FS, " ID {:08X}, Index {:04X}, Type {:04x}, Size {:016X}",
static_cast<u32>(chunk.id), static_cast<u32>(chunk.index),
static_cast<u32>(chunk.type), static_cast<u64>(chunk.size));
}
}
}
} // namespace Core
+120
View File
@@ -0,0 +1,120 @@
// Copyright 2017 Citra Emulator Project / 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <array>
#include <string>
#include <vector>
#include "common/common_types.h"
#include "common/swap.h"
#include "core/file_sys/signature.h"
namespace Core {
enum TMDContentTypeFlag : u16 {
Encrypted = 1 << 0,
Disc = 1 << 2,
CFM = 1 << 3,
Optional = 1 << 14,
Shared = 1 << 15
};
enum TMDContentIndex { Main = 0, Manual = 1, DLP = 2 };
/**
* Helper which implements an interface to read and write Title Metadata (TMD) files.
* If a file path is provided and the file exists, it can be parsed and used, otherwise
* it must be created. The TMD file can then be interpreted, modified and/or saved.
*/
class TitleMetadata {
public:
struct ContentChunk {
u32_be id;
u16_be index;
u16_be type;
u64_be size;
std::array<u8, 0x20> hash;
};
static_assert(sizeof(ContentChunk) == 0x30, "TMD ContentChunk structure size is wrong");
struct ContentInfo {
u16_be index;
u16_be command_count;
std::array<u8, 0x20> hash;
};
static_assert(sizeof(ContentInfo) == 0x24, "TMD ContentInfo structure size is wrong");
#pragma pack(push, 1)
struct Body {
std::array<char, 0x40> issuer;
u8 version;
u8 ca_crl_version;
u8 signer_crl_version;
u8 reserved;
u64_be system_version;
u64_be title_id;
u32_be title_type;
u16_be group_id;
u32_be savedata_size;
u32_be srl_private_savedata_size;
std::array<u8, 4> reserved_2;
u8 srl_flag;
std::array<u8, 0x31> reserved_3;
u32_be access_rights;
u16_be title_version;
u16_be content_count;
u16_be boot_content;
std::array<u8, 2> reserved_4;
std::array<u8, 0x20> contentinfo_hash;
std::array<ContentInfo, 64> contentinfo;
};
static_assert(sizeof(Body) == 0x9C4, "TMD body structure size is wrong");
#pragma pack(pop)
bool Load(const std::vector<u8> file_data, std::size_t offset = 0);
bool Save(FileUtil::IOFile& file);
bool Save(const std::string& file_path);
void FixHashes();
bool VerifyHashes() const;
bool ValidateSignature() const;
std::size_t GetSize() const;
u64 GetTitleID() const;
u32 GetTitleType() const;
u16 GetTitleVersion() const;
u64 GetSystemVersion() const;
std::size_t GetContentCount() const;
u32 GetBootContentID() const;
u32 GetManualContentID() const;
u32 GetDLPContentID() const;
u32 GetContentIDByIndex(u16 index) const;
u16 GetContentTypeByIndex(u16 index) const;
u64 GetContentSizeByIndex(u16 index) const;
std::array<u8, 16> GetContentCTRByIndex(u16 index) const;
ContentChunk& GetContentChunkByID(u32 content_id);
const ContentChunk& GetContentChunkByID(u32 content_id) const;
bool HasContentID(u32 content_id) const;
void SetTitleID(u64 title_id);
void SetTitleType(u32 type);
void SetTitleVersion(u16 version);
void SetSystemVersion(u64 version);
void AddContentChunk(const ContentChunk& chunk);
void Print() const;
Signature signature;
Body tmd_body;
std::vector<ContentChunk> tmd_chunks;
};
} // namespace Core