diff options
Diffstat (limited to 'src/core/file_sys')
| -rw-r--r-- | src/core/file_sys/title_metadata.cpp | 212 | ||||
| -rw-r--r-- | src/core/file_sys/title_metadata.h | 125 | 
2 files changed, 337 insertions, 0 deletions
diff --git a/src/core/file_sys/title_metadata.cpp b/src/core/file_sys/title_metadata.cpp new file mode 100644 index 000000000..1ef8840a0 --- /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 000000000..1fc157bf3 --- /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  | 
