Initial commit

This commit is contained in:
zhupengfei
2019-08-24 23:30:22 +08:00
commit 4f5a3effd8
38 changed files with 4939 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
target_sources(threeSD PRIVATE
core/data_container.cpp
core/data_container.h
core/decryptor.cpp
core/decryptor.h
core/importer.cpp
core/importer.h
core/inner_fat.cpp
core/inner_fat.h
core/key/arithmetic128.cpp
core/key/arithmetic128.h
core/key/key.cpp
core/key/key.h
)
+152
View File
@@ -0,0 +1,152 @@
// 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 "core/data_container.h"
constexpr u32 MakeMagic(char a, char b, char c, char d) {
return a | b << 8 | c << 16 | d << 24;
}
DPFSContainer::DPFSContainer(DPFSDescriptor descriptor_, u8 level1_selector_,
std::vector<u32_le> data_)
: descriptor(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");
}
u8 DPFSContainer::GetBit(u8 level, u8 selector, u64 index) const {
ASSERT_MSG(level <= 2 && selector <= 1, "Level or selector invalid");
return (data[(descriptor.levels[level].offset + selector * descriptor.levels[level].size) / 4 +
index / 32] >>
(31 - (index % 32))) &
1;
}
u8 DPFSContainer::GetByte(u8 level, u8 selector, u64 index) const {
ASSERT_MSG(level <= 2 && selector <= 1, "Level or selector invalid");
return reinterpret_cast<const u8*>(
data.data())[descriptor.levels[level].offset + selector * descriptor.levels[level].size +
index];
}
std::vector<u8> DPFSContainer::GetLevel3Data() 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[1].block_size);
auto level1_bit_index =
(level2_bit_index / 8) / std::pow(2, descriptor.levels[0].block_size);
auto level2_selector = GetBit(0, level1_selector, level1_bit_index);
auto level3_selector = GetBit(1, level2_selector, level2_bit_index);
level3_data[i] = GetByte(2, level3_selector, i);
}
return level3_data;
}
DataContainer::DataContainer(std::vector<u8> data_) : data(std::move(data_)) {
ASSERT_MSG(data.size() >= 0x200, "Data size is too small");
u32_le magic;
std::memcpy(&magic, data.data() + 0x100, sizeof(u32_le));
if (magic == MakeMagic('D', 'I', 'S', 'A')) {
InitAsDISA();
} else if (magic == MakeMagic('D', 'I', 'F', 'F')) {
InitAsDIFF();
} else {
// TODO: Add error handling
UNREACHABLE_MSG("Unknown magic");
}
}
DataContainer::~DataContainer() = default;
void DataContainer::InitAsDISA() {
DISAHeader header;
std::memcpy(&header, data.data() + 0x100, sizeof(header));
ASSERT_MSG(header.version == 0x40000, "DISA Version is not correct");
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]};
}
}
void DataContainer::InitAsDIFF() {
DIFFHeader header;
std::memcpy(&header, data.data() + 0x100, sizeof(header));
ASSERT_MSG(header.version == 0x30000, "DIFF Version is not correct");
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};
}
std::vector<u8> DataContainer::GetPartitionData(u8 index) const {
auto partition_descriptor_offset = partition_table_offset + partition_descriptors[index].offset;
DIFIHeader difi;
std::memcpy(&difi, data.data() + partition_descriptor_offset, sizeof(difi));
ASSERT_MSG(difi.magic == MakeMagic('D', 'I', 'F', 'I'), "DIFI Magic is not correct");
ASSERT_MSG(difi.version == 0x10000, "DIFI Version is not correct");
ASSERT_MSG(difi.ivfc.size >= sizeof(IVFCDescriptor), "IVFC descriptor size is too small");
IVFCDescriptor ivfc_descriptor;
std::memcpy(&ivfc_descriptor, data.data() + partition_descriptor_offset + difi.ivfc.offset,
sizeof(ivfc_descriptor));
if (difi.enable_external_IVFC_level_4) {
std::vector<u8> result(
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 result;
}
// Unwrap DPFS Tree
ASSERT_MSG(difi.dpfs.size >= sizeof(DPFSDescriptor), "DPFS descriptor size is too small");
DPFSDescriptor dpfs_descriptor;
std::memcpy(&dpfs_descriptor, data.data() + partition_descriptor_offset + difi.dpfs.offset,
sizeof(dpfs_descriptor));
std::vector<u32> partition_data(partitions[index].size / 4);
std::memcpy(partition_data.data(), data.data() + partitions[index].offset,
partitions[index].size);
DPFSContainer dpfs_container(dpfs_descriptor, difi.dpfs_level1_selector, partition_data);
auto ivfc_data = dpfs_container.GetLevel3Data();
std::vector<u8> result(ivfc_data.data() + ivfc_descriptor.levels[3].offset,
ivfc_data.data() + ivfc_descriptor.levels[3].offset +
ivfc_descriptor.levels[3].size);
return result;
}
std::vector<std::vector<u8>> DataContainer::GetIVFCLevel4Data() const {
if (partition_count == 1) {
return {GetPartitionData(0)};
} else {
return {GetPartitionData(0), GetPartitionData(1)};
}
}
+129
View File
@@ -0,0 +1,129 @@
// 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"
#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.
std::vector<u8> GetLevel3Data() const;
private:
u8 GetBit(u8 level, u8 selector, u64 index) const;
u8 GetByte(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.
std::vector<std::vector<u8>> GetIVFCLevel4Data() const;
private:
void InitAsDISA();
void InitAsDIFF();
/// Unwraps the whole container, returning the data in IVFC Level 4 of a partition.
std::vector<u8> GetPartitionData(u8 index) const;
std::vector<u8> data;
u32 partition_count;
u64_le partition_table_offset;
std::vector<DataDescriptor> partition_descriptors;
std::vector<DataDescriptor> partitions;
};
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <array>
#include <vector>
#include <cryptopp/aes.h>
#include <cryptopp/files.h>
#include <cryptopp/filters.h>
#include <cryptopp/modes.h>
#include <cryptopp/sha.h>
#include "common/assert.h"
#include "common/file_util.h"
#include "common/string_util.h"
#include "core/decryptor.h"
#include "core/key/key.h"
SDMCDecryptor::SDMCDecryptor(const std::string& root_folder_) : root_folder(root_folder_) {
ASSERT_MSG(Key::IsNormalKeyAvailable(Key::SDKey),
"SD Key must be available in order to decrypt");
if (root_folder.back() == '/' || root_folder.back() == '\\') {
// Remove '/' or '\' character at the end as we will add them back when combining path
root_folder.erase(root_folder.size() - 1);
}
}
SDMCDecryptor::~SDMCDecryptor() = default;
namespace {
std::array<u8, 16> GetFileCTR(const std::string& path) {
auto path_utf16 = Common::UTF8ToUTF16(path);
std::vector<u8> path_data(path_utf16.size() * 2 + 2, 0); // Add the '\0' character
std::memcpy(path_data.data(), path_utf16.data(), path_utf16.size() * 2);
CryptoPP::SHA256 sha;
std::array<u8, CryptoPP::SHA256::DIGESTSIZE> hash;
sha.CalculateDigest(hash.data(), path_data.data(), path_data.size());
std::array<u8, 16> ctr;
for (int i = 0; i < 16; i++) {
ctr[i] = hash[i] ^ hash[16 + i];
}
return ctr;
}
} // namespace
bool SDMCDecryptor::DecryptAndWriteFile(const std::string& source,
const std::string& destination) const {
auto ctr = GetFileCTR(source);
auto key = Key::GetNormalKey(Key::SDKey);
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption aes;
aes.SetKeyWithIV(key.data(), key.size(), ctr.data());
std::string absolute_source = root_folder + source;
try {
CryptoPP::FileSource(absolute_source.c_str(), true,
new CryptoPP::StreamTransformationFilter(
aes, new CryptoPP::FileSink(destination.c_str(), true)),
true);
} catch (CryptoPP::Exception& e) {
LOG_ERROR(Frontend, "Error decrypting and writing file: {}", e.what());
return false;
}
return true;
}
std::vector<u8> SDMCDecryptor::DecryptFile(const std::string& source) const {
auto ctr = GetFileCTR(source);
auto key = Key::GetNormalKey(Key::SDKey);
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption aes;
aes.SetKeyWithIV(key.data(), key.size(), ctr.data());
FileUtil::IOFile file(root_folder + source, "rb");
if (!file) {
LOG_ERROR(Frontend, "Could not open {}", root_folder + source);
return {};
}
auto size = file.GetSize();
std::vector<u8> encrypted_data(size);
if (file.ReadBytes(encrypted_data.data(), size) != size) {
LOG_ERROR(Frontend, "Could not read file {}", root_folder + source);
return {};
}
std::vector<u8> data(size);
aes.ProcessData(data.data(), encrypted_data.data(), encrypted_data.size());
return data;
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <string>
#include <vector>
#include "common/common_types.h"
class SDMCDecryptor {
public:
/**
* Initializes the decryptor.
* @param root_folder Path to the "Nintendo 3DS/<ID0>/<ID1>" folder.
*/
explicit SDMCDecryptor(const std::string& root_folder);
~SDMCDecryptor();
/**
* Decrypts a file from the SD card and writes it into another file.
* @param source Path to the file relative to the root folder, starting with "/".
* @param destination Path to the destination file.
* @return true on success, false otherwise
*/
bool DecryptAndWriteFile(const std::string& source, const std::string& destination) const;
/**
* Decrypts a file and reads it into a vector.
* @param source Path to the file relative to the root folder, starting with "/".
*/
std::vector<u8> DecryptFile(const std::string& source) const;
private:
std::string root_folder;
};
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "core/importer.h"
#include "core/key/key.h"
SDMCImporter::SDMCImporter(const Config& config_) : config(config_) {
Key::LoadBootromKeys(config.bootrom_path);
Key::LoadMovableSedKeys(config.movable_sed_path);
}
SDMCImporter::~SDMCImporter() = default;
+74
View File
@@ -0,0 +1,74 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <string>
#include <vector>
#include "common/common_types.h"
/**
* Type of an importable content.
* Applications, updates and DLCs are all considered titles.
*/
enum class ContentType {
Application,
Update,
DLC,
Savegame,
Extdata,
};
/**
* Struct that specifies an importable content.
*/
struct ContentSpecifier {
ContentType type;
u64 id;
};
/**
* A set of values that are used to initialize the importer.
*/
struct Config {
std::string sdmc_path; ///< SDMC root path ("Nintendo 3DS/<ID0>/<ID1>")
// Necessary system files keys are loaded from.
std::string movable_sed_path; ///< Path to movable.sed
std::string bootrom_path; ///< Path to bootrom (boot9.bin)
// The following system files are optional for importing and are only copied so that Citra
// will be able to decrypt imported encrypted ROMs.
std::string safe_mode_firm_path; ///< Path to safe mode firm
std::string secret_sector_path; ///< Path to secret sector (New3DS only)
};
class SDMCImporter {
public:
/**
* Initializes the importer.
* @param root_folder Path to the "Nintendo 3DS/<ID0>/<ID1>" folder.
*/
explicit SDMCImporter(const Config& config);
~SDMCImporter();
/**
* Dumps a specific content by its specifier.
* @return true on success, false otherwise
*/
bool ImportContent(const ContentSpecifier& specifier);
/**
* Gets a list of dumpable content specifiers.
*/
std::vector<ContentSpecifier> ListContent() const;
private:
bool ImportTitle(u64 id);
bool ImportSavegame(u64 id);
bool ImportExtdata(u64 id);
Config config;
};
+336
View File
@@ -0,0 +1,336 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <fmt/format.h>
#include "common/assert.h"
#include "common/file_util.h"
#include "core/data_container.h"
#include "core/decryptor.h"
#include "core/inner_fat.h"
constexpr u32 MakeMagic(char a, char b, char c, char d) {
return a | b << 8 | c << 16 | d << 24;
}
InnerFAT::~InnerFAT() = default;
bool InnerFAT::IsGood() const {
return is_good;
}
bool InnerFAT::ExtractDirectory(const std::string& path, std::size_t index) const {
auto entry = directory_entry_table[index];
std::array<char, 17> name_data = {}; // Append a null terminator
std::memcpy(name_data.data(), entry.name.data(), entry.name.size());
std::string name = name_data.data();
std::string new_path = name.empty() ? path : path + name + "/"; // Name is empty for root
if (!FileUtil::CreateFullPath(new_path)) {
LOG_ERROR(Frontend, "Could not create path {}", new_path);
return false;
}
// Files
u32 cur = entry.first_file_index;
while (cur != 0) {
if (!ExtractFile(new_path, cur))
return false;
cur = file_entry_table[cur].next_sibling_index;
}
// Subdirectories
cur = entry.first_subdirectory_index;
while (cur != 0) {
if (!ExtractDirectory(new_path, cur))
return false;
cur = directory_entry_table[cur].next_sibling_index;
}
return true;
}
bool InnerFAT::WriteMetadata(const std::string& path) const {
if (!FileUtil::CreateFullPath(path)) {
LOG_ERROR(Frontend, "Could not create path {}", path);
return false;
}
auto format_info = GetFormatInfo();
FileUtil::IOFile file(path, "wb");
if (!file.IsOpen()) {
LOG_ERROR(Frontend, "Could not open file {}", path);
return false;
}
if (file.WriteBytes(&format_info, sizeof(format_info)) != sizeof(format_info)) {
LOG_ERROR(Frontend, "Write data failed (file: {})", path);
return false;
}
return true;
}
SDSavegame::SDSavegame(std::vector<u8> data_) : duplicate_data(true), data(std::move(data_)) {}
SDSavegame::SDSavegame(std::vector<u8> partitionA_, std::vector<u8> partitionB_)
: duplicate_data(false), partitionA(std::move(partitionA_)),
partitionB(std::move(partitionB_)) {
is_good = Init();
}
SDSavegame::~SDSavegame() = default;
bool SDSavegame::Init() {
auto header_iter = duplicate_data ? data.data() : partitionA.data();
// Read header
std::memcpy(&header, header_iter, sizeof(header));
if (header.magic != MakeMagic('S', 'A', 'V', 'E') || header.version != 0x40000) {
LOG_ERROR(Frontend, "File is invalid, decryption errors may have happened.");
return false;
}
// Read filesystem information
std::memcpy(&fs_info, header_iter + header.filesystem_information_offset, sizeof(fs_info));
// Read data region
if (duplicate_data) {
data_region.resize(fs_info.data_region_block_count * fs_info.data_region_block_size);
std::memcpy(data_region.data(), data.data() + fs_info.data_region_offset,
data_region.size());
} else {
data_region = std::move(partitionB);
}
// 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
auto directory_entry_table_iter =
header_iter + (duplicate_data ? fs_info.data_region_offset +
fs_info.directory_entry_table.duplicate.block_index *
fs_info.data_region_block_size
: fs_info.directory_entry_table.non_duplicate);
directory_entry_table.resize(fs_info.maximum_directory_count + 2); // including head and root
std::memcpy(directory_entry_table.data(), directory_entry_table_iter,
directory_entry_table.size() * sizeof(DirectoryEntryTableEntry));
// Read file entry table
auto file_entry_table_iter =
header_iter + (duplicate_data ? fs_info.data_region_offset +
fs_info.file_entry_table.duplicate.block_index *
fs_info.data_region_block_size
: fs_info.file_entry_table.non_duplicate);
file_entry_table.resize(fs_info.maximum_file_count + 1); // including head
std::memcpy(file_entry_table.data(), file_entry_table_iter,
file_entry_table.size() * sizeof(FileEntryTableEntry));
// Read file allocation table
fat.resize(fs_info.file_allocation_table_entry_count);
std::memcpy(fat.data(), header_iter + fs_info.file_allocation_table_offset,
fat.size() * sizeof(FATNode));
return true;
}
bool SDSavegame::ExtractFile(const std::string& path, std::size_t index) const {
if (!FileUtil::CreateFullPath(path)) {
LOG_ERROR(Frontend, "Could not create path {}", path);
return false;
}
auto entry = file_entry_table[index];
std::array<char, 17> name_data = {}; // Append a null terminator
std::memcpy(name_data.data(), entry.name.data(), entry.name.size());
std::string name{name_data.data()};
FileUtil::IOFile file(path + name, "wb");
if (!file.IsOpen()) {
LOG_ERROR(Frontend, "Could not open file {}", path + name);
return false;
}
u32 block = entry.data_block_index;
if (block == 0x80000000) { // empty file
return true;
}
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;
}
std::size_t size = fs_info.data_region_block_size * (last_block - block + 1);
if (file.WriteBytes(data_region.data() + fs_info.data_region_block_size * block, size) !=
size) {
LOG_ERROR(Frontend, "Write data failed (file: {})", path + name);
return false;
}
if (block_data.v.index == 0) // last node
break;
block = block_data.v.index - 1;
}
return true;
}
bool SDSavegame::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;
}
if (!WriteMetadata(path + "00000001.metadata")) {
return false;
}
return true;
}
ArchiveFormatInfo SDSavegame::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 */ 134328448,
/* number_directories */ fs_info.maximum_directory_count,
/* number_files */ fs_info.maximum_file_count,
/* duplicate_data */ duplicate_data};
return format_info;
}
SDExtdata::SDExtdata(std::string data_path_, const SDMCDecryptor& decryptor_)
: data_path(std::move(data_path_)), decryptor(decryptor_) {
if (data_path.back() != '/' && data_path.back() != '\\') {
data_path += '/';
}
is_good = Init();
}
SDExtdata::~SDExtdata() = default;
bool SDExtdata::Init() {
// Read VSXE file
auto vsxe_raw = decryptor.DecryptFile(data_path + "00000000/00000001");
if (vsxe_raw.empty()) {
LOG_ERROR(Frontend, "Failed to load or decrypt VSXE");
return false;
}
DataContainer vsxe_container(vsxe_raw);
auto vsxe = vsxe_container.GetIVFCLevel4Data()[0];
// Read header
std::memcpy(&header, vsxe.data(), sizeof(header));
if (header.magic != MakeMagic('V', 'S', 'X', 'E') || header.version != 0x30000) {
LOG_ERROR(Frontend, "File is invalid, decryption errors may have happened.");
return false;
}
// Read filesystem information
std::memcpy(&fs_info, vsxe.data() + header.filesystem_information_offset, sizeof(fs_info));
// Read data region
data_region.resize(fs_info.data_region_block_count * fs_info.data_region_block_size);
std::memcpy(data_region.data(), vsxe.data() + fs_info.data_region_offset, data_region.size());
// Read directory entry table
directory_entry_table.resize(fs_info.maximum_directory_count + 2); // including head and root
std::memcpy(directory_entry_table.data(),
vsxe.data() + fs_info.data_region_offset +
fs_info.directory_entry_table.duplicate.block_index *
fs_info.data_region_block_size,
directory_entry_table.size() * sizeof(DirectoryEntryTableEntry));
// Read file entry table
file_entry_table.resize(fs_info.maximum_file_count + 1); // including head
std::memcpy(file_entry_table.data(),
vsxe.data() + fs_info.data_region_offset +
fs_info.file_entry_table.duplicate.block_index * fs_info.data_region_block_size,
file_entry_table.size() * sizeof(FileEntryTableEntry));
// File allocation table isn't needed here, as the only files allocated by them are
// directory/file entry tables which we already read above.
return true;
}
bool SDExtdata::Extract(std::string path) const {
if (path.back() != '/' && path.back() != '\\') {
path += '/';
}
if (!ExtractDirectory(path, 1)) {
return false;
}
if (!WriteMetadata(path + "metadata")) {
return false;
}
return true;
}
bool SDExtdata::ExtractFile(const std::string& path, std::size_t index) const {
/// Maximum amount of device files a device directory can hold.
constexpr u32 DeviceDirCapacity = 126;
auto entry = file_entry_table[index];
std::array<char, 17> name_data = {}; // Append a null terminator
std::memcpy(name_data.data(), entry.name.data(), entry.name.size());
std::string name{name_data.data()};
FileUtil::IOFile file(path + name, "wb");
if (!file) {
LOG_ERROR(Frontend, "Could not open file {}", path + name);
return false;
}
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 = decryptor.DecryptFile(device_file_path);
if (container_data.empty()) { // File does not exist?
LOG_WARNING(Frontend, "Ignoring file {}", device_file_path);
return true;
}
DataContainer container(container_data);
auto data = container.GetIVFCLevel4Data()[0];
if (file.WriteBytes(data.data(), data.size()) != data.size()) {
LOG_ERROR(Frontend, "Write data failed (file: {})", path + name);
return false;
}
return true;
}
ArchiveFormatInfo SDExtdata::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;
}
+196
View File
@@ -0,0 +1,196 @@
// 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/bit_field.h"
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/swap.h"
class SDMCDecryptor;
/// 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_pod<ArchiveFormatInfo>::value, "ArchiveFormatInfo is not POD");
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;
};
/**
* Virtual interface for the inner FAT filesystem of SD Savegames/Extdata/TitleDB.
*/
class InnerFAT {
public:
virtual ~InnerFAT();
/**
* Returns whether the filesystem is in "good" state, i.e. successfully initialized.
*/
bool IsGood() const;
/**
* Completely extracts everything from this filesystem, including files, directories
* and metadata used by Citra.
* @return true on success, false otherwise
*/
virtual bool Extract(std::string path) const = 0;
protected:
/**
* Gets the ArchiveFormatInfo of this archive, used for writing the archive metadata.
*/
virtual ArchiveFormatInfo GetFormatInfo() const = 0;
/**
* Extracts the index-th file in the file entry table to a certain path. (The path does not
* contain the file name).
* @return true on success, false otherwise
*/
virtual bool ExtractFile(const std::string& path, std::size_t index) const = 0;
/**
* Recursively extracts the index-th directory in the directory entry table.
* @return true on success, false otherwise
*/
bool ExtractDirectory(const std::string& path, std::size_t index) const;
/**
* Writes the corresponding archive metadata to a certain path.
* @return true on success, false otherwise
*/
bool WriteMetadata(const std::string& path) const;
bool is_good = false;
FATHeader header;
FileSystemInformation fs_info;
std::vector<DirectoryEntryTableEntry> directory_entry_table;
std::vector<FileEntryTableEntry> file_entry_table;
std::vector<u8> data_region;
};
class SDSavegame : public InnerFAT {
public:
explicit SDSavegame(std::vector<u8> data);
explicit SDSavegame(std::vector<u8> partitionA, std::vector<u8> partitionB);
~SDSavegame() override;
bool Extract(std::string path) const override;
private:
bool Init();
bool ExtractFile(const std::string& path, std::size_t index) const override;
ArchiveFormatInfo GetFormatInfo() const override;
std::vector<FATNode> fat;
bool duplicate_data; // Layout variant
// Temporary storage for construction data
std::vector<u8> data;
std::vector<u8> partitionA;
std::vector<u8> partitionB;
};
class SDExtdata : public InnerFAT {
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 SDExtdata(std::string data_path, const SDMCDecryptor& decryptor);
~SDExtdata() override;
bool Extract(std::string path) const override;
private:
bool Init();
bool ExtractFile(const std::string& path, std::size_t index) const override;
ArchiveFormatInfo GetFormatInfo() const override;
std::string data_path;
const SDMCDecryptor& decryptor;
};
+59
View File
@@ -0,0 +1,59 @@
// 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 <functional>
#include "core/key/arithmetic128.h"
namespace Key {
AESKey Lrot128(const AESKey& in, u32 rot) {
AESKey out;
rot %= 128;
const u32 byte_shift = rot / 8;
const u32 bit_shift = rot % 8;
for (u32 i = 0; i < 16; i++) {
const u32 wrap_index_a = (i + byte_shift) % 16;
const u32 wrap_index_b = (i + byte_shift + 1) % 16;
out[i] = ((in[wrap_index_a] << bit_shift) | (in[wrap_index_b] >> (8 - bit_shift))) & 0xFF;
}
return out;
}
AESKey Add128(const AESKey& a, const AESKey& b) {
AESKey out;
u32 carry = 0;
u32 sum = 0;
for (int i = 15; i >= 0; i--) {
sum = a[i] + b[i] + carry;
carry = sum >> 8;
out[i] = static_cast<u8>(sum & 0xff);
}
return out;
}
AESKey Add128(const AESKey& a, u64 b) {
AESKey out = a;
u32 carry = 0;
u32 sum = 0;
for (int i = 15; i >= 8; i--) {
sum = a[i] + static_cast<u8>((b >> ((15 - i) * 8)) & 0xff) + carry;
carry = sum >> 8;
out[i] = static_cast<u8>(sum & 0xff);
}
return out;
}
AESKey Xor128(const AESKey& a, const AESKey& b) {
AESKey out;
std::transform(a.cbegin(), a.cend(), b.cbegin(), out.begin(), std::bit_xor<>());
return out;
}
} // namespace Key
+17
View File
@@ -0,0 +1,17 @@
// 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 "common/common_types.h"
#include "core/key/key.h"
namespace Key {
AESKey Lrot128(const AESKey& in, u32 rot);
AESKey Add128(const AESKey& a, const AESKey& b);
AESKey Add128(const AESKey& a, u64 b);
AESKey Xor128(const AESKey& a, const AESKey& b);
} // namespace Key
+213
View File
@@ -0,0 +1,213 @@
// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <exception>
#include <optional>
#include <string>
#include <fmt/format.h>
#include "common/file_util.h"
#include "common/logging/log.h"
#include "core/key/arithmetic128.h"
#include "core/key/key.h"
namespace Key {
namespace {
// The generator constant was calculated using the 0x39 KeyX and KeyY retrieved from a 3DS and the
// normal key dumped from a Wii U solving the equation:
// NormalKey = (((KeyX ROL 2) XOR KeyY) + constant) ROL 87
// On a real 3DS the generation for the normal key is hardware based, and thus the constant can't
// get dumped . generated normal keys are also not accesible on a 3DS. The used formula for
// calculating the constant is a software implementation of what the hardware generator does.
constexpr AESKey generator_constant = {{0x1F, 0xF9, 0xE9, 0xAA, 0xC5, 0xFE, 0x04, 0x08, 0x02, 0x45,
0x91, 0xDC, 0x5D, 0x52, 0x76, 0x8A}};
struct KeyDesc {
char key_type;
std::size_t slot_id;
// This key is identical to the key with the same key_type and slot_id -1
bool same_as_before;
};
AESKey HexToKey(const std::string& hex) {
if (hex.size() < 32) {
throw std::invalid_argument("hex string is too short");
}
AESKey key;
for (std::size_t i = 0; i < key.size(); ++i) {
key[i] = static_cast<u8>(std::stoi(hex.substr(i * 2, 2), 0, 16));
}
return key;
}
struct KeySlot {
std::optional<AESKey> x;
std::optional<AESKey> y;
std::optional<AESKey> normal;
void SetKeyX(std::optional<AESKey> key) {
x = key;
GenerateNormalKey();
}
void SetKeyY(std::optional<AESKey> key) {
y = key;
GenerateNormalKey();
}
void SetNormalKey(std::optional<AESKey> key) {
normal = key;
}
void GenerateNormalKey() {
if (x && y) {
normal = Lrot128(Add128(Xor128(Lrot128(*x, 2), *y), generator_constant), 87);
} else {
normal = {};
}
}
void Clear() {
x.reset();
y.reset();
normal.reset();
}
};
std::array<KeySlot, KeySlotID::MaxKeySlotID> key_slots;
std::array<std::optional<AESKey>, 6> common_key_y_slots;
std::string KeyToString(AESKey& key) {
std::string s;
for (auto pos : key) {
s += fmt::format("{:02X}", pos);
}
return s;
}
} // namespace
void LoadBootromKeys(const std::string& path) {
constexpr std::array<KeyDesc, 80> keys = {
{{'X', 0x2C, false}, {'X', 0x2D, true}, {'X', 0x2E, true}, {'X', 0x2F, true},
{'X', 0x30, false}, {'X', 0x31, true}, {'X', 0x32, true}, {'X', 0x33, true},
{'X', 0x34, false}, {'X', 0x35, true}, {'X', 0x36, true}, {'X', 0x37, true},
{'X', 0x38, false}, {'X', 0x39, true}, {'X', 0x3A, true}, {'X', 0x3B, true},
{'X', 0x3C, false}, {'X', 0x3D, false}, {'X', 0x3E, false}, {'X', 0x3F, false},
{'Y', 0x4, false}, {'Y', 0x5, false}, {'Y', 0x6, false}, {'Y', 0x7, false},
{'Y', 0x8, false}, {'Y', 0x9, false}, {'Y', 0xA, false}, {'Y', 0xB, false},
{'N', 0xC, false}, {'N', 0xD, true}, {'N', 0xE, true}, {'N', 0xF, true},
{'N', 0x10, false}, {'N', 0x11, true}, {'N', 0x12, true}, {'N', 0x13, true},
{'N', 0x14, false}, {'N', 0x15, false}, {'N', 0x16, false}, {'N', 0x17, false},
{'N', 0x18, false}, {'N', 0x19, true}, {'N', 0x1A, true}, {'N', 0x1B, true},
{'N', 0x1C, false}, {'N', 0x1D, true}, {'N', 0x1E, true}, {'N', 0x1F, true},
{'N', 0x20, false}, {'N', 0x21, true}, {'N', 0x22, true}, {'N', 0x23, true},
{'N', 0x24, false}, {'N', 0x25, true}, {'N', 0x26, true}, {'N', 0x27, true},
{'N', 0x28, true}, {'N', 0x29, false}, {'N', 0x2A, false}, {'N', 0x2B, false},
{'N', 0x2C, false}, {'N', 0x2D, true}, {'N', 0x2E, true}, {'N', 0x2F, true},
{'N', 0x30, false}, {'N', 0x31, true}, {'N', 0x32, true}, {'N', 0x33, true},
{'N', 0x34, false}, {'N', 0x35, true}, {'N', 0x36, true}, {'N', 0x37, true},
{'N', 0x38, false}, {'N', 0x39, true}, {'N', 0x3A, true}, {'N', 0x3B, true},
{'N', 0x3C, true}, {'N', 0x3D, false}, {'N', 0x3E, false}, {'N', 0x3F, false}}};
// Bootrom sets all these keys when executed, but later some of the normal keys get overwritten
// by other applications e.g. process9. These normal keys thus aren't used by any application
// and have no value for emulation
auto file = FileUtil::IOFile(path, "rb");
if (!file) {
return;
}
const std::size_t length = file.GetSize();
if (length != 65536) {
LOG_ERROR(Key, "Bootrom9 size is wrong: {}", length);
return;
}
constexpr std::size_t KEY_SECTION_START = 55760;
file.Seek(KEY_SECTION_START, SEEK_SET); // Jump to the key section
AESKey new_key;
for (const auto& key : keys) {
if (!key.same_as_before) {
file.ReadArray(new_key.data(), new_key.size());
if (!file) {
LOG_ERROR(Key, "Reading from Bootrom9 failed");
return;
}
}
LOG_DEBUG(Key, "Loaded Slot{:#02x} Key{}: {}", key.slot_id, key.key_type,
KeyToString(new_key));
switch (key.key_type) {
case 'X':
key_slots.at(key.slot_id).SetKeyX(new_key);
break;
case 'Y':
key_slots.at(key.slot_id).SetKeyY(new_key);
break;
case 'N':
key_slots.at(key.slot_id).SetNormalKey(new_key);
break;
default:
LOG_ERROR(Key, "Invalid key type {}", key.key_type);
break;
}
}
}
void LoadMovableSedKeys(const std::string& path) {
auto file = FileUtil::IOFile(path, "rb");
if (!file) {
return;
}
const std::size_t length = file.GetSize();
if (length < 0x120) {
LOG_ERROR(Key, "movable.sed size is too small: {}", length);
return;
}
constexpr std::size_t KEY_SECTION_START = 0x118;
file.Seek(KEY_SECTION_START, SEEK_SET); // Jump to the key section
AESKey key;
file.ReadArray(key.data(), key.size());
if (!file) {
LOG_ERROR(Key, "Reading from movable.sed failed");
return;
}
SetKeyY(0x26, key);
}
void SetKeyX(std::size_t slot_id, const AESKey& key) {
key_slots.at(slot_id).SetKeyX(key);
}
void SetKeyY(std::size_t slot_id, const AESKey& key) {
key_slots.at(slot_id).SetKeyY(key);
}
void SetNormalKey(std::size_t slot_id, const AESKey& key) {
key_slots.at(slot_id).SetNormalKey(key);
}
bool IsNormalKeyAvailable(std::size_t slot_id) {
return key_slots.at(slot_id).normal.has_value();
}
AESKey GetNormalKey(std::size_t slot_id) {
return key_slots.at(slot_id).normal.value_or(AESKey{});
}
void SelectCommonKeyIndex(u8 index) {
key_slots[KeySlotID::TicketCommonKey].SetKeyY(common_key_y_slots.at(index));
}
} // namespace Key
+71
View File
@@ -0,0 +1,71 @@
// 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 <cstddef>
#include <string>
#include "common/common_types.h"
namespace Key {
enum KeySlotID : std::size_t {
// Used to decrypt the SSL client cert/private-key stored in ClCertA.
SSLKey = 0x0D,
// AES keyslots used to decrypt NCCH
NCCHSecure1 = 0x2C,
NCCHSecure2 = 0x25,
NCCHSecure3 = 0x18,
NCCHSecure4 = 0x1B,
// AES Keyslot used to generate the UDS data frame CCMP key.
UDSDataKey = 0x2D,
// AES Keyslot used to encrypt the BOSS container data.
BOSSDataKey = 0x38,
// AES Keyslot used to calculate DLP data frame checksum.
DLPDataKey = 0x39,
// AES Keyslot used to generate the StreetPass CCMP key.
CECDDataKey = 0x2E,
// AES Keyslot used by the friends module.
FRDKey = 0x36,
// AES Keyslot used by the NFC module.
NFCKey = 0x39,
// AES keyslot used for APT:Wrap/Unwrap functions
APTWrap = 0x31,
// Console-unique AES keyslot used to encrypt all data in the "Nintendo 3DS/<ID0>/<ID1>" folder
SDKey = 0x34,
// AES keyslot used for decrypting ticket title key
TicketCommonKey = 0x3D,
MaxKeySlotID = 0x40,
};
constexpr std::size_t AES_BLOCK_SIZE = 16;
using AESKey = std::array<u8, AES_BLOCK_SIZE>;
void LoadBootromKeys(const std::string& path);
void LoadMovableSedKeys(const std::string& path);
void SetKeyX(std::size_t slot_id, const AESKey& key);
void SetKeyY(std::size_t slot_id, const AESKey& key);
void SetNormalKey(std::size_t slot_id, const AESKey& key);
bool IsNormalKeyAvailable(std::size_t slot_id);
AESKey GetNormalKey(std::size_t slot_id);
void SelectCommonKeyIndex(u8 index);
} // namespace Key