From 8e10c9bb2e8690055ba07003ebd53a5215f82f8f Mon Sep 17 00:00:00 2001
From: shinyquagsire23 <mtinc2@gmail.com>
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 <cinttypes>
+#include <cryptopp/sha.h>
+#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<u16>(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<u16>(tmd_chunks.size());
+
+    CryptoPP::SHA256 chunk_hash;
+    for (u16 i = 0; i < tmd_body.content_count; i++) {
+        chunk_hash.Update(reinterpret_cast<u8*>(&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<u8*>(&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<u32>(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<u32>(tmd_body.contentinfo[i].index),
+                  static_cast<u32>(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<u16>(tmd_body.contentinfo[i].index);
+        u16 count = static_cast<u16>(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<u32>(chunk.id), static_cast<u32>(chunk.index),
+                      static_cast<u32>(chunk.type), static_cast<u64>(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 <string>
+#include <vector>
+#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<u8, 0x20> hash;
+    };
+
+    static_assert(sizeof(ContentChunk) == 0x30, "TMD ContentChunk structure size is wrong");
+
+    struct ContentInfo {
+        u16_be index;
+        u16_be command_count;
+        std::array<u8, 0x20> hash;
+    };
+
+    static_assert(sizeof(ContentInfo) == 0x24, "TMD ContentInfo structure size is wrong");
+
+#pragma pack(push, 1)
+
+    struct Body {
+        std::array<u8, 0x40> 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<u8, 4> reserved_2;
+        u8 srl_flag;
+        std::array<u8, 0x31> reserved_3;
+        u32_be access_rights;
+        u16_be title_version;
+        u16_be content_count;
+        u16_be boot_content;
+        std::array<u8, 2> reserved_4;
+        std::array<u8, 0x20> contentinfo_hash;
+        std::array<ContentInfo, 64> 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<u8> tmd_signature;
+    std::vector<ContentChunk> tmd_chunks;
+
+    std::string filepath;
+};
+
+} // namespace FileSys