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"));