add basic support for title installing
This commit is contained in:
@@ -74,6 +74,19 @@ add_executable(sphaira
|
|||||||
source/web.cpp
|
source/web.cpp
|
||||||
source/i18n.cpp
|
source/i18n.cpp
|
||||||
source/ftpsrv_helper.cpp
|
source/ftpsrv_helper.cpp
|
||||||
|
|
||||||
|
source/yati/yati.cpp
|
||||||
|
source/yati/container/nsp.cpp
|
||||||
|
source/yati/container/xci.cpp
|
||||||
|
source/yati/source/file.cpp
|
||||||
|
source/yati/source/stdio.cpp
|
||||||
|
|
||||||
|
source/yati/nx/es.cpp
|
||||||
|
source/yati/nx/keys.cpp
|
||||||
|
source/yati/nx/nca.cpp
|
||||||
|
source/yati/nx/ncm.cpp
|
||||||
|
source/yati/nx/ns.cpp
|
||||||
|
source/yati/nx/nxdumptool_rsa.c
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_definitions(sphaira PRIVATE
|
target_compile_definitions(sphaira PRIVATE
|
||||||
@@ -161,6 +174,24 @@ FetchContent_Declare(minIni
|
|||||||
GIT_TAG 11cac8b
|
GIT_TAG 11cac8b
|
||||||
)
|
)
|
||||||
|
|
||||||
|
FetchContent_Declare(zstd
|
||||||
|
GIT_REPOSITORY https://github.com/facebook/zstd.git
|
||||||
|
GIT_TAG v1.5.7
|
||||||
|
SOURCE_SUBDIR build/cmake
|
||||||
|
)
|
||||||
|
|
||||||
|
set(USE_NEW_ZSTD ON)
|
||||||
|
|
||||||
|
set(ZSTD_BUILD_STATIC ON)
|
||||||
|
set(ZSTD_BUILD_SHARED OFF)
|
||||||
|
set(ZSTD_BUILD_COMPRESSION OFF)
|
||||||
|
set(ZSTD_BUILD_DECOMPRESSION ON)
|
||||||
|
set(ZSTD_BUILD_DICTBUILDER OFF)
|
||||||
|
set(ZSTD_LEGACY_SUPPORT OFF)
|
||||||
|
set(ZSTD_MULTITHREAD_SUPPORT OFF)
|
||||||
|
set(ZSTD_BUILD_PROGRAMS OFF)
|
||||||
|
set(ZSTD_BUILD_TESTS OFF)
|
||||||
|
|
||||||
set(MININI_LIB_NAME minIni)
|
set(MININI_LIB_NAME minIni)
|
||||||
set(MININI_USE_STDIO ON)
|
set(MININI_USE_STDIO ON)
|
||||||
set(MININI_USE_NX ON)
|
set(MININI_USE_NX ON)
|
||||||
@@ -195,6 +226,7 @@ FetchContent_MakeAvailable(
|
|||||||
stb
|
stb
|
||||||
minIni
|
minIni
|
||||||
yyjson
|
yyjson
|
||||||
|
zstd
|
||||||
)
|
)
|
||||||
|
|
||||||
set(FTPSRV_LIB_BUILD TRUE)
|
set(FTPSRV_LIB_BUILD TRUE)
|
||||||
@@ -274,6 +306,11 @@ find_package(CURL REQUIRED)
|
|||||||
find_path(mbedtls_inc mbedtls REQUIRED)
|
find_path(mbedtls_inc mbedtls REQUIRED)
|
||||||
find_library(mbedcrypto_lib mbedcrypto REQUIRED)
|
find_library(mbedcrypto_lib mbedcrypto REQUIRED)
|
||||||
|
|
||||||
|
if (NOT USE_NEW_ZSTD)
|
||||||
|
find_path(zstd_inc zstd.h REQUIRED)
|
||||||
|
find_library(zstd_lib zstd REQUIRED)
|
||||||
|
endif()
|
||||||
|
|
||||||
set_target_properties(sphaira PROPERTIES
|
set_target_properties(sphaira PROPERTIES
|
||||||
C_STANDARD 11
|
C_STANDARD 11
|
||||||
C_EXTENSIONS ON
|
C_EXTENSIONS ON
|
||||||
@@ -296,6 +333,15 @@ target_link_libraries(sphaira PRIVATE
|
|||||||
${mbedcrypto_lib}
|
${mbedcrypto_lib}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (USE_NEW_ZSTD)
|
||||||
|
message(STATUS "USING UPSTREAM ZSTD")
|
||||||
|
target_link_libraries(sphaira PRIVATE libzstd_static)
|
||||||
|
else()
|
||||||
|
message(STATUS "USING LOCAL ZSTD")
|
||||||
|
target_link_libraries(sphaira PRIVATE ${zstd_lib})
|
||||||
|
target_include_directories(sphaira PRIVATE ${zstd_inc})
|
||||||
|
endif()
|
||||||
|
|
||||||
target_include_directories(sphaira PRIVATE
|
target_include_directories(sphaira PRIVATE
|
||||||
include
|
include
|
||||||
${minizip_inc}
|
${minizip_inc}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ public:
|
|||||||
~App();
|
~App();
|
||||||
void Loop();
|
void Loop();
|
||||||
|
|
||||||
|
static App* GetApp();
|
||||||
|
|
||||||
static void Exit();
|
static void Exit();
|
||||||
static void ExitRestart();
|
static void ExitRestart();
|
||||||
static auto GetVg() -> NVGcontext*;
|
static auto GetVg() -> NVGcontext*;
|
||||||
@@ -172,12 +174,31 @@ public:
|
|||||||
option::OptionBool m_ftp_enabled{INI_SECTION, "ftp_enabled", false};
|
option::OptionBool m_ftp_enabled{INI_SECTION, "ftp_enabled", false};
|
||||||
option::OptionBool m_log_enabled{INI_SECTION, "log_enabled", false};
|
option::OptionBool m_log_enabled{INI_SECTION, "log_enabled", false};
|
||||||
option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", false};
|
option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", false};
|
||||||
option::OptionBool m_install{INI_SECTION, "install", false};
|
|
||||||
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
|
|
||||||
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
|
|
||||||
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
|
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
|
||||||
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false};
|
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false};
|
||||||
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
|
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
|
||||||
|
|
||||||
|
// install options
|
||||||
|
option::OptionBool m_install{INI_SECTION, "install", false};
|
||||||
|
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
|
||||||
|
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
|
||||||
|
option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false};
|
||||||
|
option::OptionBool m_skip_if_already_installed{INI_SECTION, "skip_if_already_installed", true};
|
||||||
|
option::OptionBool m_ticket_only{INI_SECTION, "ticket_only", false};
|
||||||
|
option::OptionBool m_patch_ticket{INI_SECTION, "patch_ticket", true};
|
||||||
|
option::OptionBool m_skip_base{INI_SECTION, "skip_base", false};
|
||||||
|
option::OptionBool m_skip_patch{INI_SECTION, "skip_patch", false};
|
||||||
|
option::OptionBool m_skip_addon{INI_SECTION, "skip_addon", false};
|
||||||
|
option::OptionBool m_skip_data_patch{INI_SECTION, "skip_data_patch", false};
|
||||||
|
option::OptionBool m_skip_ticket{INI_SECTION, "skip_ticket", false};
|
||||||
|
option::OptionBool m_skip_nca_hash_verify{INI_SECTION, "skip_nca_hash_verify", false};
|
||||||
|
option::OptionBool m_skip_rsa_header_fixed_key_verify{INI_SECTION, "skip_rsa_header_fixed_key_verify", false};
|
||||||
|
option::OptionBool m_skip_rsa_npdm_fixed_key_verify{INI_SECTION, "skip_rsa_npdm_fixed_key_verify", false};
|
||||||
|
option::OptionBool m_ignore_distribution_bit{INI_SECTION, "ignore_distribution_bit", false};
|
||||||
|
option::OptionBool m_convert_to_standard_crypto{INI_SECTION, "convert_to_standard_crypto", false};
|
||||||
|
option::OptionBool m_lower_master_key{INI_SECTION, "lower_master_key", false};
|
||||||
|
option::OptionBool m_lower_system_version{INI_SECTION, "lower_system_version", false};
|
||||||
|
|
||||||
// todo: move this into it's own menu
|
// todo: move this into it's own menu
|
||||||
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
|
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,33 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
#define sphaira_USE_LOG 1
|
#define sphaira_USE_LOG 1
|
||||||
|
|
||||||
#include <cstdarg>
|
#include <stdarg.h>
|
||||||
|
|
||||||
#if sphaira_USE_LOG
|
#if sphaira_USE_LOG
|
||||||
auto log_file_init() -> bool;
|
bool log_file_init();
|
||||||
auto log_nxlink_init() -> bool;
|
bool log_nxlink_init();
|
||||||
void log_file_exit();
|
void log_file_exit();
|
||||||
void log_nxlink_exit();
|
void log_nxlink_exit();
|
||||||
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
|
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
|
||||||
void log_write_arg(const char* s, std::va_list& v);
|
void log_write_arg(const char* s, va_list* v);
|
||||||
#else
|
#else
|
||||||
inline auto log_file_init() -> bool {
|
inline bool log_file_init() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
inline auto log_nxlink_init() -> bool {
|
inline bool log_nxlink_init() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#define log_file_exit()
|
#define log_file_exit()
|
||||||
#define log_nxlink_exit()
|
#define log_nxlink_exit()
|
||||||
#define log_write(...)
|
#define log_write(...)
|
||||||
|
#define log_write_arg(...)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ struct Menu final : MenuBase {
|
|||||||
private:
|
private:
|
||||||
void SetIndex(s64 index);
|
void SetIndex(s64 index);
|
||||||
void InstallForwarder();
|
void InstallForwarder();
|
||||||
|
void InstallFile(const FileEntry& target);
|
||||||
|
void InstallFiles(const std::vector<FileEntry>& targets);
|
||||||
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
|
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
|
||||||
|
|
||||||
void LoadAssocEntriesPath(const fs::FsPath& path);
|
void LoadAssocEntriesPath(const fs::FsPath& path);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ struct ProgressBox final : Widget {
|
|||||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||||
|
|
||||||
|
auto SetTitle(const std::string& title) -> ProgressBox&;
|
||||||
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
|
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
|
||||||
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
|
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
|
||||||
void RequestExit();
|
void RequestExit();
|
||||||
|
|||||||
39
sphaira/include/yati/container/base.hpp
Normal file
39
sphaira/include/yati/container/base.hpp
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "yati/source/base.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::container {
|
||||||
|
|
||||||
|
enum class CollectionType {
|
||||||
|
CollectionType_NCA,
|
||||||
|
CollectionType_NCZ,
|
||||||
|
CollectionType_TIK,
|
||||||
|
CollectionType_CERT,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CollectionEntry {
|
||||||
|
// collection name within file.
|
||||||
|
std::string name{};
|
||||||
|
// collection offset within file.
|
||||||
|
s64 offset{};
|
||||||
|
// collection size within file, may be compressed size.
|
||||||
|
s64 size{};
|
||||||
|
};
|
||||||
|
|
||||||
|
using Collections = std::vector<CollectionEntry>;
|
||||||
|
|
||||||
|
struct Base {
|
||||||
|
using Source = source::Base;
|
||||||
|
|
||||||
|
Base(Source* source) : m_source{source} { }
|
||||||
|
virtual ~Base() = default;
|
||||||
|
virtual Result GetCollections(Collections& out) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Source* m_source;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::container
|
||||||
14
sphaira/include/yati/container/nsp.hpp
Normal file
14
sphaira/include/yati/container/nsp.hpp
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "base.hpp"
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::container {
|
||||||
|
|
||||||
|
struct Nsp final : Base {
|
||||||
|
using Base::Base;
|
||||||
|
Result GetCollections(Collections& out) override;
|
||||||
|
static Result Validate(source::Base* source);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::container
|
||||||
16
sphaira/include/yati/container/xci.hpp
Normal file
16
sphaira/include/yati/container/xci.hpp
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "base.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::container {
|
||||||
|
|
||||||
|
struct Xci final : Base {
|
||||||
|
using Base::Base;
|
||||||
|
Result GetCollections(Collections& out) override;
|
||||||
|
static Result Validate(source::Base* source);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::container
|
||||||
57
sphaira/include/yati/nx/crypto.hpp
Normal file
57
sphaira/include/yati/nx/crypto.hpp
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::crypto {
|
||||||
|
|
||||||
|
struct Aes128 {
|
||||||
|
Aes128(const void *key, bool is_encryptor) {
|
||||||
|
m_is_encryptor = is_encryptor;
|
||||||
|
aes128ContextCreate(&m_ctx, key, is_encryptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Run(void *dst, const void *src) {
|
||||||
|
if (m_is_encryptor) {
|
||||||
|
aes128EncryptBlock(&m_ctx, dst, src);
|
||||||
|
} else {
|
||||||
|
aes128DecryptBlock(&m_ctx, dst, src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Aes128Context m_ctx;
|
||||||
|
bool m_is_encryptor;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Aes128Xts {
|
||||||
|
Aes128Xts(const u8 *key, bool is_encryptor) : Aes128Xts{key, key + 0x10, is_encryptor} { }
|
||||||
|
Aes128Xts(const void *key0, const void *key1, bool is_encryptor) {
|
||||||
|
m_is_encryptor = is_encryptor;
|
||||||
|
aes128XtsContextCreate(&m_ctx, key0, key1, is_encryptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Run(void *dst, const void *src, u64 sector, u64 sector_size, u64 data_size) {
|
||||||
|
for (u64 pos = 0; pos < data_size; pos += sector_size) {
|
||||||
|
aes128XtsContextResetSector(&m_ctx, sector++, true);
|
||||||
|
if (m_is_encryptor) {
|
||||||
|
aes128XtsEncrypt(&m_ctx, static_cast<u8*>(dst) + pos, static_cast<const u8*>(src) + pos, sector_size);
|
||||||
|
} else {
|
||||||
|
aes128XtsDecrypt(&m_ctx, static_cast<u8*>(dst) + pos, static_cast<const u8*>(src) + pos, sector_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Aes128XtsContext m_ctx;
|
||||||
|
bool m_is_encryptor;
|
||||||
|
};
|
||||||
|
|
||||||
|
static inline void cryptoAes128(const void *in, void *out, const void* key, bool is_encryptor) {
|
||||||
|
Aes128(key, is_encryptor).Run(out, in);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void cryptoAes128Xts(const void* in, void* out, const u8* key, u64 sector, u64 sector_size, u64 data_size, bool is_encryptor) {
|
||||||
|
Aes128Xts(key, is_encryptor).Run(out, in, sector, sector_size, data_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::crypto
|
||||||
83
sphaira/include/yati/nx/es.hpp
Normal file
83
sphaira/include/yati/nx/es.hpp
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
#include <span>
|
||||||
|
#include "ncm.hpp"
|
||||||
|
#include "keys.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::es {
|
||||||
|
|
||||||
|
enum { TicketModule = 522 };
|
||||||
|
|
||||||
|
enum : Result {
|
||||||
|
// found ticket has missmatching rights_id from it's name.
|
||||||
|
Result_InvalidTicketBadRightsId = MAKERESULT(TicketModule, 71),
|
||||||
|
Result_InvalidTicketVersion = MAKERESULT(TicketModule, 72),
|
||||||
|
Result_InvalidTicketKeyType = MAKERESULT(TicketModule, 73),
|
||||||
|
Result_InvalidTicketKeyRevision = MAKERESULT(TicketModule, 74),
|
||||||
|
};
|
||||||
|
|
||||||
|
enum TicketSigantureType {
|
||||||
|
TicketSigantureType_RSA_4096_SHA1 = 0x010000,
|
||||||
|
TicketSigantureType_RSA_2048_SHA1 = 0x010001,
|
||||||
|
TicketSigantureType_ECDSA_SHA1 = 0x010002,
|
||||||
|
TicketSigantureType_RSA_4096_SHA256 = 0x010003,
|
||||||
|
TicketSigantureType_RSA_2048_SHA256 = 0x010004,
|
||||||
|
TicketSigantureType_ECDSA_SHA256 = 0x010005,
|
||||||
|
TicketSigantureType_HMAC_SHA1_160 = 0x010006,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum TicketTitleKeyType {
|
||||||
|
TicketTitleKeyType_Common = 0,
|
||||||
|
TicketTitleKeyType_Personalized = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum TicketPropertiesBitfield {
|
||||||
|
TicketPropertiesBitfield_None = 0,
|
||||||
|
// temporary ticket, removed on restart
|
||||||
|
TicketPropertiesBitfield_Temporary = 1 << 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TicketData {
|
||||||
|
u8 issuer[0x40];
|
||||||
|
u8 title_key_block[0x100];
|
||||||
|
u8 ticket_version1;
|
||||||
|
u8 title_key_type;
|
||||||
|
u16 ticket_version2;
|
||||||
|
u8 license_type;
|
||||||
|
u8 master_key_revision;
|
||||||
|
u16 properties_bitfield;
|
||||||
|
u8 _0x148[0x8];
|
||||||
|
u64 ticket_id;
|
||||||
|
u64 device_id;
|
||||||
|
FsRightsId rights_id;
|
||||||
|
u32 account_id;
|
||||||
|
u8 _0x174[0xC];
|
||||||
|
u8 _0x180[0x140];
|
||||||
|
};
|
||||||
|
static_assert(sizeof(TicketData) == 0x2C0);
|
||||||
|
|
||||||
|
struct EticketRsaDeviceKey {
|
||||||
|
u8 ctr[AES_128_KEY_SIZE];
|
||||||
|
u8 private_exponent[0x100];
|
||||||
|
u8 modulus[0x100];
|
||||||
|
u32 public_exponent; ///< Stored using big endian byte order. Must match ETICKET_RSA_DEVICE_KEY_PUBLIC_EXPONENT.
|
||||||
|
u8 padding[0x14];
|
||||||
|
u64 device_id;
|
||||||
|
u8 ghash[0x10];
|
||||||
|
};
|
||||||
|
static_assert(sizeof(EticketRsaDeviceKey) == 0x240);
|
||||||
|
|
||||||
|
// es functions.
|
||||||
|
Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size);
|
||||||
|
|
||||||
|
// ticket functions.
|
||||||
|
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out);
|
||||||
|
Result GetTicketData(std::span<const u8> ticket, es::TicketData* out);
|
||||||
|
Result SetTicketData(std::span<u8> ticket, const es::TicketData* in);
|
||||||
|
|
||||||
|
Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys);
|
||||||
|
Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys);
|
||||||
|
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys, bool convert_personalised);
|
||||||
|
|
||||||
|
} // namespace sphaira::es
|
||||||
70
sphaira/include/yati/nx/keys.hpp
Normal file
70
sphaira/include/yati/nx/keys.hpp
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
#include <array>
|
||||||
|
#include <cstring>
|
||||||
|
#include "defines.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::keys {
|
||||||
|
|
||||||
|
struct KeyEntry {
|
||||||
|
u8 key[AES_128_KEY_SIZE]{};
|
||||||
|
|
||||||
|
auto IsValid() const -> bool {
|
||||||
|
const KeyEntry empty{};
|
||||||
|
return std::memcmp(key, &empty, sizeof(key));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using KeySection = std::array<KeyEntry, 0x20>;
|
||||||
|
struct Keys {
|
||||||
|
u8 header_key[0x20]{};
|
||||||
|
// the below are only found if read_from_file=true
|
||||||
|
KeySection key_area_key[0x3]{}; // index
|
||||||
|
KeySection titlekek{};
|
||||||
|
KeySection master_key{};
|
||||||
|
KeyEntry eticket_rsa_kek{};
|
||||||
|
SetCalRsa2048DeviceKey eticket_device_key{};
|
||||||
|
|
||||||
|
static auto FixKey(u8 key) -> u8 {
|
||||||
|
if (key) {
|
||||||
|
return key - 1;
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto HasNcaKeyArea(u8 key, u8 index) const -> bool {
|
||||||
|
return key_area_key[index][FixKey(key)].IsValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto HasTitleKek(u8 key) const -> bool {
|
||||||
|
return titlekek[FixKey(key)].IsValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto HasMasterKey(u8 key) const -> bool {
|
||||||
|
return master_key[FixKey(key)].IsValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetNcaKeyArea(KeyEntry* out, u8 key, u8 index) const -> Result {
|
||||||
|
R_UNLESS(HasNcaKeyArea(key, index), 0x1);
|
||||||
|
*out = key_area_key[index][FixKey(key)];
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetTitleKek(KeyEntry* out, u8 key) const -> Result {
|
||||||
|
R_UNLESS(HasTitleKek(key), 0x1);
|
||||||
|
*out = titlekek[FixKey(key)];
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetMasterKey(KeyEntry* out, u8 key) const -> Result {
|
||||||
|
R_UNLESS(HasMasterKey(key), 0x1);
|
||||||
|
*out = master_key[FixKey(key)];
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void parse_hex_key(void* key, const char* hex);
|
||||||
|
Result parse_keys(Keys& out, bool read_from_file);
|
||||||
|
|
||||||
|
} // namespace sphaira::keys
|
||||||
218
sphaira/include/yati/nx/nca.hpp
Normal file
218
sphaira/include/yati/nx/nca.hpp
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
#include "keys.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::nca {
|
||||||
|
|
||||||
|
#define NCA0_MAGIC 0x3041434E
|
||||||
|
#define NCA2_MAGIC 0x3241434E
|
||||||
|
#define NCA3_MAGIC 0x3341434E
|
||||||
|
|
||||||
|
#define NCA_SECTOR_SIZE 0x200
|
||||||
|
#define NCA_XTS_SECTION_SIZE 0xC00
|
||||||
|
#define NCA_SECTION_TOTAL 0x4
|
||||||
|
#define NCA_MEDIA_REAL(x)((x * 0x200))
|
||||||
|
|
||||||
|
#define NCA_PROGRAM_LOGO_OFFSET 0x8000
|
||||||
|
#define NCA_META_CNMT_OFFSET 0xC20
|
||||||
|
|
||||||
|
enum KeyGenerationOld {
|
||||||
|
KeyGenerationOld_100 = 0x0,
|
||||||
|
KeyGenerationOld_Unused = 0x1,
|
||||||
|
KeyGenerationOld_300 = 0x2,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum KeyGeneration {
|
||||||
|
KeyGeneration_301 = 0x3,
|
||||||
|
KeyGeneration_400 = 0x4,
|
||||||
|
KeyGeneration_500 = 0x5,
|
||||||
|
KeyGeneration_600 = 0x6,
|
||||||
|
KeyGeneration_620 = 0x7,
|
||||||
|
KeyGeneration_700 = 0x8,
|
||||||
|
KeyGeneration_810 = 0x9,
|
||||||
|
KeyGeneration_900 = 0x0A,
|
||||||
|
KeyGeneration_910 = 0x0B,
|
||||||
|
KeyGeneration_1210 = 0x0C,
|
||||||
|
KeyGeneration_1300 = 0x0D,
|
||||||
|
KeyGeneration_1400 = 0x0E,
|
||||||
|
KeyGeneration_1500 = 0x0F,
|
||||||
|
KeyGeneration_1600 = 0x10,
|
||||||
|
KeyGeneration_1700 = 0x11,
|
||||||
|
KeyGeneration_1800 = 0x12,
|
||||||
|
KeyGeneration_1900 = 0x13,
|
||||||
|
KeyGeneration_Invalid = 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum KeyAreaEncryptionKeyIndex {
|
||||||
|
KeyAreaEncryptionKeyIndex_Application = 0x0,
|
||||||
|
KeyAreaEncryptionKeyIndex_Ocean = 0x1,
|
||||||
|
KeyAreaEncryptionKeyIndex_System = 0x2
|
||||||
|
};
|
||||||
|
|
||||||
|
enum DistributionType {
|
||||||
|
DistributionType_System = 0x0,
|
||||||
|
DistributionType_GameCard = 0x1
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ContentType {
|
||||||
|
ContentType_Program = 0x0,
|
||||||
|
ContentType_Meta = 0x1,
|
||||||
|
ContentType_Control = 0x2,
|
||||||
|
ContentType_Manual = 0x3,
|
||||||
|
ContentType_Data = 0x4,
|
||||||
|
ContentType_PublicData = 0x5,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum FileSystemType {
|
||||||
|
FileSystemType_RomFS = 0x0,
|
||||||
|
FileSystemType_PFS0 = 0x1
|
||||||
|
};
|
||||||
|
|
||||||
|
enum HashType {
|
||||||
|
HashType_Auto = 0x0,
|
||||||
|
HashType_HierarchicalSha256 = 0x2,
|
||||||
|
HashType_HierarchicalIntegrity = 0x3
|
||||||
|
};
|
||||||
|
|
||||||
|
enum EncryptionType {
|
||||||
|
EncryptionType_Auto = 0x0,
|
||||||
|
EncryptionType_None = 0x1,
|
||||||
|
EncryptionType_AesXts = 0x2,
|
||||||
|
EncryptionType_AesCtr = 0x3,
|
||||||
|
EncryptionType_AesCtrEx = 0x4,
|
||||||
|
EncryptionType_AesCtrSkipLayerHash = 0x5, // [14.0.0+]
|
||||||
|
EncryptionType_AesCtrExSkipLayerHash = 0x6, // [14.0.0+]
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SectionTableEntry {
|
||||||
|
u32 media_start_offset; // divided by 0x200.
|
||||||
|
u32 media_end_offset; // divided by 0x200.
|
||||||
|
u8 _0x8[0x4]; // unknown.
|
||||||
|
u8 _0xC[0x4]; // unknown.
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LayerRegion {
|
||||||
|
u64 offset;
|
||||||
|
u64 size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HierarchicalSha256Data {
|
||||||
|
u8 master_hash[0x20];
|
||||||
|
u32 block_size;
|
||||||
|
u32 layer_count;
|
||||||
|
LayerRegion hash_layer;
|
||||||
|
LayerRegion pfs0_layer;
|
||||||
|
LayerRegion unused_layers[3];
|
||||||
|
u8 _0x78[0x80];
|
||||||
|
};
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
struct HierarchicalIntegrityVerificationLevelInformation {
|
||||||
|
u64 logical_offset;
|
||||||
|
u64 hash_data_size;
|
||||||
|
u32 block_size; // log2
|
||||||
|
u32 _0x14; // reserved
|
||||||
|
};
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
struct InfoLevelHash {
|
||||||
|
u32 max_layers;
|
||||||
|
HierarchicalIntegrityVerificationLevelInformation levels[6];
|
||||||
|
u8 signature_salt[0x20];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct IntegrityMetaInfo {
|
||||||
|
u32 magic; // IVFC
|
||||||
|
u32 version;
|
||||||
|
u32 master_hash_size;
|
||||||
|
InfoLevelHash info_level_hash;
|
||||||
|
u8 master_hash[0x20];
|
||||||
|
u8 _0xE0[0x18];
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
|
||||||
|
static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
|
||||||
|
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo));
|
||||||
|
|
||||||
|
struct FsHeader {
|
||||||
|
u16 version; // always 2.
|
||||||
|
u8 fs_type; // see FileSystemType.
|
||||||
|
u8 hash_type; // see HashType.
|
||||||
|
u8 encryption_type; // see EncryptionType.
|
||||||
|
u8 metadata_hash_type;
|
||||||
|
u8 _0x6[0x2]; // empty.
|
||||||
|
|
||||||
|
union {
|
||||||
|
HierarchicalSha256Data hierarchical_sha256_data;
|
||||||
|
IntegrityMetaInfo integrity_meta_info; // used for romfs
|
||||||
|
} hash_data;
|
||||||
|
|
||||||
|
u8 patch_info[0x40];
|
||||||
|
u64 section_ctr;
|
||||||
|
u8 spares_info[0x30];
|
||||||
|
u8 compression_info[0x28];
|
||||||
|
u8 meta_data_hash_data_info[0x30];
|
||||||
|
u8 reserved[0x30];
|
||||||
|
};
|
||||||
|
static_assert(sizeof(FsHeader) == 0x200);
|
||||||
|
static_assert(sizeof(FsHeader::hash_data) == 0xF8);
|
||||||
|
|
||||||
|
struct SectionHeaderHash {
|
||||||
|
u8 sha256[0x20];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct KeyArea {
|
||||||
|
u8 area[0x10];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Header {
|
||||||
|
u8 rsa_fixed_key[0x100];
|
||||||
|
u8 rsa_npdm[0x100]; // key from npdm.
|
||||||
|
u32 magic;
|
||||||
|
u8 distribution_type; // see DistributionType.
|
||||||
|
u8 content_type; // see ContentType.
|
||||||
|
u8 old_key_gen; // see KeyGenerationOld.
|
||||||
|
u8 kaek_index; // see KeyAreaEncryptionKeyIndex.
|
||||||
|
u64 size;
|
||||||
|
u64 title_id;
|
||||||
|
u32 context_id;
|
||||||
|
u32 sdk_version;
|
||||||
|
u8 key_gen; // see KeyGeneration.
|
||||||
|
u8 sig_key_gen;
|
||||||
|
u8 _0x222[0xE]; // empty.
|
||||||
|
FsRightsId rights_id;
|
||||||
|
|
||||||
|
SectionTableEntry fs_table[NCA_SECTION_TOTAL];
|
||||||
|
SectionHeaderHash fs_header_hash[NCA_SECTION_TOTAL];
|
||||||
|
KeyArea key_area[NCA_SECTION_TOTAL];
|
||||||
|
|
||||||
|
u8 _0x340[0xC0]; // empty.
|
||||||
|
|
||||||
|
FsHeader fs_header[NCA_SECTION_TOTAL];
|
||||||
|
|
||||||
|
auto GetKeyGeneration() const -> u8 {
|
||||||
|
if (old_key_gen < key_gen) {
|
||||||
|
return key_gen;
|
||||||
|
} else {
|
||||||
|
return old_key_gen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetKeyGeneration(u8 key_generation) {
|
||||||
|
if (key_generation <= 0x2) {
|
||||||
|
old_key_gen = key_generation;
|
||||||
|
key_gen = 0;
|
||||||
|
} else {
|
||||||
|
old_key_gen = 0x2;
|
||||||
|
key_gen = key_generation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(sizeof(Header) == 0xC00);
|
||||||
|
|
||||||
|
Result DecryptKeak(const keys::Keys& keys, Header& header);
|
||||||
|
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
|
||||||
|
Result VerifyFixedKey(const Header& header);
|
||||||
|
|
||||||
|
} // namespace sphaira::nca
|
||||||
39
sphaira/include/yati/nx/ncm.hpp
Normal file
39
sphaira/include/yati/nx/ncm.hpp
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::ncm {
|
||||||
|
|
||||||
|
struct PackagedContentMeta {
|
||||||
|
u64 title_id;
|
||||||
|
u32 title_version;
|
||||||
|
u8 meta_type; // NcmContentMetaType
|
||||||
|
u8 content_meta_platform; // [17.0.0+]
|
||||||
|
NcmContentMetaHeader meta_header;
|
||||||
|
u8 install_type; // NcmContentInstallType
|
||||||
|
u8 _0x17;
|
||||||
|
u32 required_sys_version;
|
||||||
|
u8 _0x1C[0x4];
|
||||||
|
};
|
||||||
|
static_assert(sizeof(PackagedContentMeta) == 0x20);
|
||||||
|
|
||||||
|
struct ContentStorageRecord {
|
||||||
|
NcmContentMetaKey key;
|
||||||
|
u8 storage_id;
|
||||||
|
u8 padding[0x7];
|
||||||
|
};
|
||||||
|
|
||||||
|
union ExtendedHeader {
|
||||||
|
NcmApplicationMetaExtendedHeader application;
|
||||||
|
NcmPatchMetaExtendedHeader patch;
|
||||||
|
NcmAddOnContentMetaExtendedHeader addon;
|
||||||
|
NcmLegacyAddOnContentMetaExtendedHeader addon_legacy;
|
||||||
|
NcmDataPatchMetaExtendedHeader data_patch;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto GetAppId(const NcmContentMetaKey& key) -> u64;
|
||||||
|
|
||||||
|
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id);
|
||||||
|
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id);
|
||||||
|
|
||||||
|
} // namespace sphaira::ncm
|
||||||
54
sphaira/include/yati/nx/ncz.hpp
Normal file
54
sphaira/include/yati/nx/ncz.hpp
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::ncz {
|
||||||
|
|
||||||
|
#define NCZ_SECTION_MAGIC 0x4E544345535A434EUL
|
||||||
|
// todo: byteswap this
|
||||||
|
#define NCZ_BLOCK_MAGIC std::byteswap(0x4E435A424C4F434BUL)
|
||||||
|
|
||||||
|
#define NCZ_SECTION_OFFSET (0x4000 + sizeof(ncz::Header))
|
||||||
|
|
||||||
|
struct Header {
|
||||||
|
u64 magic; // NCZ_SECTION_MAGIC
|
||||||
|
u64 total_sections;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BlockHeader {
|
||||||
|
u64 magic; // NCZ_BLOCK_MAGIC
|
||||||
|
u8 version;
|
||||||
|
u8 type;
|
||||||
|
u8 padding;
|
||||||
|
u8 block_size_exponent;
|
||||||
|
u32 total_blocks;
|
||||||
|
u64 decompressed_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Block {
|
||||||
|
u32 size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BlockInfo {
|
||||||
|
u64 offset; // compressed offset.
|
||||||
|
u64 size; // compressed size.
|
||||||
|
|
||||||
|
auto InRange(u64 off) const -> bool {
|
||||||
|
return off < offset + size && off >= offset;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Section {
|
||||||
|
u64 offset;
|
||||||
|
u64 size;
|
||||||
|
u64 crypto_type;
|
||||||
|
u64 padding;
|
||||||
|
u8 key[0x10];
|
||||||
|
u8 counter[0x10];
|
||||||
|
|
||||||
|
auto InRange(u64 off) const -> bool {
|
||||||
|
return off < offset + size && off >= offset;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::ncz
|
||||||
62
sphaira/include/yati/nx/npdm.hpp
Normal file
62
sphaira/include/yati/nx/npdm.hpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::npdm {
|
||||||
|
|
||||||
|
struct Meta {
|
||||||
|
u32 magic; // "META"
|
||||||
|
u32 signature_key_generation; // +9.0.0
|
||||||
|
u32 _0x8;
|
||||||
|
u8 flags;
|
||||||
|
u8 _0xD;
|
||||||
|
u8 main_thread_priority;
|
||||||
|
u8 main_thread_core_num;
|
||||||
|
u32 _0x10;
|
||||||
|
u32 sys_resource_size; // +3.0.0
|
||||||
|
u32 version;
|
||||||
|
u32 main_thread_stack_size;
|
||||||
|
char title_name[0x10];
|
||||||
|
char product_code[0x10];
|
||||||
|
u8 _0x40[0x30];
|
||||||
|
u32 aci0_offset;
|
||||||
|
u32 aci0_size;
|
||||||
|
u32 acid_offset;
|
||||||
|
u32 acid_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Acid {
|
||||||
|
u8 rsa_sig[0x100];
|
||||||
|
u8 rsa_pub[0x100];
|
||||||
|
u32 magic; // "ACID"
|
||||||
|
u32 size;
|
||||||
|
u8 version;
|
||||||
|
u8 _0x209[0x1];
|
||||||
|
u8 _0x20A[0x2];
|
||||||
|
u32 flags;
|
||||||
|
u64 program_id_min;
|
||||||
|
u64 program_id_max;
|
||||||
|
u32 fac_offset;
|
||||||
|
u32 fac_size;
|
||||||
|
u32 sac_offset;
|
||||||
|
u32 sac_size;
|
||||||
|
u32 kac_offset;
|
||||||
|
u32 kac_size;
|
||||||
|
u8 _0x238[0x8];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Aci0 {
|
||||||
|
u32 magic; // "ACI0"
|
||||||
|
u8 _0x4[0xC];
|
||||||
|
u64 program_id;
|
||||||
|
u8 _0x18[0x8];
|
||||||
|
u32 fac_offset;
|
||||||
|
u32 fac_size;
|
||||||
|
u32 sac_offset;
|
||||||
|
u32 sac_size;
|
||||||
|
u32 kac_offset;
|
||||||
|
u32 kac_size;
|
||||||
|
u8 _0x38[0x8];
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::npdm
|
||||||
22
sphaira/include/yati/nx/ns.hpp
Normal file
22
sphaira/include/yati/nx/ns.hpp
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
#include "ncm.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::ns {
|
||||||
|
|
||||||
|
enum ApplicationRecordType {
|
||||||
|
// installed
|
||||||
|
ApplicationRecordType_Installed = 0x3,
|
||||||
|
// application is gamecard, but gamecard isn't insterted
|
||||||
|
ApplicationRecordType_GamecardMissing = 0x5,
|
||||||
|
// archived
|
||||||
|
ApplicationRecordType_Archived = 0xB,
|
||||||
|
};
|
||||||
|
|
||||||
|
Result PushApplicationRecord(Service* srv, u64 tid, const ncm::ContentStorageRecord* records, u32 count);
|
||||||
|
Result ListApplicationRecordContentMeta(Service* srv, u64 offset, u64 tid, ncm::ContentStorageRecord* out_records, u32 count, s32* entries_read);
|
||||||
|
Result DeleteApplicationRecord(Service* srv, u64 tid);
|
||||||
|
Result InvalidateApplicationControlCache(Service* srv, u64 tid);
|
||||||
|
|
||||||
|
} // namespace sphaira::ns
|
||||||
62
sphaira/include/yati/nx/nxdumptool_rsa.h
Normal file
62
sphaira/include/yati/nx/nxdumptool_rsa.h
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* rsa.c
|
||||||
|
*
|
||||||
|
* Copyright (c) 2018-2019, SciresM.
|
||||||
|
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
|
||||||
|
*
|
||||||
|
* This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool).
|
||||||
|
*
|
||||||
|
* nxdumptool is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* nxdumptool is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifndef __RSA_H__
|
||||||
|
#define __RSA_H__
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#define RSA2048_BYTES 0x100
|
||||||
|
#define RSA2048_BITS (RSA2048_BYTES * 8)
|
||||||
|
|
||||||
|
#define RSA2048_SIG_SIZE RSA2048_BYTES
|
||||||
|
#define RSA2048_PUBKEY_SIZE RSA2048_BYTES
|
||||||
|
|
||||||
|
/// Verifies a RSA-2048-PSS with SHA-256 signature.
|
||||||
|
/// Suitable for NCA and NPDM signatures.
|
||||||
|
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
|
||||||
|
bool rsa2048VerifySha256BasedPssSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size);
|
||||||
|
|
||||||
|
/// Verifies a RSA-2048-PKCS#1 v1.5 with SHA-256 signature.
|
||||||
|
/// Suitable for ticket and certificate chain signatures.
|
||||||
|
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
|
||||||
|
bool rsa2048VerifySha256BasedPkcs1v15Signature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size);
|
||||||
|
|
||||||
|
/// Performs RSA-2048-OAEP decryption.
|
||||||
|
/// Suitable to decrypt the titlekey block from personalized tickets.
|
||||||
|
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
|
||||||
|
/// 'label' and 'label_size' arguments are optional -- if not needed, these may be set to NULL and 0, respectively.
|
||||||
|
bool rsa2048OaepDecrypt(void *dst, size_t dst_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, const void *private_exponent, \
|
||||||
|
size_t private_exponent_size, const void *label, size_t label_size, size_t *out_size);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* __RSA_H__ */
|
||||||
265
sphaira/include/yati/nx/tik.h
Normal file
265
sphaira/include/yati/nx/tik.h
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
/*
|
||||||
|
* tik.h
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
|
||||||
|
*
|
||||||
|
* This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool).
|
||||||
|
*
|
||||||
|
* nxdumptool is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* nxdumptool is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifndef __TIK_H__
|
||||||
|
#define __TIK_H__
|
||||||
|
|
||||||
|
#include "signature.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define SIGNED_TIK_MIN_SIZE sizeof(TikSigHmac160) /* Assuming no ESV1/ESV2 records are available. */
|
||||||
|
#define SIGNED_TIK_MAX_SIZE 0x400 /* Max ticket entry size in the ES ticket system savedata file. */
|
||||||
|
|
||||||
|
#define TIK_FORMAT_VERSION 2
|
||||||
|
|
||||||
|
#define GENERATE_TIK_STRUCT(sigtype, tiksize) \
|
||||||
|
typedef struct { \
|
||||||
|
SignatureBlock##sigtype sig_block; \
|
||||||
|
TikCommonBlock tik_common_block; \
|
||||||
|
u8 es_section_record_data[]; \
|
||||||
|
} TikSig##sigtype; \
|
||||||
|
NXDT_ASSERT(TikSig##sigtype, tiksize);
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
TikTitleKeyType_Common = 0,
|
||||||
|
TikTitleKeyType_Personalized = 1,
|
||||||
|
TikTitleKeyType_Count = 2 ///< Total values supported by this enum.
|
||||||
|
} TikTitleKeyType;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
TikLicenseType_Permanent = 0,
|
||||||
|
TikLicenseType_Demo = 1,
|
||||||
|
TikLicenseType_Trial = 2,
|
||||||
|
TikLicenseType_Rental = 3,
|
||||||
|
TikLicenseType_Subscription = 4,
|
||||||
|
TikLicenseType_Service = 5,
|
||||||
|
TikLicenseType_Count = 6 ///< Total values supported by this enum.
|
||||||
|
} TikLicenseType;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
TikPropertyMask_None = 0,
|
||||||
|
TikPropertyMask_PreInstallation = BIT(0), ///< Determines if the title comes pre-installed on the device. Most likely unused -- a remnant from previous ticket formats.
|
||||||
|
TikPropertyMask_SharedTitle = BIT(1), ///< Determines if the title holds shared contents only. Most likely unused -- a remnant from previous ticket formats.
|
||||||
|
TikPropertyMask_AllContents = BIT(2), ///< Determines if the content index mask shall be bypassed. Most likely unused -- a remnant from previous ticket formats.
|
||||||
|
TikPropertyMask_DeviceLinkIndepedent = BIT(3), ///< Determines if the console should *not* connect to the Internet to verify if the title's being used by the primary console.
|
||||||
|
TikPropertyMask_Volatile = BIT(4), ///< Determines if the ticket copy inside ticket.bin is available after reboot. Can be encrypted.
|
||||||
|
TikPropertyMask_ELicenseRequired = BIT(5), ///< Determines if the console should connect to the Internet to perform license verification.
|
||||||
|
TikPropertyMask_Count = 6 ///< Total values supported by this enum.
|
||||||
|
} TikPropertyMask;
|
||||||
|
|
||||||
|
/// Placed after the ticket signature block.
|
||||||
|
typedef struct {
|
||||||
|
char issuer[0x40];
|
||||||
|
u8 titlekey_block[0x100];
|
||||||
|
u8 format_version; ///< Always matches TIK_FORMAT_VERSION.
|
||||||
|
u8 titlekey_type; ///< TikTitleKeyType.
|
||||||
|
u16 ticket_version;
|
||||||
|
u8 license_type; ///< TikLicenseType.
|
||||||
|
u8 key_generation; ///< NcaKeyGeneration.
|
||||||
|
u16 property_mask; ///< TikPropertyMask.
|
||||||
|
u8 reserved[0x8];
|
||||||
|
u64 ticket_id;
|
||||||
|
u64 device_id;
|
||||||
|
FsRightsId rights_id;
|
||||||
|
u32 account_id;
|
||||||
|
u32 sect_total_size;
|
||||||
|
u32 sect_hdr_offset;
|
||||||
|
u16 sect_hdr_count;
|
||||||
|
u16 sect_hdr_entry_size;
|
||||||
|
} TikCommonBlock;
|
||||||
|
|
||||||
|
NXDT_ASSERT(TikCommonBlock, 0x180);
|
||||||
|
|
||||||
|
/// ESV1/ESV2 section records are placed right after the ticket data. These aren't available in TikTitleKeyType_Common tickets.
|
||||||
|
/// These are only used if the sect_* fields from the common block are non-zero (other than 'sect_hdr_offset').
|
||||||
|
/// Each ESV2 section record is followed by a 'record_count' number of ESV1 records, each one of 'record_size' size.
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
TikSectionType_None = 0,
|
||||||
|
TikSectionType_Permanent = 1,
|
||||||
|
TikSectionType_Subscription = 2,
|
||||||
|
TikSectionType_Content = 3,
|
||||||
|
TikSectionType_ContentConsumption = 4,
|
||||||
|
TikSectionType_AccessTitle = 5,
|
||||||
|
TikSectionType_LimitedResource = 6,
|
||||||
|
TikSectionType_Count = 7 ///< Total values supported by this enum.
|
||||||
|
} TikSectionType;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
u32 sect_offset;
|
||||||
|
u32 record_size;
|
||||||
|
u32 section_size;
|
||||||
|
u16 record_count;
|
||||||
|
u16 section_type; ///< TikSectionType.
|
||||||
|
} TikESV2SectionRecord;
|
||||||
|
|
||||||
|
/// Used with TikSectionType_Permanent.
|
||||||
|
typedef struct {
|
||||||
|
u8 ref_id[0x10];
|
||||||
|
u32 ref_id_attr;
|
||||||
|
} TikESV1PermanentRecord;
|
||||||
|
|
||||||
|
/// Used with TikSectionType_Subscription.
|
||||||
|
typedef struct {
|
||||||
|
u32 limit;
|
||||||
|
u8 ref_id[0x10];
|
||||||
|
u32 ref_id_attr;
|
||||||
|
} TikESV1SubscriptionRecord;
|
||||||
|
|
||||||
|
/// Used with TikSectionType_Content.
|
||||||
|
typedef struct {
|
||||||
|
u32 offset;
|
||||||
|
u8 access_mask[0x80];
|
||||||
|
} TikESV1ContentRecord;
|
||||||
|
|
||||||
|
/// Used with TikSectionType_ContentConsumption.
|
||||||
|
typedef struct {
|
||||||
|
u16 index;
|
||||||
|
u16 code;
|
||||||
|
u32 limit;
|
||||||
|
} TikESV1ContentConsumptionRecord;
|
||||||
|
|
||||||
|
/// Used with TikSectionType_AccessTitle.
|
||||||
|
typedef struct {
|
||||||
|
u64 access_title_id;
|
||||||
|
u64 access_title_mask;
|
||||||
|
} TikESV1AccessTitleRecord;
|
||||||
|
|
||||||
|
/// Used with TikSectionType_LimitedResource.
|
||||||
|
typedef struct {
|
||||||
|
u32 limit;
|
||||||
|
u8 ref_id[0x10];
|
||||||
|
u32 ref_id_attr;
|
||||||
|
} TikESV1LimitedResourceRecord;
|
||||||
|
|
||||||
|
/// All tickets generated below use a little endian sig_type field.
|
||||||
|
GENERATE_TIK_STRUCT(Rsa4096, 0x3C0); /// RSA-4096 signature.
|
||||||
|
GENERATE_TIK_STRUCT(Rsa2048, 0x2C0); /// RSA-2048 signature.
|
||||||
|
GENERATE_TIK_STRUCT(Ecc480, 0x200); /// ECC signature.
|
||||||
|
GENERATE_TIK_STRUCT(Hmac160, 0x1C0); /// HMAC signature.
|
||||||
|
|
||||||
|
/// Ticket type.
|
||||||
|
typedef enum {
|
||||||
|
TikType_None = 0,
|
||||||
|
TikType_SigRsa4096 = 1,
|
||||||
|
TikType_SigRsa2048 = 2,
|
||||||
|
TikType_SigEcc480 = 3,
|
||||||
|
TikType_SigHmac160 = 4,
|
||||||
|
TikType_Count = 5 ///< Total values supported by this enum.
|
||||||
|
} TikType;
|
||||||
|
|
||||||
|
/// Used to store ticket type, size and raw data, as well as titlekey data.
|
||||||
|
typedef struct {
|
||||||
|
u8 type; ///< TikType.
|
||||||
|
u64 size; ///< Raw ticket size.
|
||||||
|
u8 data[SIGNED_TIK_MAX_SIZE]; ///< Raw ticket data.
|
||||||
|
u8 key_generation; ///< NcaKeyGeneration.
|
||||||
|
u8 enc_titlekey[0x10]; ///< Titlekey with titlekek crypto (RSA-OAEP unwrapped if dealing with a TikTitleKeyType_Personalized ticket).
|
||||||
|
char enc_titlekey_str[0x21]; ///< Character string representation of enc_titlekey.
|
||||||
|
u8 dec_titlekey[0x10]; ///< Titlekey without titlekek crypto. Ready to use for NCA FS section decryption.
|
||||||
|
char dec_titlekey_str[0x21]; ///< Character string representation of dec_titlekey.
|
||||||
|
char rights_id_str[0x21]; ///< Character string representation of the rights ID from the ticket.
|
||||||
|
} Ticket;
|
||||||
|
|
||||||
|
/// Retrieves a ticket from either the ES ticket system savedata file (eMMC BIS System partition) or the secure Hash FS partition from an inserted gamecard.
|
||||||
|
/// Both the input rights ID and key generation values must have been retrieved from a NCA that depends on the desired ticket.
|
||||||
|
/// Titlekey is also RSA-OAEP unwrapped (if needed) and titlekek-decrypted right away.
|
||||||
|
bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, u8 key_generation, bool use_gamecard);
|
||||||
|
|
||||||
|
/// Converts a TikTitleKeyType_Personalized ticket into a TikTitleKeyType_Common ticket and optionally generates a raw certificate chain for the new signature issuer.
|
||||||
|
/// Bear in mind the 'size' member from the Ticket parameter will be updated by this function to remove any possible references to ESV1/ESV2 records.
|
||||||
|
/// If both 'out_raw_cert_chain' and 'out_raw_cert_chain_size' pointers are provided, raw certificate chain data will be saved to them.
|
||||||
|
/// certGenerateRawCertificateChainBySignatureIssuer() is used internally, so the output buffer must be freed by the user.
|
||||||
|
bool tikConvertPersonalizedTicketToCommonTicket(Ticket *tik, u8 **out_raw_cert_chain, u64 *out_raw_cert_chain_size);
|
||||||
|
|
||||||
|
/// Helper inline functions for signed ticket blobs.
|
||||||
|
|
||||||
|
NX_INLINE TikCommonBlock *tikGetCommonBlockFromSignedTicketBlob(void *buf)
|
||||||
|
{
|
||||||
|
return (TikCommonBlock*)signatureGetPayloadFromSignedBlob(buf, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
NX_INLINE u64 tikGetSectionRecordsSizeFromSignedTicketBlob(void *buf)
|
||||||
|
{
|
||||||
|
TikCommonBlock *tik_common_block = tikGetCommonBlockFromSignedTicketBlob(buf);
|
||||||
|
if (!tik_common_block) return 0;
|
||||||
|
|
||||||
|
u64 offset = sizeof(TikCommonBlock), out_size = 0;
|
||||||
|
|
||||||
|
for(u32 i = 0; i < tik_common_block->sect_hdr_count; i++)
|
||||||
|
{
|
||||||
|
TikESV2SectionRecord *rec = (TikESV2SectionRecord*)((u8*)tik_common_block + offset);
|
||||||
|
offset += (sizeof(TikESV2SectionRecord) + ((u64)rec->record_count * (u64)rec->record_size));
|
||||||
|
out_size += offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
NX_INLINE bool tikIsValidSignedTicketBlob(void *buf)
|
||||||
|
{
|
||||||
|
u64 ticket_size = (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock));
|
||||||
|
return (ticket_size > sizeof(TikCommonBlock) && (ticket_size + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) <= SIGNED_TIK_MAX_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
NX_INLINE u64 tikGetSignedTicketBlobSize(void *buf)
|
||||||
|
{
|
||||||
|
return (tikIsValidSignedTicketBlob(buf) ? (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
NX_INLINE u64 tikGetSignedTicketBlobHashAreaSize(void *buf)
|
||||||
|
{
|
||||||
|
return (tikIsValidSignedTicketBlob(buf) ? (sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper inline functions for Ticket elements.
|
||||||
|
|
||||||
|
NX_INLINE bool tikIsValidTicket(Ticket *tik)
|
||||||
|
{
|
||||||
|
return (tik && tik->type > TikType_None && tik->type < TikType_Count && tik->size >= SIGNED_TIK_MIN_SIZE && tik->size <= SIGNED_TIK_MAX_SIZE && tikIsValidSignedTicketBlob(tik->data));
|
||||||
|
}
|
||||||
|
|
||||||
|
NX_INLINE TikCommonBlock *tikGetCommonBlockFromTicket(Ticket *tik)
|
||||||
|
{
|
||||||
|
return (tikIsValidTicket(tik) ? tikGetCommonBlockFromSignedTicketBlob(tik->data) : NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
NX_INLINE bool tikIsPersonalizedTicket(Ticket *tik)
|
||||||
|
{
|
||||||
|
TikCommonBlock *tik_common_block = tikGetCommonBlockFromTicket(tik);
|
||||||
|
return (tik_common_block ? (tik_common_block->titlekey_type == TikTitleKeyType_Personalized) : false);
|
||||||
|
}
|
||||||
|
|
||||||
|
NX_INLINE u64 tikGetHashAreaSizeFromTicket(Ticket *tik)
|
||||||
|
{
|
||||||
|
return (tikIsValidTicket(tik) ? tikGetSignedTicketBlobHashAreaSize(tik->data) : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* __TIK_H__ */
|
||||||
21
sphaira/include/yati/source/base.hpp
Normal file
21
sphaira/include/yati/source/base.hpp
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
|
struct Base {
|
||||||
|
virtual ~Base() = default;
|
||||||
|
// virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||||
|
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||||
|
|
||||||
|
Result GetOpenResult() const {
|
||||||
|
return m_open_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Result m_open_result{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::source
|
||||||
19
sphaira/include/yati/source/file.hpp
Normal file
19
sphaira/include/yati/source/file.hpp
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "base.hpp"
|
||||||
|
#include "fs.hpp"
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
|
struct File final : Base {
|
||||||
|
File(FsFileSystem* fs, const fs::FsPath& path);
|
||||||
|
~File();
|
||||||
|
|
||||||
|
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
FsFile m_file{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::source
|
||||||
20
sphaira/include/yati/source/stdio.hpp
Normal file
20
sphaira/include/yati/source/stdio.hpp
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "base.hpp"
|
||||||
|
#include "fs.hpp"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
|
struct Stdio final : Base {
|
||||||
|
Stdio(const char* path);
|
||||||
|
~Stdio();
|
||||||
|
|
||||||
|
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::FILE* m_file{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::source
|
||||||
127
sphaira/include/yati/yati.hpp
Normal file
127
sphaira/include/yati/yati.hpp
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Notes:
|
||||||
|
* - nca's that use title key encryption are decrypted using Tegra SE, whereas
|
||||||
|
* standard crypto uses software decryption.
|
||||||
|
* The latter is almost always (slightly) faster, and removed the need for es patch.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fs.hpp"
|
||||||
|
#include "source/base.hpp"
|
||||||
|
#include "ui/progress_box.hpp"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace sphaira::yati {
|
||||||
|
|
||||||
|
enum { YatiModule = 521 };
|
||||||
|
|
||||||
|
enum : Result {
|
||||||
|
// unkown container for the source provided.
|
||||||
|
Result_ContainerNotFound = MAKERESULT(YatiModule, 10),
|
||||||
|
Result_Cancelled = MAKERESULT(YatiModule, 11),
|
||||||
|
|
||||||
|
// nca required by the cnmt but not found in collection.
|
||||||
|
Result_NcaNotFound = MAKERESULT(YatiModule, 30),
|
||||||
|
Result_InvalidNcaReadSize = MAKERESULT(YatiModule, 31),
|
||||||
|
Result_InvalidNcaSigKeyGen = MAKERESULT(YatiModule, 32),
|
||||||
|
Result_InvalidNcaMagic = MAKERESULT(YatiModule, 33),
|
||||||
|
Result_InvalidNcaSignature0 = MAKERESULT(YatiModule, 34),
|
||||||
|
Result_InvalidNcaSignature1 = MAKERESULT(YatiModule, 35),
|
||||||
|
// invalid sha256 over the entire nca.
|
||||||
|
Result_InvalidNcaSha256 = MAKERESULT(YatiModule, 36),
|
||||||
|
|
||||||
|
// section could not be found.
|
||||||
|
Result_NczSectionNotFound = MAKERESULT(YatiModule, 50),
|
||||||
|
// section count == 0.
|
||||||
|
Result_InvalidNczSectionCount = MAKERESULT(YatiModule, 51),
|
||||||
|
// block could not be found.
|
||||||
|
Result_NczBlockNotFound = MAKERESULT(YatiModule, 52),
|
||||||
|
// block version != 2.
|
||||||
|
Result_InvalidNczBlockVersion = MAKERESULT(YatiModule, 53),
|
||||||
|
// block type != 1.
|
||||||
|
Result_InvalidNczBlockType = MAKERESULT(YatiModule, 54),
|
||||||
|
// block count == 0.
|
||||||
|
Result_InvalidNczBlockTotal = MAKERESULT(YatiModule, 55),
|
||||||
|
// block size exponent < 14 || > 32.
|
||||||
|
Result_InvalidNczBlockSizeExponent = MAKERESULT(YatiModule, 56),
|
||||||
|
// zstd error while decompressing ncz.
|
||||||
|
Result_InvalidNczZstdError = MAKERESULT(YatiModule, 57),
|
||||||
|
|
||||||
|
// nca has rights_id but matching ticket wasn't found.
|
||||||
|
Result_TicketNotFound = MAKERESULT(YatiModule, 70),
|
||||||
|
// found ticket has missmatching rights_id from it's name.
|
||||||
|
Result_InvalidTicketBadRightsId = MAKERESULT(YatiModule, 71),
|
||||||
|
Result_InvalidTicketVersion = MAKERESULT(YatiModule, 72),
|
||||||
|
Result_InvalidTicketKeyType = MAKERESULT(YatiModule, 73),
|
||||||
|
Result_InvalidTicketKeyRevision = MAKERESULT(YatiModule, 74),
|
||||||
|
|
||||||
|
// cert not found for the ticket.
|
||||||
|
Result_CertNotFound = MAKERESULT(YatiModule, 90),
|
||||||
|
|
||||||
|
// unable to fetch header from ncm database.
|
||||||
|
Result_NcmDbCorruptHeader = MAKERESULT(YatiModule, 110),
|
||||||
|
// unable to total infos from ncm database.
|
||||||
|
Result_NcmDbCorruptInfos = MAKERESULT(YatiModule, 111),
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
bool sd_card_install{};
|
||||||
|
|
||||||
|
// enables downgrading patch / data patch (dlc) version.
|
||||||
|
bool allow_downgrade{};
|
||||||
|
|
||||||
|
// ignores the install if already installed.
|
||||||
|
// checks that every nca is available.
|
||||||
|
bool skip_if_already_installed{};
|
||||||
|
|
||||||
|
// installs tickets only.
|
||||||
|
bool ticket_only{};
|
||||||
|
|
||||||
|
// converts personalised tickets to common tickets, allows for offline play.
|
||||||
|
// this breaks ticket signature so es needs to be patched.
|
||||||
|
// modified common tickets are patched regardless of this setting.
|
||||||
|
bool patch_ticket{};
|
||||||
|
|
||||||
|
// flags to enable / disable install of specific types.
|
||||||
|
bool skip_base{};
|
||||||
|
bool skip_patch{};
|
||||||
|
bool skip_addon{};
|
||||||
|
bool skip_data_patch{};
|
||||||
|
bool skip_ticket{};
|
||||||
|
|
||||||
|
// enables the option to skip sha256 verification.
|
||||||
|
bool skip_nca_hash_verify{};
|
||||||
|
|
||||||
|
// enables the option to skip rsa nca fixed key verification.
|
||||||
|
bool skip_rsa_header_fixed_key_verify{};
|
||||||
|
|
||||||
|
// enables the option to skip rsa npdm fixed key verification.
|
||||||
|
bool skip_rsa_npdm_fixed_key_verify{};
|
||||||
|
|
||||||
|
// if set, it will ignore the distribution bit in the nca header.
|
||||||
|
bool ignore_distribution_bit{};
|
||||||
|
|
||||||
|
// converts titlekey to standard crypto, also known as "ticketless".
|
||||||
|
// this will not work with addon (dlc), so, addon tickets will be installed.
|
||||||
|
bool convert_to_standard_crypto{};
|
||||||
|
|
||||||
|
// encrypts the keak with master key 0, this allows the game to be launched on every fw.
|
||||||
|
// implicitly performs standard crypto.
|
||||||
|
bool lower_master_key{};
|
||||||
|
|
||||||
|
// sets the system_firmware field in the cnmt extended header.
|
||||||
|
// if mkey is higher than fw version, the game still won't launch
|
||||||
|
// as the fw won't have the key to decrypt keak.
|
||||||
|
bool lower_system_version{};
|
||||||
|
};
|
||||||
|
|
||||||
|
Result InstallFromFile(FsFileSystem* fs, const fs::FsPath& path);
|
||||||
|
Result InstallFromStdioFile(const char* path);
|
||||||
|
Result InstallFromSource(std::shared_ptr<source::Base> source);
|
||||||
|
|
||||||
|
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path);
|
||||||
|
Result InstallFromStdioFile(ui::ProgressBox* pbox, const char* path);
|
||||||
|
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source);
|
||||||
|
|
||||||
|
} // namespace sphaira::yati
|
||||||
@@ -1008,6 +1008,10 @@ void App::Draw() {
|
|||||||
this->queue.presentImage(this->swapchain, slot);
|
this->queue.presentImage(this->swapchain, slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto App::GetApp() -> App* {
|
||||||
|
return g_app;
|
||||||
|
}
|
||||||
|
|
||||||
auto App::GetVg() -> NVGcontext* {
|
auto App::GetVg() -> NVGcontext* {
|
||||||
return g_app->vg;
|
return g_app->vg;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,9 +132,9 @@ void log_file_write(const char* msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void log_file_fwrite(const char* fmt, ...) {
|
void log_file_fwrite(const char* fmt, ...) {
|
||||||
std::va_list v{};
|
va_list v{};
|
||||||
va_start(v, fmt);
|
va_start(v, fmt);
|
||||||
log_write_arg(fmt, v);
|
log_write_arg(fmt, &v);
|
||||||
va_end(v);
|
va_end(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,18 +14,20 @@ std::FILE* file{};
|
|||||||
int nxlink_socket{};
|
int nxlink_socket{};
|
||||||
std::mutex mutex{};
|
std::mutex mutex{};
|
||||||
|
|
||||||
void log_write_arg_internal(const char* s, std::va_list& v) {
|
void log_write_arg_internal(const char* s, std::va_list* v) {
|
||||||
if (file) {
|
if (file) {
|
||||||
std::vfprintf(file, s, v);
|
std::vfprintf(file, s, *v);
|
||||||
std::fflush(file);
|
std::fflush(file);
|
||||||
}
|
}
|
||||||
if (nxlink_socket) {
|
if (nxlink_socket) {
|
||||||
std::vprintf(s, v);
|
std::vprintf(s, *v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
auto log_file_init() -> bool {
|
auto log_file_init() -> bool {
|
||||||
std::scoped_lock lock{mutex};
|
std::scoped_lock lock{mutex};
|
||||||
if (file) {
|
if (file) {
|
||||||
@@ -70,11 +72,11 @@ void log_write(const char* s, ...) {
|
|||||||
|
|
||||||
std::va_list v{};
|
std::va_list v{};
|
||||||
va_start(v, s);
|
va_start(v, s);
|
||||||
log_write_arg_internal(s, v);
|
log_write_arg_internal(s, &v);
|
||||||
va_end(v);
|
va_end(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
void log_write_arg(const char* s, std::va_list& v) {
|
void log_write_arg(const char* s, va_list* v) {
|
||||||
std::scoped_lock lock{mutex};
|
std::scoped_lock lock{mutex};
|
||||||
if (!file && !nxlink_socket) {
|
if (!file && !nxlink_socket) {
|
||||||
return;
|
return;
|
||||||
@@ -83,4 +85,6 @@ void log_write_arg(const char* s, std::va_list& v) {
|
|||||||
log_write_arg_internal(s, v);
|
log_write_arg_internal(s, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // extern "C"
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -7,6 +7,14 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
|
||||||
|
#include "yati/nx/nca.hpp"
|
||||||
|
#include "yati/nx/ncm.hpp"
|
||||||
|
#include "yati/nx/npdm.hpp"
|
||||||
|
#include "yati/nx/ns.hpp"
|
||||||
|
#include "yati/nx/es.hpp"
|
||||||
|
#include "yati/nx/keys.hpp"
|
||||||
|
#include "yati/nx/crypto.hpp"
|
||||||
|
|
||||||
#include "owo.hpp"
|
#include "owo.hpp"
|
||||||
#include "defines.hpp"
|
#include "defines.hpp"
|
||||||
#include "app.hpp"
|
#include "app.hpp"
|
||||||
@@ -23,52 +31,9 @@ constexpr u32 PFS0_EXEFS_HASH_BLOCK_SIZE = 0x10000;
|
|||||||
constexpr u32 PFS0_LOGO_HASH_BLOCK_SIZE = 0x1000;
|
constexpr u32 PFS0_LOGO_HASH_BLOCK_SIZE = 0x1000;
|
||||||
constexpr u32 PFS0_META_HASH_BLOCK_SIZE = 0x1000;
|
constexpr u32 PFS0_META_HASH_BLOCK_SIZE = 0x1000;
|
||||||
constexpr u32 PFS0_PADDING_SIZE = 0x200;
|
constexpr u32 PFS0_PADDING_SIZE = 0x200;
|
||||||
constexpr u32 NCA_SECTION_TOTAL = 0x4;
|
|
||||||
constexpr u32 ROMFS_ENTRY_EMPTY = 0xFFFFFFFF;
|
constexpr u32 ROMFS_ENTRY_EMPTY = 0xFFFFFFFF;
|
||||||
constexpr u32 ROMFS_FILEPARTITION_OFS = 0x200;
|
constexpr u32 ROMFS_FILEPARTITION_OFS = 0x200;
|
||||||
|
|
||||||
enum NcaDistributionType {
|
|
||||||
NcaDistributionType_System = 0x0,
|
|
||||||
NcaDistributionType_GameCard = 0x1
|
|
||||||
};
|
|
||||||
|
|
||||||
enum NcaContentType {
|
|
||||||
NcaContentType_Program = 0x0,
|
|
||||||
NcaContentType_Meta = 0x1,
|
|
||||||
NcaContentType_Control = 0x2,
|
|
||||||
NcaContentType_Manual = 0x3,
|
|
||||||
NcaContentType_Data = 0x4,
|
|
||||||
NcaContentType_PublicData = 0x5,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum NcaFileSystemType {
|
|
||||||
NcaFileSystemType_RomFS = 0x0,
|
|
||||||
NcaFileSystemType_PFS0 = 0x1
|
|
||||||
};
|
|
||||||
|
|
||||||
enum NcaHashType {
|
|
||||||
NcaHashType_Auto = 0x0,
|
|
||||||
NcaHashType_HierarchicalSha256 = 0x2,
|
|
||||||
NcaHashType_HierarchicalIntegrity = 0x3
|
|
||||||
};
|
|
||||||
|
|
||||||
enum NcaEncryptionType {
|
|
||||||
NcaEncryptionType_Auto = 0x0,
|
|
||||||
NcaEncryptionType_None = 0x1,
|
|
||||||
NcaEncryptionType_AesCtrOld = 0x2,
|
|
||||||
NcaEncryptionType_AesCtr = 0x3,
|
|
||||||
NcaEncryptionType_AesCtrEx = 0x4
|
|
||||||
};
|
|
||||||
|
|
||||||
enum NsApplicationRecordType {
|
|
||||||
// installed
|
|
||||||
NsApplicationRecordType_Installed = 0x3,
|
|
||||||
// application is gamecard, but gamecard isn't insterted
|
|
||||||
NsApplicationRecordType_GamecardMissing = 0x5,
|
|
||||||
// archived
|
|
||||||
NsApplicationRecordType_Archived = 0xB,
|
|
||||||
};
|
|
||||||
|
|
||||||
// stdio-like wrapper for std::vector
|
// stdio-like wrapper for std::vector
|
||||||
struct BufHelper {
|
struct BufHelper {
|
||||||
BufHelper() = default;
|
BufHelper() = default;
|
||||||
@@ -124,12 +89,6 @@ struct CnmtHeader {
|
|||||||
};
|
};
|
||||||
static_assert(sizeof(CnmtHeader) == 0x20);
|
static_assert(sizeof(CnmtHeader) == 0x20);
|
||||||
|
|
||||||
struct NcmContentStorageRecord {
|
|
||||||
NcmContentMetaKey key;
|
|
||||||
u8 storage_id; //
|
|
||||||
u8 padding[0x7];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NcmContentMetaData {
|
struct NcmContentMetaData {
|
||||||
NcmContentMetaHeader header;
|
NcmContentMetaHeader header;
|
||||||
NcmApplicationMetaExtendedHeader extended;
|
NcmApplicationMetaExtendedHeader extended;
|
||||||
@@ -142,7 +101,7 @@ struct NcaMetaEntry {
|
|||||||
NcaEntry nca_entry;
|
NcaEntry nca_entry;
|
||||||
NcmContentMetaHeader content_meta_header{};
|
NcmContentMetaHeader content_meta_header{};
|
||||||
NcmContentMetaKey content_meta_key{};
|
NcmContentMetaKey content_meta_key{};
|
||||||
NcmContentStorageRecord content_storage_record{};
|
ncm::ContentStorageRecord content_storage_record{};
|
||||||
NcmContentMetaData content_meta_data{};
|
NcmContentMetaData content_meta_data{};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,61 +130,6 @@ struct FileEntry {
|
|||||||
|
|
||||||
using FileEntries = std::vector<FileEntry>;
|
using FileEntries = std::vector<FileEntry>;
|
||||||
|
|
||||||
struct NpdmMeta {
|
|
||||||
u32 magic; // "META"
|
|
||||||
u32 signature_key_generation; // +9.0.0
|
|
||||||
u32 _0x8;
|
|
||||||
u8 flags;
|
|
||||||
u8 _0xD;
|
|
||||||
u8 main_thread_priority;
|
|
||||||
u8 main_thread_core_num;
|
|
||||||
u32 _0x10;
|
|
||||||
u32 sys_resource_size; // +3.0.0
|
|
||||||
u32 version;
|
|
||||||
u32 main_thread_stack_size;
|
|
||||||
char title_name[0x10];
|
|
||||||
char product_code[0x10];
|
|
||||||
u8 _0x40[0x30];
|
|
||||||
u32 aci0_offset;
|
|
||||||
u32 aci0_size;
|
|
||||||
u32 acid_offset;
|
|
||||||
u32 acid_size;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NpdmAcid {
|
|
||||||
u8 rsa_sig[0x100];
|
|
||||||
u8 rsa_pub[0x100];
|
|
||||||
u32 magic; // "ACID"
|
|
||||||
u32 size;
|
|
||||||
u8 version;
|
|
||||||
u8 _0x209[0x1];
|
|
||||||
u8 _0x20A[0x2];
|
|
||||||
u32 flags;
|
|
||||||
u64 program_id_min;
|
|
||||||
u64 program_id_max;
|
|
||||||
u32 fac_offset;
|
|
||||||
u32 fac_size;
|
|
||||||
u32 sac_offset;
|
|
||||||
u32 sac_size;
|
|
||||||
u32 kac_offset;
|
|
||||||
u32 kac_size;
|
|
||||||
u8 _0x238[0x8];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NpdmAci0 {
|
|
||||||
u32 magic; // "ACI0"
|
|
||||||
u8 _0x4[0xC];
|
|
||||||
u64 program_id;
|
|
||||||
u8 _0x18[0x8];
|
|
||||||
u32 fac_offset;
|
|
||||||
u32 fac_size;
|
|
||||||
u32 sac_offset;
|
|
||||||
u32 sac_size;
|
|
||||||
u32 kac_offset;
|
|
||||||
u32 kac_size;
|
|
||||||
u8 _0x38[0x8];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NpdmPatch {
|
struct NpdmPatch {
|
||||||
char title_name[0x10]{"Application"};
|
char title_name[0x10]{"Application"};
|
||||||
char product_code[0x10]{};
|
char product_code[0x10]{};
|
||||||
@@ -238,56 +142,6 @@ struct NcapPatch {
|
|||||||
u64 tid;
|
u64 tid;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct NcaSectionTableEntry {
|
|
||||||
u32 media_start_offset; // divided by 0x200.
|
|
||||||
u32 media_end_offset; // divided by 0x200.
|
|
||||||
u8 _0x8[0x4]; // unknown.
|
|
||||||
u8 _0xC[0x4]; // unknown.
|
|
||||||
};
|
|
||||||
|
|
||||||
struct LayerRegion {
|
|
||||||
u64 offset;
|
|
||||||
u64 size;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct HierarchicalSha256Data {
|
|
||||||
u8 master_hash[0x20];
|
|
||||||
u32 block_size;
|
|
||||||
u32 layer_count;
|
|
||||||
LayerRegion hash_layer;
|
|
||||||
LayerRegion pfs0_layer;
|
|
||||||
LayerRegion unused_layers[3];
|
|
||||||
u8 _0x78[0x80];
|
|
||||||
};
|
|
||||||
|
|
||||||
#pragma pack(push, 1)
|
|
||||||
struct HierarchicalIntegrityVerificationLevelInformation {
|
|
||||||
u64 logical_offset;
|
|
||||||
u64 hash_data_size;
|
|
||||||
u32 block_size; // log2
|
|
||||||
u32 _0x14; // reserved
|
|
||||||
};
|
|
||||||
#pragma pack(pop)
|
|
||||||
|
|
||||||
struct InfoLevelHash {
|
|
||||||
u32 max_layers;
|
|
||||||
HierarchicalIntegrityVerificationLevelInformation levels[6];
|
|
||||||
u8 signature_salt[0x20];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct IntegrityMetaInfo {
|
|
||||||
u32 magic; // IVFC
|
|
||||||
u32 version;
|
|
||||||
u32 master_hash_size;
|
|
||||||
InfoLevelHash info_level_hash;
|
|
||||||
u8 master_hash[0x20];
|
|
||||||
u8 _0xE0[0x18];
|
|
||||||
};
|
|
||||||
|
|
||||||
static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
|
|
||||||
static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
|
|
||||||
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo));
|
|
||||||
|
|
||||||
typedef struct romfs_dirent_ctx {
|
typedef struct romfs_dirent_ctx {
|
||||||
u32 entry_offset;
|
u32 entry_offset;
|
||||||
struct romfs_dirent_ctx *parent; /* Parent node */
|
struct romfs_dirent_ctx *parent; /* Parent node */
|
||||||
@@ -317,70 +171,6 @@ typedef struct {
|
|||||||
u64 file_partition_size;
|
u64 file_partition_size;
|
||||||
} romfs_ctx_t;
|
} romfs_ctx_t;
|
||||||
|
|
||||||
struct NcaFsHeader {
|
|
||||||
u16 version; // always 2.
|
|
||||||
u8 fs_type; // see NcaFileSystemType.
|
|
||||||
u8 hash_type; // see NcaHashType.
|
|
||||||
u8 encryption_type; // see NcaEncryptionType.
|
|
||||||
u8 metadata_hash_type;
|
|
||||||
u8 _0x6[0x2]; // empty.
|
|
||||||
|
|
||||||
union {
|
|
||||||
HierarchicalSha256Data hierarchical_sha256_data;
|
|
||||||
IntegrityMetaInfo integrity_meta_info; // used for romfs
|
|
||||||
} hash_data;
|
|
||||||
|
|
||||||
u8 patch_info[0x40];
|
|
||||||
u64 section_ctr;
|
|
||||||
u8 spares_info[0x30];
|
|
||||||
u8 compression_info[0x28];
|
|
||||||
u8 meta_data_hash_data_info[0x30];
|
|
||||||
u8 reserved[0x30];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NcaSectionHeaderHash {
|
|
||||||
u8 sha256[0x20];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NcaKeyArea {
|
|
||||||
u8 area[0x10];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NcaHeader {
|
|
||||||
u8 rsa_fixed_key[0x100];
|
|
||||||
u8 rsa_npdm[0x100]; // key from npdm.
|
|
||||||
u32 magic;
|
|
||||||
u8 distribution_type; // see NcaDistributionType.
|
|
||||||
u8 content_type; // see NcaContentType.
|
|
||||||
u8 old_key_gen; // see NcaOldKeyGeneration.
|
|
||||||
u8 kaek_index; // see NcaKeyAreaEncryptionKeyIndex.
|
|
||||||
u64 size;
|
|
||||||
u64 title_id;
|
|
||||||
u32 context_id;
|
|
||||||
u32 sdk_version;
|
|
||||||
u8 key_gen; // see NcaKeyGeneration.
|
|
||||||
u8 header_1_sig_key_gen;
|
|
||||||
u8 _0x222[0xE]; // empty.
|
|
||||||
FsRightsId rights_id;
|
|
||||||
|
|
||||||
NcaSectionTableEntry fs_table[NCA_SECTION_TOTAL];
|
|
||||||
NcaSectionHeaderHash fs_header_hash[NCA_SECTION_TOTAL];
|
|
||||||
NcaKeyArea key_area[NCA_SECTION_TOTAL];
|
|
||||||
|
|
||||||
u8 _0x340[0xC0]; // empty.
|
|
||||||
|
|
||||||
NcaFsHeader fs_header[NCA_SECTION_TOTAL];
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr u8 HEADER_KEK_SRC[0x10] = {
|
|
||||||
0x1F, 0x12, 0x91, 0x3A, 0x4A, 0xCB, 0xF0, 0x0D, 0x4C, 0xDE, 0x3A, 0xF6, 0xD5, 0x23, 0x88, 0x2A
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr u8 HEADER_KEY_SRC[0x20] = {
|
|
||||||
0x5A, 0x3E, 0xD8, 0x4F, 0xDE, 0xC0, 0xD8, 0x26, 0x31, 0xF7, 0xE2, 0x5D, 0x19, 0x7B, 0xF5, 0xD0,
|
|
||||||
0x1C, 0x9B, 0x7B, 0xFA, 0xF6, 0x28, 0x18, 0x3D, 0x71, 0xF6, 0x4D, 0x73, 0xF1, 0x50, 0xB9, 0xD2
|
|
||||||
};
|
|
||||||
|
|
||||||
auto write_padding(BufHelper& buf, u64 off, u64 block) -> u64 {
|
auto write_padding(BufHelper& buf, u64 off, u64 block) -> u64 {
|
||||||
const u64 size = block - (off % block);
|
const u64 size = block - (off % block);
|
||||||
if (size) {
|
if (size) {
|
||||||
@@ -632,9 +422,9 @@ auto npdm_patch_kc(std::vector<u8>& npdm, u32 off, u32 size, u32 bitmask, u32 va
|
|||||||
|
|
||||||
// todo: manually build npdm
|
// todo: manually build npdm
|
||||||
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
|
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
|
||||||
NpdmMeta meta{};
|
npdm::Meta meta{};
|
||||||
NpdmAci0 aci0{};
|
npdm::Aci0 aci0{};
|
||||||
NpdmAcid acid{};
|
npdm::Acid acid{};
|
||||||
std::memcpy(&meta, npdm.data(), sizeof(meta));
|
std::memcpy(&meta, npdm.data(), sizeof(meta));
|
||||||
std::memcpy(&aci0, npdm.data() + meta.aci0_offset, sizeof(aci0));
|
std::memcpy(&aci0, npdm.data() + meta.aci0_offset, sizeof(aci0));
|
||||||
std::memcpy(&acid, npdm.data() + meta.acid_offset, sizeof(acid));
|
std::memcpy(&acid, npdm.data() + meta.acid_offset, sizeof(acid));
|
||||||
@@ -805,7 +595,7 @@ void write_nca_padding(BufHelper& buf) {
|
|||||||
write_padding(buf, buf.tell(), 0x200);
|
write_padding(buf, buf.tell(), 0x200);
|
||||||
}
|
}
|
||||||
|
|
||||||
void nca_encrypt_header(NcaHeader* header, std::span<const u8> key) {
|
void nca_encrypt_header(nca::Header* header, std::span<const u8> key) {
|
||||||
Aes128XtsContext ctx{};
|
Aes128XtsContext ctx{};
|
||||||
aes128XtsContextCreate(&ctx, key.data(), key.data() + 0x10, true);
|
aes128XtsContextCreate(&ctx, key.data(), key.data() + 0x10, true);
|
||||||
|
|
||||||
@@ -816,41 +606,41 @@ void nca_encrypt_header(NcaHeader* header, std::span<const u8> key) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void write_nca_section(NcaHeader& nca_header, u8 index, u64 start, u64 end) {
|
void write_nca_section(nca::Header& nca_header, u8 index, u64 start, u64 end) {
|
||||||
auto& section = nca_header.fs_table[index];
|
auto& section = nca_header.fs_table[index];
|
||||||
section.media_start_offset = start / 0x200; // 0xC00 / 0x200
|
section.media_start_offset = start / 0x200; // 0xC00 / 0x200
|
||||||
section.media_end_offset = end / 0x200; // Section end offset / 200
|
section.media_end_offset = end / 0x200; // Section end offset / 200
|
||||||
section._0x8[0] = 0x1; // Always 1
|
section._0x8[0] = 0x1; // Always 1
|
||||||
}
|
}
|
||||||
|
|
||||||
void write_nca_fs_header_pfs0(NcaHeader& nca_header, u8 index, const std::vector<u8>& master_hash, u64 hash_table_size, u32 block_size) {
|
void write_nca_fs_header_pfs0(nca::Header& nca_header, u8 index, const std::vector<u8>& master_hash, u64 hash_table_size, u32 block_size) {
|
||||||
auto& fs_header = nca_header.fs_header[index];
|
auto& fs_header = nca_header.fs_header[index];
|
||||||
fs_header.hash_type = NcaHashType_HierarchicalSha256;
|
fs_header.hash_type = nca::HashType_HierarchicalSha256;
|
||||||
fs_header.fs_type = NcaFileSystemType_PFS0;
|
fs_header.fs_type = nca::FileSystemType_PFS0;
|
||||||
fs_header.version = 0x2; // Always 2
|
fs_header.version = 0x2; // Always 2
|
||||||
fs_header.hash_data.hierarchical_sha256_data.layer_count = 0x2;
|
fs_header.hash_data.hierarchical_sha256_data.layer_count = 0x2;
|
||||||
fs_header.hash_data.hierarchical_sha256_data.block_size = block_size;
|
fs_header.hash_data.hierarchical_sha256_data.block_size = block_size;
|
||||||
fs_header.encryption_type = NcaEncryptionType_None;
|
fs_header.encryption_type = nca::EncryptionType_None;
|
||||||
fs_header.hash_data.hierarchical_sha256_data.hash_layer.size = hash_table_size;
|
fs_header.hash_data.hierarchical_sha256_data.hash_layer.size = hash_table_size;
|
||||||
std::memcpy(fs_header.hash_data.hierarchical_sha256_data.master_hash, master_hash.data(), master_hash.size());
|
std::memcpy(fs_header.hash_data.hierarchical_sha256_data.master_hash, master_hash.data(), master_hash.size());
|
||||||
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
|
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
|
||||||
}
|
}
|
||||||
|
|
||||||
void write_nca_fs_header_romfs(NcaHeader& nca_header, u8 index) {
|
void write_nca_fs_header_romfs(nca::Header& nca_header, u8 index) {
|
||||||
auto& fs_header = nca_header.fs_header[index];
|
auto& fs_header = nca_header.fs_header[index];
|
||||||
fs_header.hash_type = NcaHashType_HierarchicalIntegrity;
|
fs_header.hash_type = nca::HashType_HierarchicalIntegrity;
|
||||||
fs_header.fs_type = NcaFileSystemType_RomFS;
|
fs_header.fs_type = nca::FileSystemType_RomFS;
|
||||||
fs_header.version = 0x2; // Always 2
|
fs_header.version = 0x2; // Always 2
|
||||||
fs_header.hash_data.integrity_meta_info.magic = 0x43465649;
|
fs_header.hash_data.integrity_meta_info.magic = 0x43465649;
|
||||||
fs_header.hash_data.integrity_meta_info.version = 0x20000; // Always 0x20000
|
fs_header.hash_data.integrity_meta_info.version = 0x20000; // Always 0x20000
|
||||||
fs_header.hash_data.integrity_meta_info.master_hash_size = SHA256_HASH_SIZE;
|
fs_header.hash_data.integrity_meta_info.master_hash_size = SHA256_HASH_SIZE;
|
||||||
fs_header.hash_data.integrity_meta_info.info_level_hash.max_layers = 0x7;
|
fs_header.hash_data.integrity_meta_info.info_level_hash.max_layers = 0x7;
|
||||||
fs_header.encryption_type = NcaEncryptionType_None;
|
fs_header.encryption_type = nca::EncryptionType_None;
|
||||||
fs_header.hash_data.integrity_meta_info.info_level_hash.levels[5].block_size = 0x0E; // 0x4000
|
fs_header.hash_data.integrity_meta_info.info_level_hash.levels[5].block_size = 0x0E; // 0x4000
|
||||||
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
|
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
|
||||||
}
|
}
|
||||||
|
|
||||||
void write_nca_pfs0(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
void write_nca_pfs0(nca::Header& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
||||||
const auto pfs0 = build_pfs0(entries);
|
const auto pfs0 = build_pfs0(entries);
|
||||||
const auto pfs0_hash_table = build_pfs0_hash_table(pfs0, block_size);
|
const auto pfs0_hash_table = build_pfs0_hash_table(pfs0, block_size);
|
||||||
const auto pfs0_master_hash = build_pfs0_master_hash(pfs0_hash_table);
|
const auto pfs0_master_hash = build_pfs0_master_hash(pfs0_hash_table);
|
||||||
@@ -887,7 +677,7 @@ auto ivfc_create_level(const std::vector<u8>& src) -> std::vector<u8> {
|
|||||||
return buf.buf;
|
return buf.buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
void write_nca_romfs(nca::Header& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
||||||
auto& fs_header = nca_header.fs_header[index];
|
auto& fs_header = nca_header.fs_header[index];
|
||||||
auto& meta_info = fs_header.hash_data.integrity_meta_info;
|
auto& meta_info = fs_header.hash_data.integrity_meta_info;
|
||||||
auto& info_level_hash = meta_info.info_level_hash;
|
auto& info_level_hash = meta_info.info_level_hash;
|
||||||
@@ -921,22 +711,22 @@ void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries
|
|||||||
write_nca_fs_header_romfs(nca_header, index);
|
write_nca_fs_header_romfs(nca_header, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
void write_nca_header_encypted(NcaHeader& nca_header, u64 tid, std::span<const u8> key, NcaContentType type, BufHelper& buf) {
|
void write_nca_header_encypted(nca::Header& nca_header, u64 tid, const keys::Keys& keys, nca::ContentType type, BufHelper& buf) {
|
||||||
nca_header.magic = 0x3341434E;
|
nca_header.magic = NCA3_MAGIC;
|
||||||
nca_header.distribution_type = NcaDistributionType_System;
|
nca_header.distribution_type = nca::DistributionType_System;
|
||||||
nca_header.content_type = type;
|
nca_header.content_type = type;
|
||||||
nca_header.title_id = tid;
|
nca_header.title_id = tid;
|
||||||
nca_header.sdk_version = 0x000C1100;
|
nca_header.sdk_version = 0x000C1100;
|
||||||
nca_header.size = buf.tell();
|
nca_header.size = buf.tell();
|
||||||
|
|
||||||
nca_encrypt_header(&nca_header, key);
|
nca_encrypt_header(&nca_header, keys.header_key);
|
||||||
buf.seek(0);
|
buf.seek(0);
|
||||||
buf.write(&nca_header, sizeof(nca_header));
|
buf.write(&nca_header, sizeof(nca_header));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto create_program_nca(u64 tid, std::span<const u8> key, const FileEntries& exefs, const FileEntries& romfs, const FileEntries& logo) -> NcaEntry {
|
auto create_program_nca(u64 tid, const keys::Keys& keys, const FileEntries& exefs, const FileEntries& romfs, const FileEntries& logo) -> NcaEntry {
|
||||||
BufHelper buf;
|
BufHelper buf;
|
||||||
NcaHeader nca_header{};
|
nca::Header nca_header{};
|
||||||
buf.write(&nca_header, sizeof(nca_header));
|
buf.write(&nca_header, sizeof(nca_header));
|
||||||
|
|
||||||
write_nca_pfs0(nca_header, 0, exefs, PFS0_EXEFS_HASH_BLOCK_SIZE, buf);
|
write_nca_pfs0(nca_header, 0, exefs, PFS0_EXEFS_HASH_BLOCK_SIZE, buf);
|
||||||
@@ -945,23 +735,23 @@ auto create_program_nca(u64 tid, std::span<const u8> key, const FileEntries& exe
|
|||||||
if (logo.size() == 2 && !logo[0].data.empty() && !logo[1].data.empty()) {
|
if (logo.size() == 2 && !logo[0].data.empty() && !logo[1].data.empty()) {
|
||||||
write_nca_pfs0(nca_header, 2, logo, PFS0_LOGO_HASH_BLOCK_SIZE, buf);
|
write_nca_pfs0(nca_header, 2, logo, PFS0_LOGO_HASH_BLOCK_SIZE, buf);
|
||||||
}
|
}
|
||||||
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Program, buf);
|
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Program, buf);
|
||||||
|
|
||||||
return {buf, NcmContentType_Program};
|
return {buf, NcmContentType_Program};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto create_control_nca(u64 tid, std::span<const u8> key, const FileEntries& romfs) -> NcaEntry{
|
auto create_control_nca(u64 tid, const keys::Keys& keys, const FileEntries& romfs) -> NcaEntry{
|
||||||
NcaHeader nca_header{};
|
nca::Header nca_header{};
|
||||||
BufHelper buf;
|
BufHelper buf;
|
||||||
buf.write(&nca_header, sizeof(nca_header));
|
buf.write(&nca_header, sizeof(nca_header));
|
||||||
|
|
||||||
write_nca_romfs(nca_header, 0, romfs, IVFC_HASH_BLOCK_SIZE, buf);
|
write_nca_romfs(nca_header, 0, romfs, IVFC_HASH_BLOCK_SIZE, buf);
|
||||||
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Control, buf);
|
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Control, buf);
|
||||||
|
|
||||||
return {buf, NcmContentType_Control};
|
return {buf, NcmContentType_Control};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id, const std::vector<NcaEntry>& ncas) -> NcaMetaEntry {
|
auto create_meta_nca(u64 tid, const keys::Keys& keys, NcmStorageId storage_id, const std::vector<NcaEntry>& ncas) -> NcaMetaEntry {
|
||||||
CnmtHeader cnmt_header{};
|
CnmtHeader cnmt_header{};
|
||||||
NcmApplicationMetaExtendedHeader cnmt_extended{};
|
NcmApplicationMetaExtendedHeader cnmt_extended{};
|
||||||
NcmPackagedContentInfo packaged_content_info[2]{};
|
NcmPackagedContentInfo packaged_content_info[2]{};
|
||||||
@@ -997,10 +787,10 @@ auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id,
|
|||||||
std::snprintf(cnmt_name, sizeof(cnmt_name), "Application_%016lX.cnmt", tid);
|
std::snprintf(cnmt_name, sizeof(cnmt_name), "Application_%016lX.cnmt", tid);
|
||||||
add_file_entry(cnmt, cnmt_name, cnmt_buf.buf.data(), cnmt_buf.buf.size());
|
add_file_entry(cnmt, cnmt_name, cnmt_buf.buf.data(), cnmt_buf.buf.size());
|
||||||
|
|
||||||
NcaHeader nca_header{};
|
nca::Header nca_header{};
|
||||||
buf.write(&nca_header, sizeof(nca_header));
|
buf.write(&nca_header, sizeof(nca_header));
|
||||||
write_nca_pfs0(nca_header, 0, cnmt, PFS0_META_HASH_BLOCK_SIZE, buf);
|
write_nca_pfs0(nca_header, 0, cnmt, PFS0_META_HASH_BLOCK_SIZE, buf);
|
||||||
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Meta, buf);
|
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Meta, buf);
|
||||||
|
|
||||||
// entry
|
// entry
|
||||||
NcaMetaEntry entry{buf, NcmContentType_Meta};
|
NcaMetaEntry entry{buf, NcmContentType_Meta};
|
||||||
@@ -1040,26 +830,6 @@ auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id,
|
|||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result nsDeleteApplicationRecord(Service* srv, u64 tid) {
|
|
||||||
return serviceDispatchIn(srv, 27, tid);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result nsPushApplicationRecord(Service* srv, u64 tid, const NcmContentStorageRecord* records, u32 count) {
|
|
||||||
const struct {
|
|
||||||
u8 last_modified_event;
|
|
||||||
u8 padding[0x7];
|
|
||||||
u64 tid;
|
|
||||||
} in = { NsApplicationRecordType_Installed, {0}, tid };
|
|
||||||
|
|
||||||
return serviceDispatchIn(srv, 16, in,
|
|
||||||
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
|
|
||||||
.buffers = { { records, sizeof(NcmContentStorageRecord) * count } });
|
|
||||||
}
|
|
||||||
|
|
||||||
Result nsInvalidateApplicationControlCache(Service* srv, u64 tid) {
|
|
||||||
return serviceDispatchIn(srv, 404, tid);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId storage_id) -> Result {
|
auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId storage_id) -> Result {
|
||||||
R_UNLESS(!config.nro_path.empty(), OwoError_BadArgs);
|
R_UNLESS(!config.nro_path.empty(), OwoError_BadArgs);
|
||||||
// R_UNLESS(!config.icon.empty(), OwoError_BadArgs);
|
// R_UNLESS(!config.icon.empty(), OwoError_BadArgs);
|
||||||
@@ -1073,14 +843,8 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
|||||||
R_TRY(nsInitialize());
|
R_TRY(nsInitialize());
|
||||||
ON_SCOPE_EXIT(nsExit());
|
ON_SCOPE_EXIT(nsExit());
|
||||||
|
|
||||||
// generate header kek
|
keys::Keys keys;
|
||||||
u8 header_kek[0x20];
|
R_TRY(keys::parse_keys(keys, false));
|
||||||
R_TRY(splCryptoGenerateAesKek(HEADER_KEK_SRC, 0, 0, header_kek));
|
|
||||||
// gen header key 0
|
|
||||||
u8 key[0x20];
|
|
||||||
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC, key));
|
|
||||||
// gen header key 1
|
|
||||||
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC + 0x10, key + 0x10));
|
|
||||||
|
|
||||||
// fix args to include nro path
|
// fix args to include nro path
|
||||||
if (config.args.empty()) {
|
if (config.args.empty()) {
|
||||||
@@ -1124,7 +888,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
|||||||
patch_npdm(exefs[1].data, npdm_patch);
|
patch_npdm(exefs[1].data, npdm_patch);
|
||||||
|
|
||||||
nca_entries.emplace_back(
|
nca_entries.emplace_back(
|
||||||
create_program_nca(tid, key, exefs, romfs, logo)
|
create_program_nca(tid, keys, exefs, romfs, logo)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
nca_entries.emplace_back(
|
nca_entries.emplace_back(
|
||||||
@@ -1147,18 +911,18 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
|||||||
add_file_entry(romfs, "/icon_AmericanEnglish.dat", config.icon);
|
add_file_entry(romfs, "/icon_AmericanEnglish.dat", config.icon);
|
||||||
|
|
||||||
nca_entries.emplace_back(
|
nca_entries.emplace_back(
|
||||||
create_control_nca(tid, key, romfs)
|
create_control_nca(tid, keys, romfs)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// create meta
|
// create meta
|
||||||
NcmContentMetaHeader content_meta_header;
|
NcmContentMetaHeader content_meta_header;
|
||||||
NcmContentMetaKey content_meta_key;
|
NcmContentMetaKey content_meta_key;
|
||||||
NcmContentStorageRecord content_storage_record;
|
ncm::ContentStorageRecord content_storage_record;
|
||||||
NcmContentMetaData content_meta_data;
|
NcmContentMetaData content_meta_data;
|
||||||
{
|
{
|
||||||
pbox->NewTransfer("Creating Meta"_i18n).UpdateTransfer(2, 8);
|
pbox->NewTransfer("Creating Meta"_i18n).UpdateTransfer(2, 8);
|
||||||
const auto meta_entry = create_meta_nca(tid, key, storage_id, nca_entries);
|
const auto meta_entry = create_meta_nca(tid, keys, storage_id, nca_entries);
|
||||||
|
|
||||||
nca_entries.emplace_back(meta_entry.nca_entry);
|
nca_entries.emplace_back(meta_entry.nca_entry);
|
||||||
content_meta_header = meta_entry.content_meta_header;
|
content_meta_header = meta_entry.content_meta_header;
|
||||||
@@ -1218,15 +982,15 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
|||||||
|
|
||||||
// remove previous application record
|
// remove previous application record
|
||||||
if (already_installed || hosversionBefore(2,0,0)) {
|
if (already_installed || hosversionBefore(2,0,0)) {
|
||||||
const auto rc = nsDeleteApplicationRecord(srv_ptr, tid);
|
const auto rc = ns::DeleteApplicationRecord(srv_ptr, tid);
|
||||||
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
|
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
|
||||||
}
|
}
|
||||||
|
|
||||||
R_TRY(nsPushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
|
R_TRY(ns::PushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
|
||||||
|
|
||||||
// force flush
|
// force flush
|
||||||
if (already_installed || hosversionBefore(2,0,0)) {
|
if (already_installed || hosversionBefore(2,0,0)) {
|
||||||
const auto rc = nsInvalidateApplicationControlCache(srv_ptr, tid);
|
const auto rc = ns::InvalidateApplicationControlCache(srv_ptr, tid);
|
||||||
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
|
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
#include "owo.hpp"
|
#include "owo.hpp"
|
||||||
#include "swkbd.hpp"
|
#include "swkbd.hpp"
|
||||||
#include "i18n.hpp"
|
#include "i18n.hpp"
|
||||||
|
#include "yati/yati.hpp"
|
||||||
|
#include "yati/source/file.hpp"
|
||||||
|
|
||||||
#include <minIni.h>
|
#include <minIni.h>
|
||||||
#include <minizip/unzip.h>
|
#include <minizip/unzip.h>
|
||||||
@@ -52,6 +54,10 @@ constexpr std::string_view VIDEO_EXTENSIONS[] = {
|
|||||||
constexpr std::string_view IMAGE_EXTENSIONS[] = {
|
constexpr std::string_view IMAGE_EXTENSIONS[] = {
|
||||||
"png", "jpg", "jpeg", "bmp", "gif",
|
"png", "jpg", "jpeg", "bmp", "gif",
|
||||||
};
|
};
|
||||||
|
constexpr std::string_view INSTALL_EXTENSIONS[] = {
|
||||||
|
"nsp", "xci", "nsz", "xcz",
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
struct RomDatabaseEntry {
|
struct RomDatabaseEntry {
|
||||||
std::string_view folder;
|
std::string_view folder;
|
||||||
@@ -294,6 +300,8 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
|||||||
nro_launch(GetNewPathCurrent());
|
nro_launch(GetNewPathCurrent());
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
} else if (App::GetInstallEnable() && IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) {
|
||||||
|
InstallFile(GetEntry());
|
||||||
} else {
|
} else {
|
||||||
const auto assoc_list = FindFileAssocFor();
|
const auto assoc_list = FindFileAssocFor();
|
||||||
if (!assoc_list.empty()) {
|
if (!assoc_list.empty()) {
|
||||||
@@ -406,7 +414,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
|||||||
}
|
}
|
||||||
log_write("clicked on delete\n");
|
log_write("clicked on delete\n");
|
||||||
App::Push(std::make_shared<OptionBox>(
|
App::Push(std::make_shared<OptionBox>(
|
||||||
"Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
|
"Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){
|
||||||
if (op_index && *op_index) {
|
if (op_index && *op_index) {
|
||||||
App::PopToMenu();
|
App::PopToMenu();
|
||||||
OnDeleteCallback();
|
OnDeleteCallback();
|
||||||
@@ -421,7 +429,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
|||||||
options->Add(std::make_shared<SidebarEntryCallback>("Paste"_i18n, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Paste"_i18n, [this](){
|
||||||
const std::string buf = "Paste "_i18n + std::to_string(m_selected_files.size()) + " file(s)?"_i18n;
|
const std::string buf = "Paste "_i18n + std::to_string(m_selected_files.size()) + " file(s)?"_i18n;
|
||||||
App::Push(std::make_shared<OptionBox>(
|
App::Push(std::make_shared<OptionBox>(
|
||||||
buf, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
|
buf, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){
|
||||||
if (op_index && *op_index) {
|
if (op_index && *op_index) {
|
||||||
App::PopToMenu();
|
App::PopToMenu();
|
||||||
OnPasteCallback();
|
OnPasteCallback();
|
||||||
@@ -459,6 +467,32 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if install is enabled, check if all currently selected files are installable.
|
||||||
|
if (m_entries_current.size() && App::GetInstallEnable()) {
|
||||||
|
bool should_install = true;
|
||||||
|
if (!m_selected_count) {
|
||||||
|
should_install = IsExtension(GetEntry().GetExtension(), INSTALL_EXTENSIONS);
|
||||||
|
} else {
|
||||||
|
const auto entries = GetSelectedEntries();
|
||||||
|
for (auto&e : entries) {
|
||||||
|
if (!IsExtension(e.GetExtension(), INSTALL_EXTENSIONS)) {
|
||||||
|
should_install = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (should_install) {
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("Install"_i18n, [this](){
|
||||||
|
if (!m_selected_count) {
|
||||||
|
InstallFile(GetEntry());
|
||||||
|
} else {
|
||||||
|
InstallFiles(GetSelectedEntries());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
|
||||||
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
|
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
|
||||||
ON_SCOPE_EXIT(App::Push(options));
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
@@ -628,6 +662,9 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
icon = ThemeEntryID_ICON_VIDEO;
|
icon = ThemeEntryID_ICON_VIDEO;
|
||||||
} else if (IsExtension(ext, IMAGE_EXTENSIONS)) {
|
} else if (IsExtension(ext, IMAGE_EXTENSIONS)) {
|
||||||
icon = ThemeEntryID_ICON_IMAGE;
|
icon = ThemeEntryID_ICON_IMAGE;
|
||||||
|
} else if (IsExtension(ext, INSTALL_EXTENSIONS)) {
|
||||||
|
// todo: maybe replace this icon with something else?
|
||||||
|
icon = ThemeEntryID_ICON_NRO;
|
||||||
} else if (IsExtension(ext, "zip")) {
|
} else if (IsExtension(ext, "zip")) {
|
||||||
icon = ThemeEntryID_ICON_ZIP;
|
icon = ThemeEntryID_ICON_ZIP;
|
||||||
} else if (IsExtension(ext, "nro")) {
|
} else if (IsExtension(ext, "nro")) {
|
||||||
@@ -775,6 +812,34 @@ void Menu::InstallForwarder() {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Menu::InstallFile(const FileEntry& target) {
|
||||||
|
std::vector<FileEntry> targets{target};
|
||||||
|
InstallFiles(targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::InstallFiles(const std::vector<FileEntry>& targets) {
|
||||||
|
App::Push(std::make_shared<OptionBox>("Install Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this, targets](auto op_index){
|
||||||
|
if (op_index && *op_index) {
|
||||||
|
App::PopToMenu();
|
||||||
|
|
||||||
|
App::Push(std::make_shared<ui::ProgressBox>("Installing App"_i18n, [this, targets](auto pbox) mutable -> bool {
|
||||||
|
for (auto& e : targets) {
|
||||||
|
const auto rc = yati::InstallFromFile(pbox, &m_fs->m_fs, GetNewPath(e));
|
||||||
|
if (rc == yati::Result_Cancelled) {
|
||||||
|
break;
|
||||||
|
} else if (R_FAILED(rc)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
App::Notify("Installed " + e.GetName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
|
auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
|
||||||
log_write("new scan path: %s\n", new_path.s);
|
log_write("new scan path: %s\n", new_path.s);
|
||||||
if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) {
|
if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) {
|
||||||
|
|||||||
@@ -323,10 +323,6 @@ MainMenu::MainMenu() {
|
|||||||
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::LEFT);
|
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::LEFT);
|
||||||
ON_SCOPE_EXIT(App::Push(options));
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
|
|
||||||
SidebarEntryArray::Items install_items;
|
|
||||||
install_items.push_back("System memory"_i18n);
|
|
||||||
install_items.push_back("microSD card"_i18n);
|
|
||||||
|
|
||||||
SidebarEntryArray::Items text_scroll_speed_items;
|
SidebarEntryArray::Items text_scroll_speed_items;
|
||||||
text_scroll_speed_items.push_back("Slow"_i18n);
|
text_scroll_speed_items.push_back("Slow"_i18n);
|
||||||
text_scroll_speed_items.push_back("Normal"_i18n);
|
text_scroll_speed_items.push_back("Normal"_i18n);
|
||||||
@@ -340,21 +336,94 @@ MainMenu::MainMenu() {
|
|||||||
App::SetReplaceHbmenuEnable(enable);
|
App::SetReplaceHbmenuEnable(enable);
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Install forwarders"_i18n, App::GetInstallEnable(), [this](bool& enable){
|
|
||||||
App::SetInstallEnable(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](s64& index_out){
|
|
||||||
App::SetInstallSdEnable(index_out);
|
|
||||||
}, (s64)App::GetInstallSdEnable()));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){
|
|
||||||
App::SetInstallPrompt(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [this](s64& index_out){
|
options->Add(std::make_shared<SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [this](s64& index_out){
|
||||||
App::SetTextScrollSpeed(index_out);
|
App::SetTextScrollSpeed(index_out);
|
||||||
}, (s64)App::GetTextScrollSpeed()));
|
}, (s64)App::GetTextScrollSpeed()));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("Install options"_i18n, [this](){
|
||||||
|
auto options = std::make_shared<Sidebar>("Install Options"_i18n, Sidebar::Side::LEFT);
|
||||||
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
|
|
||||||
|
SidebarEntryArray::Items install_items;
|
||||||
|
install_items.push_back("System memory"_i18n);
|
||||||
|
install_items.push_back("microSD card"_i18n);
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Enable"_i18n, App::GetInstallEnable(), [this](bool& enable){
|
||||||
|
App::SetInstallEnable(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){
|
||||||
|
App::SetInstallPrompt(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](s64& index_out){
|
||||||
|
App::SetInstallSdEnable(index_out);
|
||||||
|
}, (s64)App::GetInstallSdEnable()));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_allow_downgrade.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_skip_if_already_installed.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_ticket_only.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Patch ticket"_i18n, App::GetApp()->m_patch_ticket.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_patch_ticket.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_skip_base.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Skip Patch"_i18n, App::GetApp()->m_skip_patch.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_skip_patch.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Skip addon"_i18n, App::GetApp()->m_skip_addon.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_skip_addon.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_skip_data_patch.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_skip_ticket.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_skip_nca_hash_verify.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_ignore_distribution_bit.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_convert_to_standard_crypto.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_lower_master_key.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [this](bool& enable){
|
||||||
|
App::GetApp()->m_lower_system_version.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
}));
|
||||||
}));
|
}));
|
||||||
}})
|
}})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -102,6 +102,14 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto ProgressBox::SetTitle(const std::string& title) -> ProgressBox& {
|
||||||
|
mutexLock(&m_mutex);
|
||||||
|
m_title = title;
|
||||||
|
mutexUnlock(&m_mutex);
|
||||||
|
Yield();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
|
auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
|
||||||
mutexLock(&m_mutex);
|
mutexLock(&m_mutex);
|
||||||
m_transfer = transfer;
|
m_transfer = transfer;
|
||||||
|
|||||||
67
sphaira/source/yati/container/nsp.cpp
Normal file
67
sphaira/source/yati/container/nsp.cpp
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#include "yati/container/nsp.hpp"
|
||||||
|
#include "defines.hpp"
|
||||||
|
#include "log.hpp"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace sphaira::yati::container {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
#define PFS0_MAGIC 0x30534650
|
||||||
|
|
||||||
|
struct Pfs0Header {
|
||||||
|
u32 magic;
|
||||||
|
u32 total_files;
|
||||||
|
u32 string_table_size;
|
||||||
|
u32 padding;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Pfs0FileTableEntry {
|
||||||
|
u64 data_offset;
|
||||||
|
u64 data_size;
|
||||||
|
u32 name_offset;
|
||||||
|
u32 padding;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Result Nsp::Validate(source::Base* source) {
|
||||||
|
u32 magic;
|
||||||
|
u64 bytes_read;
|
||||||
|
R_TRY(source->Read(std::addressof(magic), 0, sizeof(magic), std::addressof(bytes_read)));
|
||||||
|
R_UNLESS(magic == PFS0_MAGIC, 0x1);
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Nsp::GetCollections(Collections& out) {
|
||||||
|
u64 bytes_read;
|
||||||
|
s64 off = 0;
|
||||||
|
|
||||||
|
// get header
|
||||||
|
Pfs0Header header{};
|
||||||
|
R_TRY(m_source->Read(std::addressof(header), off, sizeof(header), std::addressof(bytes_read)));
|
||||||
|
R_UNLESS(header.magic == PFS0_MAGIC, 0x1);
|
||||||
|
off += bytes_read;
|
||||||
|
|
||||||
|
// get file table
|
||||||
|
std::vector<Pfs0FileTableEntry> file_table(header.total_files);
|
||||||
|
R_TRY(m_source->Read(file_table.data(), off, file_table.size() * sizeof(Pfs0FileTableEntry), std::addressof(bytes_read)))
|
||||||
|
off += bytes_read;
|
||||||
|
|
||||||
|
// get string table
|
||||||
|
std::vector<char> string_table(header.string_table_size);
|
||||||
|
R_TRY(m_source->Read(string_table.data(), off, string_table.size(), std::addressof(bytes_read)))
|
||||||
|
off += bytes_read;
|
||||||
|
|
||||||
|
out.reserve(header.total_files);
|
||||||
|
for (u32 i = 0; i < header.total_files; i++) {
|
||||||
|
CollectionEntry entry;
|
||||||
|
entry.name = string_table.data() + file_table[i].name_offset;
|
||||||
|
entry.offset = off + file_table[i].data_offset;
|
||||||
|
entry.size = file_table[i].data_size;
|
||||||
|
out.emplace_back(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::container
|
||||||
95
sphaira/source/yati/container/xci.cpp
Normal file
95
sphaira/source/yati/container/xci.cpp
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#include "yati/container/xci.hpp"
|
||||||
|
#include "defines.hpp"
|
||||||
|
#include "log.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::yati::container {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
#define XCI_MAGIC std::byteswap(0x48454144)
|
||||||
|
#define HFS0_MAGIC 0x30534648
|
||||||
|
#define HFS0_HEADER_OFFSET 0xF000
|
||||||
|
|
||||||
|
struct Hfs0Header {
|
||||||
|
u32 magic;
|
||||||
|
u32 total_files;
|
||||||
|
u32 string_table_size;
|
||||||
|
u32 padding;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Hfs0FileTableEntry {
|
||||||
|
u64 data_offset;
|
||||||
|
u64 data_size;
|
||||||
|
u32 name_offset;
|
||||||
|
u32 hash_size;
|
||||||
|
u64 padding;
|
||||||
|
u8 hash[0x20];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Hfs0 {
|
||||||
|
Hfs0Header header{};
|
||||||
|
std::vector<Hfs0FileTableEntry> file_table{};
|
||||||
|
std::vector<std::string> string_table{};
|
||||||
|
s64 data_offset{};
|
||||||
|
};
|
||||||
|
|
||||||
|
Result Hfs0GetPartition(source::Base* source, s64 off, Hfs0& out) {
|
||||||
|
u64 bytes_read;
|
||||||
|
|
||||||
|
// get header
|
||||||
|
R_TRY(source->Read(std::addressof(out.header), off, sizeof(out.header), std::addressof(bytes_read)));
|
||||||
|
R_UNLESS(out.header.magic == HFS0_MAGIC, 0x1);
|
||||||
|
off += bytes_read;
|
||||||
|
|
||||||
|
// get file table
|
||||||
|
out.file_table.resize(out.header.total_files);
|
||||||
|
R_TRY(source->Read(out.file_table.data(), off, out.file_table.size() * sizeof(Hfs0FileTableEntry), std::addressof(bytes_read)))
|
||||||
|
off += bytes_read;
|
||||||
|
|
||||||
|
// get string table
|
||||||
|
std::vector<char> string_table(out.header.string_table_size);
|
||||||
|
R_TRY(source->Read(string_table.data(), off, string_table.size(), std::addressof(bytes_read)))
|
||||||
|
off += bytes_read;
|
||||||
|
|
||||||
|
for (u32 i = 0; i < out.header.total_files; i++) {
|
||||||
|
out.string_table.emplace_back(string_table.data() + out.file_table[i].name_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.data_offset = off;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Result Xci::Validate(source::Base* source) {
|
||||||
|
u32 magic;
|
||||||
|
u64 bytes_read;
|
||||||
|
R_TRY(source->Read(std::addressof(magic), 0x100, sizeof(magic), std::addressof(bytes_read)));
|
||||||
|
R_UNLESS(magic == XCI_MAGIC, 0x1);
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Xci::GetCollections(Collections& out) {
|
||||||
|
Hfs0 root{};
|
||||||
|
R_TRY(Hfs0GetPartition(m_source, HFS0_HEADER_OFFSET, root));
|
||||||
|
|
||||||
|
for (u32 i = 0; i < root.header.total_files; i++) {
|
||||||
|
if (root.string_table[i] == "secure") {
|
||||||
|
Hfs0 secure{};
|
||||||
|
R_TRY(Hfs0GetPartition(m_source, root.data_offset + root.file_table[i].data_offset, secure));
|
||||||
|
|
||||||
|
for (u32 i = 0; i < secure.header.total_files; i++) {
|
||||||
|
CollectionEntry entry;
|
||||||
|
entry.name = secure.string_table[i];
|
||||||
|
entry.offset = secure.data_offset + secure.file_table[i].data_offset;
|
||||||
|
entry.size = secure.file_table[i].data_size;
|
||||||
|
out.emplace_back(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::container
|
||||||
122
sphaira/source/yati/nx/es.cpp
Normal file
122
sphaira/source/yati/nx/es.cpp
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#include "yati/nx/es.hpp"
|
||||||
|
#include "yati/nx/crypto.hpp"
|
||||||
|
#include "yati/nx/nxdumptool_rsa.h"
|
||||||
|
#include "defines.hpp"
|
||||||
|
#include "log.hpp"
|
||||||
|
#include <memory>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace sphaira::es {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size) {
|
||||||
|
return serviceDispatch(srv, 1,
|
||||||
|
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In, SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
|
||||||
|
.buffers = { { tik_buf, tik_size }, { cert_buf, cert_size } });
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
TikPropertyMask_None = 0,
|
||||||
|
TikPropertyMask_PreInstallation = BIT(0), ///< Determines if the title comes pre-installed on the device. Most likely unused -- a remnant from previous ticket formats.
|
||||||
|
TikPropertyMask_SharedTitle = BIT(1), ///< Determines if the title holds shared contents only. Most likely unused -- a remnant from previous ticket formats.
|
||||||
|
TikPropertyMask_AllContents = BIT(2), ///< Determines if the content index mask shall be bypassed. Most likely unused -- a remnant from previous ticket formats.
|
||||||
|
TikPropertyMask_DeviceLinkIndepedent = BIT(3), ///< Determines if the console should *not* connect to the Internet to verify if the title's being used by the primary console.
|
||||||
|
TikPropertyMask_Volatile = BIT(4), ///< Determines if the ticket copy inside ticket.bin is available after reboot. Can be encrypted.
|
||||||
|
TikPropertyMask_ELicenseRequired = BIT(5), ///< Determines if the console should connect to the Internet to perform license verification.
|
||||||
|
TikPropertyMask_Count = 6 ///< Total values supported by this enum.
|
||||||
|
} TikPropertyMask;
|
||||||
|
|
||||||
|
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out) {
|
||||||
|
log_write("inside es\n");
|
||||||
|
u32 signature_type;
|
||||||
|
std::memcpy(std::addressof(signature_type), ticket.data(), sizeof(signature_type));
|
||||||
|
|
||||||
|
u32 signature_size;
|
||||||
|
switch (signature_type) {
|
||||||
|
case es::TicketSigantureType_RSA_4096_SHA1: log_write("RSA-4096 PKCS#1 v1.5 with SHA-1\n"); signature_size = 0x200; break;
|
||||||
|
case es::TicketSigantureType_RSA_2048_SHA1: log_write("RSA-2048 PKCS#1 v1.5 with SHA-1\n"); signature_size = 0x100; break;
|
||||||
|
case es::TicketSigantureType_ECDSA_SHA1: log_write("ECDSA with SHA-1\n"); signature_size = 0x3C; break;
|
||||||
|
case es::TicketSigantureType_RSA_4096_SHA256: log_write("RSA-4096 PKCS#1 v1.5 with SHA-256\n"); signature_size = 0x200; break;
|
||||||
|
case es::TicketSigantureType_RSA_2048_SHA256: log_write("RSA-2048 PKCS#1 v1.5 with SHA-256\n"); signature_size = 0x100; break;
|
||||||
|
case es::TicketSigantureType_ECDSA_SHA256: log_write("ECDSA with SHA-256\n"); signature_size = 0x3C; break;
|
||||||
|
case es::TicketSigantureType_HMAC_SHA1_160: log_write("HMAC-SHA1-160\n"); signature_size = 0x14; break;
|
||||||
|
default: log_write("unknown ticket\n"); return 0x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// align-up to 0x40.
|
||||||
|
out = ((signature_size + sizeof(signature_type)) + 0x3F) & ~0x3F;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetTicketData(std::span<const u8> ticket, es::TicketData* out) {
|
||||||
|
u64 data_off;
|
||||||
|
R_TRY(GetTicketDataOffset(ticket, data_off));
|
||||||
|
std::memcpy(out, ticket.data() + data_off, sizeof(*out));
|
||||||
|
|
||||||
|
// validate ticket data.
|
||||||
|
R_UNLESS(out->ticket_version1 == 0x2, Result_InvalidTicketVersion); // must be version 2.
|
||||||
|
R_UNLESS(out->title_key_type == es::TicketTitleKeyType_Common || out->title_key_type == es::TicketTitleKeyType_Personalized, Result_InvalidTicketKeyType);
|
||||||
|
R_UNLESS(out->master_key_revision <= 0x20, Result_InvalidTicketKeyRevision);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result SetTicketData(std::span<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) {
|
||||||
|
std::memcpy(std::addressof(out), data.title_key_block, sizeof(out));
|
||||||
|
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized) {
|
||||||
|
auto rsa_key = (const es::EticketRsaDeviceKey*)keys.eticket_device_key.key;
|
||||||
|
log_write("personalised ticket\n");
|
||||||
|
log_write("master_key_revision: %u\n", data.master_key_revision);
|
||||||
|
log_write("license_type: %u\n", data.license_type);
|
||||||
|
log_write("properties_bitfield: 0x%X\n", data.properties_bitfield);
|
||||||
|
log_write("device_id: 0x%lX vs 0x%lX\n", data.device_id, std::byteswap(rsa_key->device_id));
|
||||||
|
|
||||||
|
R_UNLESS(data.device_id == std::byteswap(rsa_key->device_id), 0x1);
|
||||||
|
log_write("device id is same\n");
|
||||||
|
|
||||||
|
u8 out_keydata[RSA2048_BYTES]{};
|
||||||
|
size_t out_keydata_size;
|
||||||
|
R_UNLESS(rsa2048OaepDecrypt(out_keydata, sizeof(out_keydata), data.title_key_block, rsa_key->modulus, &rsa_key->public_exponent, sizeof(rsa_key->public_exponent), rsa_key->private_exponent, sizeof(rsa_key->private_exponent), NULL, 0, &out_keydata_size), 0x1);
|
||||||
|
R_UNLESS(out_keydata_size >= sizeof(out), 0x1);
|
||||||
|
std::memcpy(std::addressof(out), out_keydata, sizeof(out));
|
||||||
|
} else {
|
||||||
|
R_THROW(0x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys) {
|
||||||
|
keys::KeyEntry title_kek;
|
||||||
|
R_TRY(keys.GetTitleKek(std::addressof(title_kek), key_gen));
|
||||||
|
crypto::cryptoAes128(std::addressof(out), std::addressof(out), std::addressof(title_kek), false);
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: i thought i already wrote the code for this??
|
||||||
|
// todo: patch the ticket.
|
||||||
|
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys, bool convert_personalised) {
|
||||||
|
TicketData data;
|
||||||
|
R_TRY(GetTicketData(ticket, &data));
|
||||||
|
|
||||||
|
if (data.title_key_type == es::TicketTitleKeyType_Common) {
|
||||||
|
// todo: verify common signature
|
||||||
|
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized && convert_personalised) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::es
|
||||||
130
sphaira/source/yati/nx/keys.cpp
Normal file
130
sphaira/source/yati/nx/keys.cpp
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
#include "yati/nx/keys.hpp"
|
||||||
|
#include "yati/nx/nca.hpp"
|
||||||
|
#include "yati/nx/es.hpp"
|
||||||
|
#include "yati/nx/crypto.hpp"
|
||||||
|
#include "defines.hpp"
|
||||||
|
#include "log.hpp"
|
||||||
|
#include <minIni.h>
|
||||||
|
#include <memory>
|
||||||
|
#include <bit>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace sphaira::keys {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr u8 HEADER_KEK_SRC[0x10] = {
|
||||||
|
0x1F, 0x12, 0x91, 0x3A, 0x4A, 0xCB, 0xF0, 0x0D, 0x4C, 0xDE, 0x3A, 0xF6, 0xD5, 0x23, 0x88, 0x2A
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr u8 HEADER_KEY_SRC[0x20] = {
|
||||||
|
0x5A, 0x3E, 0xD8, 0x4F, 0xDE, 0xC0, 0xD8, 0x26, 0x31, 0xF7, 0xE2, 0x5D, 0x19, 0x7B, 0xF5, 0xD0,
|
||||||
|
0x1C, 0x9B, 0x7B, 0xFA, 0xF6, 0x28, 0x18, 0x3D, 0x71, 0xF6, 0x4D, 0x73, 0xF1, 0x50, 0xB9, 0xD2
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void parse_hex_key(void* key, const char* hex) {
|
||||||
|
char low[0x11]{};
|
||||||
|
char upp[0x11]{};
|
||||||
|
std::memcpy(low, hex, 0x10);
|
||||||
|
std::memcpy(upp, hex + 0x10, 0x10);
|
||||||
|
*(u64*)key = std::byteswap(std::strtoul(low, nullptr, 0x10));
|
||||||
|
*(u64*)((u8*)key + 8) = std::byteswap(std::strtoul(upp, nullptr, 0x10));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result parse_keys(Keys& out, bool read_from_file) {
|
||||||
|
static constexpr auto find_key = [](const char* key, const char* value, const char* search_key, KeySection& key_section) -> bool {
|
||||||
|
if (!std::strncmp(key, search_key, std::strlen(search_key))) {
|
||||||
|
// get key index.
|
||||||
|
char* end;
|
||||||
|
const auto key_value_str = key + std::strlen(search_key);
|
||||||
|
const auto index = std::strtoul(key_value_str, &end, 0x10);
|
||||||
|
if (end && end != key_value_str && index < 0x20) {
|
||||||
|
KeyEntry keak;
|
||||||
|
parse_hex_key(std::addressof(keak), value);
|
||||||
|
key_section[index] = keak;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr auto find_key_single = [](const char* key, const char* value, const char* search_key, KeyEntry& key_entry) -> bool {
|
||||||
|
if (!std::strcmp(key, search_key)) {
|
||||||
|
parse_hex_key(std::addressof(key_entry), value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
|
||||||
|
auto keys = static_cast<Keys*>(UserData);
|
||||||
|
|
||||||
|
auto key_text_key_area_key_app = "key_area_key_application_";
|
||||||
|
auto key_text_key_area_key_oce = "key_area_key_ocean_";
|
||||||
|
auto key_text_key_area_key_sys = "key_area_key_system_";
|
||||||
|
auto key_text_titlekek = "titlekek_";
|
||||||
|
auto key_text_master_key = "master_key_";
|
||||||
|
auto key_text_eticket_rsa_kek = keys->eticket_device_key.generation ? "eticket_rsa_kek_personalized" : "eticket_rsa_kek";
|
||||||
|
|
||||||
|
if (find_key(Key, Value, key_text_key_area_key_app, keys->key_area_key[nca::KeyAreaEncryptionKeyIndex_Application])) {
|
||||||
|
return 1;
|
||||||
|
} else if (find_key(Key, Value, key_text_key_area_key_oce, keys->key_area_key[nca::KeyAreaEncryptionKeyIndex_Ocean])) {
|
||||||
|
return 1;
|
||||||
|
} else if (find_key(Key, Value, key_text_key_area_key_sys, keys->key_area_key[nca::KeyAreaEncryptionKeyIndex_System])) {
|
||||||
|
return 1;
|
||||||
|
} else if (find_key(Key, Value, key_text_titlekek, keys->titlekek)) {
|
||||||
|
return 1;
|
||||||
|
} else if (find_key(Key, Value, key_text_master_key, keys->master_key)) {
|
||||||
|
return 1;
|
||||||
|
} else if (find_key_single(Key, Value, key_text_eticket_rsa_kek, keys->eticket_rsa_kek)) {
|
||||||
|
log_write("found key single: key: %s value %s\n", Key, Value);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
R_TRY(splCryptoInitialize());
|
||||||
|
ON_SCOPE_EXIT(splCryptoExit());
|
||||||
|
|
||||||
|
u8 header_kek[0x20];
|
||||||
|
R_TRY(splCryptoGenerateAesKek(HEADER_KEK_SRC, 0, 0, header_kek));
|
||||||
|
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC, out.header_key));
|
||||||
|
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC + 0x10, out.header_key + 0x10));
|
||||||
|
|
||||||
|
if (read_from_file) {
|
||||||
|
// get eticket device key, needed for decrypting personalised tickets.
|
||||||
|
R_TRY(setcalInitialize());
|
||||||
|
ON_SCOPE_EXIT(setcalExit());
|
||||||
|
R_TRY(setcalGetEticketDeviceKey(std::addressof(out.eticket_device_key)));
|
||||||
|
|
||||||
|
R_UNLESS(ini_browse(cb, std::addressof(out), "/switch/prod.keys"), 0x1);
|
||||||
|
|
||||||
|
// decrypt eticket device key.
|
||||||
|
if (out.eticket_rsa_kek.IsValid()) {
|
||||||
|
auto rsa_key = (es::EticketRsaDeviceKey*)out.eticket_device_key.key;
|
||||||
|
|
||||||
|
Aes128CtrContext eticket_aes_ctx{};
|
||||||
|
aes128CtrContextCreate(&eticket_aes_ctx, &out.eticket_rsa_kek, rsa_key->ctr);
|
||||||
|
aes128CtrCrypt(&eticket_aes_ctx, &(rsa_key->private_exponent), &(rsa_key->private_exponent), sizeof(es::EticketRsaDeviceKey) - sizeof(rsa_key->ctr));
|
||||||
|
|
||||||
|
const auto public_exponent = std::byteswap(rsa_key->public_exponent);
|
||||||
|
if (public_exponent != 0x10001) {
|
||||||
|
log_write("etick decryption fail: 0x%X\n", public_exponent);
|
||||||
|
if (public_exponent == 0) {
|
||||||
|
log_write("eticket device id is NULL\n");
|
||||||
|
}
|
||||||
|
R_THROW(0x1);
|
||||||
|
} else {
|
||||||
|
log_write("eticket match\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::keys
|
||||||
154
sphaira/source/yati/nx/nca.cpp
Normal file
154
sphaira/source/yati/nx/nca.cpp
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
#include "yati/nx/nca.hpp"
|
||||||
|
#include "yati/nx/crypto.hpp"
|
||||||
|
#include "yati/nx/nxdumptool_rsa.h"
|
||||||
|
#include "log.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::nca {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr u8 g_key_area_key_application_source[0x10] = { 0x7F, 0x59, 0x97, 0x1E, 0x62, 0x9F, 0x36, 0xA1, 0x30, 0x98, 0x06, 0x6F, 0x21, 0x44, 0xC3, 0x0D };
|
||||||
|
constexpr u8 g_key_area_key_ocean_source[0x10] = { 0x32, 0x7D, 0x36, 0x08, 0x5A, 0xD1, 0x75, 0x8D, 0xAB, 0x4E, 0x6F, 0xBA, 0xA5, 0x55, 0xD8, 0x82 };
|
||||||
|
constexpr u8 g_key_area_key_system_source[0x10] = { 0x87, 0x45, 0xF1, 0xBB, 0xA6, 0xBE, 0x79, 0x64, 0x7D, 0x04, 0x8B, 0xA6, 0x7B, 0x5F, 0xDA, 0x4A };
|
||||||
|
|
||||||
|
constexpr const u8* g_key_area_key[] = {
|
||||||
|
g_key_area_key_application_source,
|
||||||
|
g_key_area_key_ocean_source,
|
||||||
|
g_key_area_key_system_source
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char nca_hdr_fixed_key_moduli_retail[0x2][0x100] = { /* Fixed RSA key used to validate NCA signature 0. */
|
||||||
|
{
|
||||||
|
0xBF, 0xBE, 0x40, 0x6C, 0xF4, 0xA7, 0x80, 0xE9, 0xF0, 0x7D, 0x0C, 0x99, 0x61, 0x1D, 0x77, 0x2F,
|
||||||
|
0x96, 0xBC, 0x4B, 0x9E, 0x58, 0x38, 0x1B, 0x03, 0xAB, 0xB1, 0x75, 0x49, 0x9F, 0x2B, 0x4D, 0x58,
|
||||||
|
0x34, 0xB0, 0x05, 0xA3, 0x75, 0x22, 0xBE, 0x1A, 0x3F, 0x03, 0x73, 0xAC, 0x70, 0x68, 0xD1, 0x16,
|
||||||
|
0xB9, 0x04, 0x46, 0x5E, 0xB7, 0x07, 0x91, 0x2F, 0x07, 0x8B, 0x26, 0xDE, 0xF6, 0x00, 0x07, 0xB2,
|
||||||
|
0xB4, 0x51, 0xF8, 0x0D, 0x0A, 0x5E, 0x58, 0xAD, 0xEB, 0xBC, 0x9A, 0xD6, 0x49, 0xB9, 0x64, 0xEF,
|
||||||
|
0xA7, 0x82, 0xB5, 0xCF, 0x6D, 0x70, 0x13, 0xB0, 0x0F, 0x85, 0xF6, 0xA9, 0x08, 0xAA, 0x4D, 0x67,
|
||||||
|
0x66, 0x87, 0xFA, 0x89, 0xFF, 0x75, 0x90, 0x18, 0x1E, 0x6B, 0x3D, 0xE9, 0x8A, 0x68, 0xC9, 0x26,
|
||||||
|
0x04, 0xD9, 0x80, 0xCE, 0x3F, 0x5E, 0x92, 0xCE, 0x01, 0xFF, 0x06, 0x3B, 0xF2, 0xC1, 0xA9, 0x0C,
|
||||||
|
0xCE, 0x02, 0x6F, 0x16, 0xBC, 0x92, 0x42, 0x0A, 0x41, 0x64, 0xCD, 0x52, 0xB6, 0x34, 0x4D, 0xAE,
|
||||||
|
0xC0, 0x2E, 0xDE, 0xA4, 0xDF, 0x27, 0x68, 0x3C, 0xC1, 0xA0, 0x60, 0xAD, 0x43, 0xF3, 0xFC, 0x86,
|
||||||
|
0xC1, 0x3E, 0x6C, 0x46, 0xF7, 0x7C, 0x29, 0x9F, 0xFA, 0xFD, 0xF0, 0xE3, 0xCE, 0x64, 0xE7, 0x35,
|
||||||
|
0xF2, 0xF6, 0x56, 0x56, 0x6F, 0x6D, 0xF1, 0xE2, 0x42, 0xB0, 0x83, 0x40, 0xA5, 0xC3, 0x20, 0x2B,
|
||||||
|
0xCC, 0x9A, 0xAE, 0xCA, 0xED, 0x4D, 0x70, 0x30, 0xA8, 0x70, 0x1C, 0x70, 0xFD, 0x13, 0x63, 0x29,
|
||||||
|
0x02, 0x79, 0xEA, 0xD2, 0xA7, 0xAF, 0x35, 0x28, 0x32, 0x1C, 0x7B, 0xE6, 0x2F, 0x1A, 0xAA, 0x40,
|
||||||
|
0x7E, 0x32, 0x8C, 0x27, 0x42, 0xFE, 0x82, 0x78, 0xEC, 0x0D, 0xEB, 0xE6, 0x83, 0x4B, 0x6D, 0x81,
|
||||||
|
0x04, 0x40, 0x1A, 0x9E, 0x9A, 0x67, 0xF6, 0x72, 0x29, 0xFA, 0x04, 0xF0, 0x9D, 0xE4, 0xF4, 0x03,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
0xAD, 0xE3, 0xE1, 0xFA, 0x04, 0x35, 0xE5, 0xB6, 0xDD, 0x49, 0xEA, 0x89, 0x29, 0xB1, 0xFF, 0xB6,
|
||||||
|
0x43, 0xDF, 0xCA, 0x96, 0xA0, 0x4A, 0x13, 0xDF, 0x43, 0xD9, 0x94, 0x97, 0x96, 0x43, 0x65, 0x48,
|
||||||
|
0x70, 0x58, 0x33, 0xA2, 0x7D, 0x35, 0x7B, 0x96, 0x74, 0x5E, 0x0B, 0x5C, 0x32, 0x18, 0x14, 0x24,
|
||||||
|
0xC2, 0x58, 0xB3, 0x6C, 0x22, 0x7A, 0xA1, 0xB7, 0xCB, 0x90, 0xA7, 0xA3, 0xF9, 0x7D, 0x45, 0x16,
|
||||||
|
0xA5, 0xC8, 0xED, 0x8F, 0xAD, 0x39, 0x5E, 0x9E, 0x4B, 0x51, 0x68, 0x7D, 0xF8, 0x0C, 0x35, 0xC6,
|
||||||
|
0x3F, 0x91, 0xAE, 0x44, 0xA5, 0x92, 0x30, 0x0D, 0x46, 0xF8, 0x40, 0xFF, 0xD0, 0xFF, 0x06, 0xD2,
|
||||||
|
0x1C, 0x7F, 0x96, 0x18, 0xDC, 0xB7, 0x1D, 0x66, 0x3E, 0xD1, 0x73, 0xBC, 0x15, 0x8A, 0x2F, 0x94,
|
||||||
|
0xF3, 0x00, 0xC1, 0x83, 0xF1, 0xCD, 0xD7, 0x81, 0x88, 0xAB, 0xDF, 0x8C, 0xEF, 0x97, 0xDD, 0x1B,
|
||||||
|
0x17, 0x5F, 0x58, 0xF6, 0x9A, 0xE9, 0xE8, 0xC2, 0x2F, 0x38, 0x15, 0xF5, 0x21, 0x07, 0xF8, 0x37,
|
||||||
|
0x90, 0x5D, 0x2E, 0x02, 0x40, 0x24, 0x15, 0x0D, 0x25, 0xB7, 0x26, 0x5D, 0x09, 0xCC, 0x4C, 0xF4,
|
||||||
|
0xF2, 0x1B, 0x94, 0x70, 0x5A, 0x9E, 0xEE, 0xED, 0x77, 0x77, 0xD4, 0x51, 0x99, 0xF5, 0xDC, 0x76,
|
||||||
|
0x1E, 0xE3, 0x6C, 0x8C, 0xD1, 0x12, 0xD4, 0x57, 0xD1, 0xB6, 0x83, 0xE4, 0xE4, 0xFE, 0xDA, 0xE9,
|
||||||
|
0xB4, 0x3B, 0x33, 0xE5, 0x37, 0x8A, 0xDF, 0xB5, 0x7F, 0x89, 0xF1, 0x9B, 0x9E, 0xB0, 0x15, 0xB2,
|
||||||
|
0x3A, 0xFE, 0xEA, 0x61, 0x84, 0x5B, 0x7D, 0x4B, 0x23, 0x12, 0x0B, 0x83, 0x12, 0xF2, 0x22, 0x6B,
|
||||||
|
0xB9, 0x22, 0x96, 0x4B, 0x26, 0x0B, 0x63, 0x5E, 0x96, 0x57, 0x52, 0xA3, 0x67, 0x64, 0x22, 0xCA,
|
||||||
|
0xD0, 0x56, 0x3E, 0x74, 0xB5, 0x98, 0x1F, 0x0D, 0xF8, 0xB3, 0x34, 0xE6, 0x98, 0x68, 0x5A, 0xAD,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsigned char acid_fixed_key_moduli_retail[0x2][0x100] = { /* Fixed RSA keys used to validate ACID signatures. */
|
||||||
|
{
|
||||||
|
0xDD, 0xC8, 0xDD, 0xF2, 0x4E, 0x6D, 0xF0, 0xCA, 0x9E, 0xC7, 0x5D, 0xC7, 0x7B, 0xAD, 0xFE, 0x7D,
|
||||||
|
0x23, 0x89, 0x69, 0xB6, 0xF2, 0x06, 0xA2, 0x02, 0x88, 0xE1, 0x55, 0x91, 0xAB, 0xCB, 0x4D, 0x50,
|
||||||
|
0x2E, 0xFC, 0x9D, 0x94, 0x76, 0xD6, 0x4C, 0xD8, 0xFF, 0x10, 0xFA, 0x5E, 0x93, 0x0A, 0xB4, 0x57,
|
||||||
|
0xAC, 0x51, 0xC7, 0x16, 0x66, 0xF4, 0x1A, 0x54, 0xC2, 0xC5, 0x04, 0x3D, 0x1B, 0xFE, 0x30, 0x20,
|
||||||
|
0x8A, 0xAC, 0x6F, 0x6F, 0xF5, 0xC7, 0xB6, 0x68, 0xB8, 0xC9, 0x40, 0x6B, 0x42, 0xAD, 0x11, 0x21,
|
||||||
|
0xE7, 0x8B, 0xE9, 0x75, 0x01, 0x86, 0xE4, 0x48, 0x9B, 0x0A, 0x0A, 0xF8, 0x7F, 0xE8, 0x87, 0xF2,
|
||||||
|
0x82, 0x01, 0xE6, 0xA3, 0x0F, 0xE4, 0x66, 0xAE, 0x83, 0x3F, 0x4E, 0x9F, 0x5E, 0x01, 0x30, 0xA4,
|
||||||
|
0x00, 0xB9, 0x9A, 0xAE, 0x5F, 0x03, 0xCC, 0x18, 0x60, 0xE5, 0xEF, 0x3B, 0x5E, 0x15, 0x16, 0xFE,
|
||||||
|
0x1C, 0x82, 0x78, 0xB5, 0x2F, 0x47, 0x7C, 0x06, 0x66, 0x88, 0x5D, 0x35, 0xA2, 0x67, 0x20, 0x10,
|
||||||
|
0xE7, 0x6C, 0x43, 0x68, 0xD3, 0xE4, 0x5A, 0x68, 0x2A, 0x5A, 0xE2, 0x6D, 0x73, 0xB0, 0x31, 0x53,
|
||||||
|
0x1C, 0x20, 0x09, 0x44, 0xF5, 0x1A, 0x9D, 0x22, 0xBE, 0x12, 0xA1, 0x77, 0x11, 0xE2, 0xA1, 0xCD,
|
||||||
|
0x40, 0x9A, 0xA2, 0x8B, 0x60, 0x9B, 0xEF, 0xA0, 0xD3, 0x48, 0x63, 0xA2, 0xF8, 0xA3, 0x2C, 0x08,
|
||||||
|
0x56, 0x52, 0x2E, 0x60, 0x19, 0x67, 0x5A, 0xA7, 0x9F, 0xDC, 0x3F, 0x3F, 0x69, 0x2B, 0x31, 0x6A,
|
||||||
|
0xB7, 0x88, 0x4A, 0x14, 0x84, 0x80, 0x33, 0x3C, 0x9D, 0x44, 0xB7, 0x3F, 0x4C, 0xE1, 0x75, 0xEA,
|
||||||
|
0x37, 0xEA, 0xE8, 0x1E, 0x7C, 0x77, 0xB7, 0xC6, 0x1A, 0xA2, 0xF0, 0x9F, 0x10, 0x61, 0xCD, 0x7B,
|
||||||
|
0x5B, 0x32, 0x4C, 0x37, 0xEF, 0xB1, 0x71, 0x68, 0x53, 0x0A, 0xED, 0x51, 0x7D, 0x35, 0x22, 0xFD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
0xE7, 0xAA, 0x25, 0xC8, 0x01, 0xA5, 0x14, 0x6B, 0x01, 0x60, 0x3E, 0xD9, 0x96, 0x5A, 0xBF, 0x90,
|
||||||
|
0xAC, 0xA7, 0xFD, 0x9B, 0x5B, 0xBD, 0x8A, 0x26, 0xB0, 0xCB, 0x20, 0x28, 0x9A, 0x72, 0x12, 0xF5,
|
||||||
|
0x20, 0x65, 0xB3, 0xB9, 0x84, 0x58, 0x1F, 0x27, 0xBC, 0x7C, 0xA2, 0xC9, 0x9E, 0x18, 0x95, 0xCF,
|
||||||
|
0xC2, 0x73, 0x2E, 0x74, 0x8C, 0x66, 0xE5, 0x9E, 0x79, 0x2B, 0xB8, 0x07, 0x0C, 0xB0, 0x4E, 0x8E,
|
||||||
|
0xAB, 0x85, 0x21, 0x42, 0xC4, 0xC5, 0x6D, 0x88, 0x9C, 0xDB, 0x15, 0x95, 0x3F, 0x80, 0xDB, 0x7A,
|
||||||
|
0x9A, 0x7D, 0x41, 0x56, 0x25, 0x17, 0x18, 0x42, 0x4D, 0x8C, 0xAC, 0xA5, 0x7B, 0xDB, 0x42, 0x5D,
|
||||||
|
0x59, 0x35, 0x45, 0x5D, 0x8A, 0x02, 0xB5, 0x70, 0xC0, 0x72, 0x35, 0x46, 0xD0, 0x1D, 0x60, 0x01,
|
||||||
|
0x4A, 0xCC, 0x1C, 0x46, 0xD3, 0xD6, 0x35, 0x52, 0xD6, 0xE1, 0xF8, 0x3B, 0x5D, 0xEA, 0xDD, 0xB8,
|
||||||
|
0xFE, 0x7D, 0x50, 0xCB, 0x35, 0x23, 0x67, 0x8B, 0xB6, 0xE4, 0x74, 0xD2, 0x60, 0xFC, 0xFD, 0x43,
|
||||||
|
0xBF, 0x91, 0x08, 0x81, 0xC5, 0x4F, 0x5D, 0x16, 0x9A, 0xC4, 0x9A, 0xC6, 0xF6, 0xF3, 0xE1, 0xF6,
|
||||||
|
0x5C, 0x07, 0xAA, 0x71, 0x6C, 0x13, 0xA4, 0xB1, 0xB3, 0x66, 0xBF, 0x90, 0x4C, 0x3D, 0xA2, 0xC4,
|
||||||
|
0x0B, 0xB8, 0x3D, 0x7A, 0x8C, 0x19, 0xFA, 0xFF, 0x6B, 0xB9, 0x1F, 0x02, 0xCC, 0xB6, 0xD3, 0x0C,
|
||||||
|
0x7D, 0x19, 0x1F, 0x47, 0xF9, 0xC7, 0x40, 0x01, 0xFA, 0x46, 0xEA, 0x0B, 0xD4, 0x02, 0xE0, 0x3D,
|
||||||
|
0x30, 0x9A, 0x1A, 0x0F, 0xEA, 0xA7, 0x66, 0x55, 0xF7, 0xCB, 0x28, 0xE2, 0xBB, 0x99, 0xE4, 0x83,
|
||||||
|
0xC3, 0x43, 0x03, 0xEE, 0xDC, 0x1F, 0x02, 0x23, 0xDD, 0xD1, 0x2D, 0x39, 0xA4, 0x65, 0x75, 0x03,
|
||||||
|
0xEF, 0x37, 0x9C, 0x06, 0xD6, 0xFA, 0xA1, 0x15, 0xF0, 0xDB, 0x17, 0x47, 0x26, 0x4F, 0x49, 0x03
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Result DecryptKeak(const keys::Keys& keys, Header& header) {
|
||||||
|
const auto key_generation = header.GetKeyGeneration();
|
||||||
|
|
||||||
|
// try with spl.
|
||||||
|
keys::KeyEntry keak;
|
||||||
|
if (R_SUCCEEDED(splCryptoGenerateAesKek(g_key_area_key[header.kaek_index], key_generation, 0, &keak))) {
|
||||||
|
for (auto& key_area : header.key_area) {
|
||||||
|
R_TRY(splCryptoGenerateAesKey(&keak, std::addressof(key_area), std::addressof(key_area)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// failed with spl, try using keys.
|
||||||
|
R_TRY(keys.GetNcaKeyArea(&keak, key_generation, header.kaek_index));
|
||||||
|
for (auto& key_area : header.key_area) {
|
||||||
|
crypto::cryptoAes128(std::addressof(key_area), std::addressof(key_area), std::addressof(keak), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation) {
|
||||||
|
header.SetKeyGeneration(key_generation);
|
||||||
|
|
||||||
|
keys::KeyEntry keak;
|
||||||
|
R_TRY(keys.GetNcaKeyArea(&keak, key_generation, header.kaek_index));
|
||||||
|
log_write("re-encrypting with: 0x%X\n", key_generation);
|
||||||
|
|
||||||
|
for (auto& key_area : header.key_area) {
|
||||||
|
crypto::cryptoAes128(std::addressof(key_area), std::addressof(key_area), std::addressof(keak), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result VerifyFixedKey(const Header& header) {
|
||||||
|
R_UNLESS(header.sig_key_gen < std::size(nca_hdr_fixed_key_moduli_retail), 0x1);
|
||||||
|
auto mod = nca_hdr_fixed_key_moduli_retail[header.sig_key_gen];
|
||||||
|
|
||||||
|
const u8 E[3] = { 1, 0, 1 };
|
||||||
|
if (!rsa2048VerifySha256BasedPssSignature(&header.magic, 0x200, header.rsa_fixed_key, mod, E, sizeof(E))) {
|
||||||
|
auto new_header = header;
|
||||||
|
// if failed, detect if this is a eshop/xci convert.
|
||||||
|
new_header.distribution_type ^= 1;
|
||||||
|
if (!rsa2048VerifySha256BasedPssSignature(&new_header.magic, 0x200, new_header.rsa_fixed_key, mod, E, sizeof(E))) {
|
||||||
|
log_write("FAILED nca header hash\n");
|
||||||
|
R_THROW(0x1);
|
||||||
|
} else {
|
||||||
|
log_write("WARNING! nca is converted! distribution_type: %u\n", new_header.distribution_type);
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::nca
|
||||||
34
sphaira/source/yati/nx/ncm.cpp
Normal file
34
sphaira/source/yati/nx/ncm.cpp
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#include "yati/nx/ncm.hpp"
|
||||||
|
#include "defines.hpp"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace sphaira::ncm {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto GetAppId(const NcmContentMetaKey& key) -> u64 {
|
||||||
|
if (key.type == NcmContentMetaType_Patch) {
|
||||||
|
return key.id ^ 0x800;
|
||||||
|
} else if (key.type == NcmContentMetaType_AddOnContent) {
|
||||||
|
return (key.id ^ 0x1000) & ~0xFFF;
|
||||||
|
} else {
|
||||||
|
return key.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id) {
|
||||||
|
bool has;
|
||||||
|
R_TRY(ncmContentStorageHas(cs, std::addressof(has), content_id));
|
||||||
|
if (has) {
|
||||||
|
R_TRY(ncmContentStorageDelete(cs, content_id));
|
||||||
|
}
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id) {
|
||||||
|
R_TRY(Delete(cs, content_id));
|
||||||
|
return ncmContentStorageRegister(cs, content_id, placeholder_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::ncm
|
||||||
39
sphaira/source/yati/nx/ns.cpp
Normal file
39
sphaira/source/yati/nx/ns.cpp
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#include "yati/nx/ns.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::ns {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Result PushApplicationRecord(Service* srv, u64 tid, const ncm::ContentStorageRecord* records, u32 count) {
|
||||||
|
const struct {
|
||||||
|
u8 last_modified_event;
|
||||||
|
u8 padding[0x7];
|
||||||
|
u64 tid;
|
||||||
|
} in = { ApplicationRecordType_Installed, {0}, tid };
|
||||||
|
|
||||||
|
return serviceDispatchIn(srv, 16, in,
|
||||||
|
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
|
||||||
|
.buffers = { { records, sizeof(*records) * count } });
|
||||||
|
}
|
||||||
|
|
||||||
|
Result ListApplicationRecordContentMeta(Service* srv, u64 offset, u64 tid, ncm::ContentStorageRecord* out_records, u32 count, s32* entries_read) {
|
||||||
|
struct {
|
||||||
|
u64 offset;
|
||||||
|
u64 tid;
|
||||||
|
} in = { offset, tid };
|
||||||
|
|
||||||
|
return serviceDispatchInOut(srv, 17, in, *entries_read,
|
||||||
|
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
|
||||||
|
.buffers = { { out_records, sizeof(*out_records) * count } });
|
||||||
|
}
|
||||||
|
|
||||||
|
Result DeleteApplicationRecord(Service* srv, u64 tid) {
|
||||||
|
return serviceDispatchIn(srv, 27, tid);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result InvalidateApplicationControlCache(Service* srv, u64 tid) {
|
||||||
|
return serviceDispatchIn(srv, 404, tid);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::ns
|
||||||
158
sphaira/source/yati/nx/nxdumptool_rsa.c
Normal file
158
sphaira/source/yati/nx/nxdumptool_rsa.c
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
* rsa.c
|
||||||
|
*
|
||||||
|
* Copyright (c) 2018-2019, SciresM.
|
||||||
|
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
|
||||||
|
*
|
||||||
|
* This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool).
|
||||||
|
*
|
||||||
|
* nxdumptool is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* nxdumptool is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "yati/nx/nxdumptool_rsa.h"
|
||||||
|
#include "log.hpp"
|
||||||
|
#include <switch.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <mbedtls/rsa.h>
|
||||||
|
#include <mbedtls/entropy.h>
|
||||||
|
#include <mbedtls/ctr_drbg.h>
|
||||||
|
#include <mbedtls/pk.h>
|
||||||
|
|
||||||
|
#define LOG_MSG_ERROR(...) log_write(__VA_ARGS__)
|
||||||
|
|
||||||
|
/* Function prototypes. */
|
||||||
|
|
||||||
|
static bool rsa2048VerifySha256BasedSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, \
|
||||||
|
bool use_pss);
|
||||||
|
|
||||||
|
bool rsa2048VerifySha256BasedPssSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size)
|
||||||
|
{
|
||||||
|
return rsa2048VerifySha256BasedSignature(data, data_size, signature, modulus, public_exponent, public_exponent_size, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool rsa2048VerifySha256BasedPkcs1v15Signature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size)
|
||||||
|
{
|
||||||
|
return rsa2048VerifySha256BasedSignature(data, data_size, signature, modulus, public_exponent, public_exponent_size, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool rsa2048OaepDecrypt(void *dst, size_t dst_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, const void *private_exponent, \
|
||||||
|
size_t private_exponent_size, const void *label, size_t label_size, size_t *out_size)
|
||||||
|
{
|
||||||
|
if (!dst || !dst_size || !signature || !modulus || !public_exponent || !public_exponent_size || !private_exponent || !private_exponent_size || (!label && label_size) || (label && !label_size) || \
|
||||||
|
!out_size)
|
||||||
|
{
|
||||||
|
LOG_MSG_ERROR("Invalid parameters!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mbedtls_entropy_context entropy = {0};
|
||||||
|
mbedtls_ctr_drbg_context ctr_drbg = {0};
|
||||||
|
mbedtls_rsa_context rsa = {0};
|
||||||
|
|
||||||
|
const char *pers = __func__;
|
||||||
|
int mbedtls_ret = 0;
|
||||||
|
bool ret = false;
|
||||||
|
|
||||||
|
/* Initialize contexts. */
|
||||||
|
mbedtls_entropy_init(&entropy);
|
||||||
|
mbedtls_ctr_drbg_init(&ctr_drbg);
|
||||||
|
mbedtls_rsa_init(&rsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256);
|
||||||
|
|
||||||
|
/* Seed the random number generator. */
|
||||||
|
mbedtls_ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const u8*)pers, strlen(pers));
|
||||||
|
if (mbedtls_ret != 0)
|
||||||
|
{
|
||||||
|
LOG_MSG_ERROR("mbedtls_ctr_drbg_seed failed! (%d).", mbedtls_ret);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Import RSA parameters. */
|
||||||
|
mbedtls_ret = mbedtls_rsa_import_raw(&rsa, (const u8*)modulus, RSA2048_BYTES, NULL, 0, NULL, 0, (const u8*)private_exponent, private_exponent_size, (const u8*)public_exponent, public_exponent_size);
|
||||||
|
if (mbedtls_ret != 0)
|
||||||
|
{
|
||||||
|
LOG_MSG_ERROR("mbedtls_rsa_import_raw failed! (%d).", mbedtls_ret);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Derive RSA prime factors. */
|
||||||
|
mbedtls_ret = mbedtls_rsa_complete(&rsa);
|
||||||
|
if (mbedtls_ret != 0)
|
||||||
|
{
|
||||||
|
LOG_MSG_ERROR("mbedtls_rsa_complete failed! (%d).", mbedtls_ret);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Perform RSA-OAEP decryption. */
|
||||||
|
mbedtls_ret = mbedtls_rsa_rsaes_oaep_decrypt(&rsa, mbedtls_ctr_drbg_random, &ctr_drbg, MBEDTLS_RSA_PRIVATE, (const u8*)label, label_size, out_size, (const u8*)signature, (u8*)dst, dst_size);
|
||||||
|
if (mbedtls_ret != 0)
|
||||||
|
{
|
||||||
|
LOG_MSG_ERROR("mbedtls_rsa_rsaes_oaep_decrypt failed! (%d).", mbedtls_ret);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = true;
|
||||||
|
|
||||||
|
end:
|
||||||
|
mbedtls_rsa_free(&rsa);
|
||||||
|
mbedtls_ctr_drbg_free(&ctr_drbg);
|
||||||
|
mbedtls_entropy_free(&entropy);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rsa2048VerifySha256BasedSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, \
|
||||||
|
bool use_pss)
|
||||||
|
{
|
||||||
|
if (!data || !data_size || !signature || !modulus || !public_exponent || !public_exponent_size)
|
||||||
|
{
|
||||||
|
LOG_MSG_ERROR("Invalid parameters!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mbedtls_ret = 0;
|
||||||
|
mbedtls_rsa_context rsa = {0};
|
||||||
|
u8 hash[SHA256_HASH_SIZE] = {0};
|
||||||
|
bool ret = false;
|
||||||
|
|
||||||
|
/* Initialize RSA context. */
|
||||||
|
mbedtls_rsa_init(&rsa, use_pss ? MBEDTLS_RSA_PKCS_V21 : MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_SHA256);
|
||||||
|
|
||||||
|
/* Import RSA parameters. */
|
||||||
|
mbedtls_ret = mbedtls_rsa_import_raw(&rsa, (const u8*)modulus, RSA2048_BYTES, NULL, 0, NULL, 0, NULL, 0, (const u8*)public_exponent, public_exponent_size);
|
||||||
|
if (mbedtls_ret != 0)
|
||||||
|
{
|
||||||
|
LOG_MSG_ERROR("mbedtls_rsa_import_raw failed! (%d).", mbedtls_ret);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calculate SHA-256 checksum for the input data. */
|
||||||
|
sha256CalculateHash(hash, data, data_size);
|
||||||
|
|
||||||
|
/* Verify signature. */
|
||||||
|
mbedtls_ret = (use_pss ? mbedtls_rsa_rsassa_pss_verify(&rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, SHA256_HASH_SIZE, hash, (const u8*)signature) : \
|
||||||
|
mbedtls_rsa_rsassa_pkcs1_v15_verify(&rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, SHA256_HASH_SIZE, hash, (const u8*)signature));
|
||||||
|
if (mbedtls_ret != 0)
|
||||||
|
{
|
||||||
|
LOG_MSG_ERROR("mbedtls_rsa_rsassa_%s_verify failed! (%d).", use_pss ? "pss" : "pkcs1_v15", mbedtls_ret);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = true;
|
||||||
|
|
||||||
|
end:
|
||||||
|
mbedtls_rsa_free(&rsa);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
20
sphaira/source/yati/source/file.cpp
Normal file
20
sphaira/source/yati/source/file.cpp
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#include "yati/source/file.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
|
File::File(FsFileSystem* fs, const fs::FsPath& path) {
|
||||||
|
m_open_result = fsFsOpenFile(fs, path, FsOpenMode_Read, std::addressof(m_file));
|
||||||
|
}
|
||||||
|
|
||||||
|
File::~File() {
|
||||||
|
if (R_SUCCEEDED(GetOpenResult())) {
|
||||||
|
fsFileClose(std::addressof(m_file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result File::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||||
|
R_TRY(GetOpenResult());
|
||||||
|
return fsFileRead(std::addressof(m_file), off, buf, size, 0, bytes_read);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::source
|
||||||
28
sphaira/source/yati/source/stdio.cpp
Normal file
28
sphaira/source/yati/source/stdio.cpp
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#include "yati/source/stdio.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
|
Stdio::Stdio(const char* path) {
|
||||||
|
m_file = std::fopen(path, "rb");
|
||||||
|
if (!m_file) {
|
||||||
|
m_open_result = fsdevGetLastResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stdio::~Stdio() {
|
||||||
|
if (R_SUCCEEDED(GetOpenResult())) {
|
||||||
|
std::fclose(m_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Stdio::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||||
|
R_TRY(GetOpenResult());
|
||||||
|
|
||||||
|
std::fseek(m_file, off, SEEK_SET);
|
||||||
|
R_TRY(fsdevGetLastResult());
|
||||||
|
|
||||||
|
*bytes_read = std::fread(buf, 1, size, m_file);
|
||||||
|
return fsdevGetLastResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::source
|
||||||
1317
sphaira/source/yati/yati.cpp
Normal file
1317
sphaira/source/yati/yati.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user