add personalised -> common convert. patch bad common tickets. fix yati installing nca's if already installed.
- dumped nsp now have the tik/cert at the end of the file table, rather than the beginning. - dumped nsp patches the ticket if needed (no personalised dumping yet). - installing titles will now patch the ticket, performing personalised -> common convert if needed, as well as fixing bad common tickets. - yati no longer tries to install ncas if they already exist. - ticket only option now actually works. - fixed some translations. - removed unused error codes.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,49 +2,132 @@
|
||||
|
||||
#include <switch.h>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
#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<typename Sig, typename Pub>
|
||||
struct Cert {
|
||||
Sig signature_block;
|
||||
CertHeader cert_header;
|
||||
Pub public_key_block;
|
||||
};
|
||||
|
||||
using CertRsa4096PubRsa4096 = Cert<SignatureBlockRsa4096, PublicKeyBlockRsa4096>;
|
||||
using CertRsa4096PubRsa2048 = Cert<SignatureBlockRsa4096, PublicKeyBlockRsa2048>;
|
||||
using CertRsa4096PubEcc480 = Cert<SignatureBlockRsa4096, PublicKeyBlockEcc480>;
|
||||
|
||||
using CertRsa2048PubRsa4096 = Cert<SignatureBlockRsa2048, PublicKeyBlockRsa4096>;
|
||||
using CertRsa2048PubRsa2048 = Cert<SignatureBlockRsa2048, PublicKeyBlockRsa2048>;
|
||||
using CertRsa2048PubEcc480 = Cert<SignatureBlockRsa2048, PublicKeyBlockEcc480>;
|
||||
|
||||
using CertEcc480PubRsa4096 = Cert<SignatureBlockEcc480, PublicKeyBlockRsa4096>;
|
||||
using CertEcc480PubRsa2048 = Cert<SignatureBlockEcc480, PublicKeyBlockRsa2048>;
|
||||
using CertEcc480PubEcc480 = Cert<SignatureBlockEcc480, PublicKeyBlockEcc480>;
|
||||
|
||||
using CertHmac160PubRsa4096 = Cert<SignatureBlockHmac160, PublicKeyBlockRsa4096>;
|
||||
using CertHmac160PubRsa2048 = Cert<SignatureBlockHmac160, PublicKeyBlockRsa2048>;
|
||||
using CertHmac160PubEcc480 = Cert<SignatureBlockHmac160, PublicKeyBlockEcc480>;
|
||||
|
||||
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<typename Sig>
|
||||
struct Ticket {
|
||||
Sig signature_block;
|
||||
TicketData data;
|
||||
};
|
||||
|
||||
using TicketRsa4096 = Ticket<SignatureBlockRsa4096>;
|
||||
using TicketRsa2048 = Ticket<SignatureBlockRsa2048>;
|
||||
using TicketEcc480 = Ticket<SignatureBlockEcc480>;
|
||||
using TicketHmac160 = Ticket<SignatureBlockHmac160>;
|
||||
|
||||
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<const u8> ticket, u64& out);
|
||||
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out, bool is_cert = false);
|
||||
Result GetTicketData(std::span<const u8> ticket, es::TicketData* out);
|
||||
Result SetTicketData(std::span<u8> 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<u8> ticket, const keys::Keys& keys);
|
||||
Result EncryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys);
|
||||
|
||||
Result ShouldPatchTicket(const TicketData& data, std::span<const u8> ticket, std::span<const u8> cert_chain, bool patch_personalised, bool& should_patch);
|
||||
Result ShouldPatchTicket(std::span<const u8> ticket, std::span<const u8> cert_chain, bool patch_personalised, bool& should_patch);
|
||||
Result PatchTicket(std::vector<u8>& ticket, std::span<const u8> cert_chain, u8 key_gen, const keys::Keys& keys, bool patch_personalised);
|
||||
|
||||
} // namespace sphaira::es
|
||||
|
||||
@@ -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<bool> skip_rsa_header_fixed_key_verify{};
|
||||
std::optional<bool> skip_rsa_npdm_fixed_key_verify{};
|
||||
std::optional<bool> ignore_distribution_bit{};
|
||||
std::optional<bool> convert_to_common_ticket{};
|
||||
std::optional<bool> convert_to_standard_crypto{};
|
||||
std::optional<bool> lower_master_key{};
|
||||
std::optional<bool> lower_system_version{};
|
||||
|
||||
@@ -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<ui::SidebarEntryBool>("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<ui::SidebarEntryBool>("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<ui::SidebarEntryBool>("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<ui::SidebarEntryBool>("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() {
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
@@ -139,11 +139,12 @@ using MetaEntries = std::vector<NsApplicationContentMetaStatus>;
|
||||
struct ContentInfoEntry {
|
||||
NsApplicationContentMetaStatus status{};
|
||||
std::vector<NcmContentInfo> content_infos{};
|
||||
std::vector<FsRightsId> rights_ids{};
|
||||
std::vector<NcmRightsId> ncm_rights_id{};
|
||||
};
|
||||
|
||||
struct TikEntry {
|
||||
FsRightsId id{};
|
||||
u8 key_gen{};
|
||||
std::vector<u8> tik_data{};
|
||||
std::vector<u8> 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<NspEntry>& 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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<const u8> ticket, u64& out) {
|
||||
Result GetTicketDataOffset(std::span<const u8> 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<const u8> 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<u8> 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<u8> 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<const u8> ticket, std::span<const u8> 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<u32>(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<const u8> 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<const u8> ticket, std::span<const u8> 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<u8>& ticket, std::span<const u8> 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -65,8 +65,12 @@ struct TikCollection {
|
||||
std::vector<u8> 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<TikCollection> 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<TikCollection> 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<TikCollection> 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,7 +1118,17 @@ Result Yati::GetLatestVersion(const CnmtCollection& cnmt, u32& version_out, bool
|
||||
}
|
||||
|
||||
Result Yati::ShouldSkip(const CnmtCollection& cnmt, bool& skip) {
|
||||
if (!skip && config.skip_if_already_installed) {
|
||||
bool has;
|
||||
R_TRY(ncmContentMetaDatabaseHas(std::addressof(db), std::addressof(has), std::addressof(cnmt.key)));
|
||||
if (has) {
|
||||
log_write("\tskipping: [ncmContentMetaDatabaseHas()]\n");
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
// skip invalid types
|
||||
if (!skip) {
|
||||
if (!(cnmt.key.type & 0x80)) {
|
||||
log_write("\tskipping: invalid: %u\n", cnmt.key.type);
|
||||
skip = true;
|
||||
@@ -1121,12 +1144,6 @@ Result Yati::ShouldSkip(const CnmtCollection& cnmt, bool& skip) {
|
||||
} 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) {
|
||||
bool has;
|
||||
R_TRY(ncmContentMetaDatabaseHas(std::addressof(db), std::addressof(has), std::addressof(cnmt.key)));
|
||||
if (has) {
|
||||
log_write("\tskipping: [ncmContentMetaDatabaseHas()]\n");
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1135,12 +1152,16 @@ Result Yati::ShouldSkip(const CnmtCollection& cnmt, bool& skip) {
|
||||
|
||||
Result Yati::ImportTickets(std::span<TikCollection> 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 {
|
||||
if (!ticket.patched) {
|
||||
log_write("patching ticket\n");
|
||||
R_TRY(es::PatchTicket(ticket.ticket, keys));
|
||||
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
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user