// Copyright 2009 Dolphin Emulator Project // Licensed under GPLv2+ // Refer to the license.txt file included. #include "DiscIO/NANDContentLoader.h" #include #include #include #include #include #include #include #include #include #include #include #include "Common/Align.h" #include "Common/CommonTypes.h" #include "Common/Crypto/AES.h" #include "Common/FileUtil.h" #include "Common/Logging/Log.h" #include "Common/MsgHandler.h" #include "Common/NandPaths.h" #include "Common/StringUtil.h" #include "Common/Swap.h" #include "DiscIO/WiiWad.h" namespace DiscIO { CNANDContentData::~CNANDContentData() = default; CSharedContent::CSharedContent(Common::FromWhichRoot root) : m_root(root) { m_Elements.clear(); m_LastID = 0; m_ContentMap = Common::RootUserPath(root) + "/shared1/content.map"; File::IOFile pFile(m_ContentMap, "rb"); SElement Element; while (pFile.ReadArray(&Element, 1)) { m_Elements.push_back(Element); m_LastID++; } } std::string CSharedContent::GetFilenameFromSHA1(const u8* hash) const { for (const auto& Element : m_Elements) { if (memcmp(hash, Element.SHA1Hash, 20) == 0) { return StringFromFormat( "%s/shared1/%c%c%c%c%c%c%c%c.app", Common::RootUserPath(m_root).c_str(), Element.FileName[0], Element.FileName[1], Element.FileName[2], Element.FileName[3], Element.FileName[4], Element.FileName[5], Element.FileName[6], Element.FileName[7]); } } return "unk"; } std::string CSharedContent::AddSharedContent(const u8* hash) { std::string filename = GetFilenameFromSHA1(hash); if (strcasecmp(filename.c_str(), "unk") == 0) { std::string id = StringFromFormat("%08x", m_LastID); SElement Element; memcpy(Element.FileName, id.c_str(), 8); memcpy(Element.SHA1Hash, hash, 20); m_Elements.push_back(Element); File::CreateFullPath(m_ContentMap); File::IOFile pFile(m_ContentMap, "ab"); pFile.WriteArray(&Element, 1); filename = StringFromFormat("%s/shared1/%s.app", Common::RootUserPath(m_root).c_str(), id.c_str()); m_LastID++; } return filename; } CNANDContentDataFile::CNANDContentDataFile(const std::string& filename) : m_filename{filename} { } CNANDContentDataFile::~CNANDContentDataFile() = default; void CNANDContentDataFile::EnsureOpen() { if (!m_file) m_file = std::make_unique(m_filename, "rb"); else if (!m_file->IsOpen()) m_file->Open(m_filename, "rb"); } void CNANDContentDataFile::Open() { EnsureOpen(); } std::vector CNANDContentDataFile::Get() { EnsureOpen(); if (!m_file->IsGood()) return {}; u64 size = m_file->GetSize(); if (size == 0) return {}; std::vector result(size); m_file->ReadBytes(result.data(), result.size()); return result; } bool CNANDContentDataFile::GetRange(u32 start, u32 size, u8* buffer) { EnsureOpen(); if (!m_file->IsGood()) return false; if (!m_file->Seek(start, SEEK_SET)) return false; return m_file->ReadBytes(buffer, static_cast(size)); } void CNANDContentDataFile::Close() { if (m_file && m_file->IsOpen()) m_file->Close(); } bool CNANDContentDataBuffer::GetRange(u32 start, u32 size, u8* buffer) { if (start + size > m_buffer.size()) return false; std::copy_n(&m_buffer[start], size, buffer); return true; } CNANDContentLoader::CNANDContentLoader(const std::string& content_name) { m_Valid = Initialize(content_name); } CNANDContentLoader::~CNANDContentLoader() { } bool CNANDContentLoader::IsValid() const { return m_Valid; } const SNANDContent* CNANDContentLoader::GetContentByID(u32 id) const { const auto iterator = std::find_if(m_Content.begin(), m_Content.end(), [id](const auto& content) { return content.m_metadata.id == id; }); return iterator != m_Content.end() ? &*iterator : nullptr; } const SNANDContent* CNANDContentLoader::GetContentByIndex(int index) const { for (auto& Content : m_Content) { if (Content.m_metadata.index == index) { return &Content; } } return nullptr; } bool CNANDContentLoader::Initialize(const std::string& name) { if (name.empty()) return false; m_Path = name; WiiWAD wad(name); std::vector data_app; if (wad.IsValid()) { m_IsWAD = true; m_ticket = wad.GetTicket(); m_tmd = wad.GetTMD(); data_app = wad.GetDataApp(); } else { std::string tmd_filename(m_Path); if (tmd_filename.back() == '/') tmd_filename += "title.tmd"; else m_Path = tmd_filename.substr(0, tmd_filename.find("title.tmd")); File::IOFile tmd_file(tmd_filename, "rb"); if (!tmd_file) { WARN_LOG(DISCIO, "CreateFromDirectory: error opening %s", tmd_filename.c_str()); return false; } std::vector bytes(File::GetSize(tmd_filename)); tmd_file.ReadBytes(bytes.data(), bytes.size()); m_tmd.SetBytes(std::move(bytes)); m_ticket = FindSignedTicket(m_tmd.GetTitleId()); } InitializeContentEntries(data_app); return true; } void CNANDContentLoader::InitializeContentEntries(const std::vector& data_app) { if (!m_ticket.IsValid()) { ERROR_LOG(IOS_ES, "No valid ticket for title %016" PRIx64, m_tmd.GetTitleId()); return; } const std::vector contents = m_tmd.GetContents(); m_Content.resize(contents.size()); u32 data_app_offset = 0; const std::vector title_key = m_ticket.GetTitleKey(); CSharedContent shared_content{Common::FromWhichRoot::FROM_SESSION_ROOT}; for (size_t i = 0; i < contents.size(); ++i) { const auto& content = contents.at(i); if (m_IsWAD) { // The content index is used as IV (2 bytes); the remaining 14 bytes are zeroes. std::array iv{}; iv[0] = static_cast(content.index >> 8) & 0xFF; iv[1] = static_cast(content.index) & 0xFF; u32 rounded_size = Common::AlignUp(static_cast(content.size), 0x40); m_Content[i].m_Data = std::make_unique(Common::AES::Decrypt( title_key.data(), iv.data(), &data_app[data_app_offset], rounded_size)); data_app_offset += rounded_size; } else { std::string filename; if (content.IsShared()) filename = shared_content.GetFilenameFromSHA1(content.sha1.data()); else filename = StringFromFormat("%s/%08x.app", m_Path.c_str(), content.id); m_Content[i].m_Data = std::make_unique(filename); } m_Content[i].m_metadata = std::move(content); } } CNANDContentManager::~CNANDContentManager() { } const CNANDContentLoader& CNANDContentManager::GetNANDLoader(const std::string& content_path) { auto it = m_map.find(content_path); if (it != m_map.end()) return *it->second; return *m_map .emplace_hint(it, std::make_pair(content_path, std::make_unique(content_path))) ->second; } const CNANDContentLoader& CNANDContentManager::GetNANDLoader(u64 title_id, Common::FromWhichRoot from) { std::string path = Common::GetTitleContentPath(title_id, from); return GetNANDLoader(path); } bool CNANDContentManager::RemoveTitle(u64 title_id, Common::FromWhichRoot from) { auto& loader = GetNANDLoader(title_id, from); if (!loader.IsValid()) return false; loader.RemoveTitle(); return GetNANDLoader(title_id, from).IsValid(); } void CNANDContentManager::ClearCache() { m_map.clear(); } void CNANDContentLoader::RemoveTitle() const { const u64 title_id = m_tmd.GetTitleId(); INFO_LOG(DISCIO, "RemoveTitle %08x/%08x", (u32)(title_id >> 32), (u32)title_id); if (IsValid()) { // remove TMD? for (const auto& content : m_Content) { if (!content.m_metadata.IsShared()) { std::string path = StringFromFormat("%s/%08x.app", m_Path.c_str(), content.m_metadata.id); INFO_LOG(DISCIO, "Delete %s", path.c_str()); File::Delete(path); } } CNANDContentManager::Access().ClearCache(); // deletes 'this' } } cUIDsys::cUIDsys(Common::FromWhichRoot root) { m_Elements.clear(); m_LastUID = 0x00001000; m_UidSys = Common::RootUserPath(root) + "/sys/uid.sys"; File::IOFile pFile(m_UidSys, "rb"); SElement Element; while (pFile.ReadArray(&Element, 1)) { *(u32*)&(Element.UID) = Common::swap32(m_LastUID++); m_Elements.push_back(Element); } pFile.Close(); if (m_Elements.empty()) { *(u64*)&(Element.titleID) = Common::swap64(TITLEID_SYSMENU); *(u32*)&(Element.UID) = Common::swap32(m_LastUID++); File::CreateFullPath(m_UidSys); pFile.Open(m_UidSys, "wb"); if (!pFile.WriteArray(&Element, 1)) ERROR_LOG(DISCIO, "Failed to write to %s", m_UidSys.c_str()); } } u32 cUIDsys::GetUIDFromTitle(u64 title_id) { for (auto& Element : m_Elements) { if (Common::swap64(title_id) == *(u64*)&(Element.titleID)) { return Common::swap32(Element.UID); } } return 0; } void cUIDsys::AddTitle(u64 title_id) { if (GetUIDFromTitle(title_id)) { INFO_LOG(DISCIO, "Title %08x%08x, already exists in uid.sys", (u32)(title_id >> 32), (u32)title_id); return; } SElement Element; *(u64*)&(Element.titleID) = Common::swap64(title_id); *(u32*)&(Element.UID) = Common::swap32(m_LastUID++); m_Elements.push_back(Element); File::CreateFullPath(m_UidSys); File::IOFile pFile(m_UidSys, "ab"); if (!pFile.WriteArray(&Element, 1)) ERROR_LOG(DISCIO, "fwrite failed"); } void cUIDsys::GetTitleIDs(std::vector& title_ids, bool owned) { for (auto& Element : m_Elements) { if ((owned && Common::CheckTitleTIK(Common::swap64(Element.titleID), Common::FROM_SESSION_ROOT)) || (!owned && Common::CheckTitleTMD(Common::swap64(Element.titleID), Common::FROM_SESSION_ROOT))) title_ids.push_back(Common::swap64(Element.titleID)); } } u64 CNANDContentManager::Install_WiiWAD(const std::string& filename) { if (filename.find(".wad") == std::string::npos) return 0; const CNANDContentLoader& content_loader = GetNANDLoader(filename); if (content_loader.IsValid() == false) return 0; const u64 title_id = content_loader.GetTMD().GetTitleId(); // copy WAD's TMD header and contents to content directory std::string content_path(Common::GetTitleContentPath(title_id, Common::FROM_CONFIGURED_ROOT)); std::string tmd_filename(Common::GetTMDFileName(title_id, Common::FROM_CONFIGURED_ROOT)); File::CreateFullPath(tmd_filename); File::IOFile tmd_file(tmd_filename, "wb"); if (!tmd_file) { PanicAlertT("WAD installation failed: error creating %s", tmd_filename.c_str()); return 0; } const auto& raw_tmd = content_loader.GetTMD().GetRawTMD(); tmd_file.WriteBytes(raw_tmd.data(), raw_tmd.size()); CSharedContent shared_content{Common::FromWhichRoot::FROM_CONFIGURED_ROOT}; for (const auto& content : content_loader.GetContent()) { std::string app_filename; if (content.m_metadata.IsShared()) app_filename = shared_content.AddSharedContent(content.m_metadata.sha1.data()); else app_filename = StringFromFormat("%s%08x.app", content_path.c_str(), content.m_metadata.id); if (!File::Exists(app_filename)) { File::CreateFullPath(app_filename); File::IOFile app_file(app_filename, "wb"); if (!app_file) { PanicAlertT("WAD installation failed: error creating %s", app_filename.c_str()); return 0; } app_file.WriteBytes(content.m_Data->Get().data(), content.m_metadata.size); } else { INFO_LOG(DISCIO, "Content %s already exists.", app_filename.c_str()); } } // Extract and copy WAD's ticket to ticket directory if (!AddTicket(content_loader.GetTicket())) { PanicAlertT("WAD installation failed: error creating ticket"); return 0; } cUIDsys uid_sys{Common::FromWhichRoot::FROM_CONFIGURED_ROOT}; uid_sys.AddTitle(title_id); ClearCache(); return title_id; } bool AddTicket(const IOS::ES::TicketReader& signed_ticket) { if (!signed_ticket.IsValid()) { return false; } u64 title_id = signed_ticket.GetTitleId(); std::string ticket_filename = Common::GetTicketFileName(title_id, Common::FROM_CONFIGURED_ROOT); File::CreateFullPath(ticket_filename); File::IOFile ticket_file(ticket_filename, "wb"); if (!ticket_file) return false; const std::vector& raw_ticket = signed_ticket.GetRawTicket(); return ticket_file.WriteBytes(raw_ticket.data(), raw_ticket.size()); } IOS::ES::TicketReader FindSignedTicket(u64 title_id) { std::string ticket_filename = Common::GetTicketFileName(title_id, Common::FROM_CONFIGURED_ROOT); File::IOFile ticket_file(ticket_filename, "rb"); if (!ticket_file) { return IOS::ES::TicketReader{}; } std::vector signed_ticket(ticket_file.GetSize()); if (!ticket_file.ReadBytes(signed_ticket.data(), signed_ticket.size())) { return IOS::ES::TicketReader{}; } return IOS::ES::TicketReader{std::move(signed_ticket)}; } } // namespace end