lots of important fixes

- DPFS container is fixed
- SD Savegame is fixed
- added slot0x25KeyX load
- added regex for titile ID
- added aes_keys.txt import
- sd savegame listing is fixed (uninitialized won't be listed any more)
- error handling is improved (removed asserts and replaced with return values)
- UI is now functional
- config is now checked in main
This commit is contained in:
zhupengfei
2019-08-30 15:33:47 +08:00
parent 8acfe9f304
commit 67e6b05e87
22 changed files with 490 additions and 78 deletions
+33 -14
View File
@@ -25,7 +25,7 @@ u8 DPFSContainer::GetBit(u8 level, u8 selector, u64 index) const {
return (data[(descriptor.levels[level].offset + selector * descriptor.levels[level].size) / 4 +
index / 32] >>
(31 - (index % 32))) &
1;
static_cast<u32_le>(1);
}
u8 DPFSContainer::GetByte(u8 level, u8 selector, u64 index) const {
@@ -38,9 +38,9 @@ u8 DPFSContainer::GetByte(u8 level, u8 selector, u64 index) const {
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 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[0].block_size);
(level2_bit_index / 8) / std::pow(2, descriptor.levels[1].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);
@@ -49,27 +49,38 @@ std::vector<u8> DPFSContainer::GetLevel3Data() const {
}
DataContainer::DataContainer(std::vector<u8> data_) : data(std::move(data_)) {
ASSERT_MSG(data.size() >= 0x200, "Data size is too small");
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')) {
InitAsDISA();
is_good = InitAsDISA();
} else if (magic == MakeMagic('D', 'I', 'F', 'F')) {
InitAsDIFF();
is_good = InitAsDIFF();
} else {
// TODO: Add error handling
UNREACHABLE_MSG("Unknown magic");
LOG_ERROR(Core, "Unknown magic 0x{:08x}", magic);
is_good = false;
}
}
DataContainer::~DataContainer() = default;
void DataContainer::InitAsDISA() {
bool DataContainer::IsGood() const {
return is_good;
}
bool DataContainer::InitAsDISA() {
DISAHeader header;
std::memcpy(&header, data.data() + 0x100, sizeof(header));
ASSERT_MSG(header.version == 0x40000, "DISA Version is not correct");
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;
@@ -86,13 +97,18 @@ void DataContainer::InitAsDISA() {
partition_descriptors = {header.partition_descriptors[0]};
partitions = {header.partitions[0]};
}
return true;
}
void DataContainer::InitAsDIFF() {
bool DataContainer::InitAsDIFF() {
DIFFHeader header;
std::memcpy(&header, data.data() + 0x100, sizeof(header));
ASSERT_MSG(header.version == 0x30000, "DIFF Version is not correct");
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;
@@ -103,6 +119,8 @@ void DataContainer::InitAsDIFF() {
partition_count = 1;
partition_descriptors = {{/* offset */ 0, /* size */ header.partition_table_size}};
partitions = {header.partition_A};
return true;
}
std::vector<u8> DataContainer::GetPartitionData(u8 index) const {
@@ -132,11 +150,12 @@ std::vector<u8> DataContainer::GetPartitionData(u8 index) const {
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::vector<u32_le> 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);
DPFSContainer dpfs_container(dpfs_descriptor, difi.dpfs_level1_selector,
std::move(partition_data));
auto ivfc_data = dpfs_container.GetLevel3Data();
std::vector<u8> result(ivfc_data.data() + ivfc_descriptor.levels[3].offset,
+5 -2
View File
@@ -116,13 +116,16 @@ public:
/// Unwraps the whole container, returning the data in IVFC Level 4 of all partitions.
std::vector<std::vector<u8>> GetIVFCLevel4Data() const;
bool IsGood() const;
private:
void InitAsDISA();
void InitAsDIFF();
bool InitAsDISA();
bool InitAsDIFF();
/// Unwraps the whole container, returning the data in IVFC Level 4 of a partition.
std::vector<u8> GetPartitionData(u8 index) const;
bool is_good = false;
std::vector<u8> data;
u32 partition_count;
u64_le partition_table_offset;
+4
View File
@@ -54,6 +54,10 @@ bool SDMCDecryptor::DecryptAndWriteFile(const std::string& source,
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption aes;
aes.SetKeyWithIV(key.data(), key.size(), ctr.data());
if (!FileUtil::CreateFullPath(destination)) {
return false;
}
std::string absolute_source = root_folder + source;
try {
CryptoPP::FileSource(absolute_source.c_str(), true,
+70 -9
View File
@@ -6,6 +6,7 @@
#include "common/assert.h"
#include "common/common_paths.h"
#include "common/file_util.h"
#include "core/data_container.h"
#include "core/decryptor.h"
#include "core/importer.h"
#include "core/inner_fat.h"
@@ -89,7 +90,13 @@ bool SDMCImporter::ImportTitle(u64 id) {
bool SDMCImporter::ImportSavegame(u64 id) {
const auto path = fmt::format("title/{:08x}/{:08x}/data/", (id >> 32), (id & 0xFFFFFFFF));
SDSavegame save(decryptor->DecryptFile(fmt::format("/{}00000001.sav", path)));
DataContainer container(decryptor->DecryptFile(fmt::format("/{}00000001.sav", path)));
if (!container.IsGood()) {
return false;
}
SDSavegame save(std::move(container.GetIVFCLevel4Data()));
if (!save.IsGood()) {
return false;
}
@@ -116,6 +123,9 @@ bool SDMCImporter::ImportSysdata(u64 id) {
case 0: { // boot9.bin
const auto target_path = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + BOOTROM9;
LOG_INFO(Core, "Copying {} from {} to {}", BOOTROM9, config.bootrom_path, target_path);
if (!FileUtil::CreateFullPath(target_path)) {
return false;
}
return FileUtil::Copy(config.bootrom_path, target_path);
}
case 1: { // safe mode firm
@@ -129,22 +139,31 @@ bool SDMCImporter::ImportSysdata(u64 id) {
real_path = config.safe_mode_firm_path + "old/";
}
return FileUtil::ForeachDirectoryEntry(
nullptr, config.safe_mode_firm_path,
nullptr, real_path,
[is_new_3ds](u64* /*num_entries_out*/, const std::string& directory,
const std::string& virtual_name) {
if (FileUtil::IsDirectory(directory + virtual_name)) {
return true;
}
return FileUtil::Copy(
directory + virtual_name,
const auto target_path =
fmt::format("{}00000000000000000000000000000000/title/00040138/{}/content/{}",
FileUtil::GetUserPath(FileUtil::UserPath::NANDDir),
(is_new_3ds ? "20000003" : "00000003"), virtual_name));
(is_new_3ds ? "20000003" : "00000003"), virtual_name);
if (!FileUtil::CreateFullPath(target_path)) {
return false;
}
return FileUtil::Copy(directory + virtual_name, target_path);
});
}
case 2: { // seed db
const auto target_path = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + SEED_DB;
LOG_INFO(Core, "Copying {} from {} to {}", SEED_DB, config.seed_db_path, target_path);
if (!FileUtil::CreateFullPath(target_path)) {
return false;
}
return FileUtil::Copy(config.seed_db_path, target_path);
}
case 3: { // secret sector
@@ -152,8 +171,23 @@ bool SDMCImporter::ImportSysdata(u64 id) {
FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + SECRET_SECTOR;
LOG_INFO(Core, "Copying {} from {} to {}", SECRET_SECTOR, config.secret_sector_path,
target_path);
if (!FileUtil::CreateFullPath(target_path)) {
return false;
}
return FileUtil::Copy(config.secret_sector_path, target_path);
}
case 4: { // aes_keys.txt
const auto target_path = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + AES_KEYS;
if (!FileUtil::CreateFullPath(target_path)) {
return false;
}
FileUtil::IOFile file(target_path, "w");
if (!file) {
return false;
}
file.WriteString("slot0x25KeyX=" + Key::KeyToString(Key::GetKeyX(0x25)) + "\n");
return true;
}
default:
UNREACHABLE_MSG("Unexpected sysdata id {}", id);
}
@@ -167,17 +201,25 @@ std::vector<ContentSpecifier> SDMCImporter::ListContent() const {
return content_list;
}
// Regex for half Title IDs
static const std::regex title_regex{"[0-9a-f]{8}"};
void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
const auto ProcessDirectory = [&out, &sdmc_path = config.sdmc_path](ContentType type,
u64 high_id) {
const auto ProcessDirectory = [& decryptor = this->decryptor, &out,
&sdmc_path = config.sdmc_path](ContentType type, u64 high_id) {
FileUtil::ForeachDirectoryEntry(
nullptr, fmt::format("{}title/{:08x}/", sdmc_path, high_id),
[type, high_id, &out](u64* /*num_entries_out*/, const std::string& directory,
const std::string& virtual_name) {
[&decryptor, type, high_id, &out](u64* /*num_entries_out*/,
const std::string& directory,
const std::string& virtual_name) {
if (!FileUtil::IsDirectory(directory + virtual_name + "/")) {
return true;
}
if (!std::regex_match(virtual_name, title_regex)) {
return true;
}
const u64 id = (high_id << 32) + std::stoull(virtual_name, nullptr, 16);
const auto citra_path = fmt::format(
"{}Nintendo "
@@ -194,6 +236,15 @@ void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
return true;
}
if (FileUtil::Exists(directory + virtual_name + "/data/")) {
// Savegames can be uninitialized.
// TODO: Is there a better way of checking this other than performing the
// decryption? (Very costy)
DataContainer container(decryptor->DecryptFile(
fmt::format("/title/{:08x}/{}/data/00000001.sav", high_id, virtual_name)));
if (!container.IsGood()) {
return true;
}
out.push_back(
{ContentType::Savegame, id, FileUtil::Exists(citra_path + "data/"),
FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/data/")});
@@ -216,6 +267,10 @@ void SDMCImporter::ListExtdata(std::vector<ContentSpecifier>& out) const {
return true;
}
if (!std::regex_match(virtual_name, title_regex)) {
return true;
}
const u64 id = std::stoull(virtual_name, nullptr, 16);
const auto citra_path =
fmt::format("{}Nintendo "
@@ -240,6 +295,12 @@ void SDMCImporter::ListSysdata(std::vector<ContentSpecifier>& out) const {
CHECK_CONTENT(0, config.bootrom_path, sysdata_path + BOOTROM9, BOOTROM9);
CHECK_CONTENT(2, config.seed_db_path, sysdata_path + SEED_DB, SEED_DB);
CHECK_CONTENT(3, config.secret_sector_path, sysdata_path + SECRET_SECTOR, SECRET_SECTOR);
if (!config.bootrom_path.empty()) {
// 47 bytes = "slot0x26KeyX=<32>\r\n" is only for Windows,
// but it's maximum_size so probably okay
out.push_back(
{ContentType::Sysdata, 4, FileUtil::Exists(sysdata_path + AES_KEYS), 47, AES_KEYS});
}
}
#undef CHECK_CONTENT
+2
View File
@@ -55,6 +55,8 @@ struct Config {
std::string safe_mode_firm_path; ///< Path to safe mode firm (A folder) (Sysdata 1)
std::string seed_db_path; ///< Path to seeddb.bin (Sysdata 2)
std::string secret_sector_path; ///< Path to secret sector (New3DS only) (Sysdata 3)
// Sysdata 4 is aes_keys.db (slot0x25KeyX)
};
class SDMCImporter {
+31 -15
View File
@@ -74,11 +74,17 @@ bool InnerFAT::WriteMetadata(const std::string& path) const {
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_)) {
SDSavegame::SDSavegame(std::vector<std::vector<u8>> partitions) {
if (partitions.size() == 1) {
duplicate_data = true;
data = std::move(partitions[0]);
} else if (partitions.size() == 2) {
duplicate_data = false;
partitionA = std::move(partitions[0]);
partitionB = std::move(partitions[1]);
} else {
UNREACHABLE();
}
is_good = Init();
}
@@ -151,9 +157,9 @@ bool SDSavegame::ExtractFile(const std::string& path, std::size_t index) const {
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 name = name_data.data();
FileUtil::IOFile file(path + name, "wb");
if (!file.IsOpen()) {
if (!file) {
LOG_ERROR(Core, "Could not open file {}", path + name);
return false;
}
@@ -163,6 +169,7 @@ bool SDSavegame::ExtractFile(const std::string& path, std::size_t index) const {
return true;
}
u64 file_size = entry.file_size;
while (true) {
// Entry index is block index + 1
auto block_data = fat[block + 1];
@@ -172,14 +179,16 @@ bool SDSavegame::ExtractFile(const std::string& path, std::size_t index) const {
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) {
const std::size_t size = fs_info.data_region_block_size * (last_block - block + 1);
const std::size_t to_write = std::min(file_size, size);
if (file.WriteBytes(data_region.data() + fs_info.data_region_block_size * block,
to_write) != to_write) {
LOG_ERROR(Core, "Write data failed (file: {})", path + name);
return false;
}
file_size -= to_write;
if (block_data.v.index == 0) // last node
if (block_data.v.index == 0 || file_size == 0) // last node
break;
block = block_data.v.index - 1;
@@ -209,7 +218,7 @@ 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,
ArchiveFormatInfo format_info = {/* total_size */ 0x40000,
/* number_directories */ fs_info.maximum_directory_count,
/* number_files */ fs_info.maximum_file_count,
/* duplicate_data */ duplicate_data};
@@ -236,7 +245,10 @@ bool SDExtdata::Init() {
LOG_ERROR(Core, "Failed to load or decrypt VSXE");
return false;
}
DataContainer vsxe_container(vsxe_raw);
DataContainer vsxe_container(std::move(vsxe_raw));
if (!vsxe_container.IsGood()) {
return false;
}
auto vsxe = vsxe_container.GetIVFCLevel4Data()[0];
// Read header
@@ -298,7 +310,7 @@ bool SDExtdata::ExtractFile(const std::string& path, std::size_t index) const {
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 name = name_data.data();
FileUtil::IOFile file(path + name, "wb");
if (!file) {
LOG_ERROR(Core, "Could not open file {}", path + name);
@@ -317,7 +329,11 @@ bool SDExtdata::ExtractFile(const std::string& path, std::size_t index) const {
return true;
}
DataContainer container(container_data);
DataContainer container(std::move(container_data));
if (!container.IsGood()) {
return false;
}
auto data = container.GetIVFCLevel4Data()[0];
if (file.WriteBytes(data.data(), data.size()) != data.size()) {
LOG_ERROR(Core, "Write data failed (file: {})", path + name);
+1 -2
View File
@@ -156,8 +156,7 @@ protected:
class SDSavegame : public InnerFAT {
public:
explicit SDSavegame(std::vector<u8> data);
explicit SDSavegame(std::vector<u8> partitionA, std::vector<u8> partitionB);
explicit SDSavegame(std::vector<std::vector<u8>> partitions);
~SDSavegame() override;
bool Extract(std::string path) const override;
+22 -4
View File
@@ -82,14 +82,15 @@ struct KeySlot {
std::array<KeySlot, KeySlotID::MaxKeySlotID> key_slots;
std::array<std::optional<AESKey>, 6> common_key_y_slots;
std::string KeyToString(AESKey& key) {
} // namespace
std::string KeyToString(const 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 = {
@@ -160,6 +161,18 @@ void LoadBootromKeys(const std::string& path) {
break;
}
}
// HACK: "Dump" 0x25 KeyX
// TODO: Is this legal?
constexpr std::array<u64, 16> offsets{{0x138A, 0xCAB, 0xD07, 0x3004, 0x2C, 0x49, 0xE6, 0x146E,
0x1126, 0xD0, 0x85C, 0x47, 0x70A, 0x112C, 0x808, 0x89}};
for (std::size_t i = 0; i < offsets.size(); ++i) {
file.Seek(offsets[i], SEEK_SET);
file.ReadBytes(&new_key[i], 1);
}
LOG_DEBUG(Key, "Loaded Slot0x25 KeyX: {}", KeyToString(new_key));
SetKeyX(0x25, new_key);
}
void LoadMovableSedKeys(const std::string& path) {
@@ -174,7 +187,7 @@ void LoadMovableSedKeys(const std::string& path) {
return;
}
constexpr std::size_t KEY_SECTION_START = 0x118;
constexpr std::size_t KEY_SECTION_START = 0x110;
file.Seek(KEY_SECTION_START, SEEK_SET); // Jump to the key section
AESKey key;
@@ -184,7 +197,8 @@ void LoadMovableSedKeys(const std::string& path) {
return;
}
SetKeyY(0x26, key);
LOG_DEBUG(Key, "Loaded Slot0x34KeyY: {}", KeyToString(key));
SetKeyY(0x34, key);
}
void ClearKeys() {
@@ -212,6 +226,10 @@ AESKey GetNormalKey(std::size_t slot_id) {
return key_slots.at(slot_id).normal.value_or(AESKey{});
}
AESKey GetKeyX(std::size_t slot_id) {
return key_slots.at(slot_id).x.value_or(AESKey{});
}
void SelectCommonKeyIndex(u8 index) {
key_slots[KeySlotID::TicketCommonKey].SetKeyY(common_key_y_slots.at(index));
}
+5
View File
@@ -56,6 +56,8 @@ constexpr std::size_t AES_BLOCK_SIZE = 16;
using AESKey = std::array<u8, AES_BLOCK_SIZE>;
std::string KeyToString(const AESKey& key);
void LoadBootromKeys(const std::string& path);
void LoadMovableSedKeys(const std::string& path);
void ClearKeys();
@@ -67,6 +69,9 @@ void SetNormalKey(std::size_t slot_id, const AESKey& key);
bool IsNormalKeyAvailable(std::size_t slot_id);
AESKey GetNormalKey(std::size_t slot_id);
// For importing aes_keys.txt
AESKey GetKeyX(std::size_t slot_id);
void SelectCommonKeyIndex(u8 index);
} // namespace Core::Key