diff --git a/Source/Core/DiscIO/CMakeLists.txt b/Source/Core/DiscIO/CMakeLists.txt index b8f337b5da..cd3fee7f1d 100644 --- a/Source/Core/DiscIO/CMakeLists.txt +++ b/Source/Core/DiscIO/CMakeLists.txt @@ -10,6 +10,7 @@ set(SRCS FileSystemGCWii.cpp Filesystem.cpp NANDContentLoader.cpp + NANDImporter.cpp TGCBlob.cpp Volume.cpp VolumeCreator.cpp diff --git a/Source/Core/DiscIO/DiscIO.vcxproj b/Source/Core/DiscIO/DiscIO.vcxproj index 9ab6365e13..4846c963ef 100644 --- a/Source/Core/DiscIO/DiscIO.vcxproj +++ b/Source/Core/DiscIO/DiscIO.vcxproj @@ -45,6 +45,7 @@ + @@ -66,6 +67,7 @@ + diff --git a/Source/Core/DiscIO/DiscIO.vcxproj.filters b/Source/Core/DiscIO/DiscIO.vcxproj.filters index 118169a0c9..d163a5c73d 100644 --- a/Source/Core/DiscIO/DiscIO.vcxproj.filters +++ b/Source/Core/DiscIO/DiscIO.vcxproj.filters @@ -33,6 +33,9 @@ NAND + + NAND + Volume\Blob @@ -92,6 +95,9 @@ NAND + + NAND + Volume\Blob diff --git a/Source/Core/DiscIO/NANDImporter.cpp b/Source/Core/DiscIO/NANDImporter.cpp new file mode 100644 index 0000000000..4cdec9d081 --- /dev/null +++ b/Source/Core/DiscIO/NANDImporter.cpp @@ -0,0 +1,222 @@ +// Copyright 2017 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "DiscIO/NANDImporter.h" + +#include +#include + +#include "Common/Crypto/AES.h" +#include "Common/FileUtil.h" +#include "Common/MsgHandler.h" +#include "Common/Swap.h" +#include "DiscIO/NANDContentLoader.h" + +namespace DiscIO +{ +constexpr size_t NAND_SIZE = 0x20000000; +constexpr size_t NAND_KEYS_SIZE = 0x400; + +NANDImporter::NANDImporter() = default; +NANDImporter::~NANDImporter() = default; + +void NANDImporter::ImportNANDBin(const std::string& path_to_bin, + std::function update_callback) +{ + m_update_callback = std::move(update_callback); + + if (!ReadNANDBin(path_to_bin)) + return; + + const std::string nand_root = File::GetUserPath(D_WIIROOT_IDX); + FindSuperblock(); + CountEntries(0); + ProcessEntry(0, nand_root); + ExportKeys(nand_root); + ExtractCertificates(nand_root); + + // We have to clear the cache so the new NAND takes effect + DiscIO::CNANDContentManager::Access().ClearCache(); +} + +bool NANDImporter::ReadNANDBin(const std::string& path_to_bin) +{ + constexpr size_t NAND_TOTAL_BLOCKS = 0x40000; + constexpr size_t NAND_BLOCK_SIZE = 0x800; + constexpr size_t NAND_ECC_BLOCK_SIZE = 0x40; + constexpr size_t NAND_BIN_SIZE = + (NAND_BLOCK_SIZE + NAND_ECC_BLOCK_SIZE) * NAND_TOTAL_BLOCKS + NAND_KEYS_SIZE; // 0x21000400 + + File::IOFile file(path_to_bin, "rb"); + if (file.GetSize() != NAND_BIN_SIZE) + { + PanicAlertT("This file does not look like a BootMii NAND backup. (0x%lx does not equal 0x%zx)", + file.GetSize(), NAND_BIN_SIZE); + return false; + } + + m_nand.resize(NAND_SIZE); + + for (size_t i = 0; i < NAND_TOTAL_BLOCKS; i++) + { + file.ReadBytes(&m_nand[i * NAND_BLOCK_SIZE], NAND_BLOCK_SIZE); + file.Seek(NAND_ECC_BLOCK_SIZE, SEEK_CUR); // We don't care about the ECC blocks + } + + m_nand_keys.resize(NAND_KEYS_SIZE); + file.ReadBytes(m_nand_keys.data(), NAND_KEYS_SIZE); + return true; +} + +void NANDImporter::FindSuperblock() +{ + constexpr size_t NAND_SUPERBLOCK_START = 0x1fc00000; + constexpr size_t NAND_SUPERBLOCK_SIZE = 0x40000; + + size_t superblock = 0; + u32 newest_version = 0; + for (size_t pos = NAND_SUPERBLOCK_START; pos < NAND_SIZE; pos += NAND_SUPERBLOCK_SIZE) + { + if (!memcmp(m_nand.data() + pos, "SFFS", 4)) + { + u32 version = Common::swap32(&m_nand[pos + 4]); + if (superblock == 0 || version > newest_version) + { + superblock = pos; + newest_version = version; + } + } + } + + m_nand_fat_offset = superblock + 0xC; + m_nand_fst_offset = m_nand_fat_offset + 0x10000; +} + +std::string NANDImporter::GetPath(const NANDFSTEntry& entry, const std::string& parent_path) +{ + std::string name(reinterpret_cast(&entry.name), sizeof(NANDFSTEntry::name)); + // Get rid of any extra null characters + while (name.back() == '\0') + name.pop_back(); + + if (name.front() == '/' || parent_path.back() == '/') + return parent_path + name; + + return parent_path + '/' + name; +} + +void NANDImporter::ProcessEntry(u16 entry_number, const std::string& parent_path) +{ + NANDFSTEntry entry; + memcpy(&entry, &m_nand[m_nand_fst_offset + sizeof(NANDFSTEntry) * Common::swap16(entry_number)], + sizeof(NANDFSTEntry)); + UpdateStatus(); + + if (entry.sib != 0xffff) + ProcessEntry(entry.sib, parent_path); + + if ((entry.mode & 3) == 1) + ProcessFile(entry, parent_path); + else if ((entry.mode & 3) == 2) + ProcessDirectory(entry, parent_path); +} + +void NANDImporter::ProcessDirectory(const NANDFSTEntry& entry, const std::string& parent_path) +{ + const std::string path = GetPath(entry, parent_path); + File::CreateDir(path); + + if (entry.sub != 0xffff) + ProcessEntry(entry.sub, path); +} + +void NANDImporter::ProcessFile(const NANDFSTEntry& entry, const std::string& parent_path) +{ + constexpr size_t NAND_AES_KEY_OFFSET = 0x158; + constexpr size_t NAND_FAT_BLOCK_SIZE = 0x4000; + + const std::string path = GetPath(entry, parent_path); + File::IOFile file(path, "wb"); + std::array key{}; + std::copy(&m_nand_keys[NAND_AES_KEY_OFFSET], &m_nand_keys[NAND_AES_KEY_OFFSET + key.size()], + key.begin()); + u16 sub = Common::swap16(entry.sub); + u32 remaining_bytes = Common::swap32(entry.size); + + while (remaining_bytes > 0) + { + std::array iv{}; + std::vector block = Common::AES::Decrypt( + key.data(), iv.data(), &m_nand[NAND_FAT_BLOCK_SIZE * sub], NAND_FAT_BLOCK_SIZE); + u32 size = remaining_bytes < NAND_FAT_BLOCK_SIZE ? remaining_bytes : NAND_FAT_BLOCK_SIZE; + file.WriteBytes(block.data(), size); + remaining_bytes -= size; + sub = Common::swap16(&m_nand[m_nand_fat_offset + 2 * sub]); + } +} + +void NANDImporter::ExtractCertificates(const std::string& nand_root) +{ + const std::string title_contents_path = + nand_root + "/title/00000001/0000000d/content/00000011.app"; + File::IOFile file(title_contents_path, "rb"); + if (!file) + { + PanicAlertT("Unable to open %s! Refer to " + "https://dolphin-emu.org/docs/guides/wii-network-guide/ to set up " + "certificates.", + title_contents_path.c_str()); + return; + } + + struct Certificate + { + u32 offset; + u32 size; + std::string filename; + }; + std::array certificates = {{{0x92834, 1005, "/clientca.pem"}, + {0x92d38, 609, "/clientcakey.pem"}, + {0x92440, 897, "/rootca.pem"}}}; + for (const Certificate& cert : certificates) + { + file.Seek(cert.offset, SEEK_SET); + std::vector pem_cert(cert.size); + file.ReadBytes(pem_cert.data(), pem_cert.size()); + + const std::string pem_file_path = nand_root + cert.filename; + File::IOFile pem_file(pem_file_path, "wb"); + if (!pem_file.WriteBytes(pem_cert.data(), pem_cert.size())) + PanicAlertT("Unable to write to file %s", pem_file_path.c_str()); + } +} + +void NANDImporter::ExportKeys(const std::string& nand_root) +{ + const std::string file_path = nand_root + "/keys.bin"; + File::IOFile file(file_path, "wb"); + if (!file.WriteBytes(m_nand_keys.data(), NAND_KEYS_SIZE)) + PanicAlertT("Unable to write to file %s", file_path.c_str()); +} + +void NANDImporter::CountEntries(u16 entry_number) +{ + NANDFSTEntry entry; + memcpy(&entry, &m_nand[m_nand_fst_offset + sizeof(NANDFSTEntry) * Common::swap16(entry_number)], + sizeof(NANDFSTEntry)); + + m_total_entries++; + + if (entry.sib != 0xffff) + CountEntries(entry.sib); + + if ((entry.mode & 3) == 2 && entry.sub != 0xffff) + CountEntries(entry.sub); +} + +void NANDImporter::UpdateStatus() +{ + m_update_callback(m_current_entry++, m_total_entries); +} +} diff --git a/Source/Core/DiscIO/NANDImporter.h b/Source/Core/DiscIO/NANDImporter.h new file mode 100644 index 0000000000..d79f733284 --- /dev/null +++ b/Source/Core/DiscIO/NANDImporter.h @@ -0,0 +1,60 @@ +// Copyright 2017 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +#include "Common/CommonTypes.h" + +namespace DiscIO +{ +class NANDImporter final +{ +public: + NANDImporter(); + ~NANDImporter(); + + void ImportNANDBin(const std::string& path_to_bin, + std::function update_callback); + void ExtractCertificates(const std::string& nand_root); + +private: +#pragma pack(push, 1) + struct NANDFSTEntry + { + u8 name[12]; + u8 mode; // 0x0C + u8 attr; // 0x0D + u16 sub; // 0x0E + u16 sib; // 0x10 + u32 size; // 0x12 + u16 x1; // 0x16 + u16 uid; // 0x18 + u16 gid; // 0x1A + u32 x3; // 0x1C + }; +#pragma pack(pop) + + bool ReadNANDBin(const std::string& path_to_bin); + void FindSuperblock(); + std::string GetPath(const NANDFSTEntry& entry, const std::string& parent_path); + void ProcessEntry(u16 entry_number, const std::string& parent_path); + void ProcessFile(const NANDFSTEntry& entry, const std::string& parent_path); + void ProcessDirectory(const NANDFSTEntry& entry, const std::string& parent_path); + void ExportKeys(const std::string& nand_root); + void CountEntries(u16 entry_number); + void UpdateStatus(); + + std::vector m_nand; + std::vector m_nand_keys; + size_t m_nand_fat_offset = 0; + size_t m_nand_fst_offset = 0; + std::function m_update_callback; + size_t m_total_entries = 0; + size_t m_current_entry = 0; +}; +} diff --git a/Source/Core/DolphinWX/Frame.h b/Source/Core/DolphinWX/Frame.h index e5453efe4a..236b0ac8be 100644 --- a/Source/Core/DolphinWX/Frame.h +++ b/Source/Core/DolphinWX/Frame.h @@ -316,6 +316,8 @@ private: void OnShowCheatsWindow(wxCommandEvent& event); void OnLoadWiiMenu(wxCommandEvent& event); void OnInstallWAD(wxCommandEvent& event); + void OnImportBootMiiBackup(wxCommandEvent& event); + void OnExtractCertificates(wxCommandEvent& event); void OnFifoPlayer(wxCommandEvent& event); void OnConnectWiimote(wxCommandEvent& event); void GameListChanged(wxCommandEvent& event); diff --git a/Source/Core/DolphinWX/FrameTools.cpp b/Source/Core/DolphinWX/FrameTools.cpp index 1d20777e07..f157dcf5fe 100644 --- a/Source/Core/DolphinWX/FrameTools.cpp +++ b/Source/Core/DolphinWX/FrameTools.cpp @@ -52,6 +52,7 @@ #include "Core/State.h" #include "DiscIO/NANDContentLoader.h" +#include "DiscIO/NANDImporter.h" #include "DolphinWX/AboutDolphin.h" #include "DolphinWX/Cheats/CheatsWindow.h" @@ -168,6 +169,8 @@ void CFrame::BindMenuBarEvents() Bind(wxEVT_MENU, &CFrame::OnNetPlay, this, IDM_NETPLAY); Bind(wxEVT_MENU, &CFrame::OnInstallWAD, this, IDM_MENU_INSTALL_WAD); Bind(wxEVT_MENU, &CFrame::OnLoadWiiMenu, this, IDM_LOAD_WII_MENU); + Bind(wxEVT_MENU, &CFrame::OnImportBootMiiBackup, this, IDM_IMPORT_NAND); + Bind(wxEVT_MENU, &CFrame::OnExtractCertificates, this, IDM_EXTRACT_CERTIFICATES); Bind(wxEVT_MENU, &CFrame::OnFifoPlayer, this, IDM_FIFOPLAYER); Bind(wxEVT_MENU, &CFrame::OnConnectWiimote, this, IDM_CONNECT_WIIMOTE1, IDM_CONNECT_BALANCEBOARD); @@ -1222,6 +1225,37 @@ void CFrame::OnInstallWAD(wxCommandEvent& event) } } +void CFrame::OnImportBootMiiBackup(wxCommandEvent& WXUNUSED(event)) +{ + if (!AskYesNoT("Merging a new NAND over your currently selected NAND will overwrite any channels " + "and savegames that already exist. This process is not reversible, so it is " + "recommended that you keep backups of both NANDs. Are you sure you want to " + "continue?")) + return; + + wxString path = wxFileSelector( + _("Select a BootMii NAND backup to import"), wxEmptyString, wxEmptyString, wxEmptyString, + _("BootMii NAND backup file (*.bin)") + "|*.bin|" + wxGetTranslation(wxALL_FILES), + wxFD_OPEN | wxFD_PREVIEW | wxFD_FILE_MUST_EXIST, this); + const std::string file_name = WxStrToStr(path); + if (file_name.empty()) + return; + + wxProgressDialog dialog(_("Importing NAND backup"), _("Working..."), 100, this, + wxPD_APP_MODAL | wxPD_ELAPSED_TIME | wxPD_SMOOTH); + DiscIO::NANDImporter().ImportNANDBin(file_name, + [&dialog](size_t current_entry, size_t total_entries) { + dialog.SetRange(total_entries); + dialog.Update(current_entry); + }); + UpdateLoadWiiMenuItem(); +} + +void CFrame::OnExtractCertificates(wxCommandEvent& WXUNUSED(event)) +{ + DiscIO::NANDImporter().ExtractCertificates(File::GetUserPath(D_WIIROOT_IDX)); +} + void CFrame::UpdateLoadWiiMenuItem() const { GetMenuBar()->Refresh(true, nullptr); diff --git a/Source/Core/DolphinWX/Globals.h b/Source/Core/DolphinWX/Globals.h index ebbc2123d7..de2402e712 100644 --- a/Source/Core/DolphinWX/Globals.h +++ b/Source/Core/DolphinWX/Globals.h @@ -101,6 +101,8 @@ enum IDM_LOAD_WII_MENU, IDM_MENU_INSTALL_WAD, IDM_LIST_INSTALL_WAD, + IDM_IMPORT_NAND, + IDM_EXTRACT_CERTIFICATES, IDM_FIFOPLAYER, IDM_CONNECT_WIIMOTE1, IDM_CONNECT_WIIMOTE2, diff --git a/Source/Core/DolphinWX/MainMenuBar.cpp b/Source/Core/DolphinWX/MainMenuBar.cpp index 765a9dca14..2454d29054 100644 --- a/Source/Core/DolphinWX/MainMenuBar.cpp +++ b/Source/Core/DolphinWX/MainMenuBar.cpp @@ -219,6 +219,8 @@ wxMenu* MainMenuBar::CreateToolsMenu() const tools_menu->Append(IDM_NETPLAY, _("Start &NetPlay...")); tools_menu->Append(IDM_MENU_INSTALL_WAD, _("Install WAD...")); tools_menu->Append(IDM_LOAD_WII_MENU, dummy_string); + tools_menu->Append(IDM_IMPORT_NAND, _("Import BootMii NAND Backup...")); + tools_menu->Append(IDM_EXTRACT_CERTIFICATES, _("Extract Certificates from NAND")); tools_menu->Append(IDM_FIFOPLAYER, _("FIFO Player")); tools_menu->AppendSeparator(); tools_menu->AppendSubMenu(wiimote_menu, _("Connect Wii Remotes"));