mirror of
https://github.com/Dark98/threeSD.git
synced 2026-07-05 16:49:08 +00:00
Pointless huge refactor
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user