forked from dolphin-emu/dolphin
		
	
		
			
				
	
	
		
			509 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			509 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
// Copyright 2009 Dolphin Emulator Project
 | 
						|
// Licensed under GPLv2+
 | 
						|
// Refer to the license.txt file included.
 | 
						|
 | 
						|
#include "DiscIO/NANDContentLoader.h"
 | 
						|
 | 
						|
#include <algorithm>
 | 
						|
#include <array>
 | 
						|
#include <cinttypes>
 | 
						|
#include <cstddef>
 | 
						|
#include <cstdio>
 | 
						|
#include <cstring>
 | 
						|
#include <functional>
 | 
						|
#include <map>
 | 
						|
#include <string>
 | 
						|
#include <utility>
 | 
						|
#include <vector>
 | 
						|
 | 
						|
#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<File::IOFile>(m_filename, "rb");
 | 
						|
  else if (!m_file->IsOpen())
 | 
						|
    m_file->Open(m_filename, "rb");
 | 
						|
}
 | 
						|
void CNANDContentDataFile::Open()
 | 
						|
{
 | 
						|
  EnsureOpen();
 | 
						|
}
 | 
						|
std::vector<u8> CNANDContentDataFile::Get()
 | 
						|
{
 | 
						|
  EnsureOpen();
 | 
						|
 | 
						|
  if (!m_file->IsGood())
 | 
						|
    return {};
 | 
						|
 | 
						|
  u64 size = m_file->GetSize();
 | 
						|
  if (size == 0)
 | 
						|
    return {};
 | 
						|
 | 
						|
  std::vector<u8> 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_t>(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<u8> 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<u8> 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<u8>& data_app)
 | 
						|
{
 | 
						|
  if (!m_ticket.IsValid())
 | 
						|
  {
 | 
						|
    ERROR_LOG(IOS_ES, "No valid ticket for title %016" PRIx64, m_tmd.GetTitleId());
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  const std::vector<IOS::ES::Content> contents = m_tmd.GetContents();
 | 
						|
  m_Content.resize(contents.size());
 | 
						|
 | 
						|
  u32 data_app_offset = 0;
 | 
						|
  const std::vector<u8> 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<u8, 16> iv{};
 | 
						|
      iv[0] = static_cast<u8>(content.index >> 8) & 0xFF;
 | 
						|
      iv[1] = static_cast<u8>(content.index) & 0xFF;
 | 
						|
 | 
						|
      u32 rounded_size = Common::AlignUp(static_cast<u32>(content.size), 0x40);
 | 
						|
 | 
						|
      m_Content[i].m_Data = std::make_unique<CNANDContentDataBuffer>(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<CNANDContentDataFile>(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<CNANDContentLoader>(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<u64>& 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<u8>& 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<u8> 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
 |