diff --git a/source/hactool_options.cpp b/source/hactool_options.cpp index c830207..9dfe3c8 100644 --- a/source/hactool_options.cpp +++ b/source/hactool_options.cpp @@ -112,6 +112,8 @@ namespace ams::hactool { options.file_type = FileType::Xci; } else if (std::strcmp(arg, "appfs") == 0) { options.file_type = FileType::AppFs; + } else if (std::strcmp(arg, "pfs") == 0 || std::strcmp(arg, "pfs0") == 0 || std::strcmp(arg, "nsp") == 0) { + options.file_type = FileType::Pfs; } else { return false; } diff --git a/source/hactool_processor.app_fs.cpp b/source/hactool_processor.app_fs.cpp index f2807a0..07c181a 100644 --- a/source/hactool_processor.app_fs.cpp +++ b/source/hactool_processor.app_fs.cpp @@ -28,6 +28,109 @@ namespace ams::hactool { constexpr const char ContentMetaFileNameExtension[] = ".cnmt"; + constexpr const char TicketFileNameExtension[] = ".tik"; + + struct alignas(4) CommonTicketData { + u32 signature_type; + u8 signature_data[0x100]; + u8 padding0[0x3C]; + char issuer[0x40]; + u8 title_key_block[0x100]; + u8 format_version; + u8 titlekey_type; + u16 ticket_version; + u8 license_type; + u8 key_generation; + u16 property_mask; + u8 reserved[8]; + u8 ticket_id[8]; + u8 device_id[8]; + u8 rights_id[0x10]; + u8 account_id[0x4]; + u32 total_section_size; + u32 section_header_offset; + u16 section_header_count; + u16 section_header_entry_size; + }; + static_assert(util::is_pod::value); + static_assert(sizeof(CommonTicketData) == 0x2C0); + + bool IsValidCommonTicketFormat(const void *data, size_t size) { + /* Check that the data is the right size for a ticket. */ + if (size != sizeof(CommonTicketData)) { + return false; + } + + /* Check the ticket. */ + const auto &ticket = *static_cast(data); + + /* Check that the ticket is an aes key. */ + if (ticket.titlekey_type != 0) { + return false; + } + + /* Check that the ticket's rights id isn't all-zero. */ + size_t i; + for (i = 0; i < util::size(ticket.rights_id); ++i) { + if (ticket.rights_id[i] != 0) { + break; + } + } + + if (i == util::size(ticket.rights_id)) { + return false; + } + + /* Check that the ticket is a proper aes-key. */ + for (i = 0; i < sizeof(spl::AesKey); ++i) { + if (ticket.title_key_block[i] != 0) { + break; + } + } + if (i == sizeof(spl::AesKey)) { + return false; + } + + for (i = sizeof(spl::AesKey); i < util::size(ticket.title_key_block); ++i) { + if (ticket.title_key_block[i] != 0) { + break; + } + } + if (i != util::size(ticket.title_key_block)) { + return false; + } + + /* Check that the ticket's section header is proper. */ + if (ticket.section_header_offset != sizeof(CommonTicketData)) { + return false; + } + + /* Ticket is good enough. */ + return true; + } + + bool TryLoadKeyFromCommonTicket(fssrv::impl::ExternalKeyManager &km, const void *data, size_t size) { + if (IsValidCommonTicketFormat(data, size)) { + /* Get the ticket. */ + const auto &ticket = *static_cast(data); + + /* Decode the rights id. */ + fs::RightsId rights_id = {}; + std::memcpy(std::addressof(rights_id), ticket.rights_id, sizeof(rights_id)); + + /* Decode the key. */ + spl::AccessKey access_key = {}; + std::memcpy(std::addressof(access_key), ticket.title_key_block, sizeof(access_key)); + + /* Register with the key manager. */ + km.Register(rights_id, access_key); + + return true; + } else { + return false; + } + } + Result ReadContentMetaFile(std::unique_ptr *out, size_t *out_size, std::shared_ptr &fs) { bool found = false; R_RETURN(fssystem::IterateDirectoryRecursively(fs.get(), @@ -69,9 +172,9 @@ namespace ams::hactool { } - Result Processor::ProcessAsApplicationFileSystem(std::shared_ptr fs, ProcessAsApplicationFileSystemCtx *ctx) { + Result Processor::ProcessAsApplicationFileSystem(std::shared_ptr fs, ProcessAsApplicationFileSystemContext *ctx) { /* Ensure we have a context. */ - ProcessAsApplicationFileSystemCtx local_ctx{}; + ProcessAsApplicationFileSystemContext local_ctx{}; if (ctx == nullptr) { ctx = std::addressof(local_ctx); } @@ -86,6 +189,33 @@ namespace ams::hactool { [&] (const fs::Path &, const fs::DirectoryEntry &) -> Result { R_SUCCEED(); }, [&] (const fs::Path &, const fs::DirectoryEntry &) -> Result { R_SUCCEED(); }, [&] (const fs::Path &path, const fs::DirectoryEntry &entry) -> Result { + /* If the path is a ticket, try to load it. */ + if (PathView(entry.name).HasSuffix(TicketFileNameExtension)) { + std::shared_ptr tik_storage; + if (const auto res = OpenFileStorage(std::addressof(tik_storage), ctx->fs, path.GetString()); R_SUCCEEDED(res)) { + /* Get ticket size. */ + s64 tik_size = -1; + if (const auto res = tik_storage->GetSize(std::addressof(tik_size)); R_SUCCEEDED(res)) { + if (tik_size >= static_cast(sizeof(CommonTicketData))) { + CommonTicketData tik_data; + if (const auto res = tik_storage->Read(0, std::addressof(tik_data), sizeof(tik_data)); R_SUCCEEDED(res)) { + if (!TryLoadKeyFromCommonTicket(m_external_nca_key_manager, std::addressof(tik_data), sizeof(tik_data))) { + fprintf(stderr, "[Warning]: Failed to load common title key from ticket file (%s). Is it not a common ticket?\n", path.GetString()); + } + } else { + fprintf(stderr, "[Warning]: Failed to read ticket file (%s): 2%03d-%04d\n", path.GetString(), res.GetModule(), res.GetDescription()); + } + } else { + fprintf(stderr, "[Warning]: Ticket file (%s) has incorrect size: 2%03d-%04d\n", path.GetString(), res.GetModule(), res.GetDescription()); + } + } else { + fprintf(stderr, "[Warning]: Failed to get size of ticket file (%s): 2%03d-%04d\n", path.GetString(), res.GetModule(), res.GetDescription()); + } + } else { + fprintf(stderr, "[Warning]: Failed to open ticket file (%s): 2%03d-%04d\n", path.GetString(), res.GetModule(), res.GetDescription()); + } + } + /* If the path isn't a meta nca, finish. */ R_SUCCEED_IF(!PathView(entry.name).HasSuffix(MetaNcaFileNameExtension)); @@ -200,7 +330,7 @@ namespace ams::hactool { R_SUCCEED(); } - void Processor::PrintAsApplicationFileSystem(ProcessAsApplicationFileSystemCtx &ctx) { + void Processor::PrintAsApplicationFileSystem(ProcessAsApplicationFileSystemContext &ctx) { auto _ = this->PrintHeader("Application File System"); { @@ -226,7 +356,7 @@ namespace ams::hactool { AMS_UNUSED(ctx); } - void Processor::SaveAsApplicationFileSystem(ProcessAsApplicationFileSystemCtx &ctx) { + void Processor::SaveAsApplicationFileSystem(ProcessAsApplicationFileSystemContext &ctx) { /* TODO */ AMS_UNUSED(ctx); } diff --git a/source/hactool_processor.hpp b/source/hactool_processor.hpp index 79ea988..8704405 100644 --- a/source/hactool_processor.hpp +++ b/source/hactool_processor.hpp @@ -75,7 +75,7 @@ namespace ams::hactool { ProcessAsNpdmContext npdm_ctx; }; - struct ProcessAsApplicationFileSystemCtx { + struct ProcessAsApplicationFileSystemContext { std::shared_ptr fs; struct ApplicationEntryData { @@ -114,7 +114,17 @@ namespace ams::hactool { PartitionData normal_partition; PartitionData secure_partition; - ProcessAsApplicationFileSystemCtx app_ctx; + ProcessAsApplicationFileSystemContext app_ctx; + }; + + struct ProcessAsPfsContext { + std::shared_ptr storage; + std::shared_ptr fs; + + bool is_exefs; + + ProcessAsNpdmContext npdm_ctx; + ProcessAsApplicationFileSystemContext app_ctx; }; private: Options m_options; @@ -206,19 +216,22 @@ namespace ams::hactool { Result ProcessAsNca(std::shared_ptr storage, ProcessAsNcaContext *ctx = nullptr); Result ProcessAsNpdm(std::shared_ptr storage, ProcessAsNpdmContext *ctx = nullptr); Result ProcessAsXci(std::shared_ptr storage, ProcessAsXciContext *ctx = nullptr); - Result ProcessAsApplicationFileSystem(std::shared_ptr fs, ProcessAsApplicationFileSystemCtx *ctx = nullptr); + Result ProcessAsPfs(std::shared_ptr storage, ProcessAsPfsContext *ctx = nullptr); + Result ProcessAsApplicationFileSystem(std::shared_ptr fs, ProcessAsApplicationFileSystemContext *ctx = nullptr); /* Printing. */ void PrintAsNca(ProcessAsNcaContext &ctx); void PrintAsNpdm(ProcessAsNpdmContext &ctx); void PrintAsXci(ProcessAsXciContext &ctx); - void PrintAsApplicationFileSystem(ProcessAsApplicationFileSystemCtx &ctx); + void PrintAsPfs(ProcessAsPfsContext &ctx); + void PrintAsApplicationFileSystem(ProcessAsApplicationFileSystemContext &ctx); /* Saving. */ void SaveAsNca(ProcessAsNcaContext &ctx); void SaveAsNpdm(ProcessAsNpdmContext &ctx); void SaveAsXci(ProcessAsXciContext &ctx); - void SaveAsApplicationFileSystem(ProcessAsApplicationFileSystemCtx &ctx); + void SaveAsPfs(ProcessAsPfsContext &ctx); + void SaveAsApplicationFileSystem(ProcessAsApplicationFileSystemContext &ctx); }; inline void Processor::PrintLineImpl(const char *fmt, ...) const { diff --git a/source/hactool_processor.main.cpp b/source/hactool_processor.main.cpp index 16fcb46..cd26a06 100644 --- a/source/hactool_processor.main.cpp +++ b/source/hactool_processor.main.cpp @@ -59,6 +59,9 @@ namespace ams::hactool { case FileType::Xci: R_TRY(this->ProcessAsXci(std::move(input))); break; + case FileType::Pfs: + R_TRY(this->ProcessAsPfs(std::move(input))); + break; AMS_UNREACHABLE_DEFAULT_CASE(); } } diff --git a/source/hactool_processor.pfs.cpp b/source/hactool_processor.pfs.cpp new file mode 100644 index 0000000..a3b3292 --- /dev/null +++ b/source/hactool_processor.pfs.cpp @@ -0,0 +1,105 @@ +/* + * Copyright (c) Atmosphère-NX + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include "hactool_processor.hpp" +#include "hactool_fs_utils.hpp" + +namespace ams::hactool { + + Result Processor::ProcessAsPfs(std::shared_ptr storage, ProcessAsPfsContext *ctx) { + /* Ensure we have a context. */ + ProcessAsPfsContext local_ctx{}; + if (ctx == nullptr) { + ctx = std::addressof(local_ctx); + } + + /* Set the fs. */ + ctx->storage = std::move(storage); + + /* Mount the partition filesystem. */ + { + /* Allocate the fs. */ + auto fs = fssystem::AllocateShared(); + R_UNLESS(fs != nullptr, fs::ResultAllocationMemoryFailedInPartitionFileSystemCreatorA()); + + /* Initialize the filesystem. */ + R_TRY(fs->Initialize(std::shared_ptr(ctx->storage))); + + /* Set the context fs. */ + ctx->fs = std::move(fs); + } + + /* Try to treat the context as an exefs. */ + std::shared_ptr npdm_storage; + { + bool is_exefs = false; + const auto check_npdm_res = fssystem::HasFile(std::addressof(is_exefs), ctx->fs.get(), fs::MakeConstantPath("/main.npdm")); + if (R_SUCCEEDED(check_npdm_res)) { + if (is_exefs) { + ctx->is_exefs = true; + + if (const auto open_npdm_res = OpenFileStorage(std::addressof(npdm_storage), ctx->fs, "/main.npdm"); R_FAILED(open_npdm_res)) { + fprintf(stderr, "[Warning]: main.npdm exists in PartitionFileSystem but could not be opened: 2%03d-%04d\n", open_npdm_res.GetModule(), open_npdm_res.GetDescription()); + } + } + } else { + fprintf(stderr, "[Warning]: Failed to check if PartitionFileSystem is exefs: 2%03d-%04d\n", check_npdm_res.GetModule(), check_npdm_res.GetDescription()); + } + } + + /* Parse as exefs or appfs. */ + if (ctx->is_exefs) { + if (const auto process_npdm_res = this->ProcessAsNpdm(std::move(npdm_storage), std::addressof(ctx->npdm_ctx)); R_FAILED(process_npdm_res)) { + fprintf(stderr, "[Warning]: Failed to process PartitionFileSystem main.npdm: 2%03d-%04d\n", process_npdm_res.GetModule(), process_npdm_res.GetDescription()); + } + } else { + if (const auto process_app_res = this->ProcessAsApplicationFileSystem(ctx->fs, std::addressof(ctx->app_ctx)); R_FAILED(process_app_res)) { + fprintf(stderr, "[Warning]: Failed to process PartitionFileSystem applications: 2%03d-%04d\n", process_app_res.GetModule(), process_app_res.GetDescription()); + } + } + + /* TODO: Recursive processing? */ + + /* Print. */ + if (ctx == std::addressof(local_ctx)) { + this->PrintAsPfs(*ctx); + } + + /* Save. */ + if (ctx == std::addressof(local_ctx)) { + this->SaveAsPfs(*ctx); + } + + R_SUCCEED(); + } + + void Processor::PrintAsPfs(ProcessAsPfsContext &ctx) { + if (ctx.is_exefs) { + this->PrintAsNpdm(ctx.npdm_ctx); + } else { + this->PrintAsApplicationFileSystem(ctx.app_ctx); + } + } + + void Processor::SaveAsPfs(ProcessAsPfsContext &ctx) { + if (ctx.is_exefs) { + this->SaveAsNpdm(ctx.npdm_ctx); + } else { + this->SaveAsApplicationFileSystem(ctx.app_ctx); + } + } + +} \ No newline at end of file