diff --git a/README.md b/README.md index 252c267..c0d0ebd 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ The output will be found in `build/MinSizeRel/sphaira.nro` - [hb-appstore](https://github.com/fortheusers/hb-appstore) - [haze](https://github.com/Atmosphere-NX/Atmosphere/tree/master/troposphere/haze) - [nxdumptool](https://github.com/DarkMatterCore/nxdumptool) (for gamecard bin dumping and rsa verify code) +- [Liam0](switch-010editor-templates) (for ticket / cert structs) - [libusbhsfs](https://github.com/DarkMatterCore/libusbhsfs) - [libnxtc](https://github.com/DarkMatterCore/libnxtc) - [oss-nvjpg](https://github.com/averne/oss-nvjpg) diff --git a/assets/romfs/i18n/en.json b/assets/romfs/i18n/en.json index 604e333..90d4b30 100644 --- a/assets/romfs/i18n/en.json +++ b/assets/romfs/i18n/en.json @@ -115,7 +115,7 @@ "Password:": "Password:", "SSID:": "SSID:", "Passphrase:": "Passphrase", - "Failed to install, press B to exit...": "Failed to install via FTP, press  to exit...", + "Failed to install, press B to exit...": "Failed to install, press  to exit...", "Install success!": "Install success!", "Install failed!": "Install failed!", "USB Install": "USB Install", diff --git a/assets/romfs/i18n/fr.json b/assets/romfs/i18n/fr.json index 99daf64..aa8abbc 100644 --- a/assets/romfs/i18n/fr.json +++ b/assets/romfs/i18n/fr.json @@ -115,9 +115,9 @@ "Password:": "Mot de passe :", "SSID:": "SSID :", "Passphrase:": "Mot de passe :", - "Failed to install, press B to exit...": "Installation via FTP échouée, appuyer sur B pour quitter...", - "Install success!": "Installation via FTP réussie !", - "Install failed!": "Installation via FTP échouée !", + "Failed to install, press B to exit...": "Installation échouée, appuyer sur B pour quitter...", + "Install success!": "Installation réussie !", + "Install failed!": "Installation échouée !", "USB Install": "Installer contenu via USB", "USB": "Installation via USB", "Connected, waiting for file list...": "Connecté, en attente de la liste des fichiers...", diff --git a/sphaira/include/app.hpp b/sphaira/include/app.hpp index 4b72943..7a1b7b9 100644 --- a/sphaira/include/app.hpp +++ b/sphaira/include/app.hpp @@ -303,6 +303,7 @@ public: 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_common_ticket{INI_SECTION, "convert_to_common_ticket", true}; 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}; @@ -313,6 +314,7 @@ public: option::OptionBool m_dump_trim_xci{"dump", "trim_xci", false}; option::OptionBool m_dump_label_trim_xci{"dump", "label_trim_xci", false}; option::OptionBool m_dump_usb_transfer_stream{"dump", "usb_transfer_stream", true}; + option::OptionBool m_dump_convert_to_common_ticket{"dump", "convert_to_common_ticket", true}; // 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/defines.hpp b/sphaira/include/defines.hpp index d54be0d..ac046ee 100644 --- a/sphaira/include/defines.hpp +++ b/sphaira/include/defines.hpp @@ -582,6 +582,11 @@ enum class SphairaResult : Result { EsFailedDecryptPersonalisedTicket, EsBadDecryptedPersonalisedTicketSize, EsBadTicketSize, + // found ticket has missmatching rights_id from it's name. + EsInvalidTicketBadRightsId, + EsInvalidTicketFromatVersion, + EsInvalidTicketKeyType, + EsInvalidTicketKeyRevision, OwoBadArgs, @@ -630,21 +635,12 @@ enum class SphairaResult : Result { YatiTicketNotFound, // found ticket has missmatching rights_id from it's name. YatiInvalidTicketBadRightsId, - YatiInvalidTicketVersion, - YatiInvalidTicketKeyType, - YatiInvalidTicketKeyRevision, // cert not found for the ticket. YatiCertNotFound, // unable to fetch header from ncm database. YatiNcmDbCorruptHeader, // unable to total infos from ncm database. YatiNcmDbCorruptInfos, - - // found ticket has missmatching rights_id from it's name. - TicketInvalidTicketBadRightsId, - TicketInvalidTicketVersion, - TicketInvalidTicketKeyType, - TicketInvalidTicketKeyRevision, }; #define MAKE_SPHAIRA_RESULT_ENUM(x) Result_##x = MAKERESULT(Module_Sphaira, (Result)SphairaResult::x) @@ -719,6 +715,10 @@ enum : Result { MAKE_SPHAIRA_RESULT_ENUM(EsFailedDecryptPersonalisedTicket), MAKE_SPHAIRA_RESULT_ENUM(EsBadDecryptedPersonalisedTicketSize), MAKE_SPHAIRA_RESULT_ENUM(EsBadTicketSize), + MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketBadRightsId), + MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketFromatVersion), + MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketKeyType), + MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketKeyRevision), MAKE_SPHAIRA_RESULT_ENUM(OwoBadArgs), MAKE_SPHAIRA_RESULT_ENUM(UsbCancelled), MAKE_SPHAIRA_RESULT_ENUM(UsbBadMagic), @@ -750,16 +750,9 @@ enum : Result { MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNczZstdError), MAKE_SPHAIRA_RESULT_ENUM(YatiTicketNotFound), MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidTicketBadRightsId), - MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidTicketVersion), - MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidTicketKeyType), - MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidTicketKeyRevision), MAKE_SPHAIRA_RESULT_ENUM(YatiCertNotFound), MAKE_SPHAIRA_RESULT_ENUM(YatiNcmDbCorruptHeader), MAKE_SPHAIRA_RESULT_ENUM(YatiNcmDbCorruptInfos), - MAKE_SPHAIRA_RESULT_ENUM(TicketInvalidTicketBadRightsId), - MAKE_SPHAIRA_RESULT_ENUM(TicketInvalidTicketVersion), - MAKE_SPHAIRA_RESULT_ENUM(TicketInvalidTicketKeyType), - MAKE_SPHAIRA_RESULT_ENUM(TicketInvalidTicketKeyRevision), }; #undef MAKE_SPHAIRA_RESULT_ENUM diff --git a/sphaira/include/yati/nx/es.hpp b/sphaira/include/yati/nx/es.hpp index 994e7ba..43706a6 100644 --- a/sphaira/include/yati/nx/es.hpp +++ b/sphaira/include/yati/nx/es.hpp @@ -2,49 +2,132 @@ #include #include +#include #include "ncm.hpp" #include "keys.hpp" namespace sphaira::es { -enum { TicketModule = 507 }; - -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 TitleKeyType : u8 { + TitleKeyType_Common = 0, + TitleKeyType_Personalized = 1, }; -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 SigType : u32 { + SigType_Rsa4096Sha1 = 65536, + SigType_Rsa2048Sha1 = 65537, + SigType_Ecc480Sha1 = 65538, + SigType_Rsa4096Sha256 = 65539, + SigType_Rsa2048Sha256 = 65540, + SigType_Ecc480Sha256 = 65541, + SigType_Hmac160Sha1 = 65542 }; -enum TicketTitleKeyType { - TicketTitleKeyType_Common = 0, - TicketTitleKeyType_Personalized = 1, +enum PubKeyType : u32 { + PubKeyType_Rsa4096 = 0, + PubKeyType_Rsa2048 = 1, + PubKeyType_Ecc480 = 2 }; -enum TicketPropertiesBitfield { - TicketPropertiesBitfield_None = 0, - // temporary ticket, removed on restart - TicketPropertiesBitfield_Temporary = 1 << 4, +struct SignatureBlockRsa4096 { + SigType sig_type; + u8 sign[0x200]; + u8 reserved_1[0x3C]; }; +static_assert(sizeof(SignatureBlockRsa4096) == 0x240); + +struct SignatureBlockRsa2048 { + SigType sig_type; + u8 sign[0x100]; + u8 reserved_1[0x3C]; +}; +static_assert(sizeof(SignatureBlockRsa2048) == 0x140); + +struct SignatureBlockEcc480 { + SigType sig_type; + u8 sign[0x3C]; + u8 reserved_1[0x40]; +}; +static_assert(sizeof(SignatureBlockEcc480) == 0x80); + +struct SignatureBlockHmac160 { + SigType sig_type; + u8 sign[0x14]; + u8 reserved_1[0x28]; +}; +static_assert(sizeof(SignatureBlockHmac160) == 0x40); + +struct CertHeader { + char issuer[0x40]; + PubKeyType pub_key_type; + char subject[0x40]; /* ServerId, DeviceId */ + u32 date; +}; +static_assert(sizeof(CertHeader) == 0x88); + +struct PublicKeyBlockRsa4096 { + u8 public_key[0x200]; + u32 public_exponent; + u8 reserved_1[0x34]; +}; +static_assert(sizeof(PublicKeyBlockRsa4096) == 0x238); + +struct PublicKeyBlockRsa2048 { + u8 public_key[0x100]; + u32 public_exponent; + u8 reserved_1[0x34]; +}; +static_assert(sizeof(PublicKeyBlockRsa2048) == 0x138); + +struct PublicKeyBlockEcc480 { + u8 public_key[0x3C]; + u8 reserved_1[0x3C]; +}; +static_assert(sizeof(PublicKeyBlockEcc480) == 0x78); + +template +struct Cert { + Sig signature_block; + CertHeader cert_header; + Pub public_key_block; +}; + +using CertRsa4096PubRsa4096 = Cert; +using CertRsa4096PubRsa2048 = Cert; +using CertRsa4096PubEcc480 = Cert; + +using CertRsa2048PubRsa4096 = Cert; +using CertRsa2048PubRsa2048 = Cert; +using CertRsa2048PubEcc480 = Cert; + +using CertEcc480PubRsa4096 = Cert; +using CertEcc480PubRsa2048 = Cert; +using CertEcc480PubEcc480 = Cert; + +using CertHmac160PubRsa4096 = Cert; +using CertHmac160PubRsa2048 = Cert; +using CertHmac160PubEcc480 = Cert; + +static_assert(sizeof(CertRsa4096PubRsa4096) == 0x500); +static_assert(sizeof(CertRsa4096PubRsa2048) == 0x400); +static_assert(sizeof(CertRsa4096PubEcc480) == 0x340); +static_assert(sizeof(CertRsa2048PubRsa4096) == 0x400); +static_assert(sizeof(CertRsa2048PubRsa2048) == 0x300); +static_assert(sizeof(CertRsa2048PubEcc480) == 0x240); +static_assert(sizeof(CertEcc480PubRsa4096) == 0x340); +static_assert(sizeof(CertEcc480PubRsa2048) == 0x240); +static_assert(sizeof(CertEcc480PubEcc480) == 0x180); +static_assert(sizeof(CertHmac160PubRsa4096) == 0x300); +static_assert(sizeof(CertHmac160PubRsa2048) == 0x200); +static_assert(sizeof(CertHmac160PubEcc480) == 0x140); struct TicketData { - u8 issuer[0x40]; + char issuer[0x40]; u8 title_key_block[0x100]; - u8 ticket_version1; + u8 format_version; u8 title_key_type; - u16 ticket_version2; - u8 license_type; + u16 version; + TitleKeyType license_type; u8 master_key_revision; u16 properties_bitfield; u8 _0x148[0x8]; @@ -52,10 +135,29 @@ struct TicketData { u64 device_id; FsRightsId rights_id; u32 account_id; - u8 _0x174[0xC]; + u32 sect_total_size; + u32 sect_hdr_offset; + u16 sect_hdr_count; + u16 sect_hdr_entry_size; }; static_assert(sizeof(TicketData) == 0x180); +template +struct Ticket { + Sig signature_block; + TicketData data; +}; + +using TicketRsa4096 = Ticket; +using TicketRsa2048 = Ticket; +using TicketEcc480 = Ticket; +using TicketHmac160 = Ticket; + +static_assert(sizeof(TicketRsa4096) == 0x3C0); +static_assert(sizeof(TicketRsa2048) == 0x2C0); +static_assert(sizeof(TicketEcc480) == 0x200); +static_assert(sizeof(TicketHmac160) == 0x1C0); + struct EticketRsaDeviceKey { u8 ctr[AES_128_KEY_SIZE]; u8 private_exponent[0x100]; @@ -89,12 +191,16 @@ Result GetCommonTicketAndCertificateSize(u64 *tik_size_out, u64 *cert_size_out, Result GetCommonTicketAndCertificateData(u64 *tik_size_out, u64 *cert_size_out, void* tik_buf, u64 tik_size, void* cert_buf, u64 cert_size, const FsRightsId* rightsId); // [4.0.0+] // ticket functions. -Result GetTicketDataOffset(std::span ticket, u64& out); +Result GetTicketDataOffset(std::span ticket, u64& out, bool is_cert = false); Result GetTicketData(std::span ticket, es::TicketData* out); -Result SetTicketData(std::span ticket, const es::TicketData* in); +// gets the title key and performs RSA-2048-OAEP if needed. 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); +Result EncryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys); + +Result ShouldPatchTicket(const TicketData& data, std::span ticket, std::span cert_chain, bool patch_personalised, bool& should_patch); +Result ShouldPatchTicket(std::span ticket, std::span cert_chain, bool patch_personalised, bool& should_patch); +Result PatchTicket(std::vector& ticket, std::span cert_chain, u8 key_gen, const keys::Keys& keys, bool patch_personalised); } // namespace sphaira::es diff --git a/sphaira/include/yati/yati.hpp b/sphaira/include/yati/yati.hpp index 85f17ef..f2d1d6b 100644 --- a/sphaira/include/yati/yati.hpp +++ b/sphaira/include/yati/yati.hpp @@ -48,6 +48,9 @@ struct Config { // if set, it will ignore the distribution bit in the nca header. bool ignore_distribution_bit{}; + // converts a personalised ticket to common. + bool convert_to_common_ticket{}; + // 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{}; @@ -69,6 +72,7 @@ struct ConfigOverride { std::optional skip_rsa_header_fixed_key_verify{}; std::optional skip_rsa_npdm_fixed_key_verify{}; std::optional ignore_distribution_bit{}; + std::optional convert_to_common_ticket{}; std::optional convert_to_standard_crypto{}; std::optional lower_master_key{}; std::optional lower_system_version{}; diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index 45eb4ac..f1401cb 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -1333,6 +1333,7 @@ App::App(const char* argv0) { else if (app->m_skip_rsa_header_fixed_key_verify.LoadFrom(Key, Value)) {} else if (app->m_skip_rsa_npdm_fixed_key_verify.LoadFrom(Key, Value)) {} else if (app->m_ignore_distribution_bit.LoadFrom(Key, Value)) {} + else if (app->m_convert_to_common_ticket.LoadFrom(Key, Value)) {} else if (app->m_convert_to_standard_crypto.LoadFrom(Key, Value)) {} else if (app->m_lower_master_key.LoadFrom(Key, Value)) {} else if (app->m_lower_system_version.LoadFrom(Key, Value)) {} @@ -1831,6 +1832,10 @@ void App::DisplayInstallOptions(bool left_side) { App::GetApp()->m_ignore_distribution_bit.Set(enable); })); + options->Add(std::make_shared("Convert to common ticket"_i18n, App::GetApp()->m_convert_to_common_ticket.Get(), [](bool& enable){ + App::GetApp()->m_convert_to_common_ticket.Set(enable); + })); + options->Add(std::make_shared("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [](bool& enable){ App::GetApp()->m_convert_to_standard_crypto.Set(enable); })); @@ -1867,6 +1872,10 @@ void App::DisplayDumpOptions(bool left_side) { options->Add(std::make_shared("Multi-threaded USB transfer"_i18n, App::GetApp()->m_dump_usb_transfer_stream.Get(), [](bool& enable){ App::GetApp()->m_dump_usb_transfer_stream.Set(enable); })); + + options->Add(std::make_shared("Convert to common ticket"_i18n, App::GetApp()->m_dump_convert_to_common_ticket.Get(), [](bool& enable){ + App::GetApp()->m_dump_convert_to_common_ticket.Set(enable); + })); } App::~App() { diff --git a/sphaira/source/ui/error_box.cpp b/sphaira/source/ui/error_box.cpp index 6b3ee0a..981cad9 100644 --- a/sphaira/source/ui/error_box.cpp +++ b/sphaira/source/ui/error_box.cpp @@ -80,6 +80,10 @@ auto GetCodeMessage(Result rc) -> const char* { case Result_EsPersonalisedTicketDeviceIdMissmatch: return "SphairaError_EsPersonalisedTicketDeviceIdMissmatch"; case Result_EsFailedDecryptPersonalisedTicket: return "SphairaError_EsFailedDecryptPersonalisedTicket"; case Result_EsBadDecryptedPersonalisedTicketSize: return "SphairaError_EsBadDecryptedPersonalisedTicketSize"; + case Result_EsInvalidTicketBadRightsId: return "SphairaError_EsInvalidTicketBadRightsId"; + case Result_EsInvalidTicketFromatVersion: return "SphairaError_EsInvalidTicketFromatVersion"; + case Result_EsInvalidTicketKeyType: return "SphairaError_EsInvalidTicketKeyType"; + case Result_EsInvalidTicketKeyRevision: return "SphairaError_EsInvalidTicketKeyRevision"; case Result_OwoBadArgs: return "SphairaError_OwoBadArgs"; case Result_UsbCancelled: return "SphairaError_UsbCancelled"; case Result_UsbBadMagic: return "SphairaError_UsbBadMagic"; @@ -111,16 +115,9 @@ auto GetCodeMessage(Result rc) -> const char* { case Result_YatiInvalidNczZstdError: return "SphairaError_YatiInvalidNczZstdError"; case Result_YatiTicketNotFound: return "SphairaError_YatiTicketNotFound"; case Result_YatiInvalidTicketBadRightsId: return "SphairaError_YatiInvalidTicketBadRightsId"; - case Result_YatiInvalidTicketVersion: return "SphairaError_YatiInvalidTicketVersion"; - case Result_YatiInvalidTicketKeyType: return "SphairaError_YatiInvalidTicketKeyType"; - case Result_YatiInvalidTicketKeyRevision: return "SphairaError_YatiInvalidTicketKeyRevision"; case Result_YatiCertNotFound: return "SphairaError_YatiCertNotFound"; case Result_YatiNcmDbCorruptHeader: return "SphairaError_YatiNcmDbCorruptHeader"; case Result_YatiNcmDbCorruptInfos: return "SphairaError_YatiNcmDbCorruptInfos"; - case Result_TicketInvalidTicketBadRightsId: return "SphairaError_TicketInvalidTicketBadRightsId"; - case Result_TicketInvalidTicketVersion: return "SphairaError_TicketInvalidTicketVersion"; - case Result_TicketInvalidTicketKeyType: return "SphairaError_TicketInvalidTicketKeyType"; - case Result_TicketInvalidTicketKeyRevision: return "SphairaError_TicketInvalidTicketKeyRevision"; } return ""; diff --git a/sphaira/source/ui/menus/game_menu.cpp b/sphaira/source/ui/menus/game_menu.cpp index 5283d30..69b842f 100644 --- a/sphaira/source/ui/menus/game_menu.cpp +++ b/sphaira/source/ui/menus/game_menu.cpp @@ -139,11 +139,12 @@ using MetaEntries = std::vector; struct ContentInfoEntry { NsApplicationContentMetaStatus status{}; std::vector content_infos{}; - std::vector rights_ids{}; + std::vector ncm_rights_id{}; }; struct TikEntry { FsRightsId id{}; + u8 key_gen{}; std::vector tik_data{}; std::vector cert_data{}; }; @@ -500,7 +501,6 @@ auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status) utilsReplaceIllegalCharacters(name_buf, true); char version[sizeof(NacpStruct::display_version) + 1]{}; - // status.storageID if (status.meta_type == NcmContentMetaType_Patch) { u64 program_id; fs::FsPath path; @@ -558,14 +558,13 @@ Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentIn NcmRightsId ncm_rights_id; R_TRY(ncmContentStorageGetRightsIdFromContentId(std::addressof(cs), std::addressof(ncm_rights_id), std::addressof(info_out.content_id), FsContentAttributes_All)); - const auto rights_id = ncm_rights_id.rights_id; - if (isRightsIdValid(rights_id)) { - const auto it = std::ranges::find_if(out.rights_ids, [&rights_id](auto& e){ - return !std::memcmp(&e, &rights_id, sizeof(rights_id)); + if (isRightsIdValid(ncm_rights_id.rights_id)) { + const auto it = std::ranges::find_if(out.ncm_rights_id, [&ncm_rights_id](auto& e){ + return !std::memcmp(&e, &ncm_rights_id, sizeof(ncm_rights_id)); }); - if (it == out.rights_ids.end()) { - out.rights_ids.emplace_back(rights_id); + if (it == out.ncm_rights_id.end()) { + out.ncm_rights_id.emplace_back(ncm_rights_id); } } @@ -582,13 +581,27 @@ Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentIn R_SUCCEED(); } -Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, NspEntry& out) { +Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::Keys& keys, NspEntry& out) { out.application_name = e.GetName(); out.path = BuildNspPath(e, info.status); s64 offset{}; - for (auto& rights_id : info.rights_ids) { - TikEntry entry{rights_id}; + for (auto& e : info.content_infos) { + char nca_name[0x200]; + std::snprintf(nca_name, sizeof(nca_name), "%s%s", hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca"); + + u64 size; + ncmContentInfoSizeToU64(std::addressof(e), std::addressof(size)); + + out.collections.emplace_back(nca_name, offset, size); + offset += size; + } + + for (auto& ncm_rights_id : info.ncm_rights_id) { + const auto rights_id = ncm_rights_id.rights_id; + const auto key_gen = ncm_rights_id.key_generation; + + TikEntry entry{rights_id, key_gen}; log_write("rights id is valid, fetching common ticket and cert\n"); u64 tik_size; @@ -601,6 +614,9 @@ Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, NspEntry& out R_TRY(es::GetCommonTicketAndCertificateData(&tik_size, &cert_size, entry.tik_data.data(), entry.tik_data.size(), entry.cert_data.data(), entry.cert_data.size(), &rights_id)); log_write("got tik_data: %zu cert_data: %zu\n", tik_size, cert_size); + // patch fake ticket / convert personalised to common if needed. + R_TRY(es::PatchTicket(entry.tik_data, entry.cert_data, key_gen, keys, App::GetApp()->m_dump_convert_to_common_ticket.Get())); + char tik_name[0x200]; std::snprintf(tik_name, sizeof(tik_name), "%s%s", hexIdToStr(rights_id).str, ".tik"); @@ -616,17 +632,6 @@ Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, NspEntry& out out.tickets.emplace_back(entry); } - for (auto& e : info.content_infos) { - char nca_name[0x200]; - std::snprintf(nca_name, sizeof(nca_name), "%s%s", hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca"); - - u64 size; - ncmContentInfoSizeToU64(std::addressof(e), std::addressof(size)); - - out.collections.emplace_back(nca_name, offset, size); - offset += size; - } - out.nsp_data = yati::container::Nsp::Build(out.collections, out.nsp_size); out.cs = GetNcmCs(info.status.storageID); @@ -639,12 +644,15 @@ Result BuildNspEntries(Entry& e, u32 flags, std::vector& out) { MetaEntries meta_entries; R_TRY(GetMetaEntries(e, meta_entries, flags)); + keys::Keys keys; + R_TRY(keys::parse_keys(keys, true)); + for (const auto& status : meta_entries) { ContentInfoEntry info; R_TRY(BuildContentEntry(status, info)); NspEntry nsp; - R_TRY(BuildNspEntry(e, info, nsp)); + R_TRY(BuildNspEntry(e, info, keys, nsp)); out.emplace_back(nsp).icon = e.image; } diff --git a/sphaira/source/ui/menus/install_stream_menu_base.cpp b/sphaira/source/ui/menus/install_stream_menu_base.cpp index 35bc737..7197952 100644 --- a/sphaira/source/ui/menus/install_stream_menu_base.cpp +++ b/sphaira/source/ui/menus/install_stream_menu_base.cpp @@ -78,6 +78,7 @@ bool Stream::Push(const void* buf, s64 size) { SCOPED_MUTEX(&m_mutex); if (m_active && m_buffer.size() >= MAX_BUFFER_SIZE) { // unlock the mutex and wait for 1s to bring transfer speed down to 1MiB/s. + log_write("[Stream::Push] buffer is full, delaying\n"); mutexUnlock(&m_mutex); ON_SCOPE_EXIT(mutexLock(&m_mutex)); diff --git a/sphaira/source/yati/nx/es.cpp b/sphaira/source/yati/nx/es.cpp index f44bc6f..9f6a279 100644 --- a/sphaira/source/yati/nx/es.cpp +++ b/sphaira/source/yati/nx/es.cpp @@ -36,6 +36,13 @@ Result ListTicket(u32 cmd_id, s32 *out_entries_written, FsRightsId* out_ids, s32 return rc; } +Result EncyrptDecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys, bool is_encryptor) { + 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), is_encryptor); + R_SUCCEED(); +} + } // namespace Result Initialize() { @@ -130,36 +137,26 @@ Result GetCommonTicketAndCertificateData(u64 *tik_size_out, u64 *cert_size_out, return rc; } -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) { +Result GetTicketDataOffset(std::span ticket, u64& out, bool is_cert) { 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; + if (is_cert) { + signature_type = std::byteswap(signature_type); + } + + switch (signature_type) { + case SigType_Rsa4096Sha1: log_write("RSA-4096 PKCS#1 v1.5 with SHA-1\n"); out = sizeof(SignatureBlockRsa4096); break; + case SigType_Rsa2048Sha1: log_write("RSA-2048 PKCS#1 v1.5 with SHA-1\n"); out = sizeof(SignatureBlockRsa2048); break; + case SigType_Ecc480Sha1: log_write("ECDSA with SHA-1\n"); out = sizeof(SignatureBlockEcc480); break; + case SigType_Rsa4096Sha256: log_write("RSA-4096 PKCS#1 v1.5 with SHA-256\n"); out = sizeof(SignatureBlockRsa4096); break; + case SigType_Rsa2048Sha256: log_write("RSA-2048 PKCS#1 v1.5 with SHA-256\n"); out = sizeof(SignatureBlockRsa2048); break; + case SigType_Ecc480Sha256: log_write("ECDSA with SHA-256\n"); out = sizeof(SignatureBlockEcc480); break; + case SigType_Hmac160Sha1: log_write("HMAC-SHA1-160\n"); out = sizeof(SignatureBlockHmac160); break; + default: log_write("unknown ticket: %u\n", signature_type); R_THROW(Result_EsBadTitleKeyType); } - // align-up to 0x40. - out = ((signature_size + sizeof(signature_type)) + 0x3F) & ~0x3F; R_SUCCEED(); } @@ -175,25 +172,18 @@ Result GetTicketData(std::span ticket, es::TicketData* out) { // validate ticket data. log_write("[ES] validating ticket data\n"); - 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_UNLESS(out->format_version == 0x2, Result_EsInvalidTicketFromatVersion); // must be version 2. + R_UNLESS(out->title_key_type == es::TitleKeyType_Common || out->title_key_type == es::TitleKeyType_Personalized, Result_EsInvalidTicketKeyType); + R_UNLESS(out->master_key_revision <= 0x20, Result_EsInvalidTicketKeyRevision); log_write("[ES] valid ticket data\n"); 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) { + if (data.title_key_type == es::TitleKeyType_Common) { std::memcpy(std::addressof(out), data.title_key_block, sizeof(out)); - } else if (data.title_key_type == es::TicketTitleKeyType_Personalized) { + } else if (data.title_key_type == es::TitleKeyType_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); @@ -217,25 +207,125 @@ Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys } 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(); + return EncyrptDecryptTitleKey(out, key_gen, keys, false); } -// todo: i thought i already wrote the code for this?? -// todo: patch the ticket. -Result PatchTicket(std::span ticket, const keys::Keys& keys) { +Result EncryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys) { + return EncyrptDecryptTitleKey(out, key_gen, keys, true); +} + +// this function is taken from nxdumptool +Result ShouldPatchTicket(const TicketData& data, std::span ticket, std::span cert_chain, bool patch_personalised, bool& should_patch) { + should_patch = false; + + if (data.title_key_type == es::TitleKeyType_Common) { + SigType tik_sig_type; + std::memcpy(&tik_sig_type, ticket.data(), sizeof(tik_sig_type)); + + if (tik_sig_type != SigType_Rsa2048Sha256) { + R_SUCCEED(); + } + + const auto cert_name = std::strrchr(data.issuer, '-') + 1; + R_UNLESS(cert_name, Result_EsBadTitleKeyType); + const auto cert_name_span = std::span{(const u8*)cert_name, std::strlen(cert_name)}; + + // find the cert from inside the cert chain. + const auto it = std::ranges::search(cert_chain, cert_name_span); + R_UNLESS(!it.empty(), Result_EsBadTitleKeyType); + const auto cert = cert_chain.subspan(std::distance(cert_chain.begin(), it.begin()) - offsetof(CertHeader, subject)); + + const auto cert_header = (const CertHeader*)cert.data(); + const auto pub_key_type = std::byteswap(cert_header->pub_key_type); + log_write("[ES] cert_header->issuer: %s\n", cert_header->issuer); + log_write("[ES] cert_header->pub_key_type: %u\n", pub_key_type); + log_write("[ES] cert_header->subject: %s\n", cert_header->subject); + + std::span public_key{}; + u32 public_exponent{}; + + switch (pub_key_type) { + case PubKeyType_Rsa4096: { + auto pub_key = (const PublicKeyBlockRsa4096*)(cert.data() + sizeof(CertHeader)); + public_key = pub_key->public_key; + public_exponent = pub_key->public_exponent; + } break; + case PubKeyType_Rsa2048: { + auto pub_key = (const PublicKeyBlockRsa2048*)(cert.data() + sizeof(CertHeader)); + public_key = pub_key->public_key; + public_exponent = pub_key->public_exponent; + } break; + case PubKeyType_Ecc480: { + R_SUCCEED(); + } break; + default: + R_THROW(Result_EsBadTitleKeyType); + } + + const auto tik = (const TicketRsa2048*)ticket.data(); + const auto check_data = ticket.subspan(offsetof(TicketRsa2048, data)); + + if (rsa2048VerifySha256BasedPkcs1v15Signature(check_data.data(), check_data.size(), tik->signature_block.sign, public_key.data(), &public_exponent, sizeof(public_exponent))) { + log_write("[ES] common ticket is same\n"); + } else { + log_write("[ES] common ticket is modified\n"); + should_patch = true; + } + + R_SUCCEED(); + } else if (data.title_key_type == es::TitleKeyType_Personalized) { + if (patch_personalised) { + log_write("[ES] patching personalised ticket\n"); + } else { + log_write("[ES] keeping personalised ticket\n"); + } + + should_patch = patch_personalised; + R_SUCCEED(); + } else { + R_THROW(Result_EsBadTitleKeyType); + } +} + +Result ShouldPatchTicket(std::span ticket, std::span cert_chain, bool patch_personalised, bool& should_patch) { 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) { + return ShouldPatchTicket(data, ticket, cert_chain, patch_personalised, should_patch); +} +Result PatchTicket(std::vector& ticket, std::span cert_chain, u8 key_gen, const keys::Keys& keys, bool patch_personalised) { + TicketData data; + R_TRY(GetTicketData(ticket, &data)); + + // check if we should create a fake common ticket. + bool should_patch; + R_TRY(ShouldPatchTicket(data, ticket, cert_chain, patch_personalised, should_patch)); + + if (!should_patch) { + R_SUCCEED(); } + // store copy of rights id an title key. + keys::KeyEntry title_key; + R_TRY(GetTitleKey(title_key, data, keys)); + const auto rights_id = data.rights_id; + + // following StandardNSP format. + TicketRsa2048 out{}; + out.signature_block.sig_type = SigType_Rsa2048Sha256; + std::memset(out.signature_block.sign, 0xFF, sizeof(out.signature_block.sign)); + std::strcpy(out.data.issuer, "Root-CA00000003-XS00000020"); + std::memcpy(out.data.title_key_block, title_key.key, sizeof(title_key.key)); + out.data.format_version = 0x2; + out.data.master_key_revision = key_gen; + out.data.rights_id = rights_id; + out.data.sect_hdr_offset = ticket.size(); + + // overwrite old ticket with new fake ticket data. + ticket.resize(sizeof(out)); + std::memcpy(ticket.data(), &out, sizeof(out)); + R_SUCCEED(); } diff --git a/sphaira/source/yati/yati.cpp b/sphaira/source/yati/yati.cpp index 1df7391..bea5ca0 100644 --- a/sphaira/source/yati/yati.cpp +++ b/sphaira/source/yati/yati.cpp @@ -65,8 +65,12 @@ struct TikCollection { std::vector cert{}; // set via the name of the ticket. FsRightsId rights_id{}; + // retrieved via the master key set in nca. + u8 key_gen{}; // set if ticket is required by an nca. bool required{}; + // set if ticket has already been patched. + bool patched{}; }; struct Yati; @@ -319,10 +323,6 @@ auto isRightsIdValid(FsRightsId id) -> bool { 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]; }; @@ -335,6 +335,38 @@ HashStr hexIdToStr(auto id) { return str; } +auto GetTicketCollection(const nca::Header& header, std::span tik) -> TikCollection* { + TikCollection* ticket{}; + + if (isRightsIdValid(header.rights_id)) { + auto it = std::ranges::find_if(tik, [&header](auto& e){ + return !std::memcmp(&header.rights_id, &e.rights_id, sizeof(e.rights_id)); + }); + + if (it != tik.end()) { + it->required = true; + it->key_gen = header.key_gen; + ticket = &(*it); + } + } + + return ticket; +} + +Result HasRequiredTicket(const nca::Header& header, TikCollection* ticket) { + if (isRightsIdValid(header.rights_id)) { + log_write("looking for ticket %s\n", hexIdToStr(header.rights_id).str); + R_UNLESS(ticket, Result_YatiTicketNotFound); + log_write("ticket found\n"); + } + R_SUCCEED(); +} + +Result HasRequiredTicket(const nca::Header& header, std::span tik) { + auto ticket = GetTicketCollection(header, tik); + return HasRequiredTicket(header, ticket); +} + // read thread reads all data from the source, it also handles // parsing ncz headers, sections and reading ncz blocks Result Yati::readFuncInternal(ThreadData* t) { @@ -532,33 +564,24 @@ Result Yati::decompressFuncInternal(ThreadData* t) { } t->write_size = header.size; - log_write("setting placeholder size: %zu\n", header.size); - R_TRY(ncmContentStorageSetPlaceHolderSize(std::addressof(cs), std::addressof(t->nca->placeholder_id), header.size)); + log_write("setting placeholder size: %zu\n", t->write_size); + R_TRY(ncmContentStorageSetPlaceHolderSize(std::addressof(cs), std::addressof(t->nca->placeholder_id), t->write_size)); if (!config.ignore_distribution_bit && 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::ranges::find_if(t->tik, [&header](auto& e){ - return !std::memcmp(&header.rights_id, &e.rights_id, sizeof(e.rights_id)); - }); + // try and get the ticket, if the nca requires it. + auto ticket = GetTicketCollection(header, t->tik); + R_TRY(HasRequiredTicket(header, ticket)); - log_write("looking for ticket %s\n", hexIdToStr(header.rights_id).str); - R_UNLESS(it != t->tik.end(), Result_YatiTicketNotFound); - log_write("ticket found\n"); - it->required = true; - ticket = &(*it); - } - - if ((config.convert_to_standard_crypto && isRightsIdValid(header.rights_id)) || config.lower_master_key) { + if ((config.convert_to_standard_crypto && ticket) || config.lower_master_key) { t->nca->modified = true; u8 keak_generation; - if (isRightsIdValid(header.rights_id)) { - const auto key_gen = getKeyGenFromRightsId(header.rights_id); + if (ticket) { + const auto key_gen = header.key_gen; log_write("converting to standard crypto: 0x%X 0x%X\n", key_gen, header.key_gen); // fetch ticket data block. @@ -568,19 +591,6 @@ Result Yati::decompressFuncInternal(ThreadData* t) { // 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_YatiInvalidTicketBadRightsId); - // 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)); @@ -815,6 +825,7 @@ Result Yati::Setup(const ConfigOverride& override) { config.skip_rsa_header_fixed_key_verify = override.skip_rsa_header_fixed_key_verify.value_or(App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get()); config.skip_rsa_npdm_fixed_key_verify = override.skip_rsa_npdm_fixed_key_verify.value_or(App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get()); config.ignore_distribution_bit = override.ignore_distribution_bit.value_or(App::GetApp()->m_ignore_distribution_bit.Get()); + config.convert_to_common_ticket = override.convert_to_common_ticket.value_or(App::GetApp()->m_convert_to_common_ticket.Get()); config.convert_to_standard_crypto = override.convert_to_standard_crypto.value_or(App::GetApp()->m_convert_to_standard_crypto.Get()); config.lower_master_key = override.lower_master_key.value_or(App::GetApp()->m_lower_master_key.Get()); config.lower_system_version = override.lower_system_version.value_or(App::GetApp()->m_lower_system_version.Get()); @@ -839,12 +850,14 @@ Result Yati::Setup(const ConfigOverride& override) { } Result Yati::InstallNcaInternal(std::span tickets, NcaCollection& nca) { - if (config.skip_if_already_installed) { + if (config.skip_if_already_installed || config.ticket_only) { R_TRY(ncmContentStorageHas(std::addressof(cs), std::addressof(nca.skipped), std::addressof(nca.content_id))); if (nca.skipped) { log_write("\tskipped nca as it's already installed ncmContentStorageHas()\n"); R_TRY(ncmContentStorageReadContentIdFile(std::addressof(cs), std::addressof(nca.header), sizeof(nca.header), std::addressof(nca.content_id), 0)); crypto::cryptoAes128Xts(std::addressof(nca.header), std::addressof(nca.header), keys.header_key, 0, 0x200, sizeof(nca.header), false); + + R_TRY(HasRequiredTicket(nca.header, tickets)); R_SUCCEED(); } } @@ -1105,23 +1118,7 @@ Result Yati::GetLatestVersion(const CnmtCollection& cnmt, u32& version_out, bool } Result Yati::ShouldSkip(const CnmtCollection& cnmt, bool& skip) { - // skip invalid types - if (!(cnmt.key.type & 0x80)) { - log_write("\tskipping: invalid: %u\n", cnmt.key.type); - skip = true; - } else if (config.skip_base && cnmt.key.type == NcmContentMetaType_Application) { - log_write("\tskipping: [NcmContentMetaType_Application]\n"); - skip = true; - } else if (config.skip_patch && cnmt.key.type == NcmContentMetaType_Patch) { - log_write("\tskipping: [NcmContentMetaType_Application]\n"); - skip = true; - } else if (config.skip_addon && cnmt.key.type == NcmContentMetaType_AddOnContent) { - log_write("\tskipping: [NcmContentMetaType_AddOnContent]\n"); - skip = true; - } else if (config.skip_data_patch && cnmt.key.type == NcmContentMetaType_DataPatch) { - log_write("\tskipping: [NcmContentMetaType_DataPatch]\n"); - skip = true; - } else if (config.skip_if_already_installed) { + if (!skip && config.skip_if_already_installed) { bool has; R_TRY(ncmContentMetaDatabaseHas(std::addressof(db), std::addressof(has), std::addressof(cnmt.key))); if (has) { @@ -1130,17 +1127,41 @@ Result Yati::ShouldSkip(const CnmtCollection& cnmt, bool& skip) { } } + // skip invalid types + if (!skip) { + if (!(cnmt.key.type & 0x80)) { + log_write("\tskipping: invalid: %u\n", cnmt.key.type); + skip = true; + } else if (config.skip_base && cnmt.key.type == NcmContentMetaType_Application) { + log_write("\tskipping: [NcmContentMetaType_Application]\n"); + skip = true; + } else if (config.skip_patch && cnmt.key.type == NcmContentMetaType_Patch) { + log_write("\tskipping: [NcmContentMetaType_Application]\n"); + skip = true; + } else if (config.skip_addon && cnmt.key.type == NcmContentMetaType_AddOnContent) { + log_write("\tskipping: [NcmContentMetaType_AddOnContent]\n"); + skip = true; + } else if (config.skip_data_patch && cnmt.key.type == NcmContentMetaType_DataPatch) { + log_write("\tskipping: [NcmContentMetaType_DataPatch]\n"); + skip = true; + } + } + R_SUCCEED(); } Result Yati::ImportTickets(std::span tickets) { for (auto& ticket : tickets) { - if (ticket.required) { + if (ticket.required || config.ticket_only) { if (config.skip_ticket) { log_write("WARNING: skipping ticket install, but it's required!\n"); } else { - log_write("patching ticket\n"); - R_TRY(es::PatchTicket(ticket.ticket, keys)); + if (!ticket.patched) { + log_write("patching ticket\n"); + R_TRY(es::PatchTicket(ticket.ticket, ticket.cert, ticket.key_gen, keys, config.convert_to_common_ticket)); + ticket.patched = true; + } + log_write("installing ticket\n"); R_TRY(es::ImportTicket(ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size())); ticket.required = false; @@ -1181,7 +1202,7 @@ Result Yati::RemoveInstalledNcas(const CnmtCollection& cnmt) { } } - for (auto& key : keys) { + for (const 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; @@ -1196,7 +1217,16 @@ Result Yati::RemoveInstalledNcas(const CnmtCollection& cnmt) { R_UNLESS(content_info_out == infos.size(), Result_YatiNcmDbCorruptInfos); log_write("size matches\n"); - for (auto& info : infos) { + for (const auto& info : infos) { + const auto it = std::ranges::find_if(cnmt.ncas, [&info](auto& e){ + return !std::memcmp(&e.content_id, &info.content_id, sizeof(e.content_id)); + }); + + // don't delete the nca if we skipped the install. + if ((it != cnmt.ncas.cend() && it->skipped) || (!std::memcmp(&cnmt.content_id, &info.content_id, sizeof(cnmt.content_id)) && cnmt.skipped)) { + continue; + } + R_TRY(ncm::Delete(std::addressof(cs), std::addressof(info.content_id))); } @@ -1215,9 +1245,11 @@ Result Yati::RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_ve const auto app_id = ncm::GetAppId(cnmt.key); // register all nca's - log_write("registering cnmt nca\n"); - R_TRY(ncm::Register(std::addressof(cs), std::addressof(cnmt.content_id), std::addressof(cnmt.placeholder_id))); - log_write("registered cnmt nca\n"); + if (!cnmt.skipped) { + log_write("registering cnmt nca\n"); + R_TRY(ncm::Register(std::addressof(cs), std::addressof(cnmt.content_id), std::addressof(cnmt.placeholder_id))); + log_write("registered cnmt nca\n"); + } for (auto& nca : cnmt.ncas) { if (!nca.skipped && nca.type != NcmContentType_DeltaFragment) {