From 8e10c9bb2e8690055ba07003ebd53a5215f82f8f Mon Sep 17 00:00:00 2001 From: shinyquagsire23 Date: Sun, 1 Oct 2017 10:32:43 -0600 Subject: [PATCH] file_sys: add class for Title Metadata (TMD) --- src/core/CMakeLists.txt | 1 + src/core/file_sys/title_metadata.cpp | 212 +++++++++++++++++++++++++++ src/core/file_sys/title_metadata.h | 125 ++++++++++++++++ 3 files changed, 338 insertions(+) create mode 100644 src/core/file_sys/title_metadata.cpp create mode 100644 src/core/file_sys/title_metadata.h diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 3ed6199916..2618da18c4 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -29,6 +29,7 @@ set(SRCS file_sys/ncch_container.cpp file_sys/path_parser.cpp file_sys/savedata_archive.cpp + file_sys/title_metadata.cpp frontend/camera/blank_camera.cpp frontend/camera/factory.cpp frontend/camera/interface.cpp diff --git a/src/core/file_sys/title_metadata.cpp b/src/core/file_sys/title_metadata.cpp new file mode 100644 index 0000000000..1ef8840a0a --- /dev/null +++ b/src/core/file_sys/title_metadata.cpp @@ -0,0 +1,212 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/alignment.h" +#include "common/file_util.h" +#include "common/logging/log.h" +#include "core/file_sys/title_metadata.h" +#include "core/loader/loader.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// FileSys namespace + +namespace FileSys { + +static u32 GetSignatureSize(u32 signature_type) { + switch (signature_type) { + case Rsa4096Sha1: + case Rsa4096Sha256: + return 0x200; + + case Rsa2048Sha1: + case Rsa2048Sha256: + return 0x100; + + case EllipticSha1: + case EcdsaSha256: + return 0x3C; + } +} + +Loader::ResultStatus TitleMetadata::Load() { + FileUtil::IOFile file(filepath, "rb"); + if (!file.IsOpen()) + return Loader::ResultStatus::Error; + + if (!file.ReadBytes(&signature_type, sizeof(u32_be))) + return Loader::ResultStatus::Error; + + // Signature lengths are variable, and the body follows the signature + u32 signature_size = GetSignatureSize(signature_type); + + tmd_signature.resize(signature_size); + if (!file.ReadBytes(&tmd_signature[0], signature_size)) + return Loader::ResultStatus::Error; + + // The TMD body start position is rounded to the nearest 0x40 after the signature + size_t body_start = Common::AlignUp(signature_size + sizeof(u32), 0x40); + file.Seek(body_start, SEEK_SET); + + // Read our TMD body, then load the amount of ContentChunks specified + if (file.ReadBytes(&tmd_body, sizeof(TitleMetadata::Body)) != sizeof(TitleMetadata::Body)) + return Loader::ResultStatus::Error; + + for (u16 i = 0; i < tmd_body.content_count; i++) { + ContentChunk chunk; + if (file.ReadBytes(&chunk, sizeof(ContentChunk)) == sizeof(ContentChunk)) { + tmd_chunks.push_back(chunk); + } else { + LOG_ERROR(Service_FS, "Malformed TMD %s, failed to load content chunk index %u!", + filepath.c_str(), i); + return Loader::ResultStatus::ErrorInvalidFormat; + } + } + + return Loader::ResultStatus::Success; +} + +Loader::ResultStatus TitleMetadata::Save() { + FileUtil::IOFile file(filepath, "wb"); + if (!file.IsOpen()) + return Loader::ResultStatus::Error; + + if (!file.WriteBytes(&signature_type, sizeof(u32_be))) + return Loader::ResultStatus::Error; + + // Signature lengths are variable, and the body follows the signature + u32 signature_size = GetSignatureSize(signature_type); + + if (!file.WriteBytes(tmd_signature.data(), signature_size)) + return Loader::ResultStatus::Error; + + // The TMD body start position is rounded to the nearest 0x40 after the signature + size_t body_start = Common::AlignUp(signature_size + sizeof(u32), 0x40); + file.Seek(body_start, SEEK_SET); + + // Update our TMD body values and hashes + tmd_body.content_count = static_cast(tmd_chunks.size()); + + // TODO(shinyquagsire23): Do TMDs with more than one contentinfo exist? + // For now we'll just adjust the first index to hold all content chunks + // and ensure that no further content info data exists. + tmd_body.contentinfo = {}; + tmd_body.contentinfo[0].index = 0; + tmd_body.contentinfo[0].command_count = static_cast(tmd_chunks.size()); + + CryptoPP::SHA256 chunk_hash; + for (u16 i = 0; i < tmd_body.content_count; i++) { + chunk_hash.Update(reinterpret_cast(&tmd_chunks[i]), sizeof(ContentChunk)); + } + chunk_hash.Final(tmd_body.contentinfo[0].hash.data()); + + CryptoPP::SHA256 contentinfo_hash; + for (size_t i = 0; i < tmd_body.contentinfo.size(); i++) { + chunk_hash.Update(reinterpret_cast(&tmd_body.contentinfo[i]), sizeof(ContentInfo)); + } + chunk_hash.Final(tmd_body.contentinfo_hash.data()); + + // Write our TMD body, then write each of our ContentChunks + if (file.WriteBytes(&tmd_body, sizeof(TitleMetadata::Body)) != sizeof(TitleMetadata::Body)) + return Loader::ResultStatus::Error; + + for (u16 i = 0; i < tmd_body.content_count; i++) { + ContentChunk chunk = tmd_chunks[i]; + if (file.WriteBytes(&chunk, sizeof(ContentChunk)) != sizeof(ContentChunk)) + return Loader::ResultStatus::Error; + } + + return Loader::ResultStatus::Success; +} + +u64 TitleMetadata::GetTitleID() const { + return tmd_body.title_id; +} + +u32 TitleMetadata::GetTitleType() const { + return tmd_body.title_type; +} + +u16 TitleMetadata::GetTitleVersion() const { + return tmd_body.title_version; +} + +u64 TitleMetadata::GetSystemVersion() const { + return tmd_body.system_version; +} + +size_t TitleMetadata::GetContentCount() const { + return tmd_chunks.size(); +} + +u32 TitleMetadata::GetBootContentID() const { + return tmd_chunks[TMDContentIndex::Main].id; +} + +u32 TitleMetadata::GetManualContentID() const { + return tmd_chunks[TMDContentIndex::Manual].id; +} + +u32 TitleMetadata::GetDLPContentID() const { + return tmd_chunks[TMDContentIndex::DLP].id; +} + +void TitleMetadata::SetTitleID(u64 title_id) { + tmd_body.title_id = title_id; +} + +void TitleMetadata::SetTitleType(u32 type) { + tmd_body.title_type = type; +} + +void TitleMetadata::SetTitleVersion(u16 version) { + tmd_body.title_version = version; +} + +void TitleMetadata::SetSystemVersion(u64 version) { + tmd_body.system_version = version; +} + +void TitleMetadata::AddContentChunk(const ContentChunk& chunk) { + tmd_chunks.push_back(chunk); +} + +void TitleMetadata::Print() const { + LOG_DEBUG(Service_FS, "%s - %u chunks", filepath.c_str(), + static_cast(tmd_body.content_count)); + + // Content info describes ranges of content chunks + LOG_DEBUG(Service_FS, "Content info:"); + for (size_t i = 0; i < tmd_body.contentinfo.size(); i++) { + if (tmd_body.contentinfo[i].command_count == 0) + break; + + LOG_DEBUG(Service_FS, " Index %04X, Command Count %04X", + static_cast(tmd_body.contentinfo[i].index), + static_cast(tmd_body.contentinfo[i].command_count)); + } + + // For each content info, print their content chunk range + for (size_t i = 0; i < tmd_body.contentinfo.size(); i++) { + u16 index = static_cast(tmd_body.contentinfo[i].index); + u16 count = static_cast(tmd_body.contentinfo[i].command_count); + + if (count == 0) + continue; + + LOG_DEBUG(Service_FS, "Content chunks for content info index %zu:", i); + for (u16 j = index; j < index + count; j++) { + // Don't attempt to print content we don't have + if (j > tmd_body.content_count) + break; + + const ContentChunk& chunk = tmd_chunks[j]; + LOG_DEBUG(Service_FS, " ID %08X, Index %04X, Type %04x, Size %016" PRIX64, + static_cast(chunk.id), static_cast(chunk.index), + static_cast(chunk.type), static_cast(chunk.size)); + } + } +} +} // namespace FileSys diff --git a/src/core/file_sys/title_metadata.h b/src/core/file_sys/title_metadata.h new file mode 100644 index 0000000000..1fc157bf3f --- /dev/null +++ b/src/core/file_sys/title_metadata.h @@ -0,0 +1,125 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "common/common_types.h" +#include "common/swap.h" + +namespace Loader { +enum class ResultStatus; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// FileSys namespace + +namespace FileSys { + +enum TMDSignatureType : u32 { + Rsa4096Sha1 = 0x10000, + Rsa2048Sha1 = 0x10001, + EllipticSha1 = 0x10002, + Rsa4096Sha256 = 0x10003, + Rsa2048Sha256 = 0x10004, + EcdsaSha256 = 0x10005 +}; + +enum TMDContentTypeFlag : u16 { + Encrypted = 1 << 1, + Disc = 1 << 2, + CFM = 1 << 3, + Optional = 1 << 14, + Shared = 1 << 15 +}; + +/** + * Helper which implements an interface to read and write Title Metadata (TMD) files. + * If a file path is provided and the file exists, it can be parsed and used, otherwise + * it must be created. The TMD file can then be interpreted, modified and/or saved. + */ +class TitleMetadata { +public: + struct ContentChunk { + u32_be id; + u16_be index; + u16_be type; + u64_be size; + std::array hash; + }; + + static_assert(sizeof(ContentChunk) == 0x30, "TMD ContentChunk structure size is wrong"); + + struct ContentInfo { + u16_be index; + u16_be command_count; + std::array hash; + }; + + static_assert(sizeof(ContentInfo) == 0x24, "TMD ContentInfo structure size is wrong"); + +#pragma pack(push, 1) + + struct Body { + std::array issuer; + u8 version; + u8 ca_crl_version; + u8 signer_crl_version; + u8 reserved; + u64_be system_version; + u64_be title_id; + u32_be title_type; + u16_be group_id; + u32_be savedata_size; + u32_be srl_private_savedata_size; + std::array reserved_2; + u8 srl_flag; + std::array reserved_3; + u32_be access_rights; + u16_be title_version; + u16_be content_count; + u16_be boot_content; + std::array reserved_4; + std::array contentinfo_hash; + std::array contentinfo; + }; + + static_assert(sizeof(Body) == 0x9C4, "TMD body structure size is wrong"); + +#pragma pack(pop) + + explicit TitleMetadata(std::string& path) : filepath(std::move(path)) {} + Loader::ResultStatus Load(); + Loader::ResultStatus Save(); + + u64 GetTitleID() const; + u32 GetTitleType() const; + u16 GetTitleVersion() const; + u64 GetSystemVersion() const; + size_t GetContentCount() const; + u32 GetBootContentID() const; + u32 GetManualContentID() const; + u32 GetDLPContentID() const; + + void SetTitleID(u64 title_id); + void SetTitleType(u32 type); + void SetTitleVersion(u16 version); + void SetSystemVersion(u64 version); + void AddContentChunk(const ContentChunk& chunk); + + void Print() const; + +private: + enum TMDContentIndex { Main = 0, Manual = 1, DLP = 2 }; + + Body tmd_body; + u32_be signature_type; + std::vector tmd_signature; + std::vector tmd_chunks; + + std::string filepath; +}; + +} // namespace FileSys