mirror of
https://github.com/Dark98/threeSD.git
synced 2026-07-03 00:38:58 +00:00
Initial commit
This commit is contained in:
@@ -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
|
||||
)
|
||||
@@ -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)};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user