From 89e82927eec3ca231b7bfc4599cbe652ba9650ed Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Sun, 20 Apr 2025 14:12:12 +0100 Subject: [PATCH] add basic support for title installing --- sphaira/CMakeLists.txt | 46 + sphaira/include/app.hpp | 27 +- sphaira/include/log.hpp | 21 +- sphaira/include/ui/menus/filebrowser.hpp | 2 + sphaira/include/ui/progress_box.hpp | 1 + sphaira/include/yati/container/base.hpp | 39 + sphaira/include/yati/container/nsp.hpp | 14 + sphaira/include/yati/container/xci.hpp | 16 + sphaira/include/yati/nx/crypto.hpp | 57 + sphaira/include/yati/nx/es.hpp | 83 ++ sphaira/include/yati/nx/keys.hpp | 70 ++ sphaira/include/yati/nx/nca.hpp | 218 ++++ sphaira/include/yati/nx/ncm.hpp | 39 + sphaira/include/yati/nx/ncz.hpp | 54 + sphaira/include/yati/nx/npdm.hpp | 62 + sphaira/include/yati/nx/ns.hpp | 22 + sphaira/include/yati/nx/nxdumptool_rsa.h | 62 + sphaira/include/yati/nx/tik.h | 265 +++++ sphaira/include/yati/source/base.hpp | 21 + sphaira/include/yati/source/file.hpp | 19 + sphaira/include/yati/source/stdio.hpp | 20 + sphaira/include/yati/yati.hpp | 127 +++ sphaira/source/app.cpp | 4 + sphaira/source/ftpsrv_helper.cpp | 4 +- sphaira/source/log.cpp | 14 +- sphaira/source/owo.cpp | 328 +----- sphaira/source/ui/menus/filebrowser.cpp | 69 +- sphaira/source/ui/menus/main_menu.cpp | 101 +- sphaira/source/ui/progress_box.cpp | 8 + sphaira/source/yati/container/nsp.cpp | 67 ++ sphaira/source/yati/container/xci.cpp | 95 ++ sphaira/source/yati/nx/es.cpp | 122 ++ sphaira/source/yati/nx/keys.cpp | 130 +++ sphaira/source/yati/nx/nca.cpp | 154 +++ sphaira/source/yati/nx/ncm.cpp | 34 + sphaira/source/yati/nx/ns.cpp | 39 + sphaira/source/yati/nx/nxdumptool_rsa.c | 158 +++ sphaira/source/yati/source/file.cpp | 20 + sphaira/source/yati/source/stdio.cpp | 28 + sphaira/source/yati/yati.cpp | 1317 ++++++++++++++++++++++ 40 files changed, 3661 insertions(+), 316 deletions(-) create mode 100644 sphaira/include/yati/container/base.hpp create mode 100644 sphaira/include/yati/container/nsp.hpp create mode 100644 sphaira/include/yati/container/xci.hpp create mode 100644 sphaira/include/yati/nx/crypto.hpp create mode 100644 sphaira/include/yati/nx/es.hpp create mode 100644 sphaira/include/yati/nx/keys.hpp create mode 100644 sphaira/include/yati/nx/nca.hpp create mode 100644 sphaira/include/yati/nx/ncm.hpp create mode 100644 sphaira/include/yati/nx/ncz.hpp create mode 100644 sphaira/include/yati/nx/npdm.hpp create mode 100644 sphaira/include/yati/nx/ns.hpp create mode 100644 sphaira/include/yati/nx/nxdumptool_rsa.h create mode 100644 sphaira/include/yati/nx/tik.h create mode 100644 sphaira/include/yati/source/base.hpp create mode 100644 sphaira/include/yati/source/file.hpp create mode 100644 sphaira/include/yati/source/stdio.hpp create mode 100644 sphaira/include/yati/yati.hpp create mode 100644 sphaira/source/yati/container/nsp.cpp create mode 100644 sphaira/source/yati/container/xci.cpp create mode 100644 sphaira/source/yati/nx/es.cpp create mode 100644 sphaira/source/yati/nx/keys.cpp create mode 100644 sphaira/source/yati/nx/nca.cpp create mode 100644 sphaira/source/yati/nx/ncm.cpp create mode 100644 sphaira/source/yati/nx/ns.cpp create mode 100644 sphaira/source/yati/nx/nxdumptool_rsa.c create mode 100644 sphaira/source/yati/source/file.cpp create mode 100644 sphaira/source/yati/source/stdio.cpp create mode 100644 sphaira/source/yati/yati.cpp diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index 8929d3c..e2eee2c 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -74,6 +74,19 @@ add_executable(sphaira source/web.cpp source/i18n.cpp source/ftpsrv_helper.cpp + + source/yati/yati.cpp + source/yati/container/nsp.cpp + source/yati/container/xci.cpp + source/yati/source/file.cpp + source/yati/source/stdio.cpp + + source/yati/nx/es.cpp + source/yati/nx/keys.cpp + source/yati/nx/nca.cpp + source/yati/nx/ncm.cpp + source/yati/nx/ns.cpp + source/yati/nx/nxdumptool_rsa.c ) target_compile_definitions(sphaira PRIVATE @@ -161,6 +174,24 @@ FetchContent_Declare(minIni GIT_TAG 11cac8b ) +FetchContent_Declare(zstd + GIT_REPOSITORY https://github.com/facebook/zstd.git + GIT_TAG v1.5.7 + SOURCE_SUBDIR build/cmake +) + +set(USE_NEW_ZSTD ON) + +set(ZSTD_BUILD_STATIC ON) +set(ZSTD_BUILD_SHARED OFF) +set(ZSTD_BUILD_COMPRESSION OFF) +set(ZSTD_BUILD_DECOMPRESSION ON) +set(ZSTD_BUILD_DICTBUILDER OFF) +set(ZSTD_LEGACY_SUPPORT OFF) +set(ZSTD_MULTITHREAD_SUPPORT OFF) +set(ZSTD_BUILD_PROGRAMS OFF) +set(ZSTD_BUILD_TESTS OFF) + set(MININI_LIB_NAME minIni) set(MININI_USE_STDIO ON) set(MININI_USE_NX ON) @@ -195,6 +226,7 @@ FetchContent_MakeAvailable( stb minIni yyjson + zstd ) set(FTPSRV_LIB_BUILD TRUE) @@ -274,6 +306,11 @@ find_package(CURL REQUIRED) find_path(mbedtls_inc mbedtls REQUIRED) find_library(mbedcrypto_lib mbedcrypto REQUIRED) +if (NOT USE_NEW_ZSTD) + find_path(zstd_inc zstd.h REQUIRED) + find_library(zstd_lib zstd REQUIRED) +endif() + set_target_properties(sphaira PROPERTIES C_STANDARD 11 C_EXTENSIONS ON @@ -296,6 +333,15 @@ target_link_libraries(sphaira PRIVATE ${mbedcrypto_lib} ) +if (USE_NEW_ZSTD) + message(STATUS "USING UPSTREAM ZSTD") + target_link_libraries(sphaira PRIVATE libzstd_static) +else() + message(STATUS "USING LOCAL ZSTD") + target_link_libraries(sphaira PRIVATE ${zstd_lib}) + target_include_directories(sphaira PRIVATE ${zstd_inc}) +endif() + target_include_directories(sphaira PRIVATE include ${minizip_inc} diff --git a/sphaira/include/app.hpp b/sphaira/include/app.hpp index 584290a..7edafb8 100644 --- a/sphaira/include/app.hpp +++ b/sphaira/include/app.hpp @@ -44,6 +44,8 @@ public: ~App(); void Loop(); + static App* GetApp(); + static void Exit(); static void ExitRestart(); static auto GetVg() -> NVGcontext*; @@ -172,12 +174,31 @@ public: option::OptionBool m_ftp_enabled{INI_SECTION, "ftp_enabled", false}; option::OptionBool m_log_enabled{INI_SECTION, "log_enabled", false}; option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", false}; - option::OptionBool m_install{INI_SECTION, "install", false}; - option::OptionBool m_install_sd{INI_SECTION, "install_sd", true}; - option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true}; option::OptionBool m_theme_music{INI_SECTION, "theme_music", true}; option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false}; option::OptionLong m_language{INI_SECTION, "language", 0}; // auto + + // install options + option::OptionBool m_install{INI_SECTION, "install", false}; + option::OptionBool m_install_sd{INI_SECTION, "install_sd", true}; + option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true}; + option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false}; + option::OptionBool m_skip_if_already_installed{INI_SECTION, "skip_if_already_installed", true}; + option::OptionBool m_ticket_only{INI_SECTION, "ticket_only", false}; + option::OptionBool m_patch_ticket{INI_SECTION, "patch_ticket", true}; + option::OptionBool m_skip_base{INI_SECTION, "skip_base", false}; + option::OptionBool m_skip_patch{INI_SECTION, "skip_patch", false}; + option::OptionBool m_skip_addon{INI_SECTION, "skip_addon", false}; + option::OptionBool m_skip_data_patch{INI_SECTION, "skip_data_patch", false}; + option::OptionBool m_skip_ticket{INI_SECTION, "skip_ticket", false}; + option::OptionBool m_skip_nca_hash_verify{INI_SECTION, "skip_nca_hash_verify", false}; + option::OptionBool m_skip_rsa_header_fixed_key_verify{INI_SECTION, "skip_rsa_header_fixed_key_verify", false}; + option::OptionBool m_skip_rsa_npdm_fixed_key_verify{INI_SECTION, "skip_rsa_npdm_fixed_key_verify", false}; + option::OptionBool m_ignore_distribution_bit{INI_SECTION, "ignore_distribution_bit", false}; + option::OptionBool m_convert_to_standard_crypto{INI_SECTION, "convert_to_standard_crypto", false}; + option::OptionBool m_lower_master_key{INI_SECTION, "lower_master_key", false}; + option::OptionBool m_lower_system_version{INI_SECTION, "lower_system_version", false}; + // todo: move this into it's own menu option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal diff --git a/sphaira/include/log.hpp b/sphaira/include/log.hpp index b21e8d9..68eb3d0 100644 --- a/sphaira/include/log.hpp +++ b/sphaira/include/log.hpp @@ -1,24 +1,33 @@ #pragma once +#ifdef __cplusplus +extern "C" { +#endif + #define sphaira_USE_LOG 1 -#include +#include #if sphaira_USE_LOG -auto log_file_init() -> bool; -auto log_nxlink_init() -> bool; +bool log_file_init(); +bool log_nxlink_init(); void log_file_exit(); void log_nxlink_exit(); void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2))); -void log_write_arg(const char* s, std::va_list& v); +void log_write_arg(const char* s, va_list* v); #else -inline auto log_file_init() -> bool { +inline bool log_file_init() { return true; } -inline auto log_nxlink_init() -> bool { +inline bool log_nxlink_init() { return true; } #define log_file_exit() #define log_nxlink_exit() #define log_write(...) +#define log_write_arg(...) +#endif + +#ifdef __cplusplus +} #endif diff --git a/sphaira/include/ui/menus/filebrowser.hpp b/sphaira/include/ui/menus/filebrowser.hpp index 9f7efa3..03503fc 100644 --- a/sphaira/include/ui/menus/filebrowser.hpp +++ b/sphaira/include/ui/menus/filebrowser.hpp @@ -122,6 +122,8 @@ struct Menu final : MenuBase { private: void SetIndex(s64 index); void InstallForwarder(); + void InstallFile(const FileEntry& target); + void InstallFiles(const std::vector& targets); auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result; void LoadAssocEntriesPath(const fs::FsPath& path); diff --git a/sphaira/include/ui/progress_box.hpp b/sphaira/include/ui/progress_box.hpp index 6ae93e1..3e8c7b7 100644 --- a/sphaira/include/ui/progress_box.hpp +++ b/sphaira/include/ui/progress_box.hpp @@ -21,6 +21,7 @@ struct ProgressBox final : Widget { auto Update(Controller* controller, TouchInfo* touch) -> void override; auto Draw(NVGcontext* vg, Theme* theme) -> void override; + auto SetTitle(const std::string& title) -> ProgressBox&; auto NewTransfer(const std::string& transfer) -> ProgressBox&; auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&; void RequestExit(); diff --git a/sphaira/include/yati/container/base.hpp b/sphaira/include/yati/container/base.hpp new file mode 100644 index 0000000..5fdfb47 --- /dev/null +++ b/sphaira/include/yati/container/base.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "yati/source/base.hpp" +#include +#include +#include + +namespace sphaira::yati::container { + +enum class CollectionType { + CollectionType_NCA, + CollectionType_NCZ, + CollectionType_TIK, + CollectionType_CERT, +}; + +struct CollectionEntry { + // collection name within file. + std::string name{}; + // collection offset within file. + s64 offset{}; + // collection size within file, may be compressed size. + s64 size{}; +}; + +using Collections = std::vector; + +struct Base { + using Source = source::Base; + + Base(Source* source) : m_source{source} { } + virtual ~Base() = default; + virtual Result GetCollections(Collections& out) = 0; + +protected: + Source* m_source; +}; + +} // namespace sphaira::yati::container diff --git a/sphaira/include/yati/container/nsp.hpp b/sphaira/include/yati/container/nsp.hpp new file mode 100644 index 0000000..d92d600 --- /dev/null +++ b/sphaira/include/yati/container/nsp.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "base.hpp" +#include + +namespace sphaira::yati::container { + +struct Nsp final : Base { + using Base::Base; + Result GetCollections(Collections& out) override; + static Result Validate(source::Base* source); +}; + +} // namespace sphaira::yati::container diff --git a/sphaira/include/yati/container/xci.hpp b/sphaira/include/yati/container/xci.hpp new file mode 100644 index 0000000..cd70147 --- /dev/null +++ b/sphaira/include/yati/container/xci.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "base.hpp" +#include +#include +#include + +namespace sphaira::yati::container { + +struct Xci final : Base { + using Base::Base; + Result GetCollections(Collections& out) override; + static Result Validate(source::Base* source); +}; + +} // namespace sphaira::yati::container diff --git a/sphaira/include/yati/nx/crypto.hpp b/sphaira/include/yati/nx/crypto.hpp new file mode 100644 index 0000000..7f3404c --- /dev/null +++ b/sphaira/include/yati/nx/crypto.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include + +namespace sphaira::crypto { + +struct Aes128 { + Aes128(const void *key, bool is_encryptor) { + m_is_encryptor = is_encryptor; + aes128ContextCreate(&m_ctx, key, is_encryptor); + } + + void Run(void *dst, const void *src) { + if (m_is_encryptor) { + aes128EncryptBlock(&m_ctx, dst, src); + } else { + aes128DecryptBlock(&m_ctx, dst, src); + } + } + +private: + Aes128Context m_ctx; + bool m_is_encryptor; +}; + +struct Aes128Xts { + Aes128Xts(const u8 *key, bool is_encryptor) : Aes128Xts{key, key + 0x10, is_encryptor} { } + Aes128Xts(const void *key0, const void *key1, bool is_encryptor) { + m_is_encryptor = is_encryptor; + aes128XtsContextCreate(&m_ctx, key0, key1, is_encryptor); + } + + void Run(void *dst, const void *src, u64 sector, u64 sector_size, u64 data_size) { + for (u64 pos = 0; pos < data_size; pos += sector_size) { + aes128XtsContextResetSector(&m_ctx, sector++, true); + if (m_is_encryptor) { + aes128XtsEncrypt(&m_ctx, static_cast(dst) + pos, static_cast(src) + pos, sector_size); + } else { + aes128XtsDecrypt(&m_ctx, static_cast(dst) + pos, static_cast(src) + pos, sector_size); + } + } + } + +private: + Aes128XtsContext m_ctx; + bool m_is_encryptor; +}; + +static inline void cryptoAes128(const void *in, void *out, const void* key, bool is_encryptor) { + Aes128(key, is_encryptor).Run(out, in); +} + +static inline void cryptoAes128Xts(const void* in, void* out, const u8* key, u64 sector, u64 sector_size, u64 data_size, bool is_encryptor) { + Aes128Xts(key, is_encryptor).Run(out, in, sector, sector_size, data_size); +} + +} // namespace sphaira::crypto diff --git a/sphaira/include/yati/nx/es.hpp b/sphaira/include/yati/nx/es.hpp new file mode 100644 index 0000000..77e4f84 --- /dev/null +++ b/sphaira/include/yati/nx/es.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include "ncm.hpp" +#include "keys.hpp" + +namespace sphaira::es { + +enum { TicketModule = 522 }; + +enum : Result { + // found ticket has missmatching rights_id from it's name. + Result_InvalidTicketBadRightsId = MAKERESULT(TicketModule, 71), + Result_InvalidTicketVersion = MAKERESULT(TicketModule, 72), + Result_InvalidTicketKeyType = MAKERESULT(TicketModule, 73), + Result_InvalidTicketKeyRevision = MAKERESULT(TicketModule, 74), +}; + +enum TicketSigantureType { + TicketSigantureType_RSA_4096_SHA1 = 0x010000, + TicketSigantureType_RSA_2048_SHA1 = 0x010001, + TicketSigantureType_ECDSA_SHA1 = 0x010002, + TicketSigantureType_RSA_4096_SHA256 = 0x010003, + TicketSigantureType_RSA_2048_SHA256 = 0x010004, + TicketSigantureType_ECDSA_SHA256 = 0x010005, + TicketSigantureType_HMAC_SHA1_160 = 0x010006, +}; + +enum TicketTitleKeyType { + TicketTitleKeyType_Common = 0, + TicketTitleKeyType_Personalized = 1, +}; + +enum TicketPropertiesBitfield { + TicketPropertiesBitfield_None = 0, + // temporary ticket, removed on restart + TicketPropertiesBitfield_Temporary = 1 << 4, +}; + +struct TicketData { + u8 issuer[0x40]; + u8 title_key_block[0x100]; + u8 ticket_version1; + u8 title_key_type; + u16 ticket_version2; + u8 license_type; + u8 master_key_revision; + u16 properties_bitfield; + u8 _0x148[0x8]; + u64 ticket_id; + u64 device_id; + FsRightsId rights_id; + u32 account_id; + u8 _0x174[0xC]; + u8 _0x180[0x140]; +}; +static_assert(sizeof(TicketData) == 0x2C0); + +struct EticketRsaDeviceKey { + u8 ctr[AES_128_KEY_SIZE]; + u8 private_exponent[0x100]; + u8 modulus[0x100]; + u32 public_exponent; ///< Stored using big endian byte order. Must match ETICKET_RSA_DEVICE_KEY_PUBLIC_EXPONENT. + u8 padding[0x14]; + u64 device_id; + u8 ghash[0x10]; +}; +static_assert(sizeof(EticketRsaDeviceKey) == 0x240); + +// es functions. +Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size); + +// ticket functions. +Result GetTicketDataOffset(std::span ticket, u64& out); +Result GetTicketData(std::span ticket, es::TicketData* out); +Result SetTicketData(std::span ticket, const es::TicketData* in); + +Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys); +Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys); +Result PatchTicket(std::span ticket, const keys::Keys& keys, bool convert_personalised); + +} // namespace sphaira::es diff --git a/sphaira/include/yati/nx/keys.hpp b/sphaira/include/yati/nx/keys.hpp new file mode 100644 index 0000000..5caf801 --- /dev/null +++ b/sphaira/include/yati/nx/keys.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include "defines.hpp" + +namespace sphaira::keys { + +struct KeyEntry { + u8 key[AES_128_KEY_SIZE]{}; + + auto IsValid() const -> bool { + const KeyEntry empty{}; + return std::memcmp(key, &empty, sizeof(key)); + } +}; + +using KeySection = std::array; +struct Keys { + u8 header_key[0x20]{}; + // the below are only found if read_from_file=true + KeySection key_area_key[0x3]{}; // index + KeySection titlekek{}; + KeySection master_key{}; + KeyEntry eticket_rsa_kek{}; + SetCalRsa2048DeviceKey eticket_device_key{}; + + static auto FixKey(u8 key) -> u8 { + if (key) { + return key - 1; + } + return key; + } + + auto HasNcaKeyArea(u8 key, u8 index) const -> bool { + return key_area_key[index][FixKey(key)].IsValid(); + } + + auto HasTitleKek(u8 key) const -> bool { + return titlekek[FixKey(key)].IsValid(); + } + + auto HasMasterKey(u8 key) const -> bool { + return master_key[FixKey(key)].IsValid(); + } + + auto GetNcaKeyArea(KeyEntry* out, u8 key, u8 index) const -> Result { + R_UNLESS(HasNcaKeyArea(key, index), 0x1); + *out = key_area_key[index][FixKey(key)]; + R_SUCCEED(); + } + + auto GetTitleKek(KeyEntry* out, u8 key) const -> Result { + R_UNLESS(HasTitleKek(key), 0x1); + *out = titlekek[FixKey(key)]; + R_SUCCEED(); + } + + auto GetMasterKey(KeyEntry* out, u8 key) const -> Result { + R_UNLESS(HasMasterKey(key), 0x1); + *out = master_key[FixKey(key)]; + R_SUCCEED(); + } +}; + +void parse_hex_key(void* key, const char* hex); +Result parse_keys(Keys& out, bool read_from_file); + +} // namespace sphaira::keys diff --git a/sphaira/include/yati/nx/nca.hpp b/sphaira/include/yati/nx/nca.hpp new file mode 100644 index 0000000..7a403ad --- /dev/null +++ b/sphaira/include/yati/nx/nca.hpp @@ -0,0 +1,218 @@ +#pragma once + +#include +#include "keys.hpp" + +namespace sphaira::nca { + +#define NCA0_MAGIC 0x3041434E +#define NCA2_MAGIC 0x3241434E +#define NCA3_MAGIC 0x3341434E + +#define NCA_SECTOR_SIZE 0x200 +#define NCA_XTS_SECTION_SIZE 0xC00 +#define NCA_SECTION_TOTAL 0x4 +#define NCA_MEDIA_REAL(x)((x * 0x200)) + +#define NCA_PROGRAM_LOGO_OFFSET 0x8000 +#define NCA_META_CNMT_OFFSET 0xC20 + +enum KeyGenerationOld { + KeyGenerationOld_100 = 0x0, + KeyGenerationOld_Unused = 0x1, + KeyGenerationOld_300 = 0x2, +}; + +enum KeyGeneration { + KeyGeneration_301 = 0x3, + KeyGeneration_400 = 0x4, + KeyGeneration_500 = 0x5, + KeyGeneration_600 = 0x6, + KeyGeneration_620 = 0x7, + KeyGeneration_700 = 0x8, + KeyGeneration_810 = 0x9, + KeyGeneration_900 = 0x0A, + KeyGeneration_910 = 0x0B, + KeyGeneration_1210 = 0x0C, + KeyGeneration_1300 = 0x0D, + KeyGeneration_1400 = 0x0E, + KeyGeneration_1500 = 0x0F, + KeyGeneration_1600 = 0x10, + KeyGeneration_1700 = 0x11, + KeyGeneration_1800 = 0x12, + KeyGeneration_1900 = 0x13, + KeyGeneration_Invalid = 0xFF, +}; + +enum KeyAreaEncryptionKeyIndex { + KeyAreaEncryptionKeyIndex_Application = 0x0, + KeyAreaEncryptionKeyIndex_Ocean = 0x1, + KeyAreaEncryptionKeyIndex_System = 0x2 +}; + +enum DistributionType { + DistributionType_System = 0x0, + DistributionType_GameCard = 0x1 +}; + +enum ContentType { + ContentType_Program = 0x0, + ContentType_Meta = 0x1, + ContentType_Control = 0x2, + ContentType_Manual = 0x3, + ContentType_Data = 0x4, + ContentType_PublicData = 0x5, +}; + +enum FileSystemType { + FileSystemType_RomFS = 0x0, + FileSystemType_PFS0 = 0x1 +}; + +enum HashType { + HashType_Auto = 0x0, + HashType_HierarchicalSha256 = 0x2, + HashType_HierarchicalIntegrity = 0x3 +}; + +enum EncryptionType { + EncryptionType_Auto = 0x0, + EncryptionType_None = 0x1, + EncryptionType_AesXts = 0x2, + EncryptionType_AesCtr = 0x3, + EncryptionType_AesCtrEx = 0x4, + EncryptionType_AesCtrSkipLayerHash = 0x5, // [14.0.0+] + EncryptionType_AesCtrExSkipLayerHash = 0x6, // [14.0.0+] +}; + +struct SectionTableEntry { + u32 media_start_offset; // divided by 0x200. + u32 media_end_offset; // divided by 0x200. + u8 _0x8[0x4]; // unknown. + u8 _0xC[0x4]; // unknown. +}; + +struct LayerRegion { + u64 offset; + u64 size; +}; + +struct HierarchicalSha256Data { + u8 master_hash[0x20]; + u32 block_size; + u32 layer_count; + LayerRegion hash_layer; + LayerRegion pfs0_layer; + LayerRegion unused_layers[3]; + u8 _0x78[0x80]; +}; + +#pragma pack(push, 1) +struct HierarchicalIntegrityVerificationLevelInformation { + u64 logical_offset; + u64 hash_data_size; + u32 block_size; // log2 + u32 _0x14; // reserved +}; +#pragma pack(pop) + +struct InfoLevelHash { + u32 max_layers; + HierarchicalIntegrityVerificationLevelInformation levels[6]; + u8 signature_salt[0x20]; +}; + +struct IntegrityMetaInfo { + u32 magic; // IVFC + u32 version; + u32 master_hash_size; + InfoLevelHash info_level_hash; + u8 master_hash[0x20]; + u8 _0xE0[0x18]; +}; + +static_assert(sizeof(HierarchicalSha256Data) == 0xF8); +static_assert(sizeof(IntegrityMetaInfo) == 0xF8); +static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo)); + +struct FsHeader { + u16 version; // always 2. + u8 fs_type; // see FileSystemType. + u8 hash_type; // see HashType. + u8 encryption_type; // see EncryptionType. + u8 metadata_hash_type; + u8 _0x6[0x2]; // empty. + + union { + HierarchicalSha256Data hierarchical_sha256_data; + IntegrityMetaInfo integrity_meta_info; // used for romfs + } hash_data; + + u8 patch_info[0x40]; + u64 section_ctr; + u8 spares_info[0x30]; + u8 compression_info[0x28]; + u8 meta_data_hash_data_info[0x30]; + u8 reserved[0x30]; +}; +static_assert(sizeof(FsHeader) == 0x200); +static_assert(sizeof(FsHeader::hash_data) == 0xF8); + +struct SectionHeaderHash { + u8 sha256[0x20]; +}; + +struct KeyArea { + u8 area[0x10]; +}; + +struct Header { + u8 rsa_fixed_key[0x100]; + u8 rsa_npdm[0x100]; // key from npdm. + u32 magic; + u8 distribution_type; // see DistributionType. + u8 content_type; // see ContentType. + u8 old_key_gen; // see KeyGenerationOld. + u8 kaek_index; // see KeyAreaEncryptionKeyIndex. + u64 size; + u64 title_id; + u32 context_id; + u32 sdk_version; + u8 key_gen; // see KeyGeneration. + u8 sig_key_gen; + u8 _0x222[0xE]; // empty. + FsRightsId rights_id; + + SectionTableEntry fs_table[NCA_SECTION_TOTAL]; + SectionHeaderHash fs_header_hash[NCA_SECTION_TOTAL]; + KeyArea key_area[NCA_SECTION_TOTAL]; + + u8 _0x340[0xC0]; // empty. + + FsHeader fs_header[NCA_SECTION_TOTAL]; + + auto GetKeyGeneration() const -> u8 { + if (old_key_gen < key_gen) { + return key_gen; + } else { + return old_key_gen; + } + } + + void SetKeyGeneration(u8 key_generation) { + if (key_generation <= 0x2) { + old_key_gen = key_generation; + key_gen = 0; + } else { + old_key_gen = 0x2; + key_gen = key_generation; + } + } +}; +static_assert(sizeof(Header) == 0xC00); + +Result DecryptKeak(const keys::Keys& keys, Header& header); +Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation); +Result VerifyFixedKey(const Header& header); + +} // namespace sphaira::nca diff --git a/sphaira/include/yati/nx/ncm.hpp b/sphaira/include/yati/nx/ncm.hpp new file mode 100644 index 0000000..95d3729 --- /dev/null +++ b/sphaira/include/yati/nx/ncm.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +namespace sphaira::ncm { + +struct PackagedContentMeta { + u64 title_id; + u32 title_version; + u8 meta_type; // NcmContentMetaType + u8 content_meta_platform; // [17.0.0+] + NcmContentMetaHeader meta_header; + u8 install_type; // NcmContentInstallType + u8 _0x17; + u32 required_sys_version; + u8 _0x1C[0x4]; +}; +static_assert(sizeof(PackagedContentMeta) == 0x20); + +struct ContentStorageRecord { + NcmContentMetaKey key; + u8 storage_id; + u8 padding[0x7]; +}; + +union ExtendedHeader { + NcmApplicationMetaExtendedHeader application; + NcmPatchMetaExtendedHeader patch; + NcmAddOnContentMetaExtendedHeader addon; + NcmLegacyAddOnContentMetaExtendedHeader addon_legacy; + NcmDataPatchMetaExtendedHeader data_patch; +}; + +auto GetAppId(const NcmContentMetaKey& key) -> u64; + +Result Delete(NcmContentStorage* cs, const NcmContentId *content_id); +Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id); + +} // namespace sphaira::ncm diff --git a/sphaira/include/yati/nx/ncz.hpp b/sphaira/include/yati/nx/ncz.hpp new file mode 100644 index 0000000..35fe3ab --- /dev/null +++ b/sphaira/include/yati/nx/ncz.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include + +namespace sphaira::ncz { + +#define NCZ_SECTION_MAGIC 0x4E544345535A434EUL +// todo: byteswap this +#define NCZ_BLOCK_MAGIC std::byteswap(0x4E435A424C4F434BUL) + +#define NCZ_SECTION_OFFSET (0x4000 + sizeof(ncz::Header)) + +struct Header { + u64 magic; // NCZ_SECTION_MAGIC + u64 total_sections; +}; + +struct BlockHeader { + u64 magic; // NCZ_BLOCK_MAGIC + u8 version; + u8 type; + u8 padding; + u8 block_size_exponent; + u32 total_blocks; + u64 decompressed_size; +}; + +struct Block { + u32 size; +}; + +struct BlockInfo { + u64 offset; // compressed offset. + u64 size; // compressed size. + + auto InRange(u64 off) const -> bool { + return off < offset + size && off >= offset; + } +}; + +struct Section { + u64 offset; + u64 size; + u64 crypto_type; + u64 padding; + u8 key[0x10]; + u8 counter[0x10]; + + auto InRange(u64 off) const -> bool { + return off < offset + size && off >= offset; + } +}; + +} // namespace sphaira::ncz diff --git a/sphaira/include/yati/nx/npdm.hpp b/sphaira/include/yati/nx/npdm.hpp new file mode 100644 index 0000000..a493b7e --- /dev/null +++ b/sphaira/include/yati/nx/npdm.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include + +namespace sphaira::npdm { + +struct Meta { + u32 magic; // "META" + u32 signature_key_generation; // +9.0.0 + u32 _0x8; + u8 flags; + u8 _0xD; + u8 main_thread_priority; + u8 main_thread_core_num; + u32 _0x10; + u32 sys_resource_size; // +3.0.0 + u32 version; + u32 main_thread_stack_size; + char title_name[0x10]; + char product_code[0x10]; + u8 _0x40[0x30]; + u32 aci0_offset; + u32 aci0_size; + u32 acid_offset; + u32 acid_size; +}; + +struct Acid { + u8 rsa_sig[0x100]; + u8 rsa_pub[0x100]; + u32 magic; // "ACID" + u32 size; + u8 version; + u8 _0x209[0x1]; + u8 _0x20A[0x2]; + u32 flags; + u64 program_id_min; + u64 program_id_max; + u32 fac_offset; + u32 fac_size; + u32 sac_offset; + u32 sac_size; + u32 kac_offset; + u32 kac_size; + u8 _0x238[0x8]; +}; + +struct Aci0 { + u32 magic; // "ACI0" + u8 _0x4[0xC]; + u64 program_id; + u8 _0x18[0x8]; + u32 fac_offset; + u32 fac_size; + u32 sac_offset; + u32 sac_size; + u32 kac_offset; + u32 kac_size; + u8 _0x38[0x8]; +}; + +} // namespace sphaira::npdm diff --git a/sphaira/include/yati/nx/ns.hpp b/sphaira/include/yati/nx/ns.hpp new file mode 100644 index 0000000..b089b3a --- /dev/null +++ b/sphaira/include/yati/nx/ns.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include "ncm.hpp" + +namespace sphaira::ns { + +enum ApplicationRecordType { + // installed + ApplicationRecordType_Installed = 0x3, + // application is gamecard, but gamecard isn't insterted + ApplicationRecordType_GamecardMissing = 0x5, + // archived + ApplicationRecordType_Archived = 0xB, +}; + +Result PushApplicationRecord(Service* srv, u64 tid, const ncm::ContentStorageRecord* records, u32 count); +Result ListApplicationRecordContentMeta(Service* srv, u64 offset, u64 tid, ncm::ContentStorageRecord* out_records, u32 count, s32* entries_read); +Result DeleteApplicationRecord(Service* srv, u64 tid); +Result InvalidateApplicationControlCache(Service* srv, u64 tid); + +} // namespace sphaira::ns diff --git a/sphaira/include/yati/nx/nxdumptool_rsa.h b/sphaira/include/yati/nx/nxdumptool_rsa.h new file mode 100644 index 0000000..aeba0ec --- /dev/null +++ b/sphaira/include/yati/nx/nxdumptool_rsa.h @@ -0,0 +1,62 @@ +/* + * rsa.c + * + * Copyright (c) 2018-2019, SciresM. + * Copyright (c) 2020-2024, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * nxdumptool is distributed in the hope that 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 . + */ + +#pragma once + +#ifndef __RSA_H__ +#define __RSA_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define RSA2048_BYTES 0x100 +#define RSA2048_BITS (RSA2048_BYTES * 8) + +#define RSA2048_SIG_SIZE RSA2048_BYTES +#define RSA2048_PUBKEY_SIZE RSA2048_BYTES + +/// Verifies a RSA-2048-PSS with SHA-256 signature. +/// Suitable for NCA and NPDM signatures. +/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively. +bool rsa2048VerifySha256BasedPssSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size); + +/// Verifies a RSA-2048-PKCS#1 v1.5 with SHA-256 signature. +/// Suitable for ticket and certificate chain signatures. +/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively. +bool rsa2048VerifySha256BasedPkcs1v15Signature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size); + +/// Performs RSA-2048-OAEP decryption. +/// Suitable to decrypt the titlekey block from personalized tickets. +/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively. +/// 'label' and 'label_size' arguments are optional -- if not needed, these may be set to NULL and 0, respectively. +bool rsa2048OaepDecrypt(void *dst, size_t dst_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, const void *private_exponent, \ + size_t private_exponent_size, const void *label, size_t label_size, size_t *out_size); + +#ifdef __cplusplus +} +#endif + +#endif /* __RSA_H__ */ diff --git a/sphaira/include/yati/nx/tik.h b/sphaira/include/yati/nx/tik.h new file mode 100644 index 0000000..c949936 --- /dev/null +++ b/sphaira/include/yati/nx/tik.h @@ -0,0 +1,265 @@ +/* + * tik.h + * + * Copyright (c) 2020-2024, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * nxdumptool is distributed in the hope that 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 . + */ + +#pragma once + +#ifndef __TIK_H__ +#define __TIK_H__ + +#include "signature.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define SIGNED_TIK_MIN_SIZE sizeof(TikSigHmac160) /* Assuming no ESV1/ESV2 records are available. */ +#define SIGNED_TIK_MAX_SIZE 0x400 /* Max ticket entry size in the ES ticket system savedata file. */ + +#define TIK_FORMAT_VERSION 2 + +#define GENERATE_TIK_STRUCT(sigtype, tiksize) \ +typedef struct { \ + SignatureBlock##sigtype sig_block; \ + TikCommonBlock tik_common_block; \ + u8 es_section_record_data[]; \ +} TikSig##sigtype; \ +NXDT_ASSERT(TikSig##sigtype, tiksize); + +typedef enum { + TikTitleKeyType_Common = 0, + TikTitleKeyType_Personalized = 1, + TikTitleKeyType_Count = 2 ///< Total values supported by this enum. +} TikTitleKeyType; + +typedef enum { + TikLicenseType_Permanent = 0, + TikLicenseType_Demo = 1, + TikLicenseType_Trial = 2, + TikLicenseType_Rental = 3, + TikLicenseType_Subscription = 4, + TikLicenseType_Service = 5, + TikLicenseType_Count = 6 ///< Total values supported by this enum. +} TikLicenseType; + +typedef enum { + TikPropertyMask_None = 0, + TikPropertyMask_PreInstallation = BIT(0), ///< Determines if the title comes pre-installed on the device. Most likely unused -- a remnant from previous ticket formats. + TikPropertyMask_SharedTitle = BIT(1), ///< Determines if the title holds shared contents only. Most likely unused -- a remnant from previous ticket formats. + TikPropertyMask_AllContents = BIT(2), ///< Determines if the content index mask shall be bypassed. Most likely unused -- a remnant from previous ticket formats. + TikPropertyMask_DeviceLinkIndepedent = BIT(3), ///< Determines if the console should *not* connect to the Internet to verify if the title's being used by the primary console. + TikPropertyMask_Volatile = BIT(4), ///< Determines if the ticket copy inside ticket.bin is available after reboot. Can be encrypted. + TikPropertyMask_ELicenseRequired = BIT(5), ///< Determines if the console should connect to the Internet to perform license verification. + TikPropertyMask_Count = 6 ///< Total values supported by this enum. +} TikPropertyMask; + +/// Placed after the ticket signature block. +typedef struct { + char issuer[0x40]; + u8 titlekey_block[0x100]; + u8 format_version; ///< Always matches TIK_FORMAT_VERSION. + u8 titlekey_type; ///< TikTitleKeyType. + u16 ticket_version; + u8 license_type; ///< TikLicenseType. + u8 key_generation; ///< NcaKeyGeneration. + u16 property_mask; ///< TikPropertyMask. + u8 reserved[0x8]; + u64 ticket_id; + u64 device_id; + FsRightsId rights_id; + u32 account_id; + u32 sect_total_size; + u32 sect_hdr_offset; + u16 sect_hdr_count; + u16 sect_hdr_entry_size; +} TikCommonBlock; + +NXDT_ASSERT(TikCommonBlock, 0x180); + +/// ESV1/ESV2 section records are placed right after the ticket data. These aren't available in TikTitleKeyType_Common tickets. +/// These are only used if the sect_* fields from the common block are non-zero (other than 'sect_hdr_offset'). +/// Each ESV2 section record is followed by a 'record_count' number of ESV1 records, each one of 'record_size' size. + +typedef enum { + TikSectionType_None = 0, + TikSectionType_Permanent = 1, + TikSectionType_Subscription = 2, + TikSectionType_Content = 3, + TikSectionType_ContentConsumption = 4, + TikSectionType_AccessTitle = 5, + TikSectionType_LimitedResource = 6, + TikSectionType_Count = 7 ///< Total values supported by this enum. +} TikSectionType; + +typedef struct { + u32 sect_offset; + u32 record_size; + u32 section_size; + u16 record_count; + u16 section_type; ///< TikSectionType. +} TikESV2SectionRecord; + +/// Used with TikSectionType_Permanent. +typedef struct { + u8 ref_id[0x10]; + u32 ref_id_attr; +} TikESV1PermanentRecord; + +/// Used with TikSectionType_Subscription. +typedef struct { + u32 limit; + u8 ref_id[0x10]; + u32 ref_id_attr; +} TikESV1SubscriptionRecord; + +/// Used with TikSectionType_Content. +typedef struct { + u32 offset; + u8 access_mask[0x80]; +} TikESV1ContentRecord; + +/// Used with TikSectionType_ContentConsumption. +typedef struct { + u16 index; + u16 code; + u32 limit; +} TikESV1ContentConsumptionRecord; + +/// Used with TikSectionType_AccessTitle. +typedef struct { + u64 access_title_id; + u64 access_title_mask; +} TikESV1AccessTitleRecord; + +/// Used with TikSectionType_LimitedResource. +typedef struct { + u32 limit; + u8 ref_id[0x10]; + u32 ref_id_attr; +} TikESV1LimitedResourceRecord; + +/// All tickets generated below use a little endian sig_type field. +GENERATE_TIK_STRUCT(Rsa4096, 0x3C0); /// RSA-4096 signature. +GENERATE_TIK_STRUCT(Rsa2048, 0x2C0); /// RSA-2048 signature. +GENERATE_TIK_STRUCT(Ecc480, 0x200); /// ECC signature. +GENERATE_TIK_STRUCT(Hmac160, 0x1C0); /// HMAC signature. + +/// Ticket type. +typedef enum { + TikType_None = 0, + TikType_SigRsa4096 = 1, + TikType_SigRsa2048 = 2, + TikType_SigEcc480 = 3, + TikType_SigHmac160 = 4, + TikType_Count = 5 ///< Total values supported by this enum. +} TikType; + +/// Used to store ticket type, size and raw data, as well as titlekey data. +typedef struct { + u8 type; ///< TikType. + u64 size; ///< Raw ticket size. + u8 data[SIGNED_TIK_MAX_SIZE]; ///< Raw ticket data. + u8 key_generation; ///< NcaKeyGeneration. + u8 enc_titlekey[0x10]; ///< Titlekey with titlekek crypto (RSA-OAEP unwrapped if dealing with a TikTitleKeyType_Personalized ticket). + char enc_titlekey_str[0x21]; ///< Character string representation of enc_titlekey. + u8 dec_titlekey[0x10]; ///< Titlekey without titlekek crypto. Ready to use for NCA FS section decryption. + char dec_titlekey_str[0x21]; ///< Character string representation of dec_titlekey. + char rights_id_str[0x21]; ///< Character string representation of the rights ID from the ticket. +} Ticket; + +/// Retrieves a ticket from either the ES ticket system savedata file (eMMC BIS System partition) or the secure Hash FS partition from an inserted gamecard. +/// Both the input rights ID and key generation values must have been retrieved from a NCA that depends on the desired ticket. +/// Titlekey is also RSA-OAEP unwrapped (if needed) and titlekek-decrypted right away. +bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, u8 key_generation, bool use_gamecard); + +/// Converts a TikTitleKeyType_Personalized ticket into a TikTitleKeyType_Common ticket and optionally generates a raw certificate chain for the new signature issuer. +/// Bear in mind the 'size' member from the Ticket parameter will be updated by this function to remove any possible references to ESV1/ESV2 records. +/// If both 'out_raw_cert_chain' and 'out_raw_cert_chain_size' pointers are provided, raw certificate chain data will be saved to them. +/// certGenerateRawCertificateChainBySignatureIssuer() is used internally, so the output buffer must be freed by the user. +bool tikConvertPersonalizedTicketToCommonTicket(Ticket *tik, u8 **out_raw_cert_chain, u64 *out_raw_cert_chain_size); + +/// Helper inline functions for signed ticket blobs. + +NX_INLINE TikCommonBlock *tikGetCommonBlockFromSignedTicketBlob(void *buf) +{ + return (TikCommonBlock*)signatureGetPayloadFromSignedBlob(buf, false); +} + +NX_INLINE u64 tikGetSectionRecordsSizeFromSignedTicketBlob(void *buf) +{ + TikCommonBlock *tik_common_block = tikGetCommonBlockFromSignedTicketBlob(buf); + if (!tik_common_block) return 0; + + u64 offset = sizeof(TikCommonBlock), out_size = 0; + + for(u32 i = 0; i < tik_common_block->sect_hdr_count; i++) + { + TikESV2SectionRecord *rec = (TikESV2SectionRecord*)((u8*)tik_common_block + offset); + offset += (sizeof(TikESV2SectionRecord) + ((u64)rec->record_count * (u64)rec->record_size)); + out_size += offset; + } + + return out_size; +} + +NX_INLINE bool tikIsValidSignedTicketBlob(void *buf) +{ + u64 ticket_size = (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock)); + return (ticket_size > sizeof(TikCommonBlock) && (ticket_size + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) <= SIGNED_TIK_MAX_SIZE); +} + +NX_INLINE u64 tikGetSignedTicketBlobSize(void *buf) +{ + return (tikIsValidSignedTicketBlob(buf) ? (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0); +} + +NX_INLINE u64 tikGetSignedTicketBlobHashAreaSize(void *buf) +{ + return (tikIsValidSignedTicketBlob(buf) ? (sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0); +} + +/// Helper inline functions for Ticket elements. + +NX_INLINE bool tikIsValidTicket(Ticket *tik) +{ + return (tik && tik->type > TikType_None && tik->type < TikType_Count && tik->size >= SIGNED_TIK_MIN_SIZE && tik->size <= SIGNED_TIK_MAX_SIZE && tikIsValidSignedTicketBlob(tik->data)); +} + +NX_INLINE TikCommonBlock *tikGetCommonBlockFromTicket(Ticket *tik) +{ + return (tikIsValidTicket(tik) ? tikGetCommonBlockFromSignedTicketBlob(tik->data) : NULL); +} + +NX_INLINE bool tikIsPersonalizedTicket(Ticket *tik) +{ + TikCommonBlock *tik_common_block = tikGetCommonBlockFromTicket(tik); + return (tik_common_block ? (tik_common_block->titlekey_type == TikTitleKeyType_Personalized) : false); +} + +NX_INLINE u64 tikGetHashAreaSizeFromTicket(Ticket *tik) +{ + return (tikIsValidTicket(tik) ? tikGetSignedTicketBlobHashAreaSize(tik->data) : 0); +} + +#ifdef __cplusplus +} +#endif + +#endif /* __TIK_H__ */ diff --git a/sphaira/include/yati/source/base.hpp b/sphaira/include/yati/source/base.hpp new file mode 100644 index 0000000..be4b7bf --- /dev/null +++ b/sphaira/include/yati/source/base.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +namespace sphaira::yati::source { + +struct Base { + virtual ~Base() = default; + // virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0; + virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0; + + Result GetOpenResult() const { + return m_open_result; + } + +protected: + Result m_open_result{}; +}; + +} // namespace sphaira::yati::source diff --git a/sphaira/include/yati/source/file.hpp b/sphaira/include/yati/source/file.hpp new file mode 100644 index 0000000..c3a5ce8 --- /dev/null +++ b/sphaira/include/yati/source/file.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "base.hpp" +#include "fs.hpp" +#include + +namespace sphaira::yati::source { + +struct File final : Base { + File(FsFileSystem* fs, const fs::FsPath& path); + ~File(); + + Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override; + +private: + FsFile m_file{}; +}; + +} // namespace sphaira::yati::source diff --git a/sphaira/include/yati/source/stdio.hpp b/sphaira/include/yati/source/stdio.hpp new file mode 100644 index 0000000..db18a6e --- /dev/null +++ b/sphaira/include/yati/source/stdio.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "base.hpp" +#include "fs.hpp" +#include +#include + +namespace sphaira::yati::source { + +struct Stdio final : Base { + Stdio(const char* path); + ~Stdio(); + + Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override; + +private: + std::FILE* m_file{}; +}; + +} // namespace sphaira::yati::source diff --git a/sphaira/include/yati/yati.hpp b/sphaira/include/yati/yati.hpp new file mode 100644 index 0000000..5be9fb3 --- /dev/null +++ b/sphaira/include/yati/yati.hpp @@ -0,0 +1,127 @@ +/* +* Notes: +* - nca's that use title key encryption are decrypted using Tegra SE, whereas +* standard crypto uses software decryption. +* The latter is almost always (slightly) faster, and removed the need for es patch. +*/ + +#pragma once + +#include "fs.hpp" +#include "source/base.hpp" +#include "ui/progress_box.hpp" +#include + +namespace sphaira::yati { + +enum { YatiModule = 521 }; + +enum : Result { + // unkown container for the source provided. + Result_ContainerNotFound = MAKERESULT(YatiModule, 10), + Result_Cancelled = MAKERESULT(YatiModule, 11), + + // nca required by the cnmt but not found in collection. + Result_NcaNotFound = MAKERESULT(YatiModule, 30), + Result_InvalidNcaReadSize = MAKERESULT(YatiModule, 31), + Result_InvalidNcaSigKeyGen = MAKERESULT(YatiModule, 32), + Result_InvalidNcaMagic = MAKERESULT(YatiModule, 33), + Result_InvalidNcaSignature0 = MAKERESULT(YatiModule, 34), + Result_InvalidNcaSignature1 = MAKERESULT(YatiModule, 35), + // invalid sha256 over the entire nca. + Result_InvalidNcaSha256 = MAKERESULT(YatiModule, 36), + + // section could not be found. + Result_NczSectionNotFound = MAKERESULT(YatiModule, 50), + // section count == 0. + Result_InvalidNczSectionCount = MAKERESULT(YatiModule, 51), + // block could not be found. + Result_NczBlockNotFound = MAKERESULT(YatiModule, 52), + // block version != 2. + Result_InvalidNczBlockVersion = MAKERESULT(YatiModule, 53), + // block type != 1. + Result_InvalidNczBlockType = MAKERESULT(YatiModule, 54), + // block count == 0. + Result_InvalidNczBlockTotal = MAKERESULT(YatiModule, 55), + // block size exponent < 14 || > 32. + Result_InvalidNczBlockSizeExponent = MAKERESULT(YatiModule, 56), + // zstd error while decompressing ncz. + Result_InvalidNczZstdError = MAKERESULT(YatiModule, 57), + + // nca has rights_id but matching ticket wasn't found. + Result_TicketNotFound = MAKERESULT(YatiModule, 70), + // found ticket has missmatching rights_id from it's name. + Result_InvalidTicketBadRightsId = MAKERESULT(YatiModule, 71), + Result_InvalidTicketVersion = MAKERESULT(YatiModule, 72), + Result_InvalidTicketKeyType = MAKERESULT(YatiModule, 73), + Result_InvalidTicketKeyRevision = MAKERESULT(YatiModule, 74), + + // cert not found for the ticket. + Result_CertNotFound = MAKERESULT(YatiModule, 90), + + // unable to fetch header from ncm database. + Result_NcmDbCorruptHeader = MAKERESULT(YatiModule, 110), + // unable to total infos from ncm database. + Result_NcmDbCorruptInfos = MAKERESULT(YatiModule, 111), +}; + +struct Config { + bool sd_card_install{}; + + // enables downgrading patch / data patch (dlc) version. + bool allow_downgrade{}; + + // ignores the install if already installed. + // checks that every nca is available. + bool skip_if_already_installed{}; + + // installs tickets only. + bool ticket_only{}; + + // converts personalised tickets to common tickets, allows for offline play. + // this breaks ticket signature so es needs to be patched. + // modified common tickets are patched regardless of this setting. + bool patch_ticket{}; + + // flags to enable / disable install of specific types. + bool skip_base{}; + bool skip_patch{}; + bool skip_addon{}; + bool skip_data_patch{}; + bool skip_ticket{}; + + // enables the option to skip sha256 verification. + bool skip_nca_hash_verify{}; + + // enables the option to skip rsa nca fixed key verification. + bool skip_rsa_header_fixed_key_verify{}; + + // enables the option to skip rsa npdm fixed key verification. + bool skip_rsa_npdm_fixed_key_verify{}; + + // if set, it will ignore the distribution bit in the nca header. + bool ignore_distribution_bit{}; + + // converts titlekey to standard crypto, also known as "ticketless". + // this will not work with addon (dlc), so, addon tickets will be installed. + bool convert_to_standard_crypto{}; + + // encrypts the keak with master key 0, this allows the game to be launched on every fw. + // implicitly performs standard crypto. + bool lower_master_key{}; + + // sets the system_firmware field in the cnmt extended header. + // if mkey is higher than fw version, the game still won't launch + // as the fw won't have the key to decrypt keak. + bool lower_system_version{}; +}; + +Result InstallFromFile(FsFileSystem* fs, const fs::FsPath& path); +Result InstallFromStdioFile(const char* path); +Result InstallFromSource(std::shared_ptr source); + +Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path); +Result InstallFromStdioFile(ui::ProgressBox* pbox, const char* path); +Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr source); + +} // namespace sphaira::yati diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index 191af6e..1ced627 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -1008,6 +1008,10 @@ void App::Draw() { this->queue.presentImage(this->swapchain, slot); } +auto App::GetApp() -> App* { + return g_app; +} + auto App::GetVg() -> NVGcontext* { return g_app->vg; } diff --git a/sphaira/source/ftpsrv_helper.cpp b/sphaira/source/ftpsrv_helper.cpp index 4c2f6e7..7946a46 100644 --- a/sphaira/source/ftpsrv_helper.cpp +++ b/sphaira/source/ftpsrv_helper.cpp @@ -132,9 +132,9 @@ void log_file_write(const char* msg) { } void log_file_fwrite(const char* fmt, ...) { - std::va_list v{}; + va_list v{}; va_start(v, fmt); - log_write_arg(fmt, v); + log_write_arg(fmt, &v); va_end(v); } diff --git a/sphaira/source/log.cpp b/sphaira/source/log.cpp index e875656..cb7823c 100644 --- a/sphaira/source/log.cpp +++ b/sphaira/source/log.cpp @@ -14,18 +14,20 @@ std::FILE* file{}; int nxlink_socket{}; std::mutex mutex{}; -void log_write_arg_internal(const char* s, std::va_list& v) { +void log_write_arg_internal(const char* s, std::va_list* v) { if (file) { - std::vfprintf(file, s, v); + std::vfprintf(file, s, *v); std::fflush(file); } if (nxlink_socket) { - std::vprintf(s, v); + std::vprintf(s, *v); } } } // namespace +extern "C" { + auto log_file_init() -> bool { std::scoped_lock lock{mutex}; if (file) { @@ -70,11 +72,11 @@ void log_write(const char* s, ...) { std::va_list v{}; va_start(v, s); - log_write_arg_internal(s, v); + log_write_arg_internal(s, &v); va_end(v); } -void log_write_arg(const char* s, std::va_list& v) { +void log_write_arg(const char* s, va_list* v) { std::scoped_lock lock{mutex}; if (!file && !nxlink_socket) { return; @@ -83,4 +85,6 @@ void log_write_arg(const char* s, std::va_list& v) { log_write_arg_internal(s, v); } +} // extern "C" + #endif diff --git a/sphaira/source/owo.cpp b/sphaira/source/owo.cpp index 0f0d2b0..7efaf9d 100644 --- a/sphaira/source/owo.cpp +++ b/sphaira/source/owo.cpp @@ -7,6 +7,14 @@ #include #include +#include "yati/nx/nca.hpp" +#include "yati/nx/ncm.hpp" +#include "yati/nx/npdm.hpp" +#include "yati/nx/ns.hpp" +#include "yati/nx/es.hpp" +#include "yati/nx/keys.hpp" +#include "yati/nx/crypto.hpp" + #include "owo.hpp" #include "defines.hpp" #include "app.hpp" @@ -23,52 +31,9 @@ constexpr u32 PFS0_EXEFS_HASH_BLOCK_SIZE = 0x10000; constexpr u32 PFS0_LOGO_HASH_BLOCK_SIZE = 0x1000; constexpr u32 PFS0_META_HASH_BLOCK_SIZE = 0x1000; constexpr u32 PFS0_PADDING_SIZE = 0x200; -constexpr u32 NCA_SECTION_TOTAL = 0x4; constexpr u32 ROMFS_ENTRY_EMPTY = 0xFFFFFFFF; constexpr u32 ROMFS_FILEPARTITION_OFS = 0x200; -enum NcaDistributionType { - NcaDistributionType_System = 0x0, - NcaDistributionType_GameCard = 0x1 -}; - -enum NcaContentType { - NcaContentType_Program = 0x0, - NcaContentType_Meta = 0x1, - NcaContentType_Control = 0x2, - NcaContentType_Manual = 0x3, - NcaContentType_Data = 0x4, - NcaContentType_PublicData = 0x5, -}; - -enum NcaFileSystemType { - NcaFileSystemType_RomFS = 0x0, - NcaFileSystemType_PFS0 = 0x1 -}; - -enum NcaHashType { - NcaHashType_Auto = 0x0, - NcaHashType_HierarchicalSha256 = 0x2, - NcaHashType_HierarchicalIntegrity = 0x3 -}; - -enum NcaEncryptionType { - NcaEncryptionType_Auto = 0x0, - NcaEncryptionType_None = 0x1, - NcaEncryptionType_AesCtrOld = 0x2, - NcaEncryptionType_AesCtr = 0x3, - NcaEncryptionType_AesCtrEx = 0x4 -}; - -enum NsApplicationRecordType { - // installed - NsApplicationRecordType_Installed = 0x3, - // application is gamecard, but gamecard isn't insterted - NsApplicationRecordType_GamecardMissing = 0x5, - // archived - NsApplicationRecordType_Archived = 0xB, -}; - // stdio-like wrapper for std::vector struct BufHelper { BufHelper() = default; @@ -124,12 +89,6 @@ struct CnmtHeader { }; static_assert(sizeof(CnmtHeader) == 0x20); -struct NcmContentStorageRecord { - NcmContentMetaKey key; - u8 storage_id; // - u8 padding[0x7]; -}; - struct NcmContentMetaData { NcmContentMetaHeader header; NcmApplicationMetaExtendedHeader extended; @@ -142,7 +101,7 @@ struct NcaMetaEntry { NcaEntry nca_entry; NcmContentMetaHeader content_meta_header{}; NcmContentMetaKey content_meta_key{}; - NcmContentStorageRecord content_storage_record{}; + ncm::ContentStorageRecord content_storage_record{}; NcmContentMetaData content_meta_data{}; }; @@ -171,61 +130,6 @@ struct FileEntry { using FileEntries = std::vector; -struct NpdmMeta { - u32 magic; // "META" - u32 signature_key_generation; // +9.0.0 - u32 _0x8; - u8 flags; - u8 _0xD; - u8 main_thread_priority; - u8 main_thread_core_num; - u32 _0x10; - u32 sys_resource_size; // +3.0.0 - u32 version; - u32 main_thread_stack_size; - char title_name[0x10]; - char product_code[0x10]; - u8 _0x40[0x30]; - u32 aci0_offset; - u32 aci0_size; - u32 acid_offset; - u32 acid_size; -}; - -struct NpdmAcid { - u8 rsa_sig[0x100]; - u8 rsa_pub[0x100]; - u32 magic; // "ACID" - u32 size; - u8 version; - u8 _0x209[0x1]; - u8 _0x20A[0x2]; - u32 flags; - u64 program_id_min; - u64 program_id_max; - u32 fac_offset; - u32 fac_size; - u32 sac_offset; - u32 sac_size; - u32 kac_offset; - u32 kac_size; - u8 _0x238[0x8]; -}; - -struct NpdmAci0 { - u32 magic; // "ACI0" - u8 _0x4[0xC]; - u64 program_id; - u8 _0x18[0x8]; - u32 fac_offset; - u32 fac_size; - u32 sac_offset; - u32 sac_size; - u32 kac_offset; - u32 kac_size; - u8 _0x38[0x8]; -}; - struct NpdmPatch { char title_name[0x10]{"Application"}; char product_code[0x10]{}; @@ -238,56 +142,6 @@ struct NcapPatch { u64 tid; }; -struct NcaSectionTableEntry { - u32 media_start_offset; // divided by 0x200. - u32 media_end_offset; // divided by 0x200. - u8 _0x8[0x4]; // unknown. - u8 _0xC[0x4]; // unknown. -}; - -struct LayerRegion { - u64 offset; - u64 size; -}; - -struct HierarchicalSha256Data { - u8 master_hash[0x20]; - u32 block_size; - u32 layer_count; - LayerRegion hash_layer; - LayerRegion pfs0_layer; - LayerRegion unused_layers[3]; - u8 _0x78[0x80]; -}; - -#pragma pack(push, 1) -struct HierarchicalIntegrityVerificationLevelInformation { - u64 logical_offset; - u64 hash_data_size; - u32 block_size; // log2 - u32 _0x14; // reserved -}; -#pragma pack(pop) - -struct InfoLevelHash { - u32 max_layers; - HierarchicalIntegrityVerificationLevelInformation levels[6]; - u8 signature_salt[0x20]; -}; - -struct IntegrityMetaInfo { - u32 magic; // IVFC - u32 version; - u32 master_hash_size; - InfoLevelHash info_level_hash; - u8 master_hash[0x20]; - u8 _0xE0[0x18]; -}; - -static_assert(sizeof(HierarchicalSha256Data) == 0xF8); -static_assert(sizeof(IntegrityMetaInfo) == 0xF8); -static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo)); - typedef struct romfs_dirent_ctx { u32 entry_offset; struct romfs_dirent_ctx *parent; /* Parent node */ @@ -317,70 +171,6 @@ typedef struct { u64 file_partition_size; } romfs_ctx_t; -struct NcaFsHeader { - u16 version; // always 2. - u8 fs_type; // see NcaFileSystemType. - u8 hash_type; // see NcaHashType. - u8 encryption_type; // see NcaEncryptionType. - u8 metadata_hash_type; - u8 _0x6[0x2]; // empty. - - union { - HierarchicalSha256Data hierarchical_sha256_data; - IntegrityMetaInfo integrity_meta_info; // used for romfs - } hash_data; - - u8 patch_info[0x40]; - u64 section_ctr; - u8 spares_info[0x30]; - u8 compression_info[0x28]; - u8 meta_data_hash_data_info[0x30]; - u8 reserved[0x30]; -}; - -struct NcaSectionHeaderHash { - u8 sha256[0x20]; -}; - -struct NcaKeyArea { - u8 area[0x10]; -}; - -struct NcaHeader { - u8 rsa_fixed_key[0x100]; - u8 rsa_npdm[0x100]; // key from npdm. - u32 magic; - u8 distribution_type; // see NcaDistributionType. - u8 content_type; // see NcaContentType. - u8 old_key_gen; // see NcaOldKeyGeneration. - u8 kaek_index; // see NcaKeyAreaEncryptionKeyIndex. - u64 size; - u64 title_id; - u32 context_id; - u32 sdk_version; - u8 key_gen; // see NcaKeyGeneration. - u8 header_1_sig_key_gen; - u8 _0x222[0xE]; // empty. - FsRightsId rights_id; - - NcaSectionTableEntry fs_table[NCA_SECTION_TOTAL]; - NcaSectionHeaderHash fs_header_hash[NCA_SECTION_TOTAL]; - NcaKeyArea key_area[NCA_SECTION_TOTAL]; - - u8 _0x340[0xC0]; // empty. - - NcaFsHeader fs_header[NCA_SECTION_TOTAL]; -}; - -constexpr u8 HEADER_KEK_SRC[0x10] = { - 0x1F, 0x12, 0x91, 0x3A, 0x4A, 0xCB, 0xF0, 0x0D, 0x4C, 0xDE, 0x3A, 0xF6, 0xD5, 0x23, 0x88, 0x2A -}; - -constexpr u8 HEADER_KEY_SRC[0x20] = { - 0x5A, 0x3E, 0xD8, 0x4F, 0xDE, 0xC0, 0xD8, 0x26, 0x31, 0xF7, 0xE2, 0x5D, 0x19, 0x7B, 0xF5, 0xD0, - 0x1C, 0x9B, 0x7B, 0xFA, 0xF6, 0x28, 0x18, 0x3D, 0x71, 0xF6, 0x4D, 0x73, 0xF1, 0x50, 0xB9, 0xD2 -}; - auto write_padding(BufHelper& buf, u64 off, u64 block) -> u64 { const u64 size = block - (off % block); if (size) { @@ -632,9 +422,9 @@ auto npdm_patch_kc(std::vector& npdm, u32 off, u32 size, u32 bitmask, u32 va // todo: manually build npdm void patch_npdm(std::vector& npdm, const NpdmPatch& patch) { - NpdmMeta meta{}; - NpdmAci0 aci0{}; - NpdmAcid acid{}; + npdm::Meta meta{}; + npdm::Aci0 aci0{}; + npdm::Acid acid{}; std::memcpy(&meta, npdm.data(), sizeof(meta)); std::memcpy(&aci0, npdm.data() + meta.aci0_offset, sizeof(aci0)); std::memcpy(&acid, npdm.data() + meta.acid_offset, sizeof(acid)); @@ -805,7 +595,7 @@ void write_nca_padding(BufHelper& buf) { write_padding(buf, buf.tell(), 0x200); } -void nca_encrypt_header(NcaHeader* header, std::span key) { +void nca_encrypt_header(nca::Header* header, std::span key) { Aes128XtsContext ctx{}; aes128XtsContextCreate(&ctx, key.data(), key.data() + 0x10, true); @@ -816,41 +606,41 @@ void nca_encrypt_header(NcaHeader* header, std::span key) { } } -void write_nca_section(NcaHeader& nca_header, u8 index, u64 start, u64 end) { +void write_nca_section(nca::Header& nca_header, u8 index, u64 start, u64 end) { auto& section = nca_header.fs_table[index]; section.media_start_offset = start / 0x200; // 0xC00 / 0x200 section.media_end_offset = end / 0x200; // Section end offset / 200 section._0x8[0] = 0x1; // Always 1 } -void write_nca_fs_header_pfs0(NcaHeader& nca_header, u8 index, const std::vector& master_hash, u64 hash_table_size, u32 block_size) { +void write_nca_fs_header_pfs0(nca::Header& nca_header, u8 index, const std::vector& master_hash, u64 hash_table_size, u32 block_size) { auto& fs_header = nca_header.fs_header[index]; - fs_header.hash_type = NcaHashType_HierarchicalSha256; - fs_header.fs_type = NcaFileSystemType_PFS0; + fs_header.hash_type = nca::HashType_HierarchicalSha256; + fs_header.fs_type = nca::FileSystemType_PFS0; fs_header.version = 0x2; // Always 2 fs_header.hash_data.hierarchical_sha256_data.layer_count = 0x2; fs_header.hash_data.hierarchical_sha256_data.block_size = block_size; - fs_header.encryption_type = NcaEncryptionType_None; + fs_header.encryption_type = nca::EncryptionType_None; fs_header.hash_data.hierarchical_sha256_data.hash_layer.size = hash_table_size; std::memcpy(fs_header.hash_data.hierarchical_sha256_data.master_hash, master_hash.data(), master_hash.size()); sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header)); } -void write_nca_fs_header_romfs(NcaHeader& nca_header, u8 index) { +void write_nca_fs_header_romfs(nca::Header& nca_header, u8 index) { auto& fs_header = nca_header.fs_header[index]; - fs_header.hash_type = NcaHashType_HierarchicalIntegrity; - fs_header.fs_type = NcaFileSystemType_RomFS; + fs_header.hash_type = nca::HashType_HierarchicalIntegrity; + fs_header.fs_type = nca::FileSystemType_RomFS; fs_header.version = 0x2; // Always 2 fs_header.hash_data.integrity_meta_info.magic = 0x43465649; fs_header.hash_data.integrity_meta_info.version = 0x20000; // Always 0x20000 fs_header.hash_data.integrity_meta_info.master_hash_size = SHA256_HASH_SIZE; fs_header.hash_data.integrity_meta_info.info_level_hash.max_layers = 0x7; - fs_header.encryption_type = NcaEncryptionType_None; + fs_header.encryption_type = nca::EncryptionType_None; fs_header.hash_data.integrity_meta_info.info_level_hash.levels[5].block_size = 0x0E; // 0x4000 sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header)); } -void write_nca_pfs0(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) { +void write_nca_pfs0(nca::Header& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) { const auto pfs0 = build_pfs0(entries); const auto pfs0_hash_table = build_pfs0_hash_table(pfs0, block_size); const auto pfs0_master_hash = build_pfs0_master_hash(pfs0_hash_table); @@ -887,7 +677,7 @@ auto ivfc_create_level(const std::vector& src) -> std::vector { return buf.buf; } -void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) { +void write_nca_romfs(nca::Header& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) { auto& fs_header = nca_header.fs_header[index]; auto& meta_info = fs_header.hash_data.integrity_meta_info; auto& info_level_hash = meta_info.info_level_hash; @@ -921,22 +711,22 @@ void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries write_nca_fs_header_romfs(nca_header, index); } -void write_nca_header_encypted(NcaHeader& nca_header, u64 tid, std::span key, NcaContentType type, BufHelper& buf) { - nca_header.magic = 0x3341434E; - nca_header.distribution_type = NcaDistributionType_System; +void write_nca_header_encypted(nca::Header& nca_header, u64 tid, const keys::Keys& keys, nca::ContentType type, BufHelper& buf) { + nca_header.magic = NCA3_MAGIC; + nca_header.distribution_type = nca::DistributionType_System; nca_header.content_type = type; nca_header.title_id = tid; nca_header.sdk_version = 0x000C1100; nca_header.size = buf.tell(); - nca_encrypt_header(&nca_header, key); + nca_encrypt_header(&nca_header, keys.header_key); buf.seek(0); buf.write(&nca_header, sizeof(nca_header)); } -auto create_program_nca(u64 tid, std::span key, const FileEntries& exefs, const FileEntries& romfs, const FileEntries& logo) -> NcaEntry { +auto create_program_nca(u64 tid, const keys::Keys& keys, const FileEntries& exefs, const FileEntries& romfs, const FileEntries& logo) -> NcaEntry { BufHelper buf; - NcaHeader nca_header{}; + nca::Header nca_header{}; buf.write(&nca_header, sizeof(nca_header)); write_nca_pfs0(nca_header, 0, exefs, PFS0_EXEFS_HASH_BLOCK_SIZE, buf); @@ -945,23 +735,23 @@ auto create_program_nca(u64 tid, std::span key, const FileEntries& exe if (logo.size() == 2 && !logo[0].data.empty() && !logo[1].data.empty()) { write_nca_pfs0(nca_header, 2, logo, PFS0_LOGO_HASH_BLOCK_SIZE, buf); } - write_nca_header_encypted(nca_header, tid, key, NcaContentType_Program, buf); + write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Program, buf); return {buf, NcmContentType_Program}; } -auto create_control_nca(u64 tid, std::span key, const FileEntries& romfs) -> NcaEntry{ - NcaHeader nca_header{}; +auto create_control_nca(u64 tid, const keys::Keys& keys, const FileEntries& romfs) -> NcaEntry{ + nca::Header nca_header{}; BufHelper buf; buf.write(&nca_header, sizeof(nca_header)); write_nca_romfs(nca_header, 0, romfs, IVFC_HASH_BLOCK_SIZE, buf); - write_nca_header_encypted(nca_header, tid, key, NcaContentType_Control, buf); + write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Control, buf); return {buf, NcmContentType_Control}; } -auto create_meta_nca(u64 tid, std::span key, NcmStorageId storage_id, const std::vector& ncas) -> NcaMetaEntry { +auto create_meta_nca(u64 tid, const keys::Keys& keys, NcmStorageId storage_id, const std::vector& ncas) -> NcaMetaEntry { CnmtHeader cnmt_header{}; NcmApplicationMetaExtendedHeader cnmt_extended{}; NcmPackagedContentInfo packaged_content_info[2]{}; @@ -997,10 +787,10 @@ auto create_meta_nca(u64 tid, std::span key, NcmStorageId storage_id, std::snprintf(cnmt_name, sizeof(cnmt_name), "Application_%016lX.cnmt", tid); add_file_entry(cnmt, cnmt_name, cnmt_buf.buf.data(), cnmt_buf.buf.size()); - NcaHeader nca_header{}; + nca::Header nca_header{}; buf.write(&nca_header, sizeof(nca_header)); write_nca_pfs0(nca_header, 0, cnmt, PFS0_META_HASH_BLOCK_SIZE, buf); - write_nca_header_encypted(nca_header, tid, key, NcaContentType_Meta, buf); + write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Meta, buf); // entry NcaMetaEntry entry{buf, NcmContentType_Meta}; @@ -1040,26 +830,6 @@ auto create_meta_nca(u64 tid, std::span key, NcmStorageId storage_id, return entry; } -Result nsDeleteApplicationRecord(Service* srv, u64 tid) { - return serviceDispatchIn(srv, 27, tid); -} - -Result nsPushApplicationRecord(Service* srv, u64 tid, const NcmContentStorageRecord* records, u32 count) { - const struct { - u8 last_modified_event; - u8 padding[0x7]; - u64 tid; - } in = { NsApplicationRecordType_Installed, {0}, tid }; - - return serviceDispatchIn(srv, 16, in, - .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In }, - .buffers = { { records, sizeof(NcmContentStorageRecord) * count } }); -} - -Result nsInvalidateApplicationControlCache(Service* srv, u64 tid) { - return serviceDispatchIn(srv, 404, tid); -} - auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId storage_id) -> Result { R_UNLESS(!config.nro_path.empty(), OwoError_BadArgs); // R_UNLESS(!config.icon.empty(), OwoError_BadArgs); @@ -1073,14 +843,8 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor R_TRY(nsInitialize()); ON_SCOPE_EXIT(nsExit()); - // generate header kek - u8 header_kek[0x20]; - R_TRY(splCryptoGenerateAesKek(HEADER_KEK_SRC, 0, 0, header_kek)); - // gen header key 0 - u8 key[0x20]; - R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC, key)); - // gen header key 1 - R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC + 0x10, key + 0x10)); + keys::Keys keys; + R_TRY(keys::parse_keys(keys, false)); // fix args to include nro path if (config.args.empty()) { @@ -1124,7 +888,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor patch_npdm(exefs[1].data, npdm_patch); nca_entries.emplace_back( - create_program_nca(tid, key, exefs, romfs, logo) + create_program_nca(tid, keys, exefs, romfs, logo) ); } else { nca_entries.emplace_back( @@ -1147,18 +911,18 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor add_file_entry(romfs, "/icon_AmericanEnglish.dat", config.icon); nca_entries.emplace_back( - create_control_nca(tid, key, romfs) + create_control_nca(tid, keys, romfs) ); } // create meta NcmContentMetaHeader content_meta_header; NcmContentMetaKey content_meta_key; - NcmContentStorageRecord content_storage_record; + ncm::ContentStorageRecord content_storage_record; NcmContentMetaData content_meta_data; { pbox->NewTransfer("Creating Meta"_i18n).UpdateTransfer(2, 8); - const auto meta_entry = create_meta_nca(tid, key, storage_id, nca_entries); + const auto meta_entry = create_meta_nca(tid, keys, storage_id, nca_entries); nca_entries.emplace_back(meta_entry.nca_entry); content_meta_header = meta_entry.content_meta_header; @@ -1218,15 +982,15 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor // remove previous application record if (already_installed || hosversionBefore(2,0,0)) { - const auto rc = nsDeleteApplicationRecord(srv_ptr, tid); + const auto rc = ns::DeleteApplicationRecord(srv_ptr, tid); R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc); } - R_TRY(nsPushApplicationRecord(srv_ptr, tid, &content_storage_record, 1)); + R_TRY(ns::PushApplicationRecord(srv_ptr, tid, &content_storage_record, 1)); // force flush if (already_installed || hosversionBefore(2,0,0)) { - const auto rc = nsInvalidateApplicationControlCache(srv_ptr, tid); + const auto rc = ns::InvalidateApplicationControlCache(srv_ptr, tid); R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc); } } diff --git a/sphaira/source/ui/menus/filebrowser.cpp b/sphaira/source/ui/menus/filebrowser.cpp index d279bd6..7399c7a 100644 --- a/sphaira/source/ui/menus/filebrowser.cpp +++ b/sphaira/source/ui/menus/filebrowser.cpp @@ -18,6 +18,8 @@ #include "owo.hpp" #include "swkbd.hpp" #include "i18n.hpp" +#include "yati/yati.hpp" +#include "yati/source/file.hpp" #include #include @@ -52,6 +54,10 @@ constexpr std::string_view VIDEO_EXTENSIONS[] = { constexpr std::string_view IMAGE_EXTENSIONS[] = { "png", "jpg", "jpeg", "bmp", "gif", }; +constexpr std::string_view INSTALL_EXTENSIONS[] = { + "nsp", "xci", "nsz", "xcz", +}; + struct RomDatabaseEntry { std::string_view folder; @@ -294,6 +300,8 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 nro_launch(GetNewPathCurrent()); } })); + } else if (App::GetInstallEnable() && IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) { + InstallFile(GetEntry()); } else { const auto assoc_list = FindFileAssocFor(); if (!assoc_list.empty()) { @@ -406,7 +414,7 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 } log_write("clicked on delete\n"); App::Push(std::make_shared( - "Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){ + "Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){ if (op_index && *op_index) { App::PopToMenu(); OnDeleteCallback(); @@ -421,7 +429,7 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 options->Add(std::make_shared("Paste"_i18n, [this](){ const std::string buf = "Paste "_i18n + std::to_string(m_selected_files.size()) + " file(s)?"_i18n; App::Push(std::make_shared( - buf, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){ + buf, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){ if (op_index && *op_index) { App::PopToMenu(); OnPasteCallback(); @@ -459,6 +467,32 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 })); } + // if install is enabled, check if all currently selected files are installable. + if (m_entries_current.size() && App::GetInstallEnable()) { + bool should_install = true; + if (!m_selected_count) { + should_install = IsExtension(GetEntry().GetExtension(), INSTALL_EXTENSIONS); + } else { + const auto entries = GetSelectedEntries(); + for (auto&e : entries) { + if (!IsExtension(e.GetExtension(), INSTALL_EXTENSIONS)) { + should_install = false; + break; + } + } + } + + if (should_install) { + options->Add(std::make_shared("Install"_i18n, [this](){ + if (!m_selected_count) { + InstallFile(GetEntry()); + } else { + InstallFiles(GetSelectedEntries()); + } + })); + } + } + options->Add(std::make_shared("Advanced"_i18n, [this](){ auto options = std::make_shared("Advanced Options"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(options)); @@ -628,6 +662,9 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { icon = ThemeEntryID_ICON_VIDEO; } else if (IsExtension(ext, IMAGE_EXTENSIONS)) { icon = ThemeEntryID_ICON_IMAGE; + } else if (IsExtension(ext, INSTALL_EXTENSIONS)) { + // todo: maybe replace this icon with something else? + icon = ThemeEntryID_ICON_NRO; } else if (IsExtension(ext, "zip")) { icon = ThemeEntryID_ICON_ZIP; } else if (IsExtension(ext, "nro")) { @@ -775,6 +812,34 @@ void Menu::InstallForwarder() { )); } +void Menu::InstallFile(const FileEntry& target) { + std::vector targets{target}; + InstallFiles(targets); +} + +void Menu::InstallFiles(const std::vector& targets) { + App::Push(std::make_shared("Install Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this, targets](auto op_index){ + if (op_index && *op_index) { + App::PopToMenu(); + + App::Push(std::make_shared("Installing App"_i18n, [this, targets](auto pbox) mutable -> bool { + for (auto& e : targets) { + const auto rc = yati::InstallFromFile(pbox, &m_fs->m_fs, GetNewPath(e)); + if (rc == yati::Result_Cancelled) { + break; + } else if (R_FAILED(rc)) { + return false; + } else { + App::Notify("Installed " + e.GetName()); + } + } + + return true; + })); + } + })); +} + auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result { log_write("new scan path: %s\n", new_path.s); if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) { diff --git a/sphaira/source/ui/menus/main_menu.cpp b/sphaira/source/ui/menus/main_menu.cpp index 0043e9e..287674d 100644 --- a/sphaira/source/ui/menus/main_menu.cpp +++ b/sphaira/source/ui/menus/main_menu.cpp @@ -323,10 +323,6 @@ MainMenu::MainMenu() { auto options = std::make_shared("Advanced Options"_i18n, Sidebar::Side::LEFT); ON_SCOPE_EXIT(App::Push(options)); - SidebarEntryArray::Items install_items; - install_items.push_back("System memory"_i18n); - install_items.push_back("microSD card"_i18n); - SidebarEntryArray::Items text_scroll_speed_items; text_scroll_speed_items.push_back("Slow"_i18n); text_scroll_speed_items.push_back("Normal"_i18n); @@ -340,21 +336,94 @@ MainMenu::MainMenu() { App::SetReplaceHbmenuEnable(enable); }, "Enabled"_i18n, "Disabled"_i18n)); - options->Add(std::make_shared("Install forwarders"_i18n, App::GetInstallEnable(), [this](bool& enable){ - App::SetInstallEnable(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Install location"_i18n, install_items, [this](s64& index_out){ - App::SetInstallSdEnable(index_out); - }, (s64)App::GetInstallSdEnable())); - - options->Add(std::make_shared("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){ - App::SetInstallPrompt(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - options->Add(std::make_shared("Text scroll speed"_i18n, text_scroll_speed_items, [this](s64& index_out){ App::SetTextScrollSpeed(index_out); }, (s64)App::GetTextScrollSpeed())); + + options->Add(std::make_shared("Install options"_i18n, [this](){ + auto options = std::make_shared("Install Options"_i18n, Sidebar::Side::LEFT); + ON_SCOPE_EXIT(App::Push(options)); + + SidebarEntryArray::Items install_items; + install_items.push_back("System memory"_i18n); + install_items.push_back("microSD card"_i18n); + + options->Add(std::make_shared("Enable"_i18n, App::GetInstallEnable(), [this](bool& enable){ + App::SetInstallEnable(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){ + App::SetInstallPrompt(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Install location"_i18n, install_items, [this](s64& index_out){ + App::SetInstallSdEnable(index_out); + }, (s64)App::GetInstallSdEnable())); + + options->Add(std::make_shared("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [this](bool& enable){ + App::GetApp()->m_allow_downgrade.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [this](bool& enable){ + App::GetApp()->m_skip_if_already_installed.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [this](bool& enable){ + App::GetApp()->m_ticket_only.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Patch ticket"_i18n, App::GetApp()->m_patch_ticket.Get(), [this](bool& enable){ + App::GetApp()->m_patch_ticket.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [this](bool& enable){ + App::GetApp()->m_skip_base.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip Patch"_i18n, App::GetApp()->m_skip_patch.Get(), [this](bool& enable){ + App::GetApp()->m_skip_patch.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip addon"_i18n, App::GetApp()->m_skip_addon.Get(), [this](bool& enable){ + App::GetApp()->m_skip_addon.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [this](bool& enable){ + App::GetApp()->m_skip_data_patch.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [this](bool& enable){ + App::GetApp()->m_skip_ticket.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [this](bool& enable){ + App::GetApp()->m_skip_nca_hash_verify.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [this](bool& enable){ + App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [this](bool& enable){ + App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [this](bool& enable){ + App::GetApp()->m_ignore_distribution_bit.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [this](bool& enable){ + App::GetApp()->m_convert_to_standard_crypto.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [this](bool& enable){ + App::GetApp()->m_lower_master_key.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [this](bool& enable){ + App::GetApp()->m_lower_system_version.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + })); })); }}) ); diff --git a/sphaira/source/ui/progress_box.cpp b/sphaira/source/ui/progress_box.cpp index 6ecbb19..8d44709 100644 --- a/sphaira/source/ui/progress_box.cpp +++ b/sphaira/source/ui/progress_box.cpp @@ -102,6 +102,14 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void { } } +auto ProgressBox::SetTitle(const std::string& title) -> ProgressBox& { + mutexLock(&m_mutex); + m_title = title; + mutexUnlock(&m_mutex); + Yield(); + return *this; +} + auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& { mutexLock(&m_mutex); m_transfer = transfer; diff --git a/sphaira/source/yati/container/nsp.cpp b/sphaira/source/yati/container/nsp.cpp new file mode 100644 index 0000000..2bc3993 --- /dev/null +++ b/sphaira/source/yati/container/nsp.cpp @@ -0,0 +1,67 @@ +#include "yati/container/nsp.hpp" +#include "defines.hpp" +#include "log.hpp" +#include + +namespace sphaira::yati::container { +namespace { + +#define PFS0_MAGIC 0x30534650 + +struct Pfs0Header { + u32 magic; + u32 total_files; + u32 string_table_size; + u32 padding; +}; + +struct Pfs0FileTableEntry { + u64 data_offset; + u64 data_size; + u32 name_offset; + u32 padding; +}; + +} // namespace + +Result Nsp::Validate(source::Base* source) { + u32 magic; + u64 bytes_read; + R_TRY(source->Read(std::addressof(magic), 0, sizeof(magic), std::addressof(bytes_read))); + R_UNLESS(magic == PFS0_MAGIC, 0x1); + R_SUCCEED(); +} + +Result Nsp::GetCollections(Collections& out) { + u64 bytes_read; + s64 off = 0; + + // get header + Pfs0Header header{}; + R_TRY(m_source->Read(std::addressof(header), off, sizeof(header), std::addressof(bytes_read))); + R_UNLESS(header.magic == PFS0_MAGIC, 0x1); + off += bytes_read; + + // get file table + std::vector file_table(header.total_files); + R_TRY(m_source->Read(file_table.data(), off, file_table.size() * sizeof(Pfs0FileTableEntry), std::addressof(bytes_read))) + off += bytes_read; + + // get string table + std::vector string_table(header.string_table_size); + R_TRY(m_source->Read(string_table.data(), off, string_table.size(), std::addressof(bytes_read))) + off += bytes_read; + + out.reserve(header.total_files); + for (u32 i = 0; i < header.total_files; i++) { + CollectionEntry entry; + entry.name = string_table.data() + file_table[i].name_offset; + entry.offset = off + file_table[i].data_offset; + entry.size = file_table[i].data_size; + out.emplace_back(entry); + } + + R_SUCCEED(); +} + +} // namespace sphaira::yati::container diff --git a/sphaira/source/yati/container/xci.cpp b/sphaira/source/yati/container/xci.cpp new file mode 100644 index 0000000..ab0213f --- /dev/null +++ b/sphaira/source/yati/container/xci.cpp @@ -0,0 +1,95 @@ +#include "yati/container/xci.hpp" +#include "defines.hpp" +#include "log.hpp" + +namespace sphaira::yati::container { +namespace { + +#define XCI_MAGIC std::byteswap(0x48454144) +#define HFS0_MAGIC 0x30534648 +#define HFS0_HEADER_OFFSET 0xF000 + +struct Hfs0Header { + u32 magic; + u32 total_files; + u32 string_table_size; + u32 padding; +}; + +struct Hfs0FileTableEntry { + u64 data_offset; + u64 data_size; + u32 name_offset; + u32 hash_size; + u64 padding; + u8 hash[0x20]; +}; + +struct Hfs0 { + Hfs0Header header{}; + std::vector file_table{}; + std::vector string_table{}; + s64 data_offset{}; +}; + +Result Hfs0GetPartition(source::Base* source, s64 off, Hfs0& out) { + u64 bytes_read; + + // get header + R_TRY(source->Read(std::addressof(out.header), off, sizeof(out.header), std::addressof(bytes_read))); + R_UNLESS(out.header.magic == HFS0_MAGIC, 0x1); + off += bytes_read; + + // get file table + out.file_table.resize(out.header.total_files); + R_TRY(source->Read(out.file_table.data(), off, out.file_table.size() * sizeof(Hfs0FileTableEntry), std::addressof(bytes_read))) + off += bytes_read; + + // get string table + std::vector string_table(out.header.string_table_size); + R_TRY(source->Read(string_table.data(), off, string_table.size(), std::addressof(bytes_read))) + off += bytes_read; + + for (u32 i = 0; i < out.header.total_files; i++) { + out.string_table.emplace_back(string_table.data() + out.file_table[i].name_offset); + } + + out.data_offset = off; + R_SUCCEED(); +} + +} // namespace + +Result Xci::Validate(source::Base* source) { + u32 magic; + u64 bytes_read; + R_TRY(source->Read(std::addressof(magic), 0x100, sizeof(magic), std::addressof(bytes_read))); + R_UNLESS(magic == XCI_MAGIC, 0x1); + R_SUCCEED(); +} + +Result Xci::GetCollections(Collections& out) { + Hfs0 root{}; + R_TRY(Hfs0GetPartition(m_source, HFS0_HEADER_OFFSET, root)); + + for (u32 i = 0; i < root.header.total_files; i++) { + if (root.string_table[i] == "secure") { + Hfs0 secure{}; + R_TRY(Hfs0GetPartition(m_source, root.data_offset + root.file_table[i].data_offset, secure)); + + for (u32 i = 0; i < secure.header.total_files; i++) { + CollectionEntry entry; + entry.name = secure.string_table[i]; + entry.offset = secure.data_offset + secure.file_table[i].data_offset; + entry.size = secure.file_table[i].data_size; + out.emplace_back(entry); + } + + R_SUCCEED(); + } + } + + return 0x1; +} + +} // namespace sphaira::yati::container diff --git a/sphaira/source/yati/nx/es.cpp b/sphaira/source/yati/nx/es.cpp new file mode 100644 index 0000000..6fb5027 --- /dev/null +++ b/sphaira/source/yati/nx/es.cpp @@ -0,0 +1,122 @@ +#include "yati/nx/es.hpp" +#include "yati/nx/crypto.hpp" +#include "yati/nx/nxdumptool_rsa.h" +#include "defines.hpp" +#include "log.hpp" +#include +#include + +namespace sphaira::es { +namespace { + +} // namespace + +Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size) { + return serviceDispatch(srv, 1, + .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In, SfBufferAttr_HipcMapAlias | SfBufferAttr_In }, + .buffers = { { tik_buf, tik_size }, { cert_buf, cert_size } }); +} + +typedef enum { + TikPropertyMask_None = 0, + TikPropertyMask_PreInstallation = BIT(0), ///< Determines if the title comes pre-installed on the device. Most likely unused -- a remnant from previous ticket formats. + TikPropertyMask_SharedTitle = BIT(1), ///< Determines if the title holds shared contents only. Most likely unused -- a remnant from previous ticket formats. + TikPropertyMask_AllContents = BIT(2), ///< Determines if the content index mask shall be bypassed. Most likely unused -- a remnant from previous ticket formats. + TikPropertyMask_DeviceLinkIndepedent = BIT(3), ///< Determines if the console should *not* connect to the Internet to verify if the title's being used by the primary console. + TikPropertyMask_Volatile = BIT(4), ///< Determines if the ticket copy inside ticket.bin is available after reboot. Can be encrypted. + TikPropertyMask_ELicenseRequired = BIT(5), ///< Determines if the console should connect to the Internet to perform license verification. + TikPropertyMask_Count = 6 ///< Total values supported by this enum. +} TikPropertyMask; + +Result GetTicketDataOffset(std::span ticket, u64& out) { + log_write("inside es\n"); + u32 signature_type; + std::memcpy(std::addressof(signature_type), ticket.data(), sizeof(signature_type)); + + u32 signature_size; + switch (signature_type) { + case es::TicketSigantureType_RSA_4096_SHA1: log_write("RSA-4096 PKCS#1 v1.5 with SHA-1\n"); signature_size = 0x200; break; + case es::TicketSigantureType_RSA_2048_SHA1: log_write("RSA-2048 PKCS#1 v1.5 with SHA-1\n"); signature_size = 0x100; break; + case es::TicketSigantureType_ECDSA_SHA1: log_write("ECDSA with SHA-1\n"); signature_size = 0x3C; break; + case es::TicketSigantureType_RSA_4096_SHA256: log_write("RSA-4096 PKCS#1 v1.5 with SHA-256\n"); signature_size = 0x200; break; + case es::TicketSigantureType_RSA_2048_SHA256: log_write("RSA-2048 PKCS#1 v1.5 with SHA-256\n"); signature_size = 0x100; break; + case es::TicketSigantureType_ECDSA_SHA256: log_write("ECDSA with SHA-256\n"); signature_size = 0x3C; break; + case es::TicketSigantureType_HMAC_SHA1_160: log_write("HMAC-SHA1-160\n"); signature_size = 0x14; break; + default: log_write("unknown ticket\n"); return 0x1; + } + + // align-up to 0x40. + out = ((signature_size + sizeof(signature_type)) + 0x3F) & ~0x3F; + R_SUCCEED(); +} + +Result GetTicketData(std::span ticket, es::TicketData* out) { + u64 data_off; + R_TRY(GetTicketDataOffset(ticket, data_off)); + std::memcpy(out, ticket.data() + data_off, sizeof(*out)); + + // validate ticket data. + R_UNLESS(out->ticket_version1 == 0x2, Result_InvalidTicketVersion); // must be version 2. + R_UNLESS(out->title_key_type == es::TicketTitleKeyType_Common || out->title_key_type == es::TicketTitleKeyType_Personalized, Result_InvalidTicketKeyType); + R_UNLESS(out->master_key_revision <= 0x20, Result_InvalidTicketKeyRevision); + + R_SUCCEED(); +} + +Result SetTicketData(std::span ticket, const es::TicketData* in) { + u64 data_off; + R_TRY(GetTicketDataOffset(ticket, data_off)); + std::memcpy(ticket.data() + data_off, in, sizeof(*in)); + R_SUCCEED(); +} + +Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys) { + if (data.title_key_type == es::TicketTitleKeyType_Common) { + std::memcpy(std::addressof(out), data.title_key_block, sizeof(out)); + } else if (data.title_key_type == es::TicketTitleKeyType_Personalized) { + auto rsa_key = (const es::EticketRsaDeviceKey*)keys.eticket_device_key.key; + log_write("personalised ticket\n"); + log_write("master_key_revision: %u\n", data.master_key_revision); + log_write("license_type: %u\n", data.license_type); + log_write("properties_bitfield: 0x%X\n", data.properties_bitfield); + log_write("device_id: 0x%lX vs 0x%lX\n", data.device_id, std::byteswap(rsa_key->device_id)); + + R_UNLESS(data.device_id == std::byteswap(rsa_key->device_id), 0x1); + log_write("device id is same\n"); + + u8 out_keydata[RSA2048_BYTES]{}; + size_t out_keydata_size; + R_UNLESS(rsa2048OaepDecrypt(out_keydata, sizeof(out_keydata), data.title_key_block, rsa_key->modulus, &rsa_key->public_exponent, sizeof(rsa_key->public_exponent), rsa_key->private_exponent, sizeof(rsa_key->private_exponent), NULL, 0, &out_keydata_size), 0x1); + R_UNLESS(out_keydata_size >= sizeof(out), 0x1); + std::memcpy(std::addressof(out), out_keydata, sizeof(out)); + } else { + R_THROW(0x1); + } + + R_SUCCEED(); +} + +Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys) { + keys::KeyEntry title_kek; + R_TRY(keys.GetTitleKek(std::addressof(title_kek), key_gen)); + crypto::cryptoAes128(std::addressof(out), std::addressof(out), std::addressof(title_kek), false); + + R_SUCCEED(); +} + +// todo: i thought i already wrote the code for this?? +// todo: patch the ticket. +Result PatchTicket(std::span ticket, const keys::Keys& keys, bool convert_personalised) { + TicketData data; + R_TRY(GetTicketData(ticket, &data)); + + if (data.title_key_type == es::TicketTitleKeyType_Common) { + // todo: verify common signature + } else if (data.title_key_type == es::TicketTitleKeyType_Personalized && convert_personalised) { + + } + + R_SUCCEED(); +} + +} // namespace sphaira::es diff --git a/sphaira/source/yati/nx/keys.cpp b/sphaira/source/yati/nx/keys.cpp new file mode 100644 index 0000000..cc53200 --- /dev/null +++ b/sphaira/source/yati/nx/keys.cpp @@ -0,0 +1,130 @@ +#include "yati/nx/keys.hpp" +#include "yati/nx/nca.hpp" +#include "yati/nx/es.hpp" +#include "yati/nx/crypto.hpp" +#include "defines.hpp" +#include "log.hpp" +#include +#include +#include +#include + +namespace sphaira::keys { +namespace { + +constexpr u8 HEADER_KEK_SRC[0x10] = { + 0x1F, 0x12, 0x91, 0x3A, 0x4A, 0xCB, 0xF0, 0x0D, 0x4C, 0xDE, 0x3A, 0xF6, 0xD5, 0x23, 0x88, 0x2A +}; + +constexpr u8 HEADER_KEY_SRC[0x20] = { + 0x5A, 0x3E, 0xD8, 0x4F, 0xDE, 0xC0, 0xD8, 0x26, 0x31, 0xF7, 0xE2, 0x5D, 0x19, 0x7B, 0xF5, 0xD0, + 0x1C, 0x9B, 0x7B, 0xFA, 0xF6, 0x28, 0x18, 0x3D, 0x71, 0xF6, 0x4D, 0x73, 0xF1, 0x50, 0xB9, 0xD2 +}; + +} // namespace + +void parse_hex_key(void* key, const char* hex) { + char low[0x11]{}; + char upp[0x11]{}; + std::memcpy(low, hex, 0x10); + std::memcpy(upp, hex + 0x10, 0x10); + *(u64*)key = std::byteswap(std::strtoul(low, nullptr, 0x10)); + *(u64*)((u8*)key + 8) = std::byteswap(std::strtoul(upp, nullptr, 0x10)); +} + +Result parse_keys(Keys& out, bool read_from_file) { + static constexpr auto find_key = [](const char* key, const char* value, const char* search_key, KeySection& key_section) -> bool { + if (!std::strncmp(key, search_key, std::strlen(search_key))) { + // get key index. + char* end; + const auto key_value_str = key + std::strlen(search_key); + const auto index = std::strtoul(key_value_str, &end, 0x10); + if (end && end != key_value_str && index < 0x20) { + KeyEntry keak; + parse_hex_key(std::addressof(keak), value); + key_section[index] = keak; + return true; + } + } + + return false; + }; + + static constexpr auto find_key_single = [](const char* key, const char* value, const char* search_key, KeyEntry& key_entry) -> bool { + if (!std::strcmp(key, search_key)) { + parse_hex_key(std::addressof(key_entry), value); + return true; + } + + return false; + }; + + static constexpr auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int { + auto keys = static_cast(UserData); + + auto key_text_key_area_key_app = "key_area_key_application_"; + auto key_text_key_area_key_oce = "key_area_key_ocean_"; + auto key_text_key_area_key_sys = "key_area_key_system_"; + auto key_text_titlekek = "titlekek_"; + auto key_text_master_key = "master_key_"; + auto key_text_eticket_rsa_kek = keys->eticket_device_key.generation ? "eticket_rsa_kek_personalized" : "eticket_rsa_kek"; + + if (find_key(Key, Value, key_text_key_area_key_app, keys->key_area_key[nca::KeyAreaEncryptionKeyIndex_Application])) { + return 1; + } else if (find_key(Key, Value, key_text_key_area_key_oce, keys->key_area_key[nca::KeyAreaEncryptionKeyIndex_Ocean])) { + return 1; + } else if (find_key(Key, Value, key_text_key_area_key_sys, keys->key_area_key[nca::KeyAreaEncryptionKeyIndex_System])) { + return 1; + } else if (find_key(Key, Value, key_text_titlekek, keys->titlekek)) { + return 1; + } else if (find_key(Key, Value, key_text_master_key, keys->master_key)) { + return 1; + } else if (find_key_single(Key, Value, key_text_eticket_rsa_kek, keys->eticket_rsa_kek)) { + log_write("found key single: key: %s value %s\n", Key, Value); + return 1; + } + + return 1; + }; + + R_TRY(splCryptoInitialize()); + ON_SCOPE_EXIT(splCryptoExit()); + + u8 header_kek[0x20]; + R_TRY(splCryptoGenerateAesKek(HEADER_KEK_SRC, 0, 0, header_kek)); + R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC, out.header_key)); + R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC + 0x10, out.header_key + 0x10)); + + if (read_from_file) { + // get eticket device key, needed for decrypting personalised tickets. + R_TRY(setcalInitialize()); + ON_SCOPE_EXIT(setcalExit()); + R_TRY(setcalGetEticketDeviceKey(std::addressof(out.eticket_device_key))); + + R_UNLESS(ini_browse(cb, std::addressof(out), "/switch/prod.keys"), 0x1); + + // decrypt eticket device key. + if (out.eticket_rsa_kek.IsValid()) { + auto rsa_key = (es::EticketRsaDeviceKey*)out.eticket_device_key.key; + + Aes128CtrContext eticket_aes_ctx{}; + aes128CtrContextCreate(&eticket_aes_ctx, &out.eticket_rsa_kek, rsa_key->ctr); + aes128CtrCrypt(&eticket_aes_ctx, &(rsa_key->private_exponent), &(rsa_key->private_exponent), sizeof(es::EticketRsaDeviceKey) - sizeof(rsa_key->ctr)); + + const auto public_exponent = std::byteswap(rsa_key->public_exponent); + if (public_exponent != 0x10001) { + log_write("etick decryption fail: 0x%X\n", public_exponent); + if (public_exponent == 0) { + log_write("eticket device id is NULL\n"); + } + R_THROW(0x1); + } else { + log_write("eticket match\n"); + } + } + } + + R_SUCCEED(); +} + +} // namespace sphaira::keys diff --git a/sphaira/source/yati/nx/nca.cpp b/sphaira/source/yati/nx/nca.cpp new file mode 100644 index 0000000..9ba80e6 --- /dev/null +++ b/sphaira/source/yati/nx/nca.cpp @@ -0,0 +1,154 @@ +#include "yati/nx/nca.hpp" +#include "yati/nx/crypto.hpp" +#include "yati/nx/nxdumptool_rsa.h" +#include "log.hpp" + +namespace sphaira::nca { +namespace { + +constexpr u8 g_key_area_key_application_source[0x10] = { 0x7F, 0x59, 0x97, 0x1E, 0x62, 0x9F, 0x36, 0xA1, 0x30, 0x98, 0x06, 0x6F, 0x21, 0x44, 0xC3, 0x0D }; +constexpr u8 g_key_area_key_ocean_source[0x10] = { 0x32, 0x7D, 0x36, 0x08, 0x5A, 0xD1, 0x75, 0x8D, 0xAB, 0x4E, 0x6F, 0xBA, 0xA5, 0x55, 0xD8, 0x82 }; +constexpr u8 g_key_area_key_system_source[0x10] = { 0x87, 0x45, 0xF1, 0xBB, 0xA6, 0xBE, 0x79, 0x64, 0x7D, 0x04, 0x8B, 0xA6, 0x7B, 0x5F, 0xDA, 0x4A }; + +constexpr const u8* g_key_area_key[] = { + g_key_area_key_application_source, + g_key_area_key_ocean_source, + g_key_area_key_system_source +}; + +const unsigned char nca_hdr_fixed_key_moduli_retail[0x2][0x100] = { /* Fixed RSA key used to validate NCA signature 0. */ + { + 0xBF, 0xBE, 0x40, 0x6C, 0xF4, 0xA7, 0x80, 0xE9, 0xF0, 0x7D, 0x0C, 0x99, 0x61, 0x1D, 0x77, 0x2F, + 0x96, 0xBC, 0x4B, 0x9E, 0x58, 0x38, 0x1B, 0x03, 0xAB, 0xB1, 0x75, 0x49, 0x9F, 0x2B, 0x4D, 0x58, + 0x34, 0xB0, 0x05, 0xA3, 0x75, 0x22, 0xBE, 0x1A, 0x3F, 0x03, 0x73, 0xAC, 0x70, 0x68, 0xD1, 0x16, + 0xB9, 0x04, 0x46, 0x5E, 0xB7, 0x07, 0x91, 0x2F, 0x07, 0x8B, 0x26, 0xDE, 0xF6, 0x00, 0x07, 0xB2, + 0xB4, 0x51, 0xF8, 0x0D, 0x0A, 0x5E, 0x58, 0xAD, 0xEB, 0xBC, 0x9A, 0xD6, 0x49, 0xB9, 0x64, 0xEF, + 0xA7, 0x82, 0xB5, 0xCF, 0x6D, 0x70, 0x13, 0xB0, 0x0F, 0x85, 0xF6, 0xA9, 0x08, 0xAA, 0x4D, 0x67, + 0x66, 0x87, 0xFA, 0x89, 0xFF, 0x75, 0x90, 0x18, 0x1E, 0x6B, 0x3D, 0xE9, 0x8A, 0x68, 0xC9, 0x26, + 0x04, 0xD9, 0x80, 0xCE, 0x3F, 0x5E, 0x92, 0xCE, 0x01, 0xFF, 0x06, 0x3B, 0xF2, 0xC1, 0xA9, 0x0C, + 0xCE, 0x02, 0x6F, 0x16, 0xBC, 0x92, 0x42, 0x0A, 0x41, 0x64, 0xCD, 0x52, 0xB6, 0x34, 0x4D, 0xAE, + 0xC0, 0x2E, 0xDE, 0xA4, 0xDF, 0x27, 0x68, 0x3C, 0xC1, 0xA0, 0x60, 0xAD, 0x43, 0xF3, 0xFC, 0x86, + 0xC1, 0x3E, 0x6C, 0x46, 0xF7, 0x7C, 0x29, 0x9F, 0xFA, 0xFD, 0xF0, 0xE3, 0xCE, 0x64, 0xE7, 0x35, + 0xF2, 0xF6, 0x56, 0x56, 0x6F, 0x6D, 0xF1, 0xE2, 0x42, 0xB0, 0x83, 0x40, 0xA5, 0xC3, 0x20, 0x2B, + 0xCC, 0x9A, 0xAE, 0xCA, 0xED, 0x4D, 0x70, 0x30, 0xA8, 0x70, 0x1C, 0x70, 0xFD, 0x13, 0x63, 0x29, + 0x02, 0x79, 0xEA, 0xD2, 0xA7, 0xAF, 0x35, 0x28, 0x32, 0x1C, 0x7B, 0xE6, 0x2F, 0x1A, 0xAA, 0x40, + 0x7E, 0x32, 0x8C, 0x27, 0x42, 0xFE, 0x82, 0x78, 0xEC, 0x0D, 0xEB, 0xE6, 0x83, 0x4B, 0x6D, 0x81, + 0x04, 0x40, 0x1A, 0x9E, 0x9A, 0x67, 0xF6, 0x72, 0x29, 0xFA, 0x04, 0xF0, 0x9D, 0xE4, 0xF4, 0x03, + }, + { + 0xAD, 0xE3, 0xE1, 0xFA, 0x04, 0x35, 0xE5, 0xB6, 0xDD, 0x49, 0xEA, 0x89, 0x29, 0xB1, 0xFF, 0xB6, + 0x43, 0xDF, 0xCA, 0x96, 0xA0, 0x4A, 0x13, 0xDF, 0x43, 0xD9, 0x94, 0x97, 0x96, 0x43, 0x65, 0x48, + 0x70, 0x58, 0x33, 0xA2, 0x7D, 0x35, 0x7B, 0x96, 0x74, 0x5E, 0x0B, 0x5C, 0x32, 0x18, 0x14, 0x24, + 0xC2, 0x58, 0xB3, 0x6C, 0x22, 0x7A, 0xA1, 0xB7, 0xCB, 0x90, 0xA7, 0xA3, 0xF9, 0x7D, 0x45, 0x16, + 0xA5, 0xC8, 0xED, 0x8F, 0xAD, 0x39, 0x5E, 0x9E, 0x4B, 0x51, 0x68, 0x7D, 0xF8, 0x0C, 0x35, 0xC6, + 0x3F, 0x91, 0xAE, 0x44, 0xA5, 0x92, 0x30, 0x0D, 0x46, 0xF8, 0x40, 0xFF, 0xD0, 0xFF, 0x06, 0xD2, + 0x1C, 0x7F, 0x96, 0x18, 0xDC, 0xB7, 0x1D, 0x66, 0x3E, 0xD1, 0x73, 0xBC, 0x15, 0x8A, 0x2F, 0x94, + 0xF3, 0x00, 0xC1, 0x83, 0xF1, 0xCD, 0xD7, 0x81, 0x88, 0xAB, 0xDF, 0x8C, 0xEF, 0x97, 0xDD, 0x1B, + 0x17, 0x5F, 0x58, 0xF6, 0x9A, 0xE9, 0xE8, 0xC2, 0x2F, 0x38, 0x15, 0xF5, 0x21, 0x07, 0xF8, 0x37, + 0x90, 0x5D, 0x2E, 0x02, 0x40, 0x24, 0x15, 0x0D, 0x25, 0xB7, 0x26, 0x5D, 0x09, 0xCC, 0x4C, 0xF4, + 0xF2, 0x1B, 0x94, 0x70, 0x5A, 0x9E, 0xEE, 0xED, 0x77, 0x77, 0xD4, 0x51, 0x99, 0xF5, 0xDC, 0x76, + 0x1E, 0xE3, 0x6C, 0x8C, 0xD1, 0x12, 0xD4, 0x57, 0xD1, 0xB6, 0x83, 0xE4, 0xE4, 0xFE, 0xDA, 0xE9, + 0xB4, 0x3B, 0x33, 0xE5, 0x37, 0x8A, 0xDF, 0xB5, 0x7F, 0x89, 0xF1, 0x9B, 0x9E, 0xB0, 0x15, 0xB2, + 0x3A, 0xFE, 0xEA, 0x61, 0x84, 0x5B, 0x7D, 0x4B, 0x23, 0x12, 0x0B, 0x83, 0x12, 0xF2, 0x22, 0x6B, + 0xB9, 0x22, 0x96, 0x4B, 0x26, 0x0B, 0x63, 0x5E, 0x96, 0x57, 0x52, 0xA3, 0x67, 0x64, 0x22, 0xCA, + 0xD0, 0x56, 0x3E, 0x74, 0xB5, 0x98, 0x1F, 0x0D, 0xF8, 0xB3, 0x34, 0xE6, 0x98, 0x68, 0x5A, 0xAD, + } +}; + +const unsigned char acid_fixed_key_moduli_retail[0x2][0x100] = { /* Fixed RSA keys used to validate ACID signatures. */ + { + 0xDD, 0xC8, 0xDD, 0xF2, 0x4E, 0x6D, 0xF0, 0xCA, 0x9E, 0xC7, 0x5D, 0xC7, 0x7B, 0xAD, 0xFE, 0x7D, + 0x23, 0x89, 0x69, 0xB6, 0xF2, 0x06, 0xA2, 0x02, 0x88, 0xE1, 0x55, 0x91, 0xAB, 0xCB, 0x4D, 0x50, + 0x2E, 0xFC, 0x9D, 0x94, 0x76, 0xD6, 0x4C, 0xD8, 0xFF, 0x10, 0xFA, 0x5E, 0x93, 0x0A, 0xB4, 0x57, + 0xAC, 0x51, 0xC7, 0x16, 0x66, 0xF4, 0x1A, 0x54, 0xC2, 0xC5, 0x04, 0x3D, 0x1B, 0xFE, 0x30, 0x20, + 0x8A, 0xAC, 0x6F, 0x6F, 0xF5, 0xC7, 0xB6, 0x68, 0xB8, 0xC9, 0x40, 0x6B, 0x42, 0xAD, 0x11, 0x21, + 0xE7, 0x8B, 0xE9, 0x75, 0x01, 0x86, 0xE4, 0x48, 0x9B, 0x0A, 0x0A, 0xF8, 0x7F, 0xE8, 0x87, 0xF2, + 0x82, 0x01, 0xE6, 0xA3, 0x0F, 0xE4, 0x66, 0xAE, 0x83, 0x3F, 0x4E, 0x9F, 0x5E, 0x01, 0x30, 0xA4, + 0x00, 0xB9, 0x9A, 0xAE, 0x5F, 0x03, 0xCC, 0x18, 0x60, 0xE5, 0xEF, 0x3B, 0x5E, 0x15, 0x16, 0xFE, + 0x1C, 0x82, 0x78, 0xB5, 0x2F, 0x47, 0x7C, 0x06, 0x66, 0x88, 0x5D, 0x35, 0xA2, 0x67, 0x20, 0x10, + 0xE7, 0x6C, 0x43, 0x68, 0xD3, 0xE4, 0x5A, 0x68, 0x2A, 0x5A, 0xE2, 0x6D, 0x73, 0xB0, 0x31, 0x53, + 0x1C, 0x20, 0x09, 0x44, 0xF5, 0x1A, 0x9D, 0x22, 0xBE, 0x12, 0xA1, 0x77, 0x11, 0xE2, 0xA1, 0xCD, + 0x40, 0x9A, 0xA2, 0x8B, 0x60, 0x9B, 0xEF, 0xA0, 0xD3, 0x48, 0x63, 0xA2, 0xF8, 0xA3, 0x2C, 0x08, + 0x56, 0x52, 0x2E, 0x60, 0x19, 0x67, 0x5A, 0xA7, 0x9F, 0xDC, 0x3F, 0x3F, 0x69, 0x2B, 0x31, 0x6A, + 0xB7, 0x88, 0x4A, 0x14, 0x84, 0x80, 0x33, 0x3C, 0x9D, 0x44, 0xB7, 0x3F, 0x4C, 0xE1, 0x75, 0xEA, + 0x37, 0xEA, 0xE8, 0x1E, 0x7C, 0x77, 0xB7, 0xC6, 0x1A, 0xA2, 0xF0, 0x9F, 0x10, 0x61, 0xCD, 0x7B, + 0x5B, 0x32, 0x4C, 0x37, 0xEF, 0xB1, 0x71, 0x68, 0x53, 0x0A, 0xED, 0x51, 0x7D, 0x35, 0x22, 0xFD, + }, + { + 0xE7, 0xAA, 0x25, 0xC8, 0x01, 0xA5, 0x14, 0x6B, 0x01, 0x60, 0x3E, 0xD9, 0x96, 0x5A, 0xBF, 0x90, + 0xAC, 0xA7, 0xFD, 0x9B, 0x5B, 0xBD, 0x8A, 0x26, 0xB0, 0xCB, 0x20, 0x28, 0x9A, 0x72, 0x12, 0xF5, + 0x20, 0x65, 0xB3, 0xB9, 0x84, 0x58, 0x1F, 0x27, 0xBC, 0x7C, 0xA2, 0xC9, 0x9E, 0x18, 0x95, 0xCF, + 0xC2, 0x73, 0x2E, 0x74, 0x8C, 0x66, 0xE5, 0x9E, 0x79, 0x2B, 0xB8, 0x07, 0x0C, 0xB0, 0x4E, 0x8E, + 0xAB, 0x85, 0x21, 0x42, 0xC4, 0xC5, 0x6D, 0x88, 0x9C, 0xDB, 0x15, 0x95, 0x3F, 0x80, 0xDB, 0x7A, + 0x9A, 0x7D, 0x41, 0x56, 0x25, 0x17, 0x18, 0x42, 0x4D, 0x8C, 0xAC, 0xA5, 0x7B, 0xDB, 0x42, 0x5D, + 0x59, 0x35, 0x45, 0x5D, 0x8A, 0x02, 0xB5, 0x70, 0xC0, 0x72, 0x35, 0x46, 0xD0, 0x1D, 0x60, 0x01, + 0x4A, 0xCC, 0x1C, 0x46, 0xD3, 0xD6, 0x35, 0x52, 0xD6, 0xE1, 0xF8, 0x3B, 0x5D, 0xEA, 0xDD, 0xB8, + 0xFE, 0x7D, 0x50, 0xCB, 0x35, 0x23, 0x67, 0x8B, 0xB6, 0xE4, 0x74, 0xD2, 0x60, 0xFC, 0xFD, 0x43, + 0xBF, 0x91, 0x08, 0x81, 0xC5, 0x4F, 0x5D, 0x16, 0x9A, 0xC4, 0x9A, 0xC6, 0xF6, 0xF3, 0xE1, 0xF6, + 0x5C, 0x07, 0xAA, 0x71, 0x6C, 0x13, 0xA4, 0xB1, 0xB3, 0x66, 0xBF, 0x90, 0x4C, 0x3D, 0xA2, 0xC4, + 0x0B, 0xB8, 0x3D, 0x7A, 0x8C, 0x19, 0xFA, 0xFF, 0x6B, 0xB9, 0x1F, 0x02, 0xCC, 0xB6, 0xD3, 0x0C, + 0x7D, 0x19, 0x1F, 0x47, 0xF9, 0xC7, 0x40, 0x01, 0xFA, 0x46, 0xEA, 0x0B, 0xD4, 0x02, 0xE0, 0x3D, + 0x30, 0x9A, 0x1A, 0x0F, 0xEA, 0xA7, 0x66, 0x55, 0xF7, 0xCB, 0x28, 0xE2, 0xBB, 0x99, 0xE4, 0x83, + 0xC3, 0x43, 0x03, 0xEE, 0xDC, 0x1F, 0x02, 0x23, 0xDD, 0xD1, 0x2D, 0x39, 0xA4, 0x65, 0x75, 0x03, + 0xEF, 0x37, 0x9C, 0x06, 0xD6, 0xFA, 0xA1, 0x15, 0xF0, 0xDB, 0x17, 0x47, 0x26, 0x4F, 0x49, 0x03 + } +}; + +} // namespace + +Result DecryptKeak(const keys::Keys& keys, Header& header) { + const auto key_generation = header.GetKeyGeneration(); + + // try with spl. + keys::KeyEntry keak; + if (R_SUCCEEDED(splCryptoGenerateAesKek(g_key_area_key[header.kaek_index], key_generation, 0, &keak))) { + for (auto& key_area : header.key_area) { + R_TRY(splCryptoGenerateAesKey(&keak, std::addressof(key_area), std::addressof(key_area))); + } + } else { + // failed with spl, try using keys. + R_TRY(keys.GetNcaKeyArea(&keak, key_generation, header.kaek_index)); + for (auto& key_area : header.key_area) { + crypto::cryptoAes128(std::addressof(key_area), std::addressof(key_area), std::addressof(keak), false); + } + } + + R_SUCCEED(); +} + +Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation) { + header.SetKeyGeneration(key_generation); + + keys::KeyEntry keak; + R_TRY(keys.GetNcaKeyArea(&keak, key_generation, header.kaek_index)); + log_write("re-encrypting with: 0x%X\n", key_generation); + + for (auto& key_area : header.key_area) { + crypto::cryptoAes128(std::addressof(key_area), std::addressof(key_area), std::addressof(keak), true); + } + + R_SUCCEED(); +} + +Result VerifyFixedKey(const Header& header) { + R_UNLESS(header.sig_key_gen < std::size(nca_hdr_fixed_key_moduli_retail), 0x1); + auto mod = nca_hdr_fixed_key_moduli_retail[header.sig_key_gen]; + + const u8 E[3] = { 1, 0, 1 }; + if (!rsa2048VerifySha256BasedPssSignature(&header.magic, 0x200, header.rsa_fixed_key, mod, E, sizeof(E))) { + auto new_header = header; + // if failed, detect if this is a eshop/xci convert. + new_header.distribution_type ^= 1; + if (!rsa2048VerifySha256BasedPssSignature(&new_header.magic, 0x200, new_header.rsa_fixed_key, mod, E, sizeof(E))) { + log_write("FAILED nca header hash\n"); + R_THROW(0x1); + } else { + log_write("WARNING! nca is converted! distribution_type: %u\n", new_header.distribution_type); + R_SUCCEED(); + } + } + + R_SUCCEED(); +} + +} // namespace sphaira::nca diff --git a/sphaira/source/yati/nx/ncm.cpp b/sphaira/source/yati/nx/ncm.cpp new file mode 100644 index 0000000..a338816 --- /dev/null +++ b/sphaira/source/yati/nx/ncm.cpp @@ -0,0 +1,34 @@ +#include "yati/nx/ncm.hpp" +#include "defines.hpp" +#include + +namespace sphaira::ncm { +namespace { + +} // namespace + +auto GetAppId(const NcmContentMetaKey& key) -> u64 { + if (key.type == NcmContentMetaType_Patch) { + return key.id ^ 0x800; + } else if (key.type == NcmContentMetaType_AddOnContent) { + return (key.id ^ 0x1000) & ~0xFFF; + } else { + return key.id; + } +} + +Result Delete(NcmContentStorage* cs, const NcmContentId *content_id) { + bool has; + R_TRY(ncmContentStorageHas(cs, std::addressof(has), content_id)); + if (has) { + R_TRY(ncmContentStorageDelete(cs, content_id)); + } + R_SUCCEED(); +} + +Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id) { + R_TRY(Delete(cs, content_id)); + return ncmContentStorageRegister(cs, content_id, placeholder_id); +} + +} // namespace sphaira::ncm diff --git a/sphaira/source/yati/nx/ns.cpp b/sphaira/source/yati/nx/ns.cpp new file mode 100644 index 0000000..ecd88c3 --- /dev/null +++ b/sphaira/source/yati/nx/ns.cpp @@ -0,0 +1,39 @@ +#include "yati/nx/ns.hpp" + +namespace sphaira::ns { +namespace { + +} // namespace + +Result PushApplicationRecord(Service* srv, u64 tid, const ncm::ContentStorageRecord* records, u32 count) { + const struct { + u8 last_modified_event; + u8 padding[0x7]; + u64 tid; + } in = { ApplicationRecordType_Installed, {0}, tid }; + + return serviceDispatchIn(srv, 16, in, + .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In }, + .buffers = { { records, sizeof(*records) * count } }); +} + +Result ListApplicationRecordContentMeta(Service* srv, u64 offset, u64 tid, ncm::ContentStorageRecord* out_records, u32 count, s32* entries_read) { + struct { + u64 offset; + u64 tid; + } in = { offset, tid }; + + return serviceDispatchInOut(srv, 17, in, *entries_read, + .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out }, + .buffers = { { out_records, sizeof(*out_records) * count } }); +} + +Result DeleteApplicationRecord(Service* srv, u64 tid) { + return serviceDispatchIn(srv, 27, tid); +} + +Result InvalidateApplicationControlCache(Service* srv, u64 tid) { + return serviceDispatchIn(srv, 404, tid); +} + +} // namespace sphaira::ns diff --git a/sphaira/source/yati/nx/nxdumptool_rsa.c b/sphaira/source/yati/nx/nxdumptool_rsa.c new file mode 100644 index 0000000..75121e6 --- /dev/null +++ b/sphaira/source/yati/nx/nxdumptool_rsa.c @@ -0,0 +1,158 @@ +/* + * rsa.c + * + * Copyright (c) 2018-2019, SciresM. + * Copyright (c) 2020-2024, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * nxdumptool is distributed in the hope that 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 "yati/nx/nxdumptool_rsa.h" +#include "log.hpp" +#include +#include + +#include +#include +#include +#include + +#define LOG_MSG_ERROR(...) log_write(__VA_ARGS__) + +/* Function prototypes. */ + +static bool rsa2048VerifySha256BasedSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, \ + bool use_pss); + +bool rsa2048VerifySha256BasedPssSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size) +{ + return rsa2048VerifySha256BasedSignature(data, data_size, signature, modulus, public_exponent, public_exponent_size, true); +} + +bool rsa2048VerifySha256BasedPkcs1v15Signature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size) +{ + return rsa2048VerifySha256BasedSignature(data, data_size, signature, modulus, public_exponent, public_exponent_size, false); +} + +bool rsa2048OaepDecrypt(void *dst, size_t dst_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, const void *private_exponent, \ + size_t private_exponent_size, const void *label, size_t label_size, size_t *out_size) +{ + if (!dst || !dst_size || !signature || !modulus || !public_exponent || !public_exponent_size || !private_exponent || !private_exponent_size || (!label && label_size) || (label && !label_size) || \ + !out_size) + { + LOG_MSG_ERROR("Invalid parameters!"); + return false; + } + + mbedtls_entropy_context entropy = {0}; + mbedtls_ctr_drbg_context ctr_drbg = {0}; + mbedtls_rsa_context rsa = {0}; + + const char *pers = __func__; + int mbedtls_ret = 0; + bool ret = false; + + /* Initialize contexts. */ + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&ctr_drbg); + mbedtls_rsa_init(&rsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256); + + /* Seed the random number generator. */ + mbedtls_ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const u8*)pers, strlen(pers)); + if (mbedtls_ret != 0) + { + LOG_MSG_ERROR("mbedtls_ctr_drbg_seed failed! (%d).", mbedtls_ret); + goto end; + } + + /* Import RSA parameters. */ + mbedtls_ret = mbedtls_rsa_import_raw(&rsa, (const u8*)modulus, RSA2048_BYTES, NULL, 0, NULL, 0, (const u8*)private_exponent, private_exponent_size, (const u8*)public_exponent, public_exponent_size); + if (mbedtls_ret != 0) + { + LOG_MSG_ERROR("mbedtls_rsa_import_raw failed! (%d).", mbedtls_ret); + goto end; + } + + /* Derive RSA prime factors. */ + mbedtls_ret = mbedtls_rsa_complete(&rsa); + if (mbedtls_ret != 0) + { + LOG_MSG_ERROR("mbedtls_rsa_complete failed! (%d).", mbedtls_ret); + goto end; + } + + /* Perform RSA-OAEP decryption. */ + mbedtls_ret = mbedtls_rsa_rsaes_oaep_decrypt(&rsa, mbedtls_ctr_drbg_random, &ctr_drbg, MBEDTLS_RSA_PRIVATE, (const u8*)label, label_size, out_size, (const u8*)signature, (u8*)dst, dst_size); + if (mbedtls_ret != 0) + { + LOG_MSG_ERROR("mbedtls_rsa_rsaes_oaep_decrypt failed! (%d).", mbedtls_ret); + goto end; + } + + ret = true; + +end: + mbedtls_rsa_free(&rsa); + mbedtls_ctr_drbg_free(&ctr_drbg); + mbedtls_entropy_free(&entropy); + + return ret; +} + +static bool rsa2048VerifySha256BasedSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, \ + bool use_pss) +{ + if (!data || !data_size || !signature || !modulus || !public_exponent || !public_exponent_size) + { + LOG_MSG_ERROR("Invalid parameters!"); + return false; + } + + int mbedtls_ret = 0; + mbedtls_rsa_context rsa = {0}; + u8 hash[SHA256_HASH_SIZE] = {0}; + bool ret = false; + + /* Initialize RSA context. */ + mbedtls_rsa_init(&rsa, use_pss ? MBEDTLS_RSA_PKCS_V21 : MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_SHA256); + + /* Import RSA parameters. */ + mbedtls_ret = mbedtls_rsa_import_raw(&rsa, (const u8*)modulus, RSA2048_BYTES, NULL, 0, NULL, 0, NULL, 0, (const u8*)public_exponent, public_exponent_size); + if (mbedtls_ret != 0) + { + LOG_MSG_ERROR("mbedtls_rsa_import_raw failed! (%d).", mbedtls_ret); + goto end; + } + + /* Calculate SHA-256 checksum for the input data. */ + sha256CalculateHash(hash, data, data_size); + + /* Verify signature. */ + mbedtls_ret = (use_pss ? mbedtls_rsa_rsassa_pss_verify(&rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, SHA256_HASH_SIZE, hash, (const u8*)signature) : \ + mbedtls_rsa_rsassa_pkcs1_v15_verify(&rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, SHA256_HASH_SIZE, hash, (const u8*)signature)); + if (mbedtls_ret != 0) + { + LOG_MSG_ERROR("mbedtls_rsa_rsassa_%s_verify failed! (%d).", use_pss ? "pss" : "pkcs1_v15", mbedtls_ret); + goto end; + } + + ret = true; + +end: + mbedtls_rsa_free(&rsa); + + return ret; +} diff --git a/sphaira/source/yati/source/file.cpp b/sphaira/source/yati/source/file.cpp new file mode 100644 index 0000000..100c5ef --- /dev/null +++ b/sphaira/source/yati/source/file.cpp @@ -0,0 +1,20 @@ +#include "yati/source/file.hpp" + +namespace sphaira::yati::source { + +File::File(FsFileSystem* fs, const fs::FsPath& path) { + m_open_result = fsFsOpenFile(fs, path, FsOpenMode_Read, std::addressof(m_file)); +} + +File::~File() { + if (R_SUCCEEDED(GetOpenResult())) { + fsFileClose(std::addressof(m_file)); + } +} + +Result File::Read(void* buf, s64 off, s64 size, u64* bytes_read) { + R_TRY(GetOpenResult()); + return fsFileRead(std::addressof(m_file), off, buf, size, 0, bytes_read); +} + +} // namespace sphaira::yati::source diff --git a/sphaira/source/yati/source/stdio.cpp b/sphaira/source/yati/source/stdio.cpp new file mode 100644 index 0000000..1a70590 --- /dev/null +++ b/sphaira/source/yati/source/stdio.cpp @@ -0,0 +1,28 @@ +#include "yati/source/stdio.hpp" + +namespace sphaira::yati::source { + +Stdio::Stdio(const char* path) { + m_file = std::fopen(path, "rb"); + if (!m_file) { + m_open_result = fsdevGetLastResult(); + } +} + +Stdio::~Stdio() { + if (R_SUCCEEDED(GetOpenResult())) { + std::fclose(m_file); + } +} + +Result Stdio::Read(void* buf, s64 off, s64 size, u64* bytes_read) { + R_TRY(GetOpenResult()); + + std::fseek(m_file, off, SEEK_SET); + R_TRY(fsdevGetLastResult()); + + *bytes_read = std::fread(buf, 1, size, m_file); + return fsdevGetLastResult(); +} + +} // namespace sphaira::yati::source diff --git a/sphaira/source/yati/yati.cpp b/sphaira/source/yati/yati.cpp new file mode 100644 index 0000000..40f0a79 --- /dev/null +++ b/sphaira/source/yati/yati.cpp @@ -0,0 +1,1317 @@ +#include "yati/yati.hpp" +#include "yati/source/file.hpp" +#include "yati/source/stdio.hpp" +#include "yati/container/nsp.hpp" +#include "yati/container/xci.hpp" + +#include "yati/nx/ncz.hpp" +#include "yati/nx/nca.hpp" +#include "yati/nx/ncm.hpp" +#include "yati/nx/ns.hpp" +#include "yati/nx/es.hpp" +#include "yati/nx/keys.hpp" +#include "yati/nx/crypto.hpp" + +#include "ui/progress_box.hpp" +#include "app.hpp" +#include "i18n.hpp" +#include "log.hpp" + +#include +#include +#include + +namespace sphaira::yati { +namespace { + +constexpr NcmStorageId NCM_STORAGE_IDS[]{ + NcmStorageId_BuiltInUser, + NcmStorageId_SdCard, +}; + +// custom allocator for std::vector that respects alignment. +// https://en.cppreference.com/w/cpp/named_req/Allocator +template +struct CustomVectorAllocator { +public: + // https://en.cppreference.com/w/cpp/memory/new/operator_new + auto allocate(std::size_t n) -> T* { + log_write("allocating ptr size: %zu\n", n); + return new(align) T[n]; + } + + // https://en.cppreference.com/w/cpp/memory/new/operator_delete + auto deallocate(T* p, std::size_t n) noexcept -> void { + log_write("deleting ptr size: %zu\n", n); + ::operator delete[] (p, n, align); + } + +private: + static constexpr inline std::align_val_t align{Align}; +}; + +template +struct PageAllocator : CustomVectorAllocator { + using value_type = T; // used by std::vector +}; + +template +bool operator==(const PageAllocator &, const PageAllocator &) { return true; } + +using PageAlignedVector = std::vector>; + +constexpr u32 KEYGEN_LIMIT = 0x20; + +#if 0 +struct FwVersion { + u32 value; + auto relstep() const -> u8 { return (value >> 0) & 0xFFFF; } + auto micro() const -> u8 { return (value >> 16) & 0x000F; } + auto minor() const -> u8 { return (value >> 20) & 0x003F; } + auto major() const -> u8 { return (value >> 26) & 0x003F; } + auto hos() const -> u32 { return MAKEHOSVERSION(major(), minor(), micro()); } +}; +#endif + +struct NcaCollection : container::CollectionEntry { + // NcmContentType + u8 type{}; + NcmContentId content_id{}; + NcmPlaceHolderId placeholder_id{}; + // new hash of the nca.. + u8 hash[SHA256_HASH_SIZE]{}; + // set true if nca has been modified. + bool modified{}; +}; + +struct CnmtCollection : NcaCollection { + // list of all nca's the cnmt depends on + std::vector ncas{}; + // only set if any of the nca's depend on a ticket / cert. + // if set, the ticket / cert will be installed once all nca's have installed. + std::vector rights_id{}; + + NcmContentMetaHeader header{}; + NcmContentMetaKey key{}; + NcmContentInfo content_info{}; + std::vector extended_header{}; + std::vector infos{}; +}; + +struct TikCollection { + // raw data of the ticket / cert. + std::vector ticket{}; + std::vector cert{}; + // set via the name of the ticket. + FsRightsId rights_id{}; + // set if ticket is required by an nca. + bool required{}; +}; + +struct Yati; + +const u64 INFLATE_BUFFER_MAX = 1024*1024*4; + +struct ThreadBuffer { + ThreadBuffer() { + buf.reserve(INFLATE_BUFFER_MAX); + } + + PageAlignedVector buf; + s64 off; +}; + +template +struct RingBuf { +private: + ThreadBuffer buf[Size]{}; + unsigned r_index{}; + unsigned w_index{}; + + static_assert((sizeof(RingBuf::buf) & (sizeof(RingBuf::buf) - 1)) == 0, "Must be power of 2!"); + +public: + void ringbuf_reset() { + this->r_index = this->w_index; + } + + unsigned ringbuf_capacity() const { + return sizeof(this->buf) / sizeof(this->buf[0]); + } + + unsigned ringbuf_size() const { + return (this->w_index - this->r_index) % (ringbuf_capacity() * 2U); + } + + unsigned ringbuf_free() const { + return ringbuf_capacity() - ringbuf_size(); + } + + void ringbuf_push(PageAlignedVector& buf_in, s64 off_in) { + auto& value = this->buf[this->w_index % ringbuf_capacity()]; + value.off = off_in; + std::swap(value.buf, buf_in); + + this->w_index = (this->w_index + 1U) % (ringbuf_capacity() * 2U); + } + + void ringbuf_pop(PageAlignedVector& buf_out, s64& off_out) { + auto& value = this->buf[this->r_index % ringbuf_capacity()]; + off_out = value.off; + std::swap(value.buf, buf_out); + + this->r_index = (this->r_index + 1U) % (ringbuf_capacity() * 2U); + } +}; + +struct ThreadData { + ThreadData(Yati* _yati, std::span _tik, NcaCollection* _nca) + : yati{_yati}, tik{_tik}, nca{_nca} { + mutexInit(std::addressof(read_mutex)); + mutexInit(std::addressof(write_mutex)); + + condvarInit(std::addressof(can_read)); + condvarInit(std::addressof(can_decompress)); + condvarInit(std::addressof(can_decompress_write)); + condvarInit(std::addressof(can_write)); + + sha256ContextCreate(&sha256); + // this will be updated with the actual size from nca header. + write_size = nca->size; + + read_buffer_size = 1024*1024*4; + max_buffer_size = std::max(read_buffer_size, INFLATE_BUFFER_MAX); + } + + auto GetResults() -> Result; + void WakeAllThreads(); + + Result Read(void* buf, s64 size, u64* bytes_read); + + Result SetDecompressBuf(PageAlignedVector& buf, s64 off, s64 size) { + buf.resize(size); + + mutexLock(std::addressof(read_mutex)); + if (!read_buffers.ringbuf_free()) { + R_TRY(condvarWait(std::addressof(can_read), std::addressof(read_mutex))); + } + + ON_SCOPE_EXIT(mutexUnlock(std::addressof(read_mutex))); + R_TRY(GetResults()); + read_buffers.ringbuf_push(buf, off); + return condvarWakeOne(std::addressof(can_decompress)); + } + + Result GetDecompressBuf(PageAlignedVector& buf_out, s64& off_out) { + mutexLock(std::addressof(read_mutex)); + if (!read_buffers.ringbuf_size()) { + R_TRY(condvarWait(std::addressof(can_decompress), std::addressof(read_mutex))); + } + + ON_SCOPE_EXIT(mutexUnlock(std::addressof(read_mutex))); + R_TRY(GetResults()); + read_buffers.ringbuf_pop(buf_out, off_out); + return condvarWakeOne(std::addressof(can_read)); + } + + Result SetWriteBuf(PageAlignedVector& buf, s64 size, bool skip_verify) { + buf.resize(size); + if (!skip_verify) { + sha256ContextUpdate(std::addressof(sha256), buf.data(), buf.size()); + } + + mutexLock(std::addressof(write_mutex)); + if (!write_buffers.ringbuf_free()) { + R_TRY(condvarWait(std::addressof(can_decompress_write), std::addressof(write_mutex))); + } + + ON_SCOPE_EXIT(mutexUnlock(std::addressof(write_mutex))); + R_TRY(GetResults()); + write_buffers.ringbuf_push(buf, 0); + return condvarWakeOne(std::addressof(can_write)); + } + + Result GetWriteBuf(PageAlignedVector& buf_out, s64& off_out) { + mutexLock(std::addressof(write_mutex)); + if (!write_buffers.ringbuf_size()) { + R_TRY(condvarWait(std::addressof(can_write), std::addressof(write_mutex))); + } + + ON_SCOPE_EXIT(mutexUnlock(std::addressof(write_mutex))); + R_TRY(GetResults()); + write_buffers.ringbuf_pop(buf_out, off_out); + return condvarWakeOne(std::addressof(can_decompress_write)); + } + + // these need to be copied + Yati* yati{}; + std::span tik{}; + NcaCollection* nca{}; + + // these need to be created + Mutex read_mutex{}; + Mutex write_mutex{}; + + CondVar can_read{}; + CondVar can_decompress{}; + CondVar can_decompress_write{}; + CondVar can_write{}; + + RingBuf<4> read_buffers{}; + RingBuf<4> write_buffers{}; + + ncz::BlockHeader ncz_block_header{}; + std::vector ncz_sections{}; + std::vector ncz_blocks{}; + + Sha256Context sha256{}; + + u64 read_buffer_size{}; + u64 max_buffer_size{}; + + // these are shared between threads + volatile s64 read_offset{}; + volatile s64 decompress_offset{}; + volatile s64 write_offset{}; + volatile s64 write_size{}; + + volatile Result read_result{}; + volatile Result decompress_result{}; + volatile Result write_result{}; +}; + +struct Yati { + Yati(ui::ProgressBox*, std::shared_ptr); + ~Yati(); + + Result Setup(container::Collections& out); + Result InstallNca(std::span tickets, NcaCollection& nca); + Result InstallCnmtNca(std::span tickets, CnmtCollection& cnmt, const container::Collections& collections); + Result InstallControlNca(std::span tickets, const CnmtCollection& cnmt, NcaCollection& nca); + + Result readFuncInternal(ThreadData* t); + Result decompressFuncInternal(ThreadData* t); + Result writeFuncInternal(ThreadData* t); + +// private: + ui::ProgressBox* pbox{}; + std::shared_ptr source{}; + + // for all content storages + NcmContentStorage ncm_cs[2]{}; + NcmContentMetaDatabase ncm_db[2]{}; + // these point to the above struct + NcmContentStorage cs{}; + NcmContentMetaDatabase db{}; + NcmStorageId storage_id{}; + + Service es{}; + Service ns_app{}; + std::unique_ptr container{}; + Config config{}; + keys::Keys keys{}; +}; + +auto ThreadData::GetResults() -> Result { + R_UNLESS(!yati->pbox->ShouldExit(), Result_Cancelled); + R_TRY(read_result); + R_TRY(decompress_result); + R_TRY(write_result); + R_SUCCEED(); +} + +void ThreadData::WakeAllThreads() { + condvarWakeAll(std::addressof(can_read)); + condvarWakeAll(std::addressof(can_decompress)); + condvarWakeAll(std::addressof(can_decompress_write)); + condvarWakeAll(std::addressof(can_write)); + + mutexUnlock(std::addressof(read_mutex)); + mutexUnlock(std::addressof(write_mutex)); +} + +Result ThreadData::Read(void* buf, s64 size, u64* bytes_read) { + size = std::min(size, nca->size - read_offset); + const auto rc = yati->source->Read(buf, nca->offset + read_offset, size, bytes_read); + read_offset += *bytes_read; + R_UNLESS(size == *bytes_read, Result_InvalidNcaReadSize); + return rc; +} + +auto isRightsIdValid(FsRightsId id) -> bool { + FsRightsId empty_id{}; + return 0 != std::memcmp(std::addressof(id), std::addressof(empty_id), sizeof(id)); +} + +auto getKeyGenFromRightsId(FsRightsId id) -> u8 { + return id.c[sizeof(id) - 1]; +} + +struct HashStr { + char str[0x21]; +}; + +HashStr hexIdToStr(auto id) { + HashStr str{}; + const auto id_lower = std::byteswap(*(u64*)id.c); + const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8)); + std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper); + return str; +} + +// read thread reads all data from the source, it also handles +// parsing ncz headers, sections and reading ncz blocks +Result Yati::readFuncInternal(ThreadData* t) { + PageAlignedVector buf; + buf.reserve(t->max_buffer_size); + + while (t->read_offset < t->nca->size && R_SUCCEEDED(t->GetResults())) { + const auto buffer_offset = t->read_offset; + + // read more data + s64 read_size = t->read_buffer_size; + if (!t->read_offset) { + read_size = NCZ_SECTION_OFFSET; + } + + u64 bytes_read{}; + buf.resize(read_size); + R_TRY(t->Read(buf.data(), read_size, std::addressof(bytes_read))); + auto buf_size = bytes_read; + + // read enough bytes for ncz, check magic + if (t->read_offset == NCZ_SECTION_OFFSET) { + // check for ncz section header. + ncz::Header header{}; + std::memcpy(std::addressof(header), buf.data() + 0x4000, sizeof(header)); + if (header.magic == NCZ_SECTION_MAGIC) { + // validate section header. + R_UNLESS(header.total_sections, Result_InvalidNczSectionCount); + + buf_size = 0x4000; + log_write("found ncz, total number of sections: %zu\n", header.total_sections); + t->ncz_sections.resize(header.total_sections); + R_TRY(t->Read(t->ncz_sections.data(), t->ncz_sections.size() * sizeof(ncz::Section), std::addressof(bytes_read))); + + // check for ncz block header. + const auto read_off = t->read_offset; + R_TRY(t->Read(std::addressof(t->ncz_block_header), sizeof(t->ncz_block_header), std::addressof(bytes_read))); + if (t->ncz_block_header.magic != NCZ_BLOCK_MAGIC) { + t->read_offset = read_off; + } else { + // validate block header. + R_UNLESS(t->ncz_block_header.version == 0x2, Result_InvalidNczBlockVersion); + R_UNLESS(t->ncz_block_header.type == 0x1, Result_InvalidNczBlockType); + R_UNLESS(t->ncz_block_header.total_blocks, Result_InvalidNczBlockTotal); + R_UNLESS(t->ncz_block_header.block_size_exponent >= 14 && t->ncz_block_header.block_size_exponent <= 32, Result_InvalidNczBlockSizeExponent); + + // read blocks (array of block sizes). + std::vector blocks(t->ncz_block_header.total_blocks); + R_TRY(t->Read(blocks.data(), blocks.size() * sizeof(ncz::Block), std::addressof(bytes_read))); + + // calculate offsets for each block. + auto block_offset = t->read_offset; + for (const auto& block : blocks) { + t->ncz_blocks.emplace_back(block_offset, block.size); + block_offset += block.size; + } + } + } + } + + R_TRY(t->SetDecompressBuf(buf, buffer_offset, buf_size)); + } + + log_write("read success\n"); + R_SUCCEED(); +} + +// decompress thread handles decrypting / modifying the nca header, decompressing ncz +// and calculating the running sha256. +Result Yati::decompressFuncInternal(ThreadData* t) { + // only used for ncz files. + auto dctx = ZSTD_createDCtx(); + ON_SCOPE_EXIT(ZSTD_freeDCtx(dctx)); + const auto chunk_size = ZSTD_DStreamOutSize(); + const ncz::Section* ncz_section{}; + const ncz::BlockInfo* ncz_block{}; + bool is_ncz{}; + + s64 inflate_offset{}; + Aes128CtrContext ctx{}; + PageAlignedVector inflate_buf{}; + inflate_buf.reserve(t->max_buffer_size); + + s64 written{}; + s64 decompress_buf_off{}; + PageAlignedVector buf{}; + buf.reserve(t->max_buffer_size); + + // encrypts the nca and passes the buffer to the write thread. + const auto ncz_flush = [&](s64 size) -> Result { + if (!inflate_offset) { + R_SUCCEED(); + } + + // if we are not moving the whole vector, then we need to keep + // the remaining data. + // rather that copying the entire vector to the write thread, + // only copy (store) the remaining amount. + PageAlignedVector temp_vector{}; + if (size < inflate_offset) { + temp_vector.resize(inflate_offset - size); + std::memcpy(temp_vector.data(), inflate_buf.data() + size, temp_vector.size()); + } + + for (s64 off = 0; off < size;) { + // log_write("looking for section\n"); + if (!ncz_section || !ncz_section->InRange(written)) { + auto it = std::find_if(t->ncz_sections.cbegin(), t->ncz_sections.cend(), [written](auto& e){ + return e.InRange(written); + }); + + R_UNLESS(it != t->ncz_sections.cend(), Result_NczSectionNotFound); + ncz_section = &(*it); + + if (ncz_section->crypto_type >= nca::EncryptionType_AesCtr) { + const auto swp = std::byteswap(u64(written) >> 4); + u8 counter[0x16]; + std::memcpy(counter + 0x0, ncz_section->counter, 0x8); + std::memcpy(counter + 0x8, &swp, 0x8); + aes128CtrContextCreate(&ctx, ncz_section->key, counter); + } + } + + const auto total_size = ncz_section->offset + ncz_section->size; + const auto chunk_size = std::min(total_size - written, size - off); + + if (ncz_section->crypto_type >= nca::EncryptionType_AesCtr) { + aes128CtrCrypt(&ctx, inflate_buf.data() + off, inflate_buf.data() + off, chunk_size); + } + + written += chunk_size; + off += chunk_size; + } + + R_TRY(t->SetWriteBuf(inflate_buf, size, config.skip_nca_hash_verify)); + inflate_offset -= size; + + // restore remaining data to the swapped buffer. + if (!temp_vector.empty()) { + log_write("storing data size: %zu\n", temp_vector.size()); + inflate_buf = temp_vector; + } + + R_SUCCEED(); + }; + + while (t->decompress_offset < t->write_size && R_SUCCEEDED(t->GetResults())) { + R_TRY(t->GetDecompressBuf(buf, decompress_buf_off)); + + // do we have an nsz? if so, setup buffers. + if (!is_ncz && !t->ncz_sections.empty()) { + log_write("YES IT FOUND NCZ\n"); + is_ncz = true; + } + + // if we don't have a ncz or it's before the ncz header, pass buffer directly to write + if (!is_ncz || !decompress_buf_off) { + // check nca header + if (!decompress_buf_off) { + nca::Header header{}; + crypto::cryptoAes128Xts(buf.data(), std::addressof(header), keys.header_key, 0, 0x200, sizeof(header), false); + log_write("verifying nca header magic\n"); + R_UNLESS(header.magic == 0x3341434E, Result_InvalidNcaMagic); + log_write("nca magic is ok! type: %u\n", header.content_type); + + if (!config.skip_rsa_header_fixed_key_verify) { + log_write("verifying nca fixed key\n"); + R_TRY(nca::VerifyFixedKey(header)); + log_write("nca fixed key is ok! type: %u\n", header.content_type); + } else { + log_write("skipping nca verification\n"); + } + + t->write_size = header.size; + R_TRY(ncmContentStorageSetPlaceHolderSize(std::addressof(cs), std::addressof(t->nca->placeholder_id), header.size)); + + if (header.distribution_type == nca::DistributionType_GameCard) { + header.distribution_type = nca::DistributionType_System; + t->nca->modified = true; + } + + TikCollection* ticket = nullptr; + if (isRightsIdValid(header.rights_id)) { + auto it = std::find_if(t->tik.begin(), t->tik.end(), [header](auto& e){ + return !std::memcmp(&header.rights_id, &e.rights_id, sizeof(e.rights_id)); + }); + + R_UNLESS(it != t->tik.end(), Result_TicketNotFound); + it->required = true; + ticket = &(*it); + } + + if ((config.convert_to_standard_crypto && isRightsIdValid(header.rights_id)) || config.lower_master_key) { + t->nca->modified = true; + u8 keak_generation; + + if (isRightsIdValid(header.rights_id)) { + const auto key_gen = getKeyGenFromRightsId(header.rights_id); + log_write("converting to standard crypto: 0x%X 0x%X\n", key_gen, header.key_gen); + + // fetch ticket data block. + es::TicketData ticket_data; + R_TRY(es::GetTicketData(ticket->ticket, std::addressof(ticket_data))); + + // validate that this indeed the correct ticket. + R_UNLESS(!std::memcmp(std::addressof(header.rights_id), std::addressof(ticket_data.rights_id), sizeof(header.rights_id)), Result_InvalidTicketBadRightsId); + + // some scene releases use buggy software which set the master key + // revision in the properties bitfield...lol, still happens in 2025. + // to fix this, get mkey rev from the rights id + // todo: verify this code. + if (ticket_data.title_key_type == es::TicketTitleKeyType_Common) { + if (!ticket_data.master_key_revision && ticket_data.master_key_revision != getKeyGenFromRightsId(ticket_data.rights_id) && ticket_data.properties_bitfield) { + // get the actual mkey + ticket_data.master_key_revision = getKeyGenFromRightsId(ticket_data.rights_id); + // unset the properties + ticket_data.properties_bitfield = 0; + } + } + + // decrypt title key. + keys::KeyEntry title_key; + R_TRY(es::GetTitleKey(title_key, ticket_data, keys)); + R_TRY(es::DecryptTitleKey(title_key, key_gen, keys)); + + std::memset(header.key_area, 0, sizeof(header.key_area)); + std::memcpy(&header.key_area[0x2], &title_key, sizeof(title_key)); + + keak_generation = key_gen; + ticket->required = false; + } else if (config.lower_master_key) { + R_TRY(nca::DecryptKeak(keys, header)); + } + + if (config.lower_master_key) { + keak_generation = 0; + } + + R_TRY(nca::EncryptKeak(keys, header, keak_generation)); + std::memset(&header.rights_id, 0, sizeof(header.rights_id)); + } + + if (t->nca->modified) { + crypto::cryptoAes128Xts(std::addressof(header), buf.data(), keys.header_key, 0, 0x200, sizeof(header), true); + } + } + + written += buf.size(); + t->decompress_offset += buf.size(); + R_TRY(t->SetWriteBuf(buf, buf.size(), config.skip_nca_hash_verify)); + } else if (is_ncz) { + u64 buf_off{}; + while (buf_off < buf.size()) { + std::span buffer{buf.data() + buf_off, buf.size() - buf_off}; + bool compressed = true; + + // todo: blocks need to use read offset, as the offset + size is compressed range. + if (t->ncz_blocks.size()) { + if (!ncz_block || !ncz_block->InRange(decompress_buf_off)) { + auto it = std::find_if(t->ncz_blocks.cbegin(), t->ncz_blocks.cend(), [decompress_buf_off](auto& e){ + return e.InRange(decompress_buf_off); + }); + + R_UNLESS(it != t->ncz_blocks.cend(), Result_NczBlockNotFound); + // log_write("looking found block\n"); + ncz_block = &(*it); + } + + // https://github.com/nicoboss/nsz/issues/79 + auto decompressedBlockSize = 1 << t->ncz_block_header.block_size_exponent; + // special handling for the last block to check it's actually compressed + if (ncz_block->offset == t->ncz_blocks.back().offset) { + log_write("last block special handling\n"); + decompressedBlockSize = t->ncz_block_header.decompressed_size % decompressedBlockSize; + } + + // check if this block is compressed. + compressed = ncz_block->size < decompressedBlockSize; + + // clip read size as blocks can be up to 32GB in size! + const auto size = std::min(buf.size() - buf_off, ncz_block->size); + buffer = {buf.data() + buf_off, size}; + } + + if (compressed) { + // log_write("COMPRESSED block\n"); + ZSTD_inBuffer input = { buffer.data(), buffer.size(), 0 }; + while (input.pos < input.size) { + R_TRY(t->GetResults()); + + inflate_buf.resize(inflate_offset + chunk_size); + ZSTD_outBuffer output = { inflate_buf.data() + inflate_offset, chunk_size, 0 }; + const auto res = ZSTD_decompressStream(dctx, std::addressof(output), std::addressof(input)); + R_UNLESS(!ZSTD_isError(res), Result_InvalidNczZstdError); + + t->decompress_offset += output.pos; + inflate_offset += output.pos; + if (inflate_offset >= INFLATE_BUFFER_MAX) { + // log_write("flushing compressed data: %zd vs %zd diff: %zd\n", inflate_offset, INFLATE_BUFFER_MAX, inflate_offset - INFLATE_BUFFER_MAX); + R_TRY(ncz_flush(INFLATE_BUFFER_MAX)); + } + } + } else { + inflate_buf.resize(inflate_offset + buffer.size()); + std::memcpy(inflate_buf.data() + inflate_offset, buffer.data(), buffer.size()); + + t->decompress_offset += buffer.size(); + inflate_offset += buffer.size(); + if (inflate_offset >= INFLATE_BUFFER_MAX) { + // log_write("flushing copy data\n"); + R_TRY(ncz_flush(INFLATE_BUFFER_MAX)); + } + } + + buf_off += buffer.size(); + decompress_buf_off += buffer.size(); + } + } + } + + // flush remaining data. + if (is_ncz && inflate_offset) { + log_write("flushing remaining\n"); + R_TRY(ncz_flush(inflate_offset)); + } + + log_write("decompress thread done!\n"); + + // get final hash output. + sha256ContextGetHash(std::addressof(t->sha256), t->nca->hash); + + R_SUCCEED(); +} + +// write thread writes data to the nca placeholder. +Result Yati::writeFuncInternal(ThreadData* t) { + PageAlignedVector buf; + buf.reserve(t->max_buffer_size); + + while (t->write_offset < t->write_size && R_SUCCEEDED(t->GetResults())) { + s64 dummy_off; + R_TRY(t->GetWriteBuf(buf, dummy_off)); + R_TRY(ncmContentStorageWritePlaceHolder(std::addressof(cs), std::addressof(t->nca->placeholder_id), t->write_offset, buf.data(), buf.size())); + t->write_offset += buf.size(); + } + + log_write("finished write thread!\n"); + R_SUCCEED(); +} + +void readFunc(void* d) { + auto t = static_cast(d); + t->read_result = t->yati->readFuncInternal(t); + log_write("read thread returned now\n"); +} + +void decompressFunc(void* d) { + log_write("hello decomp thread func\n"); + auto t = static_cast(d); + t->decompress_result = t->yati->decompressFuncInternal(t); + log_write("decompress thread returned now\n"); +} + +void writeFunc(void* d) { + auto t = static_cast(d); + t->write_result = t->yati->writeFuncInternal(t); + log_write("write thread returned now\n"); +} + +// stdio-like wrapper for std::vector +struct BufHelper { + BufHelper() = default; + BufHelper(std::span data) { + write(data); + } + + void write(const void* data, u64 size) { + if (offset + size >= buf.size()) { + buf.resize(offset + size); + } + std::memcpy(buf.data() + offset, data, size); + offset += size; + } + + void write(std::span data) { + write(data.data(), data.size()); + } + + void seek(u64 where_to) { + offset = where_to; + } + + [[nodiscard]] + auto tell() const { + return offset; + } + + std::vector buf{}; + u64 offset{}; +}; + +Yati::Yati(ui::ProgressBox* _pbox, std::shared_ptr _source) : pbox{_pbox}, source{_source} { + appletSetMediaPlaybackState(true); +} + +Yati::~Yati() { + splCryptoExit(); + serviceClose(std::addressof(ns_app)); + nsExit(); + + for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) { + ncmContentMetaDatabaseClose(std::addressof(ncm_db[i])); + ncmContentStorageClose(std::addressof(ncm_cs[i])); + } + + serviceClose(std::addressof(es)); + appletSetMediaPlaybackState(false); +} + +Result Yati::Setup(container::Collections& out) { + config.sd_card_install = App::GetApp()->m_install_sd.Get(); + config.allow_downgrade = App::GetApp()->m_allow_downgrade.Get(); + config.skip_if_already_installed = App::GetApp()->m_skip_if_already_installed.Get(); + config.ticket_only = App::GetApp()->m_ticket_only.Get(); + config.patch_ticket = App::GetApp()->m_patch_ticket.Get(); + config.skip_base = App::GetApp()->m_skip_base.Get(); + config.skip_patch = App::GetApp()->m_skip_patch.Get(); + config.skip_addon = App::GetApp()->m_skip_addon.Get(); + config.skip_data_patch = App::GetApp()->m_skip_data_patch.Get(); + config.skip_ticket = App::GetApp()->m_skip_ticket.Get(); + config.skip_nca_hash_verify = App::GetApp()->m_skip_nca_hash_verify.Get(); + config.skip_rsa_header_fixed_key_verify = App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(); + config.skip_rsa_npdm_fixed_key_verify = App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(); + config.ignore_distribution_bit = App::GetApp()->m_ignore_distribution_bit.Get(); + config.convert_to_standard_crypto = App::GetApp()->m_convert_to_standard_crypto.Get(); + config.lower_master_key = App::GetApp()->m_lower_master_key.Get(); + config.lower_system_version = App::GetApp()->m_lower_system_version.Get(); + storage_id = config.sd_card_install ? NcmStorageId_SdCard : NcmStorageId_BuiltInUser; + + R_TRY(source->GetOpenResult()); + R_TRY(splCryptoInitialize()); + R_TRY(nsInitialize()); + R_TRY(nsGetApplicationManagerInterface(std::addressof(ns_app))); + R_TRY(smGetService(std::addressof(es), "es")); + + for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) { + R_TRY(ncmOpenContentMetaDatabase(std::addressof(ncm_db[i]), NCM_STORAGE_IDS[i])); + R_TRY(ncmOpenContentStorage(std::addressof(ncm_cs[i]), NCM_STORAGE_IDS[i])); + } + + cs = ncm_cs[config.sd_card_install]; + db = ncm_db[config.sd_card_install]; + + if (R_SUCCEEDED(container::Nsp::Validate(source.get()))) { + log_write("found nsp\n"); + container = std::make_unique(source.get()); + } else if (R_SUCCEEDED(container::Xci::Validate(source.get()))) { + log_write("found xci\n"); + container = std::make_unique(source.get()); + } else { + log_write("found unknown container\n"); + } + + R_UNLESS(container, Result_ContainerNotFound); + R_TRY(container->GetCollections(out)); + + R_TRY(parse_keys(keys, true)); + R_SUCCEED(); +} + +Result Yati::InstallNca(std::span tickets, NcaCollection& nca) { + log_write("in install nca\n"); + pbox->NewTransfer(nca.name); + keys::parse_hex_key(std::addressof(nca.content_id), nca.name.c_str()); + log_write("generateing placeholder\n"); + R_TRY(ncmContentStorageGeneratePlaceHolderId(std::addressof(cs), std::addressof(nca.placeholder_id))); + log_write("creating placeholder\n"); + R_TRY(ncmContentStorageCreatePlaceHolder(std::addressof(cs), std::addressof(nca.content_id), std::addressof(nca.placeholder_id), nca.size)); + + log_write("opening thread\n"); + ThreadData t_data{this, tickets, std::addressof(nca)}; + + #define READ_THREAD_CORE 1 + #define DECOMPRESS_THREAD_CORE 2 + #define WRITE_THREAD_CORE 0 + // #define READ_THREAD_CORE 2 + // #define DECOMPRESS_THREAD_CORE 2 + // #define WRITE_THREAD_CORE 2 + + Thread t_read{}; + R_TRY(threadCreate(&t_read, readFunc, std::addressof(t_data), nullptr, 1024*64, 0x20, READ_THREAD_CORE)); + ON_SCOPE_EXIT(threadClose(&t_read)); + + Thread t_decompress{}; + R_TRY(threadCreate(&t_decompress, decompressFunc, std::addressof(t_data), nullptr, 1024*64, 0x20, DECOMPRESS_THREAD_CORE)); + ON_SCOPE_EXIT(threadClose(&t_decompress)); + + Thread t_write{}; + R_TRY(threadCreate(&t_write, writeFunc, std::addressof(t_data), nullptr, 1024*64, 0x20, WRITE_THREAD_CORE)); + ON_SCOPE_EXIT(threadClose(&t_write)); + + log_write("starting threads\n"); + R_TRY(threadStart(std::addressof(t_read))); + ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_read))); + + R_TRY(threadStart(std::addressof(t_decompress))); + ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_decompress))); + + R_TRY(threadStart(std::addressof(t_write))); + ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_write))); + + while (t_data.write_offset != t_data.write_size && R_SUCCEEDED(t_data.GetResults())) { + pbox->UpdateTransfer(t_data.write_offset, t_data.write_size); + svcSleepThread(1e+6); + } + + // wait for all threads to close. + log_write("waiting for threads to close\n"); + for (;;) { + t_data.WakeAllThreads(); + pbox->Yield(); + + if (R_FAILED(waitSingleHandle(t_read.handle, 1000))) { + continue; + } else if (R_FAILED(waitSingleHandle(t_decompress.handle, 1000))) { + continue; + } else if (R_FAILED(waitSingleHandle(t_write.handle, 1000))) { + continue; + } + break; + } + log_write("threads closed\n"); + + // if any of the threads failed, wake up all threads so they can exit. + if (R_FAILED(t_data.GetResults())) { + log_write("some reads failed, waking threads: %s\n", nca.name.c_str()); + log_write("returning due to fail: %s\n", nca.name.c_str()); + return t_data.GetResults(); + } + R_TRY(t_data.GetResults()); + + NcmContentId content_id{}; + std::memcpy(std::addressof(content_id), nca.hash, sizeof(content_id)); + + log_write("old id: %s new id: %s\n", hexIdToStr(nca.content_id).str, hexIdToStr(content_id).str); + log_write("doing register: %s\n", nca.name.c_str()); + if (!config.skip_nca_hash_verify && !nca.modified) { + if (std::memcmp(&nca.content_id, nca.hash, sizeof(nca.content_id))) { + log_write("nca hash is invalid!!!!\n"); + R_UNLESS(!std::memcmp(&nca.content_id, nca.hash, sizeof(nca.content_id)), Result_InvalidNcaSha256); + } else { + log_write("nca hash is valid!\n"); + } + } else { + log_write("skipping nca sha256 verify\n"); + } + + R_SUCCEED(); +} + +Result Yati::InstallCnmtNca(std::span tickets, CnmtCollection& cnmt, const container::Collections& collections) { + R_TRY(InstallNca(tickets, cnmt)); + + fs::FsPath path; + R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs))); + R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(cnmt.placeholder_id))); + + FsFileSystem fs; + R_TRY(fsOpenFileSystem(std::addressof(fs), FsFileSystemType_ContentMeta, path)); + ON_SCOPE_EXIT(fsFsClose(std::addressof(fs))); + + FsDir dir; + R_TRY(fsFsOpenDirectory(std::addressof(fs), fs::FsPath{"/"}, FsDirOpenMode_ReadFiles, std::addressof(dir))); + ON_SCOPE_EXIT(fsDirClose(std::addressof(dir))); + + s64 total_entries; + FsDirectoryEntry buf; + R_TRY(fsDirRead(std::addressof(dir), std::addressof(total_entries), 1, std::addressof(buf))); + + FsFile file; + R_TRY(fsFsOpenFile(std::addressof(fs), fs::AppendPath("/", buf.name), FsOpenMode_Read, std::addressof(file))); + ON_SCOPE_EXIT(fsFileClose(std::addressof(file))); + + s64 offset{}; + u64 bytes_read; + ncm::PackagedContentMeta header; + R_TRY(fsFileRead(std::addressof(file), offset, std::addressof(header), sizeof(header), 0, std::addressof(bytes_read))); + offset += bytes_read; + + // read extended header + cnmt.extended_header.resize(header.meta_header.extended_header_size); + R_TRY(fsFileRead(std::addressof(file), offset, cnmt.extended_header.data(), cnmt.extended_header.size(), 0, std::addressof(bytes_read))); + offset += bytes_read; + + // read infos. + std::vector infos(header.meta_header.content_count); + R_TRY(fsFileRead(std::addressof(file), offset, infos.data(), infos.size() * sizeof(NcmPackagedContentInfo), 0, std::addressof(bytes_read))); + offset += bytes_read; + + for (const auto& info : infos) { + if (info.info.content_type == NcmContentType_DeltaFragment) { + continue; + } + + const auto str = hexIdToStr(info.info.content_id); + const auto it = std::find_if(collections.cbegin(), collections.cend(), [str](auto& e){ + return e.name.find(str.str) != e.name.npos; + }); + + R_UNLESS(it != collections.cend(), Result_NcaNotFound); + + log_write("found: %s\n", str.str); + cnmt.infos.emplace_back(info); + auto& nca = cnmt.ncas.emplace_back(*it); + nca.type = info.info.content_type; + } + + // update header + cnmt.header = header.meta_header; + cnmt.header.content_count = cnmt.infos.size() + 1; + cnmt.header.storage_id = 0; + + cnmt.key.id = header.title_id; + cnmt.key.version = header.title_version; + cnmt.key.type = header.meta_type; + cnmt.key.install_type = NcmContentInstallType_Full; + std::memset(cnmt.key.padding, 0, sizeof(cnmt.key.padding)); + + cnmt.content_info.content_id = cnmt.content_id; + cnmt.content_info.content_type = NcmContentType_Meta; + cnmt.content_info.attr = 0; + ncmU64ToContentInfoSize(cnmt.size, &cnmt.content_info); + cnmt.content_info.id_offset = 0; + + if (config.lower_system_version) { + auto extended_header = (ncm::ExtendedHeader*)cnmt.extended_header.data(); + log_write("patching version\n"); + if (cnmt.key.type == NcmContentMetaType_Application) { + extended_header->application.required_system_version = 0; + } else if (cnmt.key.type == NcmContentMetaType_Patch) { + extended_header->patch.required_system_version = 0; + } + } + + // sort ncas + const auto sorter = [](NcaCollection& lhs, NcaCollection& rhs) -> bool { + return lhs.type > rhs.type; + }; + + std::sort(cnmt.ncas.begin(), cnmt.ncas.end(), sorter); + + log_write("found all cnmts\n"); + R_SUCCEED(); +} + +Result Yati::InstallControlNca(std::span tickets, const CnmtCollection& cnmt, NcaCollection& nca) { + R_TRY(InstallNca(tickets, nca)); + + fs::FsPath path; + R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs))); + R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(nca.placeholder_id))); + + log_write("got control path: %s\n", path.s); + FsFileSystem fs; + R_TRY(fsOpenFileSystemWithId(std::addressof(fs), ncm::GetAppId(cnmt.key), FsFileSystemType_ContentControl, path, FsContentAttributes_All)); + ON_SCOPE_EXIT(fsFsClose(std::addressof(fs))); + log_write("opened control path fs: %s\n", path.s); + + FsFile file; + R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/control.nacp"}, FsOpenMode_Read, std::addressof(file))); + ON_SCOPE_EXIT(fsFileClose(std::addressof(file))); + log_write("got control path file: %s\n", path.s); + + NacpLanguageEntry entry; + u64 bytes_read; + R_TRY(fsFileRead(&file, 0, &entry, sizeof(entry), 0, &bytes_read)); + pbox->SetTitle("Installing "_i18n + entry.name); + + R_SUCCEED(); +} + +Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr source) { + auto yati = std::make_unique(pbox, source); + + container::Collections collections{}; + R_TRY(yati->Setup(collections)); + + std::vector tickets{}; + for (const auto& collection : collections) { + if (collection.name.ends_with(".tik")) { + TikCollection entry{}; + keys::parse_hex_key(entry.rights_id.c, collection.name.c_str()); + const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert"; + + const auto cert = std::find_if(collections.cbegin(), collections.cend(), [str](auto& e){ + return e.name.find(str) != e.name.npos; + }); + + R_UNLESS(cert != collections.cend(), Result_CertNotFound); + entry.ticket.resize(collection.size); + entry.cert.resize(cert->size); + + u64 bytes_read; + R_TRY(source->Read(entry.ticket.data(), collection.offset, entry.ticket.size(), &bytes_read)); + R_TRY(source->Read(entry.cert.data(), cert->offset, entry.cert.size(), &bytes_read)); + tickets.emplace_back(entry); + } + } + + std::vector cnmts{}; + for (const auto& collection : collections) { + log_write("found collection: %s\n", collection.name.c_str()); + if (collection.name.ends_with(".cnmt.nca")) { + auto& cnmt = cnmts.emplace_back(NcaCollection{collection}); + cnmt.type = NcmContentType_Meta; + } + } + + for (auto& cnmt : cnmts) { + ON_SCOPE_EXIT( + ncmContentStorageDeletePlaceHolder(std::addressof(yati->cs), std::addressof(cnmt.placeholder_id)); + for (auto& nca : cnmt.ncas) { + ncmContentStorageDeletePlaceHolder(std::addressof(yati->cs), std::addressof(nca.placeholder_id)); + } + ); + + R_TRY(yati->InstallCnmtNca(tickets, cnmt, collections)); + + bool skip = false; + const auto app_id = ncm::GetAppId(cnmt.key); + bool has_records; + R_TRY(nsIsAnyApplicationEntityInstalled(app_id, &has_records)); + + // TODO: fix this when gamecard is inserted as it will only return records + // for the gamecard... + // may have to use ncm directly to get the keys, then parse that. + u32 latest_version_num = cnmt.key.version; + if (has_records) { + s32 meta_count{}; + R_TRY(nsCountApplicationContentMeta(app_id, &meta_count)); + R_UNLESS(meta_count > 0, 0x1); + + std::vector records(meta_count); + s32 count; + R_TRY(ns::ListApplicationRecordContentMeta(std::addressof(yati->ns_app), 0, app_id, records.data(), records.size(), &count)); + R_UNLESS(count == records.size(), 0x1); + + for (auto& record : records) { + log_write("found record: 0x%016lX type: %u version: %u\n", record.key.id, record.key.type, record.key.version); + log_write("cnmt record: 0x%016lX type: %u version: %u\n", cnmt.key.id, cnmt.key.type, cnmt.key.version); + + if (record.key.id == cnmt.key.id && cnmt.key.version == record.key.version && yati->config.skip_if_already_installed) { + log_write("skipping as already installed\n"); + skip = true; + } + + // check if we are downgrading + if (cnmt.key.type == NcmContentMetaType_Patch) { + if (cnmt.key.type == record.key.type && cnmt.key.version < record.key.version && !yati->config.allow_downgrade) { + log_write("skipping due to it being lower\n"); + skip = true; + } + } else { + latest_version_num = std::max(latest_version_num, record.key.version); + } + } + } + + // skip invalid types + if (!(cnmt.key.type & 0x80)) { + log_write("\tskipping: invalid: %u\n", cnmt.key.type); + skip = true; + } else if (yati->config.skip_base && cnmt.key.type == NcmContentMetaType_Application) { + log_write("\tskipping: [NcmContentMetaType_Application]\n"); + skip = true; + } else if (yati->config.skip_patch && cnmt.key.type == NcmContentMetaType_Patch) { + log_write("\tskipping: [NcmContentMetaType_Application]\n"); + skip = true; + } else if (yati->config.skip_addon && cnmt.key.type == NcmContentMetaType_AddOnContent) { + log_write("\tskipping: [NcmContentMetaType_AddOnContent]\n"); + skip = true; + } else if (yati->config.skip_data_patch && cnmt.key.type == NcmContentMetaType_DataPatch) { + log_write("\tskipping: [NcmContentMetaType_DataPatch]\n"); + skip = true; + } + + if (skip) { + log_write("skipping install!\n"); + continue; + } + + log_write("installing nca's\n"); + for (auto& nca : cnmt.ncas) { + if (nca.type == NcmContentType_Control) { + log_write("installing control nca\n"); + R_TRY(yati->InstallControlNca(tickets, cnmt, nca)); + } else { + R_TRY(yati->InstallNca(tickets, nca)); + } + } + + // log_write("exiting early :)\n"); + // return 0; + + for (auto& ticket : tickets) { + if (ticket.required) { + if (yati->config.skip_ticket) { + log_write("WARNING: skipping ticket install, but it's required!\n"); + } else { + log_write("patching ticket\n"); + if (yati->config.patch_ticket) { + R_TRY(es::PatchTicket(ticket.ticket, yati->keys, false)); + } + log_write("installing ticket\n"); + R_TRY(es::ImportTicket(std::addressof(yati->es), ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size())); + ticket.required = false; + } + } + } + + log_write("listing keys\n"); + + // remove current entries (if any). + s32 db_list_total; + s32 db_list_count; + u64 id_min = cnmt.key.id; + u64 id_max = cnmt.key.id; + std::vector keys(1); + + // if installing a patch, remove all previously installed patches. + if (cnmt.key.type == NcmContentMetaType_Patch) { + id_min = 0; + id_max = UINT64_MAX; + } + + for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) { + auto& cs = yati->ncm_cs[i]; + auto& db = yati->ncm_db[i]; + + std::vector keys(1); + R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), static_cast(cnmt.key.type), app_id, id_min, id_max, NcmContentInstallType_Full)); + + if (db_list_total != keys.size()) { + keys.resize(db_list_total); + if (keys.size()) { + R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), static_cast(cnmt.key.type), app_id, id_min, id_max, NcmContentInstallType_Full)); + } + } + + for (auto& key : keys) { + log_write("found key: 0x%016lX type: %u version: %u\n", key.id, key.type, key.version); + NcmContentMetaHeader header; + u64 out_size; + log_write("trying to get from db\n"); + R_TRY(ncmContentMetaDatabaseGet(std::addressof(db), std::addressof(key), std::addressof(out_size), std::addressof(header), sizeof(header))); + R_UNLESS(out_size == sizeof(header), Result_NcmDbCorruptHeader); + log_write("trying to list infos\n"); + + std::vector infos(header.content_count); + s32 content_info_out; + R_TRY(ncmContentMetaDatabaseListContentInfo(std::addressof(db), std::addressof(content_info_out), infos.data(), infos.size(), std::addressof(key), 0)); + R_UNLESS(content_info_out == infos.size(), Result_NcmDbCorruptInfos); + log_write("size matches\n"); + + for (auto& info : infos) { + R_TRY(ncm::Delete(std::addressof(cs), std::addressof(info.content_id))); + } + + log_write("trying to remove it\n"); + R_TRY(ncmContentMetaDatabaseRemove(std::addressof(db), std::addressof(key))); + R_TRY(ncmContentMetaDatabaseCommit(std::addressof(db))); + log_write("all done with this key\n\n"); + } + } + + log_write("done with keys\n"); + + // register all nca's + log_write("registering cnmt nca\n"); + R_TRY(ncm::Register(std::addressof(yati->cs), std::addressof(cnmt.content_id), std::addressof(cnmt.placeholder_id))); + log_write("registered cnmt nca\n"); + + for (auto& nca : cnmt.ncas) { + log_write("registering nca: %s\n", nca.name.c_str()); + R_TRY(ncm::Register(std::addressof(yati->cs), std::addressof(nca.content_id), std::addressof(nca.placeholder_id))); + log_write("registered nca: %s\n", nca.name.c_str()); + } + + log_write("register'd all ncas\n"); + + { + BufHelper buf{}; + buf.write(std::addressof(cnmt.header), sizeof(cnmt.header)); + buf.write(cnmt.extended_header.data(), cnmt.extended_header.size()); + buf.write(std::addressof(cnmt.content_info), sizeof(cnmt.content_info)); + + for (auto& info : cnmt.infos) { + buf.write(std::addressof(info.info), sizeof(info.info)); + } + + pbox->NewTransfer("Updating ncm databse"_i18n); + R_TRY(ncmContentMetaDatabaseSet(std::addressof(yati->db), std::addressof(cnmt.key), buf.buf.data(), buf.tell())); + R_TRY(ncmContentMetaDatabaseCommit(std::addressof(yati->db))); + } + + { + ncm::ContentStorageRecord content_storage_record{}; + content_storage_record.key = cnmt.key; + content_storage_record.storage_id = yati->storage_id; + pbox->NewTransfer("Pushing application record"_i18n); + + R_TRY(ns::PushApplicationRecord(std::addressof(yati->ns_app), app_id, std::addressof(content_storage_record), 1)); + if (hosversionAtLeast(6,0,0)) { + R_TRY(avmInitialize()); + ON_SCOPE_EXIT(avmExit()); + + R_TRY(avmPushLaunchVersion(app_id, latest_version_num)); + } + log_write("pushed\n"); + } + } + + log_write("success!\n"); + R_SUCCEED(); +} + +} // namespace + +Result InstallFromFile(FsFileSystem* fs, const fs::FsPath& path) { + return InstallFromSource(std::make_shared(fs, path)); +} + +Result InstallFromStdioFile(const char* path) { + return InstallFromSource(std::make_shared(path)); +} + +Result InstallFromSource(std::shared_ptr source) { + App::Push(std::make_shared("Installing App"_i18n, [source](auto pbox) mutable -> bool { + return R_SUCCEEDED(InstallFromSource(pbox, source)); + })); + R_SUCCEED(); +} + +Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path) { + return InstallFromSource(pbox, std::make_shared(fs, path)); +} + +Result InstallFromStdioFile(ui::ProgressBox* pbox, const char* path) { + return InstallFromSource(pbox, std::make_shared(path)); +} + +Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr source) { + return InstallInternal(pbox, source); +} + +} // namespace sphaira::yati