16 Commits

Author SHA1 Message Date
ITotalJustice
90f8a62823 display useful info in ftp menu (ip, port, user, pass, ssid, passphrase) 2025-04-23 01:00:36 +01:00
ITotalJustice
e2a1c8b5e3 fix yati not setting correct version, add support for using zip name when creating forwarder, remove some dead code.
fixes #126
fixes #127
2025-04-22 23:15:16 +01:00
ITotalJustice
21f6f4b74d [skip ci] fix file assoc always using internal name, fix menu showing wrong time
fixes #126
2025-04-22 00:08:26 +01:00
ITotalJustice
75d3b3ee0d [skip-ci] initial support for stream installs, add ftp installs.
do NOT build or release binaries of this version, it is not complete and there will be dragons.
2025-04-21 23:23:59 +01:00
ITotalJustice
0dde379932 don't return from usb menu on error, wait until the user presses B 2025-04-21 13:33:36 +01:00
ITotalJustice
9800bbecdf add basic support for gamecard installing 2025-04-21 13:30:46 +01:00
ITotalJustice
60e915c255 enable screenshot permissions in applet mode. 2025-04-21 12:40:37 +01:00
ITotalJustice
786f8a42fa send file name and size via usb, add requirements.txt for usb.py 2025-04-21 01:41:20 +01:00
ITotalJustice
5a4a0f75f2 add support for mame and neogeo, as well as alias for rom folder names 2025-04-20 22:03:53 +01:00
ITotalJustice
5aca92a2cc fix usb menu name being set as irs (copy-paste bug). 2025-04-20 18:11:17 +01:00
ITotalJustice
7471885119 bump version for release 0.6.3 -> 0.7.0 2025-04-20 18:08:55 +01:00
ITotalJustice
5038fb0c28 add basic usb install support (see commit message).
transfers seems to work, although i have done very little testing.
i plan to extend the usb script so that it supports normal usb transfers, such as uploading and
downloading files to and from the switch.

however, most users are likely better off using mtp for said transfers.

the usb transfer code was taken from Haze, which is part of Atmosphere.
2025-04-20 18:04:35 +01:00
ITotalJustice
ff9f493460 fix skwbd numpa not showing 2025-04-20 14:18:02 +01:00
ITotalJustice
89e82927ee add basic support for title installing 2025-04-20 14:12:12 +01:00
ITotalJustice
651d9fa495 remove theme shuffle option
fixes #121
2025-04-19 22:55:08 +01:00
ITotalJustice
3141100457 fix themezer search...by actually *doing* the search.
fixes #111
2025-04-19 22:50:56 +01:00
67 changed files with 5791 additions and 440 deletions

2
.gitignore vendored
View File

@@ -22,3 +22,5 @@ libs/tweeny
compile_commands.json
out
usb_test/

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/fbneo_libretro_libnx.nro
supported_extensions=zip|7z|cue|ccd
database=FBNeo - Arcade Games
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/mame2000_libretro_libnx.nro
supported_extensions=zip|7z
database=MAME 2000
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/mame2003_libretro_libnx.nro
supported_extensions=zip
database=MAME 2003
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/mame2003_plus_libretro_libnx.nro
supported_extensions=zip
database=MAME 2003-Plus
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/xrick_libretro_libnx.nro
supported_extensions=zip
database=Rick Dangerous
use_base_name=true

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.13)
set(sphaira_VERSION 0.6.3)
set(sphaira_VERSION 0.7.0)
project(sphaira
VERSION ${sphaira_VERSION}
@@ -46,6 +46,9 @@ add_executable(sphaira
source/ui/menus/menu_base.cpp
source/ui/menus/themezer.cpp
source/ui/menus/ghdl.cpp
source/ui/menus/usb_menu.cpp
source/ui/menus/ftp_menu.cpp
source/ui/menus/gc_menu.cpp
source/ui/error_box.cpp
source/ui/notification.cpp
@@ -74,6 +77,22 @@ add_executable(sphaira
source/web.cpp
source/i18n.cpp
source/ftpsrv_helper.cpp
source/yati/yati.cpp
source/yati/container/nsp.cpp
source/yati/container/xci.cpp
source/yati/source/file.cpp
source/yati/source/stdio.cpp
source/yati/source/usb.cpp
source/yati/source/stream.cpp
source/yati/source/stream_file.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
@@ -127,7 +146,8 @@ set(FETCHCONTENT_QUIET FALSE)
FetchContent_Declare(ftpsrv
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
GIT_TAG 1.2.2
# GIT_TAG 1.2.2
GIT_TAG f8a30fd
SOURCE_SUBDIR NONE
)
@@ -161,6 +181,24 @@ FetchContent_Declare(minIni
GIT_TAG 11cac8b
)
FetchContent_Declare(zstd
GIT_REPOSITORY https://github.com/facebook/zstd.git
GIT_TAG v1.5.7
SOURCE_SUBDIR build/cmake
)
set(USE_NEW_ZSTD ON)
set(ZSTD_BUILD_STATIC ON)
set(ZSTD_BUILD_SHARED OFF)
set(ZSTD_BUILD_COMPRESSION OFF)
set(ZSTD_BUILD_DECOMPRESSION ON)
set(ZSTD_BUILD_DICTBUILDER OFF)
set(ZSTD_LEGACY_SUPPORT OFF)
set(ZSTD_MULTITHREAD_SUPPORT OFF)
set(ZSTD_BUILD_PROGRAMS OFF)
set(ZSTD_BUILD_TESTS OFF)
set(MININI_LIB_NAME minIni)
set(MININI_USE_STDIO ON)
set(MININI_USE_NX ON)
@@ -195,6 +233,7 @@ FetchContent_MakeAvailable(
stb
minIni
yyjson
zstd
)
set(FTPSRV_LIB_BUILD TRUE)
@@ -274,6 +313,11 @@ find_package(CURL REQUIRED)
find_path(mbedtls_inc mbedtls REQUIRED)
find_library(mbedcrypto_lib mbedcrypto REQUIRED)
if (NOT USE_NEW_ZSTD)
find_path(zstd_inc zstd.h REQUIRED)
find_library(zstd_lib zstd REQUIRED)
endif()
set_target_properties(sphaira PROPERTIES
C_STANDARD 11
C_EXTENSIONS ON
@@ -296,6 +340,15 @@ target_link_libraries(sphaira PRIVATE
${mbedcrypto_lib}
)
if (USE_NEW_ZSTD)
message(STATUS "USING UPSTREAM ZSTD")
target_link_libraries(sphaira PRIVATE libzstd_static)
else()
message(STATUS "USING LOCAL ZSTD")
target_link_libraries(sphaira PRIVATE ${zstd_lib})
target_include_directories(sphaira PRIVATE ${zstd_inc})
endif()
target_include_directories(sphaira PRIVATE
include
${minizip_inc}

View File

@@ -44,6 +44,8 @@ public:
~App();
void Loop();
static App* GetApp();
static void Exit();
static void ExitRestart();
static auto GetVg() -> NVGcontext*;
@@ -62,7 +64,7 @@ public:
static void SetTheme(s64 theme_index);
static auto GetThemeIndex() -> s64;
static auto GetDefaultImage(int* w = nullptr, int* h = nullptr) -> int;
static auto GetDefaultImage() -> int;
// returns argv[0]
static auto GetExePath() -> fs::FsPath;
@@ -77,7 +79,6 @@ public:
static auto GetInstallEnable() -> bool;
static auto GetInstallSdEnable() -> bool;
static auto GetInstallPrompt() -> bool;
static auto GetThemeShuffleEnable() -> bool;
static auto GetThemeMusicEnable() -> bool;
static auto Get12HourTimeEnable() -> bool;
static auto GetLanguage() -> long;
@@ -91,7 +92,6 @@ public:
static void SetInstallEnable(bool enable);
static void SetInstallSdEnable(bool enable);
static void SetInstallPrompt(bool enable);
static void SetThemeShuffleEnable(bool enable);
static void SetThemeMusicEnable(bool enable);
static void Set12HourTimeEnable(bool enable);
static void SetLanguage(long index);
@@ -174,13 +174,30 @@ public:
option::OptionBool m_ftp_enabled{INI_SECTION, "ftp_enabled", false};
option::OptionBool m_log_enabled{INI_SECTION, "log_enabled", false};
option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", false};
option::OptionBool m_install{INI_SECTION, "install", false};
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
option::OptionBool m_theme_shuffle{INI_SECTION, "theme_shuffle", false};
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false};
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
// install options
option::OptionBool m_install{INI_SECTION, "install", false};
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false};
option::OptionBool m_skip_if_already_installed{INI_SECTION, "skip_if_already_installed", true};
option::OptionBool m_ticket_only{INI_SECTION, "ticket_only", false};
option::OptionBool m_skip_base{INI_SECTION, "skip_base", false};
option::OptionBool m_skip_patch{INI_SECTION, "skip_patch", false};
option::OptionBool m_skip_addon{INI_SECTION, "skip_addon", false};
option::OptionBool m_skip_data_patch{INI_SECTION, "skip_data_patch", false};
option::OptionBool m_skip_ticket{INI_SECTION, "skip_ticket", false};
option::OptionBool m_skip_nca_hash_verify{INI_SECTION, "skip_nca_hash_verify", false};
option::OptionBool m_skip_rsa_header_fixed_key_verify{INI_SECTION, "skip_rsa_header_fixed_key_verify", false};
option::OptionBool m_skip_rsa_npdm_fixed_key_verify{INI_SECTION, "skip_rsa_npdm_fixed_key_verify", false};
option::OptionBool m_ignore_distribution_bit{INI_SECTION, "ignore_distribution_bit", false};
option::OptionBool m_convert_to_standard_crypto{INI_SECTION, "convert_to_standard_crypto", false};
option::OptionBool m_lower_master_key{INI_SECTION, "lower_master_key", false};
option::OptionBool m_lower_system_version{INI_SECTION, "lower_system_version", false};
// todo: move this into it's own menu
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal

View File

@@ -454,4 +454,10 @@ struct FsNativeContentStorage final : FsNative {
}
};
struct FsNativeGameCard final : FsNative {
FsNativeGameCard(const FsGameCardHandle* handle, FsGameCardPartition partition, bool ignore_read_only = true) : FsNative{ignore_read_only} {
m_open_result = fsOpenGameCardFileSystem(&m_fs, handle, partition);
}
};
} // namespace fs

View File

@@ -1,8 +1,22 @@
#pragma once
#include <functional>
namespace sphaira::ftpsrv {
bool Init();
void Exit();
using OnInstallStart = std::function<bool(void* user, const char* path)>;
using OnInstallWrite = std::function<bool(void* user, const void* buf, size_t size)>;
using OnInstallClose = std::function<void(void* user)>;
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
void DisableInstallMode();
unsigned GetPort();
bool IsAnon();
const char* GetUser();
const char* GetPass();
} // namespace sphaira::ftpsrv

View File

@@ -1,24 +1,33 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#define sphaira_USE_LOG 1
#include <cstdarg>
#include <stdarg.h>
#if sphaira_USE_LOG
auto log_file_init() -> bool;
auto log_nxlink_init() -> bool;
bool log_file_init();
bool log_nxlink_init();
void log_file_exit();
void log_nxlink_exit();
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
void log_write_arg(const char* s, std::va_list& v);
void log_write_arg(const char* s, va_list* v);
#else
inline auto log_file_init() -> bool {
inline bool log_file_init() {
return true;
}
inline auto log_nxlink_init() -> bool {
inline bool log_nxlink_init() {
return true;
}
#define log_file_exit()
#define log_nxlink_exit()
#define log_write(...)
#define log_write_arg(...)
#endif
#ifdef __cplusplus
}
#endif

View File

@@ -89,6 +89,19 @@ struct FileAssocEntry {
std::string name{}; // ini name
std::vector<std::string> ext{}; // list of ext
std::vector<std::string> database{}; // list of systems
bool use_base_name{}; // if set, uses base name (rom.zip) otherwise uses internal name (rom.gba)
auto IsExtension(std::string_view extension, std::string_view internal_extension) const -> bool {
for (const auto& assoc_ext : ext) {
if (extension.length() == assoc_ext.length() && !strncasecmp(assoc_ext.data(), extension.data(), assoc_ext.length())) {
return true;
}
if (internal_extension.length() == assoc_ext.length() && !strncasecmp(assoc_ext.data(), internal_extension.data(), assoc_ext.length())) {
return true;
}
}
return false;
}
};
struct LastFile {
@@ -122,6 +135,8 @@ struct Menu final : MenuBase {
private:
void SetIndex(s64 index);
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;
void LoadAssocEntriesPath(const fs::FsPath& path);

View File

@@ -0,0 +1,59 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "yati/source/stream.hpp"
namespace sphaira::ui::menu::ftp {
enum class State {
// not connected.
None,
// just connected, starts the transfer.
Connected,
// set whilst transfer is in progress.
Progress,
// set when the transfer is finished.
Done,
// failed to connect.
Failed,
};
struct StreamFtp final : yati::source::Stream {
StreamFtp(const fs::FsPath& path, std::stop_token token);
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
bool Push(const void* buf, s64 size);
void Disable();
// private:
fs::FsPath m_path{};
std::stop_token m_token{};
std::vector<u8> m_buffer{};
Mutex m_mutex{};
bool m_active{};
// bool m_push_exit{};
};
struct Menu final : MenuBase {
Menu();
~Menu();
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
// this should be private
// private:
std::shared_ptr<StreamFtp> m_source;
Thread m_thread{};
Mutex m_mutex{};
// the below are shared across threads, lock with the above mutex!
State m_state{State::None};
const char* m_user{};
const char* m_pass{};
unsigned m_port{};
bool m_anon{};
};
} // namespace sphaira::ui::menu::ftp

View File

@@ -0,0 +1,38 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "yati/container/base.hpp"
#include "yati/source/base.hpp"
namespace sphaira::ui::menu::gc {
enum class State {
// no gamecard inserted.
None,
// set whilst transfer is in progress.
Progress,
// set when the transfer is finished.
Done,
// set when no gamecard is inserted.
NotFound,
// failed to parse gamecard.
Failed,
};
struct Menu final : MenuBase {
Menu();
~Menu();
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
Result ScanGamecard();
private:
std::unique_ptr<fs::FsNativeGameCard> m_fs{};
FsDeviceOperator m_dev_op{};
yati::container::Collections m_collections{};
State m_state{State::None};
};
} // namespace sphaira::ui::menu::gc

View File

@@ -29,6 +29,7 @@ private:
std::string m_title_sub_heading{};
std::string m_sub_heading{};
protected:
struct tm m_tm{};
TimeStamp m_poll_timestamp{};
u32 m_battery_percetange{};

View File

@@ -0,0 +1,43 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "yati/source/usb.hpp"
namespace sphaira::ui::menu::usb {
enum class State {
// not connected.
None,
// just connected, starts the transfer.
Connected,
// set whilst transfer is in progress.
Progress,
// set when the transfer is finished.
Done,
// failed to connect.
Failed,
};
struct Menu final : MenuBase {
Menu();
~Menu();
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
// this should be private
// private:
std::shared_ptr<yati::source::Usb> m_usb_source{};
bool m_was_mtp_enabled{};
Thread m_thread{};
Mutex m_mutex{};
// the below are shared across threads, lock with the above mutex!
State m_state{State::None};
bool m_usb_has_connection{};
u32 m_usb_speed{};
u32 m_usb_count{};
};
} // namespace sphaira::ui::menu::usb

View File

@@ -21,6 +21,7 @@ struct ProgressBox final : Widget {
auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
auto SetTitle(const std::string& title) -> ProgressBox&;
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
void RequestExit();

View File

@@ -0,0 +1,43 @@
#pragma once
#include "yati/source/base.hpp"
#include <vector>
#include <string>
#include <memory>
#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(std::shared_ptr<Source> source) : m_source{source} { }
virtual ~Base() = default;
virtual Result GetCollections(Collections& out) = 0;
auto GetSource() const {
return m_source;
}
protected:
std::shared_ptr<Source> m_source;
};
} // namespace sphaira::yati::container

View File

@@ -0,0 +1,13 @@
#pragma once
#include "base.hpp"
#include <switch.h>
namespace sphaira::yati::container {
struct Nsp final : Base {
using Base::Base;
Result GetCollections(Collections& out) override;
};
} // namespace sphaira::yati::container

View File

@@ -0,0 +1,15 @@
#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;
};
} // namespace sphaira::yati::container

View 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

View 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);
} // namespace sphaira::es

View 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

View 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

View 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

View 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

View 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

View 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

View 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__ */

View 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__ */

View File

@@ -0,0 +1,25 @@
#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;
virtual bool IsStream() const {
return false;
}
Result GetOpenResult() const {
return m_open_result;
}
protected:
Result m_open_result{};
};
} // namespace sphaira::yati::source

View 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

View 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 fs::FsPath& path);
~Stdio();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
private:
std::FILE* m_file{};
};
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,28 @@
#pragma once
#include "base.hpp"
#include <vector>
#include <switch.h>
namespace sphaira::yati::source {
// streams are for data that do not allow for random access,
// such as FTP or MTP.
struct Stream : Base {
virtual ~Stream() = default;
virtual Result ReadChunk(void* buf, s64 size, u64* bytes_read) = 0;
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
bool IsStream() const override {
return true;
}
protected:
Result m_open_result{};
private:
s64 m_offset{};
};
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,22 @@
// this is used for testing that streams work, this code isn't used in normal
// release builds as it is slower / less feature complete than normal.
#pragma once
#include "stream.hpp"
#include "fs.hpp"
#include <switch.h>
namespace sphaira::yati::source {
struct StreamFile final : Stream {
StreamFile(FsFileSystem* fs, const fs::FsPath& path);
~StreamFile();
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
private:
FsFile m_file{};
s64 m_offset{};
};
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,52 @@
#pragma once
#include "base.hpp"
#include "fs.hpp"
#include <switch.h>
namespace sphaira::yati::source {
struct Usb final : Base {
enum { USBModule = 523 };
enum : Result {
Result_BadMagic = MAKERESULT(USBModule, 0),
Result_BadVersion = MAKERESULT(USBModule, 1),
Result_BadCount = MAKERESULT(USBModule, 2),
Result_BadTransferSize = MAKERESULT(USBModule, 3),
Result_BadTotalSize = MAKERESULT(USBModule, 4),
};
Usb(u64 transfer_timeout);
~Usb();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
Result Finished() const;
Result Init();
Result WaitForConnection(u64 timeout, u32& speed, u32& count);
Result GetFileInfo(std::string& name_out, u64& size_out);
private:
enum UsbSessionEndpoint {
UsbSessionEndpoint_In = 0,
UsbSessionEndpoint_Out = 1,
};
Result SendCommand(s64 off, s64 size) const;
Result InternalRead(void* buf, s64 off, s64 size) const;
bool GetConfigured() const;
Event *GetCompletionEvent(UsbSessionEndpoint ep) const;
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const;
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const;
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const;
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) const;
private:
UsbDsInterface* m_interface{};
UsbDsEndpoint* m_endpoints[2]{};
u64 m_transfer_timeout{};
};
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,121 @@
/*
* 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 "container/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{};
// 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(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path);
Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path);
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const fs::FsPath& path);
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container);
Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections);
} // namespace sphaira::yati

View File

@@ -544,7 +544,7 @@ auto App::GetThemeIndex() -> s64 {
return g_app->m_theme_index;
}
auto App::GetDefaultImage(int* w, int* h) -> int {
auto App::GetDefaultImage() -> int {
return g_app->m_default_image;
}
@@ -580,10 +580,6 @@ auto App::GetInstallPrompt() -> bool {
return g_app->m_install_prompt.Get();
}
auto App::GetThemeShuffleEnable() -> bool {
return g_app->m_theme_shuffle.Get();
}
auto App::GetThemeMusicEnable() -> bool {
return g_app->m_theme_music.Get();
}
@@ -741,10 +737,6 @@ void App::SetInstallPrompt(bool enable) {
g_app->m_install_prompt.Set(enable);
}
void App::SetThemeShuffleEnable(bool enable) {
g_app->m_theme_shuffle.Set(enable);
}
void App::SetThemeMusicEnable(bool enable) {
g_app->m_theme_music.Set(enable);
PlaySoundEffect(SoundEffect::SoundEffect_Music);
@@ -1016,6 +1008,10 @@ void App::Draw() {
this->queue.presentImage(this->swapchain, slot);
}
auto App::GetApp() -> App* {
return g_app;
}
auto App::GetVg() -> NVGcontext* {
return g_app->vg;
}
@@ -1323,11 +1319,7 @@ App::App(const char* argv0) {
fs::FsPath theme_path{};
constexpr fs::FsPath default_theme_path{"romfs:/themes/abyss_theme.ini"};
if (App::GetThemeShuffleEnable() && m_theme_meta_entries.size()) {
theme_path = m_theme_meta_entries[randomGet64() % m_theme_meta_entries.size()].ini_path;
} else {
ini_gets("config", "theme", default_theme_path, theme_path, sizeof(theme_path), CONFIG_PATH);
}
ini_gets("config", "theme", default_theme_path, theme_path, sizeof(theme_path), CONFIG_PATH);
// try and load previous theme, default to previous version otherwise.
ThemeMeta theme_meta;
@@ -1400,8 +1392,33 @@ App::App(const char* argv0) {
}
}
// soon (tm)
// ui::bubble::Init();
struct EventDay {
u8 day;
u8 month;
};
static constexpr EventDay event_days[] = {
{ .day = 1, .month = 1 }, // New years
{ .day = 3, .month = 3 }, // March 3 (switch 1)
{ .day = 10, .month = 5 }, // June 10 (switch 2)
{ .day = 15, .month = 5 }, // June 15
{ .day = 25, .month = 12 }, // Christmas
{ .day = 26, .month = 12 },
{ .day = 27, .month = 12 },
{ .day = 28, .month = 12 },
};
const auto time = std::time(nullptr);
const auto tm = std::localtime(&time);
for (auto e : event_days) {
if (e.day == tm->tm_mday && e.month == (tm->tm_mon + 1)) {
ui::bubble::Init();
break;
}
}
App::Push(std::make_shared<ui::menu::main::MainMenu>());
log_write("finished app constructor\n");

View File

@@ -12,14 +12,30 @@
#include <nx/vfs_nx.h>
#include <nx/utils.h>
namespace sphaira::ftpsrv {
namespace {
struct InstallSharedData {
std::mutex mutex;
std::deque<std::string> queued_files;
void* user;
OnInstallStart on_start;
OnInstallWrite on_write;
OnInstallClose on_close;
bool in_progress;
bool enabled;
};
const char* INI_PATH = "/config/ftpsrv/config.ini";
FtpSrvConfig g_ftpsrv_config = {0};
volatile bool g_should_exit = false;
bool g_is_running{false};
Thread g_thread;
std::mutex g_mutex{};
InstallSharedData g_shared_data{};
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
sphaira::App::NotifyFlashLed();
@@ -29,6 +45,235 @@ void ftp_progress_callback(void) {
sphaira::App::NotifyFlashLed();
}
const char* SUPPORTED_EXT[] = {
".nsp", ".xci", ".nsz", ".xcz",
};
struct VfsUserData {
char* path;
int valid;
};
// ive given up with good names.
void on_thing() {
log_write("[FTP] doing on_thing\n");
std::scoped_lock lock{g_shared_data.mutex};
log_write("[FTP] locked on_thing\n");
if (!g_shared_data.in_progress) {
if (!g_shared_data.queued_files.empty()) {
log_write("[FTP] pushing new file data\n");
if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.user, g_shared_data.queued_files[0].c_str())) {
g_shared_data.queued_files.clear();
} else {
log_write("[FTP] success on new file push\n");
g_shared_data.in_progress = true;
}
}
}
}
int vfs_install_open(void* user, const char* path, enum FtpVfsOpenMode mode) {
{
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
data->valid = 0;
if (mode != FtpVfsOpenMode_WRITE) {
errno = EACCES;
return -1;
}
if (!g_shared_data.enabled) {
errno = EACCES;
return -1;
}
const char* ext = strrchr(path, '.');
if (!ext) {
errno = EACCES;
return -1;
}
bool found = false;
for (size_t i = 0; i < std::size(SUPPORTED_EXT); i++) {
if (!strcasecmp(ext, SUPPORTED_EXT[i])) {
found = true;
break;
}
}
if (!found) {
errno = EINVAL;
return -1;
}
// check if we already have this file queued.
auto it = std::find(g_shared_data.queued_files.cbegin(), g_shared_data.queued_files.cend(), path);
if (it != g_shared_data.queued_files.cend()) {
errno = EEXIST;
return -1;
}
g_shared_data.queued_files.push_back(path);
data->path = strdup(path);
data->valid = true;
}
on_thing();
log_write("[FTP] got file: %s\n", path);
return 0;
}
int vfs_install_read(void* user, void* buf, size_t size) {
errno = EACCES;
return -1;
}
int vfs_install_write(void* user, const void* buf, size_t size) {
std::scoped_lock lock{g_shared_data.mutex};
if (!g_shared_data.enabled) {
errno = EACCES;
return -1;
}
auto data = static_cast<VfsUserData*>(user);
if (!data->valid) {
errno = EACCES;
return -1;
}
if (!g_shared_data.on_write || !g_shared_data.on_write(g_shared_data.user, buf, size)) {
errno = EIO;
return -1;
}
return size;
}
int vfs_install_seek(void* user, const void* buf, size_t size, size_t off) {
errno = ESPIPE;
return -1;
}
int vfs_install_isfile_open(void* user) {
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
return data->valid;
}
int vfs_install_isfile_ready(void* user) {
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
const auto ready = !g_shared_data.queued_files.empty() && data->path == g_shared_data.queued_files[0];
return ready;
}
int vfs_install_close(void* user) {
{
log_write("[FTP] closing file\n");
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
if (data->valid) {
log_write("[FTP] closing valid file\n");
auto it = std::find(g_shared_data.queued_files.cbegin(), g_shared_data.queued_files.cend(), data->path);
if (it != g_shared_data.queued_files.cend()) {
if (it == g_shared_data.queued_files.cbegin()) {
log_write("[FTP] closing current file\n");
if (g_shared_data.on_close) {
g_shared_data.on_close(g_shared_data.user);
}
g_shared_data.in_progress = false;
} else {
log_write("[FTP] closing other file...\n");
}
g_shared_data.queued_files.erase(it);
} else {
log_write("[FTP] could not find file in queue...\n");
}
if (data->path) {
free(data->path);
}
data->valid = 0;
}
memset(data, 0, sizeof(*data));
}
on_thing();
return 0;
}
int vfs_install_opendir(void* user, const char* path) {
return 0;
}
const char* vfs_install_readdir(void* user, void* user_entry) {
return NULL;
}
int vfs_install_dirlstat(void* user, const void* user_entry, const char* path, struct stat* st) {
st->st_nlink = 1;
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
return 0;
}
int vfs_install_isdir_open(void* user) {
return 1;
}
int vfs_install_closedir(void* user) {
return 0;
}
int vfs_install_stat(const char* path, struct stat* st) {
st->st_nlink = 1;
st->st_mode = S_IFDIR | S_IWUSR | S_IWGRP | S_IWOTH;
return 0;
}
int vfs_install_mkdir(const char* path) {
return -1;
}
int vfs_install_unlink(const char* path) {
return -1;
}
int vfs_install_rmdir(const char* path) {
return -1;
}
int vfs_install_rename(const char* src, const char* dst) {
return -1;
}
FtpVfs g_vfs_install = {
.open = vfs_install_open,
.read = vfs_install_read,
.write = vfs_install_write,
.seek = vfs_install_seek,
.close = vfs_install_close,
.isfile_open = vfs_install_isfile_open,
.isfile_ready = vfs_install_isfile_ready,
.opendir = vfs_install_opendir,
.readdir = vfs_install_readdir,
.dirlstat = vfs_install_dirlstat,
.closedir = vfs_install_closedir,
.isdir_open = vfs_install_isdir_open,
.stat = vfs_install_stat,
.lstat = vfs_install_stat,
.mkdir = vfs_install_mkdir,
.unlink = vfs_install_unlink,
.rmdir = vfs_install_rmdir,
.rename = vfs_install_rename,
};
void loop(void* arg) {
while (!g_should_exit) {
ftpsrv_init(&g_ftpsrv_config);
@@ -44,8 +289,6 @@ void loop(void* arg) {
} // namespace
namespace sphaira::ftpsrv {
bool Init() {
std::scoped_lock lock{g_mutex};
if (g_is_running) {
@@ -84,6 +327,9 @@ bool Init() {
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH);
save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH);
mount_devices = true;
g_ftpsrv_config.timeout = 0;
if (!g_ftpsrv_config.port) {
return false;
}
@@ -93,7 +339,13 @@ bool Init() {
g_ftpsrv_config.anon = true;
}
vfs_nx_init(mount_devices, save_writable, mount_bis);
const VfsNxCustomPath custom = {
.name = "install",
.user = NULL,
.func = &g_vfs_install,
};
vfs_nx_init(&custom, mount_devices, save_writable, mount_bis);
Result rc;
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*16, 0x2C, 2))) {
@@ -123,6 +375,40 @@ void Exit() {
fsdev_wrapUnmountAll();
}
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) {
std::scoped_lock lock{g_shared_data.mutex};
g_shared_data.user = user;
g_shared_data.on_start = on_start;
g_shared_data.on_write = on_write;
g_shared_data.on_close = on_close;
g_shared_data.enabled = true;
}
void DisableInstallMode() {
std::scoped_lock lock{g_shared_data.mutex};
g_shared_data.enabled = false;
}
unsigned GetPort() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.port;
}
bool IsAnon() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.anon;
}
const char* GetUser() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.user;
}
const char* GetPass() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.pass;
}
} // namespace sphaira::ftpsrv
extern "C" {
@@ -132,9 +418,9 @@ void log_file_write(const char* msg) {
}
void log_file_fwrite(const char* fmt, ...) {
std::va_list v{};
va_list v{};
va_start(v, fmt);
log_write_arg(fmt, v);
log_write_arg(fmt, &v);
va_end(v);
}

View File

@@ -14,18 +14,20 @@ std::FILE* file{};
int nxlink_socket{};
std::mutex mutex{};
void log_write_arg_internal(const char* s, std::va_list& v) {
void log_write_arg_internal(const char* s, std::va_list* v) {
if (file) {
std::vfprintf(file, s, v);
std::vfprintf(file, s, *v);
std::fflush(file);
}
if (nxlink_socket) {
std::vprintf(s, v);
std::vprintf(s, *v);
}
}
} // namespace
extern "C" {
auto log_file_init() -> bool {
std::scoped_lock lock{mutex};
if (file) {
@@ -70,11 +72,11 @@ void log_write(const char* s, ...) {
std::va_list v{};
va_start(v, s);
log_write_arg_internal(s, v);
log_write_arg_internal(s, &v);
va_end(v);
}
void log_write_arg(const char* s, std::va_list& v) {
void log_write_arg(const char* s, va_list* v) {
std::scoped_lock lock{mutex};
if (!file && !nxlink_socket) {
return;
@@ -83,4 +85,6 @@ void log_write_arg(const char* s, std::va_list& v) {
log_write_arg_internal(s, v);
}
} // extern "C"
#endif

View File

@@ -66,6 +66,9 @@ void userAppInit(void) {
if (R_FAILED(rc = ncmInitialize()))
diagAbortWithResult(rc);
// it doesn't matter if this fails.
appletSetScreenShotPermission(AppletScreenShotPermission_Enable);
log_nxlink_init();
}

View File

@@ -7,6 +7,14 @@
#include <string_view>
#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 "defines.hpp"
#include "app.hpp"
@@ -23,52 +31,9 @@ constexpr u32 PFS0_EXEFS_HASH_BLOCK_SIZE = 0x10000;
constexpr u32 PFS0_LOGO_HASH_BLOCK_SIZE = 0x1000;
constexpr u32 PFS0_META_HASH_BLOCK_SIZE = 0x1000;
constexpr u32 PFS0_PADDING_SIZE = 0x200;
constexpr u32 NCA_SECTION_TOTAL = 0x4;
constexpr u32 ROMFS_ENTRY_EMPTY = 0xFFFFFFFF;
constexpr u32 ROMFS_FILEPARTITION_OFS = 0x200;
enum NcaDistributionType {
NcaDistributionType_System = 0x0,
NcaDistributionType_GameCard = 0x1
};
enum NcaContentType {
NcaContentType_Program = 0x0,
NcaContentType_Meta = 0x1,
NcaContentType_Control = 0x2,
NcaContentType_Manual = 0x3,
NcaContentType_Data = 0x4,
NcaContentType_PublicData = 0x5,
};
enum NcaFileSystemType {
NcaFileSystemType_RomFS = 0x0,
NcaFileSystemType_PFS0 = 0x1
};
enum NcaHashType {
NcaHashType_Auto = 0x0,
NcaHashType_HierarchicalSha256 = 0x2,
NcaHashType_HierarchicalIntegrity = 0x3
};
enum NcaEncryptionType {
NcaEncryptionType_Auto = 0x0,
NcaEncryptionType_None = 0x1,
NcaEncryptionType_AesCtrOld = 0x2,
NcaEncryptionType_AesCtr = 0x3,
NcaEncryptionType_AesCtrEx = 0x4
};
enum NsApplicationRecordType {
// installed
NsApplicationRecordType_Installed = 0x3,
// application is gamecard, but gamecard isn't insterted
NsApplicationRecordType_GamecardMissing = 0x5,
// archived
NsApplicationRecordType_Archived = 0xB,
};
// stdio-like wrapper for std::vector
struct BufHelper {
BufHelper() = default;
@@ -124,12 +89,6 @@ struct CnmtHeader {
};
static_assert(sizeof(CnmtHeader) == 0x20);
struct NcmContentStorageRecord {
NcmContentMetaKey key;
u8 storage_id; //
u8 padding[0x7];
};
struct NcmContentMetaData {
NcmContentMetaHeader header;
NcmApplicationMetaExtendedHeader extended;
@@ -142,7 +101,7 @@ struct NcaMetaEntry {
NcaEntry nca_entry;
NcmContentMetaHeader content_meta_header{};
NcmContentMetaKey content_meta_key{};
NcmContentStorageRecord content_storage_record{};
ncm::ContentStorageRecord content_storage_record{};
NcmContentMetaData content_meta_data{};
};
@@ -171,61 +130,6 @@ struct FileEntry {
using FileEntries = std::vector<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 {
char title_name[0x10]{"Application"};
char product_code[0x10]{};
@@ -238,56 +142,6 @@ struct NcapPatch {
u64 tid;
};
struct NcaSectionTableEntry {
u32 media_start_offset; // divided by 0x200.
u32 media_end_offset; // divided by 0x200.
u8 _0x8[0x4]; // unknown.
u8 _0xC[0x4]; // unknown.
};
struct LayerRegion {
u64 offset;
u64 size;
};
struct HierarchicalSha256Data {
u8 master_hash[0x20];
u32 block_size;
u32 layer_count;
LayerRegion hash_layer;
LayerRegion pfs0_layer;
LayerRegion unused_layers[3];
u8 _0x78[0x80];
};
#pragma pack(push, 1)
struct HierarchicalIntegrityVerificationLevelInformation {
u64 logical_offset;
u64 hash_data_size;
u32 block_size; // log2
u32 _0x14; // reserved
};
#pragma pack(pop)
struct InfoLevelHash {
u32 max_layers;
HierarchicalIntegrityVerificationLevelInformation levels[6];
u8 signature_salt[0x20];
};
struct IntegrityMetaInfo {
u32 magic; // IVFC
u32 version;
u32 master_hash_size;
InfoLevelHash info_level_hash;
u8 master_hash[0x20];
u8 _0xE0[0x18];
};
static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo));
typedef struct romfs_dirent_ctx {
u32 entry_offset;
struct romfs_dirent_ctx *parent; /* Parent node */
@@ -317,70 +171,6 @@ typedef struct {
u64 file_partition_size;
} romfs_ctx_t;
struct NcaFsHeader {
u16 version; // always 2.
u8 fs_type; // see NcaFileSystemType.
u8 hash_type; // see NcaHashType.
u8 encryption_type; // see NcaEncryptionType.
u8 metadata_hash_type;
u8 _0x6[0x2]; // empty.
union {
HierarchicalSha256Data hierarchical_sha256_data;
IntegrityMetaInfo integrity_meta_info; // used for romfs
} hash_data;
u8 patch_info[0x40];
u64 section_ctr;
u8 spares_info[0x30];
u8 compression_info[0x28];
u8 meta_data_hash_data_info[0x30];
u8 reserved[0x30];
};
struct NcaSectionHeaderHash {
u8 sha256[0x20];
};
struct NcaKeyArea {
u8 area[0x10];
};
struct NcaHeader {
u8 rsa_fixed_key[0x100];
u8 rsa_npdm[0x100]; // key from npdm.
u32 magic;
u8 distribution_type; // see NcaDistributionType.
u8 content_type; // see NcaContentType.
u8 old_key_gen; // see NcaOldKeyGeneration.
u8 kaek_index; // see NcaKeyAreaEncryptionKeyIndex.
u64 size;
u64 title_id;
u32 context_id;
u32 sdk_version;
u8 key_gen; // see NcaKeyGeneration.
u8 header_1_sig_key_gen;
u8 _0x222[0xE]; // empty.
FsRightsId rights_id;
NcaSectionTableEntry fs_table[NCA_SECTION_TOTAL];
NcaSectionHeaderHash fs_header_hash[NCA_SECTION_TOTAL];
NcaKeyArea key_area[NCA_SECTION_TOTAL];
u8 _0x340[0xC0]; // empty.
NcaFsHeader fs_header[NCA_SECTION_TOTAL];
};
constexpr u8 HEADER_KEK_SRC[0x10] = {
0x1F, 0x12, 0x91, 0x3A, 0x4A, 0xCB, 0xF0, 0x0D, 0x4C, 0xDE, 0x3A, 0xF6, 0xD5, 0x23, 0x88, 0x2A
};
constexpr u8 HEADER_KEY_SRC[0x20] = {
0x5A, 0x3E, 0xD8, 0x4F, 0xDE, 0xC0, 0xD8, 0x26, 0x31, 0xF7, 0xE2, 0x5D, 0x19, 0x7B, 0xF5, 0xD0,
0x1C, 0x9B, 0x7B, 0xFA, 0xF6, 0x28, 0x18, 0x3D, 0x71, 0xF6, 0x4D, 0x73, 0xF1, 0x50, 0xB9, 0xD2
};
auto write_padding(BufHelper& buf, u64 off, u64 block) -> u64 {
const u64 size = block - (off % block);
if (size) {
@@ -632,9 +422,9 @@ auto npdm_patch_kc(std::vector<u8>& npdm, u32 off, u32 size, u32 bitmask, u32 va
// todo: manually build npdm
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
NpdmMeta meta{};
NpdmAci0 aci0{};
NpdmAcid acid{};
npdm::Meta meta{};
npdm::Aci0 aci0{};
npdm::Acid acid{};
std::memcpy(&meta, npdm.data(), sizeof(meta));
std::memcpy(&aci0, npdm.data() + meta.aci0_offset, sizeof(aci0));
std::memcpy(&acid, npdm.data() + meta.acid_offset, sizeof(acid));
@@ -805,7 +595,7 @@ void write_nca_padding(BufHelper& buf) {
write_padding(buf, buf.tell(), 0x200);
}
void nca_encrypt_header(NcaHeader* header, std::span<const u8> key) {
void nca_encrypt_header(nca::Header* header, std::span<const u8> key) {
Aes128XtsContext ctx{};
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];
section.media_start_offset = start / 0x200; // 0xC00 / 0x200
section.media_end_offset = end / 0x200; // Section end offset / 200
section._0x8[0] = 0x1; // Always 1
}
void write_nca_fs_header_pfs0(NcaHeader& nca_header, u8 index, const std::vector<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];
fs_header.hash_type = NcaHashType_HierarchicalSha256;
fs_header.fs_type = NcaFileSystemType_PFS0;
fs_header.hash_type = nca::HashType_HierarchicalSha256;
fs_header.fs_type = nca::FileSystemType_PFS0;
fs_header.version = 0x2; // Always 2
fs_header.hash_data.hierarchical_sha256_data.layer_count = 0x2;
fs_header.hash_data.hierarchical_sha256_data.block_size = block_size;
fs_header.encryption_type = NcaEncryptionType_None;
fs_header.encryption_type = nca::EncryptionType_None;
fs_header.hash_data.hierarchical_sha256_data.hash_layer.size = hash_table_size;
std::memcpy(fs_header.hash_data.hierarchical_sha256_data.master_hash, master_hash.data(), master_hash.size());
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
}
void write_nca_fs_header_romfs(NcaHeader& nca_header, u8 index) {
void write_nca_fs_header_romfs(nca::Header& nca_header, u8 index) {
auto& fs_header = nca_header.fs_header[index];
fs_header.hash_type = NcaHashType_HierarchicalIntegrity;
fs_header.fs_type = NcaFileSystemType_RomFS;
fs_header.hash_type = nca::HashType_HierarchicalIntegrity;
fs_header.fs_type = nca::FileSystemType_RomFS;
fs_header.version = 0x2; // Always 2
fs_header.hash_data.integrity_meta_info.magic = 0x43465649;
fs_header.hash_data.integrity_meta_info.version = 0x20000; // Always 0x20000
fs_header.hash_data.integrity_meta_info.master_hash_size = SHA256_HASH_SIZE;
fs_header.hash_data.integrity_meta_info.info_level_hash.max_layers = 0x7;
fs_header.encryption_type = NcaEncryptionType_None;
fs_header.encryption_type = nca::EncryptionType_None;
fs_header.hash_data.integrity_meta_info.info_level_hash.levels[5].block_size = 0x0E; // 0x4000
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
}
void write_nca_pfs0(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
void write_nca_pfs0(nca::Header& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
const auto pfs0 = build_pfs0(entries);
const auto pfs0_hash_table = build_pfs0_hash_table(pfs0, block_size);
const auto pfs0_master_hash = build_pfs0_master_hash(pfs0_hash_table);
@@ -887,7 +677,7 @@ auto ivfc_create_level(const std::vector<u8>& src) -> std::vector<u8> {
return buf.buf;
}
void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
void write_nca_romfs(nca::Header& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
auto& fs_header = nca_header.fs_header[index];
auto& meta_info = fs_header.hash_data.integrity_meta_info;
auto& info_level_hash = meta_info.info_level_hash;
@@ -921,22 +711,22 @@ void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries
write_nca_fs_header_romfs(nca_header, index);
}
void write_nca_header_encypted(NcaHeader& nca_header, u64 tid, std::span<const u8> key, NcaContentType type, BufHelper& buf) {
nca_header.magic = 0x3341434E;
nca_header.distribution_type = NcaDistributionType_System;
void write_nca_header_encypted(nca::Header& nca_header, u64 tid, const keys::Keys& keys, nca::ContentType type, BufHelper& buf) {
nca_header.magic = NCA3_MAGIC;
nca_header.distribution_type = nca::DistributionType_System;
nca_header.content_type = type;
nca_header.title_id = tid;
nca_header.sdk_version = 0x000C1100;
nca_header.size = buf.tell();
nca_encrypt_header(&nca_header, key);
nca_encrypt_header(&nca_header, keys.header_key);
buf.seek(0);
buf.write(&nca_header, sizeof(nca_header));
}
auto create_program_nca(u64 tid, std::span<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;
NcaHeader nca_header{};
nca::Header nca_header{};
buf.write(&nca_header, sizeof(nca_header));
write_nca_pfs0(nca_header, 0, exefs, PFS0_EXEFS_HASH_BLOCK_SIZE, buf);
@@ -945,23 +735,23 @@ auto create_program_nca(u64 tid, std::span<const u8> key, const FileEntries& exe
if (logo.size() == 2 && !logo[0].data.empty() && !logo[1].data.empty()) {
write_nca_pfs0(nca_header, 2, logo, PFS0_LOGO_HASH_BLOCK_SIZE, buf);
}
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Program, buf);
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Program, buf);
return {buf, NcmContentType_Program};
}
auto create_control_nca(u64 tid, std::span<const u8> key, const FileEntries& romfs) -> NcaEntry{
NcaHeader nca_header{};
auto create_control_nca(u64 tid, const keys::Keys& keys, const FileEntries& romfs) -> NcaEntry{
nca::Header nca_header{};
BufHelper buf;
buf.write(&nca_header, sizeof(nca_header));
write_nca_romfs(nca_header, 0, romfs, IVFC_HASH_BLOCK_SIZE, buf);
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Control, buf);
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Control, buf);
return {buf, NcmContentType_Control};
}
auto create_meta_nca(u64 tid, std::span<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{};
NcmApplicationMetaExtendedHeader cnmt_extended{};
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);
add_file_entry(cnmt, cnmt_name, cnmt_buf.buf.data(), cnmt_buf.buf.size());
NcaHeader nca_header{};
nca::Header nca_header{};
buf.write(&nca_header, sizeof(nca_header));
write_nca_pfs0(nca_header, 0, cnmt, PFS0_META_HASH_BLOCK_SIZE, buf);
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Meta, buf);
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Meta, buf);
// entry
NcaMetaEntry entry{buf, NcmContentType_Meta};
@@ -1040,26 +830,6 @@ auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id,
return entry;
}
Result nsDeleteApplicationRecord(Service* srv, u64 tid) {
return serviceDispatchIn(srv, 27, tid);
}
Result nsPushApplicationRecord(Service* srv, u64 tid, const NcmContentStorageRecord* records, u32 count) {
const struct {
u8 last_modified_event;
u8 padding[0x7];
u64 tid;
} in = { NsApplicationRecordType_Installed, {0}, tid };
return serviceDispatchIn(srv, 16, in,
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
.buffers = { { records, sizeof(NcmContentStorageRecord) * count } });
}
Result nsInvalidateApplicationControlCache(Service* srv, u64 tid) {
return serviceDispatchIn(srv, 404, tid);
}
auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId storage_id) -> Result {
R_UNLESS(!config.nro_path.empty(), OwoError_BadArgs);
// R_UNLESS(!config.icon.empty(), OwoError_BadArgs);
@@ -1073,14 +843,8 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
R_TRY(nsInitialize());
ON_SCOPE_EXIT(nsExit());
// generate header kek
u8 header_kek[0x20];
R_TRY(splCryptoGenerateAesKek(HEADER_KEK_SRC, 0, 0, header_kek));
// gen header key 0
u8 key[0x20];
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC, key));
// gen header key 1
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC + 0x10, key + 0x10));
keys::Keys keys;
R_TRY(keys::parse_keys(keys, false));
// fix args to include nro path
if (config.args.empty()) {
@@ -1124,7 +888,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
patch_npdm(exefs[1].data, npdm_patch);
nca_entries.emplace_back(
create_program_nca(tid, key, exefs, romfs, logo)
create_program_nca(tid, keys, exefs, romfs, logo)
);
} else {
nca_entries.emplace_back(
@@ -1147,18 +911,18 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
add_file_entry(romfs, "/icon_AmericanEnglish.dat", config.icon);
nca_entries.emplace_back(
create_control_nca(tid, key, romfs)
create_control_nca(tid, keys, romfs)
);
}
// create meta
NcmContentMetaHeader content_meta_header;
NcmContentMetaKey content_meta_key;
NcmContentStorageRecord content_storage_record;
ncm::ContentStorageRecord content_storage_record;
NcmContentMetaData content_meta_data;
{
pbox->NewTransfer("Creating Meta"_i18n).UpdateTransfer(2, 8);
const auto meta_entry = create_meta_nca(tid, key, storage_id, nca_entries);
const auto meta_entry = create_meta_nca(tid, keys, storage_id, nca_entries);
nca_entries.emplace_back(meta_entry.nca_entry);
content_meta_header = meta_entry.content_meta_header;
@@ -1218,15 +982,15 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
// remove previous application record
if (already_installed || hosversionBefore(2,0,0)) {
const auto rc = nsDeleteApplicationRecord(srv_ptr, tid);
const auto rc = ns::DeleteApplicationRecord(srv_ptr, tid);
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
}
R_TRY(nsPushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
R_TRY(ns::PushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
// force flush
if (already_installed || hosversionBefore(2,0,0)) {
const auto rc = nsInvalidateApplicationControlCache(srv_ptr, tid);
const auto rc = ns::InvalidateApplicationControlCache(srv_ptr, tid);
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
}
}

View File

@@ -42,14 +42,15 @@ Result ShowInternal(Config& cfg, const char* guide, const char* initial, s64 len
} // namespace
Result ShowText(std::string& out, const char* guide, const char* initial, s64 len_min, s64 len_max) {
Config cfg;
Config cfg{};
R_TRY(ShowInternal(cfg, guide, initial, len_min, len_max));
out = cfg.out_text;
R_SUCCEED();
}
Result ShowNumPad(s64& out, const char* guide, const char* initial, s64 len_min, s64 len_max) {
Config cfg;
Config cfg{};
cfg.numpad = true;
R_TRY(ShowInternal(cfg, guide, initial, len_min, len_max));
out = std::atoll(cfg.out_text);
R_SUCCEED();

View File

@@ -18,6 +18,8 @@
#include "owo.hpp"
#include "swkbd.hpp"
#include "i18n.hpp"
#include "yati/yati.hpp"
#include "yati/source/file.hpp"
#include <minIni.h>
#include <minizip/unzip.h>
@@ -52,13 +54,35 @@ constexpr std::string_view VIDEO_EXTENSIONS[] = {
constexpr std::string_view IMAGE_EXTENSIONS[] = {
"png", "jpg", "jpeg", "bmp", "gif",
};
struct RomDatabaseEntry {
std::string_view folder;
std::string_view database;
constexpr std::string_view INSTALL_EXTENSIONS[] = {
"nsp", "xci", "nsz", "xcz",
};
struct RomDatabaseEntry {
// uses the naming scheme from retropie.
std::string_view folder{};
// uses the naming scheme from Retroarch.
std::string_view database{};
// custom alias, to make everyone else happy.
std::array<std::string_view, 4> alias{};
// compares against all of the above strings.
auto IsDatabase(std::string_view name) const {
if (name == folder || name == database) {
return true;
}
for (const auto& str : alias) {
if (!str.empty() && name == str) {
return true;
}
}
return false;
}
};
// using PathPair = std::pair<std::string_view, std::string_view>;
constexpr RomDatabaseEntry PATHS[]{
{ "3do", "The 3DO Company - 3DO"},
{ "atari800", "Atari - 8-bit"},
@@ -94,6 +118,14 @@ constexpr RomDatabaseEntry PATHS[]{
{ "pico8", "Sega - PICO"},
{ "wonderswan", "Bandai - WonderSwan"},
{ "wonderswancolor", "Bandai - WonderSwan Color"},
{ "mame", "MAME 2000", { "MAME", "mame-libretro", } },
{ "mame", "MAME 2003", { "MAME", "mame-libretro", } },
{ "mame", "MAME 2003-Plus", { "MAME", "mame-libretro", } },
{ "neogeo", "SNK - Neo Geo Pocket" },
{ "neogeo", "SNK - Neo Geo Pocket Color" },
{ "neogeo", "SNK - Neo Geo CD" },
};
constexpr fs::FsPath DAYBREAK_PATH{"/switch/daybreak.nro"};
@@ -114,49 +146,49 @@ auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool {
// tries to find database path using folder name
// names are taken from retropie
// retroarch database names can also be used
auto GetRomDatabaseFromPath(std::string_view path) -> int {
using RomDatabaseIndexs = std::vector<size_t>;
auto GetRomDatabaseFromPath(std::string_view path) -> RomDatabaseIndexs {
if (path.length() <= 1) {
return -1;
return {};
}
// this won't fail :)
RomDatabaseIndexs indexs;
const auto db_name = path.substr(path.find_last_of('/') + 1);
// log_write("new path: %s\n", db_name.c_str());
for (int i = 0; i < std::size(PATHS); i++) {
auto& p = PATHS[i];
if ((
p.folder.length() == db_name.length() && !strncasecmp(p.folder.data(), db_name.data(), p.folder.length())) ||
(p.database.length() == db_name.length() && !strncasecmp(p.database.data(), db_name.data(), p.database.length()))) {
const auto& p = PATHS[i];
if (p.IsDatabase(db_name)) {
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
return i;
indexs.emplace_back(i);
}
}
// if we failed, try again but with the folder about
// "/roms/psx/scooby-doo/scooby-doo.bin", this will check psx
const auto last_off = path.substr(0, path.find_last_of('/'));
if (const auto off = last_off.find_last_of('/'); off != std::string_view::npos) {
const auto db_name2 = last_off.substr(off + 1);
// printf("got db: %s\n", db_name2.c_str());
for (int i = 0; i < std::size(PATHS); i++) {
auto& p = PATHS[i];
if ((
p.folder.length() == db_name2.length() && !strcasecmp(p.folder.data(), db_name2.data())) ||
(p.database.length() == db_name2.length() && !strcasecmp(p.database.data(), db_name2.data()))) {
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
return i;
if (indexs.empty()) {
const auto last_off = path.substr(0, path.find_last_of('/'));
if (const auto off = last_off.find_last_of('/'); off != std::string_view::npos) {
const auto db_name2 = last_off.substr(off + 1);
// printf("got db: %s\n", db_name2.c_str());
for (int i = 0; i < std::size(PATHS); i++) {
const auto& p = PATHS[i];
if (p.IsDatabase(db_name2)) {
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
indexs.emplace_back(i);
}
}
}
}
return -1;
return indexs;
}
//
auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, std::string extension, int db_idx, const NroEntry& nro) {
auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, const RomDatabaseIndexs& db_indexs, const NroEntry& nro) {
// if no db entries, use nro icon
if (db_idx < 0) {
if (db_indexs.empty()) {
log_write("using nro image\n");
return nro_get_icon(nro.path, nro.icon_size, nro.icon_offset);
}
@@ -178,49 +210,51 @@ auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, std::
#define RA_THUMBNAIL_PATH "/retroarch/thumbnails/"
#define RA_BOXART_EXT ".png"
const auto system_name = std::string{PATHS[db_idx].database.data(), PATHS[db_idx].database.length()};//GetDatabaseFromExt(database, extension);
auto system_name_gh = system_name + "/master";
for (auto& c : system_name_gh) {
if (c == ' ') {
c = '_';
for (auto db_idx : db_indexs) {
const auto system_name = std::string{PATHS[db_idx].database.data(), PATHS[db_idx].database.length()};//GetDatabaseFromExt(database, extension);
auto system_name_gh = system_name + "/master";
for (auto& c : system_name_gh) {
if (c == ' ') {
c = '_';
}
}
}
std::string filename_gh;
filename_gh.reserve(filename.size());
for (auto c : filename) {
if (c == ' ') {
filename_gh += "%20";
} else {
filename_gh.push_back(c);
std::string filename_gh;
filename_gh.reserve(filename.size());
for (auto c : filename) {
if (c == ' ') {
filename_gh += "%20";
} else {
filename_gh.push_back(c);
}
}
}
const std::string thumbnail_path = system_name + RA_BOXART_NAME + filename + RA_BOXART_EXT;
const std::string ra_thumbnail_path = RA_THUMBNAIL_PATH + thumbnail_path;
const std::string ra_thumbnail_url = RA_BOXART_URL + thumbnail_path;
const std::string gh_thumbnail_url = GH_BOXART_URL + system_name_gh + RA_BOXART_NAME + filename_gh + RA_BOXART_EXT;
const std::string thumbnail_path = system_name + RA_BOXART_NAME + filename + RA_BOXART_EXT;
const std::string ra_thumbnail_path = RA_THUMBNAIL_PATH + thumbnail_path;
const std::string ra_thumbnail_url = RA_BOXART_URL + thumbnail_path;
const std::string gh_thumbnail_url = GH_BOXART_URL + system_name_gh + RA_BOXART_NAME + filename_gh + RA_BOXART_EXT;
log_write("starting image convert on: %s\n", ra_thumbnail_path.c_str());
// try and find icon locally
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Trying to load "_i18n + ra_thumbnail_path);
std::vector<u8> image_file;
if (R_SUCCEEDED(fs->read_entire_file(ra_thumbnail_path.c_str(), image_file))) {
return image_file;
log_write("starting image convert on: %s\n", ra_thumbnail_path.c_str());
// try and find icon locally
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Trying to load "_i18n + ra_thumbnail_path);
std::vector<u8> image_file;
if (R_SUCCEEDED(fs->read_entire_file(ra_thumbnail_path.c_str(), image_file))) {
return image_file;
}
}
}
// try and download icon
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Downloading "_i18n + gh_thumbnail_url);
const auto result = curl::Api().ToMemory(
curl::Url{gh_thumbnail_url},
curl::OnProgress{pbox->OnDownloadProgressCallback()}
);
// try and download icon
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Downloading "_i18n + gh_thumbnail_url);
const auto result = curl::Api().ToMemory(
curl::Url{gh_thumbnail_url},
curl::OnProgress{pbox->OnDownloadProgressCallback()}
);
if (result.success && !result.data.empty()) {
return result.data;
if (result.success && !result.data.empty()) {
return result.data;
}
}
}
@@ -294,6 +328,8 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
nro_launch(GetNewPathCurrent());
}
}));
} else if (App::GetInstallEnable() && IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) {
InstallFile(GetEntry());
} else {
const auto assoc_list = FindFileAssocFor();
if (!assoc_list.empty()) {
@@ -406,7 +442,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
}
log_write("clicked on delete\n");
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) {
App::PopToMenu();
OnDeleteCallback();
@@ -421,7 +457,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
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;
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) {
App::PopToMenu();
OnPasteCallback();
@@ -459,6 +495,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](){
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
@@ -628,6 +690,9 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
icon = ThemeEntryID_ICON_VIDEO;
} else if (IsExtension(ext, IMAGE_EXTENSIONS)) {
icon = ThemeEntryID_ICON_IMAGE;
} else if (IsExtension(ext, INSTALL_EXTENSIONS)) {
// todo: maybe replace this icon with something else?
icon = ThemeEntryID_ICON_NRO;
} else if (IsExtension(ext, "zip")) {
icon = ThemeEntryID_ICON_ZIP;
} else if (IsExtension(ext, "nro")) {
@@ -747,8 +812,7 @@ void Menu::InstallForwarder() {
return false;
}
log_write("got nro data\n");
std::string file_name = GetEntry().GetInternalName();
std::string extension = GetEntry().GetInternalExtension();
auto file_name = assoc.use_base_name ? GetEntry().GetName() : GetEntry().GetInternalName();
if (auto pos = file_name.find_last_of('.'); pos != std::string::npos) {
log_write("got filename\n");
@@ -756,7 +820,7 @@ void Menu::InstallForwarder() {
log_write("got filename2: %s\n\n", file_name.c_str());
}
const auto db_idx = GetRomDatabaseFromPath(m_path);
const auto db_indexs = GetRomDatabaseFromPath(m_path);
OwoConfig config{};
config.nro_path = assoc.path.toString();
@@ -764,7 +828,7 @@ void Menu::InstallForwarder() {
config.name = nro.nacp.lang[0].name + std::string{" | "} + file_name;
// config.name = file_name;
config.nacp = nro.nacp;
config.icon = GetRomIcon(m_fs.get(), pbox, file_name, extension, db_idx, nro);
config.icon = GetRomIcon(m_fs.get(), pbox, file_name, db_indexs, nro);
return R_SUCCEEDED(App::Install(pbox, config));
}));
@@ -775,6 +839,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 {
log_write("new scan path: %s\n", new_path.s);
if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) {
@@ -847,32 +939,32 @@ auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
auto Menu::FindFileAssocFor() -> std::vector<FileAssocEntry> {
// only support roms in correctly named folders, sorry!
const auto db_idx = GetRomDatabaseFromPath(m_path);
const auto db_indexs = GetRomDatabaseFromPath(m_path);
const auto& entry = GetEntry();
const auto extension = entry.internal_extension.empty() ? entry.extension : entry.internal_extension;
if (extension.empty()) {
const auto extension = entry.extension;
const auto internal_extension = entry.internal_extension.empty() ? entry.extension : entry.internal_extension;
if (extension.empty() && internal_extension.empty()) {
// log_write("failed to get extension for db: %s path: %s\n", database_entry.c_str(), m_path);
return {};
}
// log_write("got extension for db: %s path: %s\n", database_entry.c_str(), m_path);
std::vector<FileAssocEntry> out_entries;
if (db_idx >= 0) {
if (!db_indexs.empty()) {
// if database isn't empty, then we are in a valid folder
// search for an entry that matches the db and ext
for (const auto& assoc : m_assoc_entries) {
for (const auto& assoc_db : assoc.database) {
if (assoc_db == PATHS[db_idx].folder || assoc_db == PATHS[db_idx].database) {
for (const auto& assoc_ext : assoc.ext) {
if (assoc_ext == extension) {
log_write("found ext: %s assoc_ext: %s assoc.ext: %s\n", assoc.path.s, assoc_ext.c_str(), extension.c_str());
// if (assoc_db == PATHS[db_idx].folder || assoc_db == PATHS[db_idx].database) {
for (auto db_idx : db_indexs) {
if (PATHS[db_idx].IsDatabase(assoc_db)) {
if (assoc.IsExtension(extension, internal_extension)) {
out_entries.emplace_back(assoc);
goto jump;
}
}
}
}
jump:
}
} else {
// otherwise, if not in a valid folder, find an entry that doesn't
@@ -883,11 +975,9 @@ auto Menu::FindFileAssocFor() -> std::vector<FileAssocEntry> {
// to be in the correct folder, ie psx, to know what system that .iso is for.
for (const auto& assoc : m_assoc_entries) {
if (assoc.database.empty()) {
for (const auto& assoc_ext : assoc.ext) {
if (assoc_ext == extension) {
log_write("found ext: %s\n", assoc.path.s);
out_entries.emplace_back(assoc);
}
if (assoc.IsExtension(extension, internal_extension)) {
log_write("found ext: %s\n", assoc.path.s);
out_entries.emplace_back(assoc);
}
}
}
@@ -944,6 +1034,10 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) {
}
}
}
} else if (!strcmp(Key, "use_base_name")) {
if (!strcmp(Value, "true") || !strcmp(Value, "1")) {
assoc->use_base_name = true;
}
}
return 1;
}, &assoc, full_path);
@@ -979,7 +1073,7 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) {
continue;
}
// log_write("\tpath: %s\n", assoc.path.c_str());
// log_write("\tpath: %s\n", assoc.path.s);
// log_write("\tname: %s\n", assoc.name.c_str());
// for (const auto& ext : assoc.ext) {
// log_write("\t\text: %s\n", ext.c_str());

View File

@@ -0,0 +1,296 @@
#include "ui/menus/ftp_menu.hpp"
#include "yati/yati.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include "ftpsrv_helper.hpp"
#include <cstring>
namespace sphaira::ui::menu::ftp {
namespace {
constexpr u64 MAX_BUFFER_SIZE = 1024*1024*32;
constexpr u64 SLEEPNS = 1000;
volatile bool IN_PUSH_THREAD{};
bool OnInstallStart(void* user, const char* path) {
auto menu = (Menu*)user;
log_write("[INSTALL] inside OnInstallStart()\n");
for (;;) {
mutexLock(&menu->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
if (menu->m_state != State::Progress) {
break;
}
if (menu->GetToken().stop_requested()) {
return false;
}
svcSleepThread(1e+6);
}
log_write("[INSTALL] OnInstallStart() got state: %u\n", (u8)menu->m_state);
if (menu->m_source) {
log_write("[INSTALL] OnInstallStart() we have source\n");
for (;;) {
mutexLock(&menu->m_source->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_source->m_mutex));
if (!IN_PUSH_THREAD) {
break;
}
if (menu->GetToken().stop_requested()) {
return false;
}
svcSleepThread(1e+6);
}
log_write("[INSTALL] OnInstallStart() stopped polling source\n");
}
log_write("[INSTALL] OnInstallStart() doing make_shared\n");
menu->m_source = std::make_shared<StreamFtp>(path, menu->GetToken());
mutexLock(&menu->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
menu->m_state = State::Connected;
log_write("[INSTALL] OnInstallStart() done make shared\n");
return true;
}
bool OnInstallWrite(void* user, const void* buf, size_t size) {
auto menu = (Menu*)user;
return menu->m_source->Push(buf, size);
}
void OnInstallClose(void* user) {
auto menu = (Menu*)user;
menu->m_source->Disable();
}
} // namespace
StreamFtp::StreamFtp(const fs::FsPath& path, std::stop_token token) {
m_path = path;
m_token = token;
m_buffer.reserve(MAX_BUFFER_SIZE);
m_active = true;
}
Result StreamFtp::ReadChunk(void* buf, s64 size, u64* bytes_read) {
while (!m_token.stop_requested()) {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (m_buffer.empty()) {
if (!m_active) {
break;
}
svcSleepThread(SLEEPNS);
} else {
size = std::min<s64>(size, m_buffer.size());
std::memcpy(buf, m_buffer.data(), size);
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size);
*bytes_read = size;
R_SUCCEED();
}
}
return 0x1;
}
bool StreamFtp::Push(const void* buf, s64 size) {
IN_PUSH_THREAD = true;
ON_SCOPE_EXIT(IN_PUSH_THREAD = false);
while (!m_token.stop_requested()) {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (!m_active) {
break;
}
if (m_buffer.size() + size >= MAX_BUFFER_SIZE) {
svcSleepThread(SLEEPNS);
} else {
const auto offset = m_buffer.size();
m_buffer.resize(offset + size);
std::memcpy(m_buffer.data() + offset, buf, size);
return true;
}
}
return false;
}
void StreamFtp::Disable() {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
m_active = false;
}
Menu::Menu() : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
mutexInit(&m_mutex);
ftpsrv::InitInstallMode(this, OnInstallStart, OnInstallWrite, OnInstallClose);
m_port = ftpsrv::GetPort();
m_anon = ftpsrv::IsAnon();
if (!m_anon) {
m_user = ftpsrv::GetUser();
m_pass = ftpsrv::GetPass();
}
}
Menu::~Menu() {
// signal for thread to exit and wait.
ftpsrv::DisableInstallMode();
m_stop_source.request_stop();
if (m_source) {
m_source->Disable();
}
log_write("closing data!!!!\n");
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
switch (m_state) {
case State::None:
break;
case State::Connected:
log_write("set to progress\n");
m_state = State::Progress;
log_write("got connection\n");
App::Push(std::make_shared<ui::ProgressBox>("Installing App"_i18n, [this](auto pbox) mutable -> bool {
log_write("inside progress box\n");
const auto rc = yati::InstallFromSource(pbox, m_source, m_source->m_path);
if (R_FAILED(rc)) {
m_source->Disable();
return false;
}
log_write("progress box is done\n");
return true;
}, [this](bool result){
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (result) {
App::Notify("Ftp install success!"_i18n);
m_state = State::Done;
} else {
App::Notify("Ftp install failed!"_i18n);
m_state = State::Failed;
}
}));
break;
case State::Progress:
case State::Done:
case State::Failed:
break;
}
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (m_ip) {
if (m_type == NifmInternetConnectionType_WiFi) {
SetSubHeading("Connection Type: WiFi | Strength: "_i18n + std::to_string(m_strength));
} else {
SetSubHeading("Connection Type: Ethernet"_i18n);
}
} else {
SetSubHeading("Connection Type: None"_i18n);
}
const float start_x = 80;
const float font_size = 22;
const float spacing = 33;
float start_y = 125;
float bounds[4];
nvgFontSize(vg, font_size);
// note: textbounds strips spaces...todo: use nvgTextGlyphPositions() instead.
#define draw(key, ...) \
gfx::textBounds(vg, start_x, start_y, bounds, key.c_str()); \
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT), key.c_str()); \
gfx::drawTextArgs(vg, bounds[2], start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED), __VA_ARGS__); \
start_y += spacing;
if (m_ip) {
draw("Host:"_i18n, " %u.%u.%u.%u", m_ip&0xFF, (m_ip>>8)&0xFF, (m_ip>>16)&0xFF, (m_ip>>24)&0xFF);
draw("Port:"_i18n, " %u", m_port);
if (!m_anon) {
draw("Username:"_i18n, " %s", m_user);
draw("Password:"_i18n, " %s", m_pass);
}
if (m_type == NifmInternetConnectionType_WiFi) {
NifmNetworkProfileData profile{};
if (R_SUCCEEDED(nifmGetCurrentNetworkProfile(&profile))) {
const auto& settings = profile.wireless_setting_data;
std::string passphrase;
std::transform(std::cbegin(settings.passphrase), std::cend(settings.passphrase), passphrase.begin(), toascii);
draw("SSID:"_i18n, " %.*s", settings.ssid_len, settings.ssid);
draw("Passphrase:"_i18n, " %s", passphrase.c_str());
}
}
}
#undef draw
switch (m_state) {
case State::None:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
break;
case State::Connected:
break;
case State::Progress:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Transferring data..."_i18n.c_str());
break;
case State::Done:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to exit..."_i18n.c_str());
break;
case State::Failed:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to install via FTP, press B to exit..."_i18n.c_str());
break;
}
}
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
}
} // namespace sphaira::ui::menu::ftp

View File

@@ -0,0 +1,187 @@
#include "ui/menus/gc_menu.hpp"
#include "yati/yati.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include <cstring>
namespace sphaira::ui::menu::gc {
namespace {
auto InRange(u64 off, u64 offset, u64 size) -> bool {
return off < offset + size && off >= offset;
}
struct GcSource final : yati::source::Base {
GcSource(const yati::container::Collections& collections, fs::FsNativeGameCard* fs);
~GcSource();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read);
const yati::container::Collections& m_collections;
fs::FsNativeGameCard* m_fs{};
FsFile m_file{};
s64 m_offset{};
s64 m_size{};
};
GcSource::GcSource(const yati::container::Collections& collections, fs::FsNativeGameCard* fs)
: m_collections{collections}
, m_fs{fs} {
m_offset = -1;
}
GcSource::~GcSource() {
fsFileClose(&m_file);
}
Result GcSource::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
// check is we need to open a new file.
if (!InRange(off, m_offset, m_size)) {
fsFileClose(&m_file);
m_file = {};
// find new file based on the offset.
bool found = false;
for (auto& collection : m_collections) {
if (InRange(off, collection.offset, collection.size)) {
found = true;
m_offset = collection.offset;
m_size = collection.size;
R_TRY(m_fs->OpenFile(fs::AppendPath("/", collection.name), FsOpenMode_Read, &m_file));
break;
}
}
// this will never fail, unless i break something in yati.
R_UNLESS(found, 0x1);
}
return fsFileRead(&m_file, off - m_offset, buf, size, 0, bytes_read);
}
} // namespace
Menu::Menu() : MenuBase{"GameCard"_i18n} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
SetAction(Button::X, Action{"Refresh"_i18n, [this](){
m_state = State::None;
}});
fsOpenDeviceOperator(std::addressof(m_dev_op));
}
Menu::~Menu() {
// manually close this as it needs(?) to be closed before dev_op.
m_fs.reset();
fsDeviceOperatorClose(std::addressof(m_dev_op));
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
switch (m_state) {
case State::None: {
bool gc_inserted;
if (R_FAILED(fsDeviceOperatorIsGameCardInserted(std::addressof(m_dev_op), std::addressof(gc_inserted)))) {
m_state = State::Failed;
} else {
if (!gc_inserted) {
m_state = State::NotFound;
} else {
if (R_FAILED(ScanGamecard())) {
m_state = State::Failed;
}
}
}
} break;
case State::Progress:
case State::Done:
case State::NotFound:
case State::Failed:
break;
}
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
switch (m_state) {
case State::None:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
break;
case State::Progress:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Transferring data..."_i18n.c_str());
break;
case State::NotFound:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "No GameCard inserted, press X to refresh"_i18n.c_str());
break;
case State::Done:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Installed GameCard, press B to exit..."_i18n.c_str());
break;
case State::Failed:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to scan GameCard..."_i18n.c_str());
break;
}
}
Result Menu::ScanGamecard() {
m_state = State::None;
m_fs.reset();
m_collections.clear();
FsGameCardHandle gc_handle;
R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(gc_handle)));
m_fs = std::make_unique<fs::FsNativeGameCard>(std::addressof(gc_handle), FsGameCardPartition_Secure, false);
R_TRY(m_fs->GetFsOpenResult());
FsDir dir;
R_TRY(m_fs->OpenDirectory("/", FsDirOpenMode_ReadFiles, std::addressof(dir)));
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
s64 count;
R_TRY(m_fs->DirGetEntryCount(std::addressof(dir), std::addressof(count)));
std::vector<FsDirectoryEntry> buf(count);
s64 total_entries;
R_TRY(m_fs->DirRead(std::addressof(dir), std::addressof(total_entries), buf.size(), buf.data()));
m_collections.reserve(total_entries);
s64 offset{};
for (s64 i = 0; i < total_entries; i++) {
yati::container::CollectionEntry entry{};
entry.name = buf[i].name;
entry.offset = offset;
entry.size = buf[i].file_size;
m_collections.emplace_back(entry);
offset += buf[i].file_size;
}
m_state = State::Progress;
App::Push(std::make_shared<ui::ProgressBox>("Installing App"_i18n, [this](auto pbox) mutable -> bool {
auto source = std::make_shared<GcSource>(m_collections, m_fs.get());
return R_SUCCEEDED(yati::InstallFromCollections(pbox, source, m_collections));
}, [this](bool result){
if (result) {
App::Notify("Gc install success!"_i18n);
m_state = State::Done;
} else {
App::Notify("Gc install failed!"_i18n);
m_state = State::Failed;
}
}));
R_SUCCEED();
}
} // namespace sphaira::ui::menu::gc

View File

@@ -2,6 +2,9 @@
#include "ui/menus/irs_menu.hpp"
#include "ui/menus/themezer.hpp"
#include "ui/menus/ghdl.hpp"
#include "ui/menus/usb_menu.hpp"
#include "ui/menus/ftp_menu.hpp"
#include "ui/menus/gc_menu.hpp"
#include "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
@@ -246,10 +249,6 @@ MainMenu::MainMenu() {
App::SetTheme(index_out);
}, App::GetThemeIndex()));
options->Add(std::make_shared<SidebarEntryBool>("Shuffle"_i18n, App::GetThemeShuffleEnable(), [this](bool& enable){
App::SetThemeShuffleEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [this](bool& enable){
App::SetThemeMusicEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
@@ -321,16 +320,28 @@ MainMenu::MainMenu() {
WebShow("https://lite.duckduckgo.com/lite");
}));
}
if (App::GetApp()->m_install.Get()) {
if (App::GetFtpEnable()) {
options->Add(std::make_shared<SidebarEntryCallback>("Ftp Install"_i18n, [](){
App::Push(std::make_shared<menu::ftp::Menu>());
}));
}
options->Add(std::make_shared<SidebarEntryCallback>("Usb Install"_i18n, [](){
App::Push(std::make_shared<menu::usb::Menu>());
}));
options->Add(std::make_shared<SidebarEntryCallback>("GameCard Install"_i18n, [](){
App::Push(std::make_shared<menu::gc::Menu>());
}));
}
}));
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
SidebarEntryArray::Items install_items;
install_items.push_back("System memory"_i18n);
install_items.push_back("microSD card"_i18n);
SidebarEntryArray::Items text_scroll_speed_items;
text_scroll_speed_items.push_back("Slow"_i18n);
text_scroll_speed_items.push_back("Normal"_i18n);
@@ -344,21 +355,90 @@ MainMenu::MainMenu() {
App::SetReplaceHbmenuEnable(enable);
}, "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){
App::SetTextScrollSpeed(index_out);
}, (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>("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));
}));
}));
}})
);

View File

@@ -18,17 +18,18 @@ MenuBase::~MenuBase() {
void MenuBase::Update(Controller* controller, TouchInfo* touch) {
Widget::Update(controller, touch);
// update every second.
if (m_poll_timestamp.GetSeconds() >= 1) {
UpdateVars();
}
}
void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
Widget::Draw(vg, theme);
// update every second, do this in Draw because Update() isn't called if it
// doesn't have focus.
if (m_poll_timestamp.GetSeconds() >= 1) {
UpdateVars();
}
const float start_y = 70;
const float font_size = 22;
const float spacing = 30;

View File

@@ -365,7 +365,13 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
SetAction(Button::B, Action{"Back"_i18n, [this]{
SetPop();
// if search is valid, then we are in search mode, return back to normal.
if (!m_search.empty()) {
m_search.clear();
InvalidateAllPages();
} else {
SetPop();
}
}});
this->SetActions(
@@ -480,6 +486,8 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out)) && !out.empty()) {
m_search = out;
// PackListDownload();
InvalidateAllPages();
}
}));
}}),
@@ -652,11 +660,9 @@ void Menu::OnFocusGained() {
}
void Menu::InvalidateAllPages() {
for (auto& e : m_pages) {
e.m_packList.clear();
e.m_ready = PageLoadState::None;
}
m_pages.clear();
m_pages.resize(1);
m_page_index = 0;
PackListDownload();
}

View File

@@ -0,0 +1,177 @@
#include "ui/menus/usb_menu.hpp"
#include "yati/yati.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include <cstring>
namespace sphaira::ui::menu::usb {
namespace {
constexpr u64 CONNECTION_TIMEOUT = 1e+9 * 3;
constexpr u64 TRANSFER_TIMEOUT = 1e+9 * 5;
void thread_func(void* user) {
auto app = static_cast<Menu*>(user);
for (;;) {
if (app->GetToken().stop_requested()) {
break;
}
const auto rc = app->m_usb_source->WaitForConnection(CONNECTION_TIMEOUT, app->m_usb_speed, app->m_usb_count);
mutexLock(&app->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&app->m_mutex));
if (R_SUCCEEDED(rc)) {
app->m_state = State::Connected;
break;
} else if (R_FAILED(rc) && R_VALUE(rc) != 0xEA01) {
log_write("got: 0x%X value: 0x%X\n", rc, R_VALUE(rc));
app->m_state = State::Failed;
break;
}
}
}
} // namespace
Menu::Menu() : MenuBase{"USB"_i18n} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
// if mtp is enabled, disable it for now.
m_was_mtp_enabled = App::GetMtpEnable();
if (m_was_mtp_enabled) {
App::Notify("Disable MTP for usb install"_i18n);
App::SetMtpEnable(false);
}
// 3 second timeout for transfers.
m_usb_source = std::make_shared<yati::source::Usb>(TRANSFER_TIMEOUT);
if (R_FAILED(m_usb_source->GetOpenResult())) {
log_write("usb init open\n");
m_state = State::Failed;
} else {
if (R_FAILED(m_usb_source->Init())) {
log_write("usb init failed\n");
m_state = State::Failed;
}
}
mutexInit(&m_mutex);
if (m_state != State::Failed) {
threadCreate(&m_thread, thread_func, this, nullptr, 1024*32, 0x2C, 1);
threadStart(&m_thread);
}
}
Menu::~Menu() {
// signal for thread to exit and wait.
m_stop_source.request_stop();
threadWaitForExit(&m_thread);
threadClose(&m_thread);
// free usb source before re-enabling mtp.
log_write("closing data!!!!\n");
m_usb_source.reset();
if (m_was_mtp_enabled) {
App::Notify("Re-enabled MTP"_i18n);
App::SetMtpEnable(true);
}
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
switch (m_state) {
case State::None:
break;
case State::Connected:
log_write("set to progress\n");
m_state = State::Progress;
log_write("got connection\n");
App::Push(std::make_shared<ui::ProgressBox>("Installing App"_i18n, [this](auto pbox) mutable -> bool {
log_write("inside progress box\n");
for (u32 i = 0; i < m_usb_count; i++) {
std::string file_name;
u64 file_size;
if (R_FAILED(m_usb_source->GetFileInfo(file_name, file_size))) {
return false;
}
log_write("got file name: %s size: %lX\n", file_name.c_str(), file_size);
const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name);
if (R_FAILED(rc)) {
return false;
}
App::Notify("Installed via usb"_i18n);
m_usb_source->Finished();
}
return true;
}, [this](bool result){
if (result) {
App::Notify("Usb install success!"_i18n);
m_state = State::Done;
} else {
App::Notify("Usb install failed!"_i18n);
m_state = State::Failed;
}
}));
break;
case State::Progress:
break;
case State::Done:
break;
case State::Failed:
break;
}
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
switch (m_state) {
case State::None:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
break;
case State::Connected:
break;
case State::Progress:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Transferring data..."_i18n.c_str());
break;
case State::Done:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to exit..."_i18n.c_str());
break;
case State::Failed:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to init usb, press B to exit..."_i18n.c_str());
break;
}
}
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
}
} // namespace sphaira::ui::menu::usb

View File

@@ -102,6 +102,14 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
}
}
auto ProgressBox::SetTitle(const std::string& title) -> ProgressBox& {
mutexLock(&m_mutex);
m_title = title;
mutexUnlock(&m_mutex);
Yield();
return *this;
}
auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
mutexLock(&m_mutex);
m_transfer = transfer;

View File

@@ -0,0 +1,59 @@
#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::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

View File

@@ -0,0 +1,87 @@
#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::GetCollections(Collections& out) {
Hfs0 root{};
R_TRY(Hfs0GetPartition(m_source.get(), 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.get(), 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

View 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) {
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) {
}
R_SUCCEED();
}
} // namespace sphaira::es

View 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

View 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

View 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

View 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

View 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;
}

View 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

View File

@@ -0,0 +1,28 @@
#include "yati/source/stdio.hpp"
namespace sphaira::yati::source {
Stdio::Stdio(const fs::FsPath& 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

View File

@@ -0,0 +1,41 @@
#include "yati/source/stream.hpp"
#include "defines.hpp"
#include "log.hpp"
namespace sphaira::yati::source {
Result Stream::Read(void* _buf, s64 off, s64 size, u64* bytes_read_out) {
// streams don't allow for random access (seeking backwards).
R_UNLESS(off >= m_offset, 0x1);
auto buf = static_cast<u8*>(_buf);
*bytes_read_out = 0;
// check if we already have some data in the buffer.
while (size) {
// while it is invalid to seek backwards, it is valid to seek forwards.
// this can be done to skip padding, skip undeeded files etc.
// to handle this, simply read the data into a buffer and discard it.
if (off > m_offset) {
const auto skip_size = off - m_offset;
std::vector<u8> temp_buf(skip_size);
u64 bytes_read;
R_TRY(ReadChunk(temp_buf.data(), temp_buf.size(), &bytes_read));
m_offset += bytes_read;
} else {
u64 bytes_read;
R_TRY(ReadChunk(buf, size, &bytes_read));
*bytes_read_out += bytes_read;
buf += bytes_read;
off += bytes_read;
m_offset += bytes_read;
size -= bytes_read;
}
}
R_SUCCEED();
}
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,23 @@
#include "yati/source/stream_file.hpp"
#include "log.hpp"
namespace sphaira::yati::source {
StreamFile::StreamFile(FsFileSystem* fs, const fs::FsPath& path) {
m_open_result = fsFsOpenFile(fs, path, FsOpenMode_Read, std::addressof(m_file));
}
StreamFile::~StreamFile() {
if (R_SUCCEEDED(GetOpenResult())) {
fsFileClose(std::addressof(m_file));
}
}
Result StreamFile::ReadChunk(void* buf, s64 size, u64* bytes_read) {
R_TRY(GetOpenResult());
const auto rc = fsFileRead(std::addressof(m_file), m_offset, buf, size, 0, bytes_read);
m_offset += *bytes_read;
return rc;
}
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,383 @@
/*
* Copyright (c) Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Most of the usb transfer code was taken from Haze.
#include "yati/source/usb.hpp"
#include "log.hpp"
namespace sphaira::yati::source {
namespace {
constexpr u32 MAGIC = 0x53504841;
constexpr u32 VERSION = 2;
struct SendHeader {
u32 magic;
u32 version;
};
struct RecvHeader {
u32 magic;
u32 version;
u32 bcdUSB;
u32 count;
};
} // namespace
Usb::Usb(u64 transfer_timeout) {
m_open_result = usbDsInitialize();
m_transfer_timeout = transfer_timeout;
}
Usb::~Usb() {
if (R_SUCCEEDED(GetOpenResult())) {
usbDsExit();
}
}
Result Usb::Init() {
log_write("doing USB init\n");
R_TRY(m_open_result);
u8 iManufacturer, iProduct, iSerialNumber;
static const u16 supported_langs[1] = {0x0409};
// Send language descriptor
R_TRY(usbDsAddUsbLanguageStringDescriptor(NULL, supported_langs, sizeof(supported_langs)/sizeof(u16)));
// Send manufacturer
R_TRY(usbDsAddUsbStringDescriptor(&iManufacturer, "Nintendo"));
// Send product
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
// Send serial number
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, "SerialNumber"));
// Send device descriptors
struct usb_device_descriptor device_descriptor = {
.bLength = USB_DT_DEVICE_SIZE,
.bDescriptorType = USB_DT_DEVICE,
.bcdUSB = 0x0110,
.bDeviceClass = 0x00,
.bDeviceSubClass = 0x00,
.bDeviceProtocol = 0x00,
.bMaxPacketSize0 = 0x40,
.idVendor = 0x057e,
.idProduct = 0x3000,
.bcdDevice = 0x0100,
.iManufacturer = iManufacturer,
.iProduct = iProduct,
.iSerialNumber = iSerialNumber,
.bNumConfigurations = 0x01
};
// Full Speed is USB 1.1
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Full, &device_descriptor));
// High Speed is USB 2.0
device_descriptor.bcdUSB = 0x0200;
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_High, &device_descriptor));
// Super Speed is USB 3.0
device_descriptor.bcdUSB = 0x0300;
// Upgrade packet size to 512
device_descriptor.bMaxPacketSize0 = 0x09;
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Super, &device_descriptor));
// Define Binary Object Store
const u8 bos[0x16] = {
0x05, // .bLength
USB_DT_BOS, // .bDescriptorType
0x16, 0x00, // .wTotalLength
0x02, // .bNumDeviceCaps
// USB 2.0
0x07, // .bLength
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
0x02, // .bDevCapabilityType
0x02, 0x00, 0x00, 0x00, // dev_capability_data
// USB 3.0
0x0A, // .bLength
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
0x03, /* .bDevCapabilityType */
0x00, /* .bmAttributes */
0x0E, 0x00, /* .wSpeedSupported */
0x03, /* .bFunctionalitySupport */
0x00, /* .bU1DevExitLat */
0x00, 0x00 /* .bU2DevExitLat */
};
R_TRY(usbDsSetBinaryObjectStore(bos, sizeof(bos)));
struct usb_interface_descriptor interface_descriptor = {
.bLength = USB_DT_INTERFACE_SIZE,
.bDescriptorType = USB_DT_INTERFACE,
.bInterfaceNumber = USBDS_DEFAULT_InterfaceNumber, // set below
.bNumEndpoints = static_cast<u8>(std::size(m_endpoints)),
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
};
struct usb_endpoint_descriptor endpoint_descriptor_in = {
.bLength = USB_DT_ENDPOINT_SIZE,
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = USB_ENDPOINT_IN,
.bmAttributes = USB_TRANSFER_TYPE_BULK,
.wMaxPacketSize = 0x40,
};
struct usb_endpoint_descriptor endpoint_descriptor_out = {
.bLength = USB_DT_ENDPOINT_SIZE,
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = USB_ENDPOINT_OUT,
.bmAttributes = USB_TRANSFER_TYPE_BULK,
.wMaxPacketSize = 0x40,
};
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
.bLength = sizeof(struct usb_ss_endpoint_companion_descriptor),
.bDescriptorType = USB_DT_SS_ENDPOINT_COMPANION,
.bMaxBurst = 0x0F,
.bmAttributes = 0x00,
.wBytesPerInterval = 0x00,
};
R_TRY(usbDsRegisterInterface(&m_interface));
interface_descriptor.bInterfaceNumber = m_interface->interface_index;
endpoint_descriptor_in.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
// Full Speed Config
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
// High Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x200;
endpoint_descriptor_out.wMaxPacketSize = 0x200;
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
// Super Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x400;
endpoint_descriptor_out.wMaxPacketSize = 0x400;
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
//Setup endpoints.
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_In], endpoint_descriptor_in.bEndpointAddress));
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_Out], endpoint_descriptor_out.bEndpointAddress));
R_TRY(usbDsInterface_EnableInterface(m_interface));
R_TRY(usbDsEnable());
log_write("success USB init\n");
R_SUCCEED();
}
Result Usb::WaitForConnection(u64 timeout, u32& speed, u32& count) {
const SendHeader send_header{
.magic = MAGIC,
.version = VERSION,
};
alignas(0x1000) u8 aligned[0x1000]{};
std::memcpy(aligned, std::addressof(send_header), sizeof(send_header));
// send header.
u32 transferredSize;
R_TRY(TransferPacketImpl(false, aligned, sizeof(send_header), &transferredSize, timeout));
// receive header.
struct RecvHeader recv_header{};
R_TRY(TransferPacketImpl(true, aligned, sizeof(recv_header), &transferredSize, timeout));
// copy data into header struct.
std::memcpy(&recv_header, aligned, sizeof(recv_header));
// validate received header.
R_UNLESS(recv_header.magic == MAGIC, Result_BadMagic);
R_UNLESS(recv_header.version == VERSION, Result_BadVersion);
R_UNLESS(recv_header.count > 0, Result_BadCount);
count = recv_header.count;
speed = recv_header.bcdUSB;
R_SUCCEED();
}
Result Usb::GetFileInfo(std::string& name_out, u64& size_out) {
struct {
u64 size;
u64 name_length;
} file_info_meta;
alignas(0x1000) u8 aligned[0x1000]{};
// receive meta.
u32 transferredSize;
R_TRY(TransferPacketImpl(true, aligned, sizeof(file_info_meta), &transferredSize, m_transfer_timeout));
std::memcpy(&file_info_meta, aligned, sizeof(file_info_meta));
R_UNLESS(file_info_meta.name_length < sizeof(aligned), 0x1);
R_TRY(TransferPacketImpl(true, aligned, file_info_meta.name_length, &transferredSize, m_transfer_timeout));
name_out.resize(file_info_meta.name_length);
std::memcpy(name_out.data(), aligned, name_out.size());
size_out = file_info_meta.size;
R_SUCCEED();
}
bool Usb::GetConfigured() const {
UsbState usb_state;
usbDsGetState(std::addressof(usb_state));
return usb_state == UsbState_Configured;
}
Event *Usb::GetCompletionEvent(UsbSessionEndpoint ep) const {
return std::addressof(m_endpoints[ep]->CompletionEvent);
}
Result Usb::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const {
auto event = GetCompletionEvent(ep);
const auto rc = eventWait(event, timeout);
if (R_FAILED(rc)) {
R_TRY(usbDsEndpoint_Cancel(m_endpoints[ep]));
eventClear(event);
}
return rc;
}
Result Usb::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const {
return usbDsEndpoint_PostBufferAsync(m_endpoints[ep], buffer, size, out_urb_id);
}
Result Usb::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const {
UsbDsReportData report_data;
R_TRY(eventClear(std::addressof(m_endpoints[ep]->CompletionEvent)));
R_TRY(usbDsEndpoint_GetReportData(m_endpoints[ep], std::addressof(report_data)));
R_TRY(usbDsParseReportData(std::addressof(report_data), urb_id, out_requested_size, out_transferred_size));
R_SUCCEED();
}
Result Usb::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) const {
u32 urb_id;
/* If we're not configured yet, wait to become configured first. */
// R_TRY(usbDsWaitReady(timeout));
if (!GetConfigured()) {
R_TRY(eventWait(usbDsGetStateChangeEvent(), timeout));
R_TRY(eventClear(usbDsGetStateChangeEvent()));
R_THROW(0xEA01);
}
/* Select the appropriate endpoint and begin a transfer. */
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
R_TRY(TransferAsync(ep, page, size, std::addressof(urb_id)));
/* Try to wait for the event. */
R_TRY(WaitTransferCompletion(ep, timeout));
/* Return what we transferred. */
return GetTransferResult(ep, urb_id, nullptr, out_size_transferred);
}
Result Usb::SendCommand(s64 off, s64 size) const {
struct {
u32 hash;
u32 magic;
s64 off;
s64 size;
} meta{0, 0, off, size};
alignas(0x1000) static u8 aligned[0x1000]{};
std::memcpy(aligned, std::addressof(meta), sizeof(meta));
u32 transferredSize;
return TransferPacketImpl(false, aligned, sizeof(meta), &transferredSize, m_transfer_timeout);
}
Result Usb::Finished() const {
return SendCommand(0, 0);
}
Result Usb::InternalRead(void* _buf, s64 off, s64 size) const {
u8* buf = (u8*)_buf;
alignas(0x1000) u8 aligned[0x1000]{};
const auto stored_size = size;
s64 total = 0;
while (size) {
auto read_size = size;
auto read_buf = buf;
if (u64(buf) & 0xFFF) {
read_size = std::min<u64>(size, sizeof(aligned) - (u64(buf) & 0xFFF));
read_buf = aligned;
log_write("unaligned read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
} else if (read_size & 0xFFF) {
if (read_size <= 0xFFF) {
log_write("unaligned small read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
read_buf = aligned;
} else {
log_write("unaligned big read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
// read as much as possible into buffer, the rest will
// be handled in a second read which will be aligned size aligned.
read_size = read_size & ~0xFFF;
}
}
R_TRY(SendCommand(off, read_size));
u32 transferredSize{};
R_TRY(TransferPacketImpl(true, read_buf, read_size, &transferredSize, m_transfer_timeout));
R_UNLESS(transferredSize <= read_size, Result_BadTransferSize);
if (read_buf == aligned) {
std::memcpy(buf, aligned, transferredSize);
}
if (transferredSize < read_size) {
log_write("reading less than expected! %u vs %zd stored: %zd\n", transferredSize, read_size, stored_size);
}
off += transferredSize;
buf += transferredSize;
size -= transferredSize;
total += transferredSize;
}
R_UNLESS(total == stored_size, Result_BadTotalSize);
R_SUCCEED();
}
Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
R_TRY(GetOpenResult());
R_TRY(InternalRead(buf, off, size));
*bytes_read = size;
R_SUCCEED();
}
} // namespace sphaira::yati::source

1454
sphaira/source/yati/yati.cpp Normal file

File diff suppressed because it is too large Load Diff

1
tools/requirements.txt Normal file
View File

@@ -0,0 +1 @@
pyusb

141
tools/usb_total.py Normal file
View File

@@ -0,0 +1,141 @@
# based on usb.py from Tinfoil, by Adubbz.
import struct
import sys
import os
import usb.core
import usb.util
import time
import glob
from pathlib import Path
# magic number (SPHA) for the script and switch.
MAGIC = 0x53504841
# version of the usb script.
VERSION = 2
# list of supported extensions.
EXTS = (".nsp", ".xci", ".nsz", ".xcz")
def verify_switch(bcdUSB, count, in_ep, out_ep):
header = in_ep.read(8, timeout=0)
switch_magic = struct.unpack('<I', header[0:4])[0]
switch_version = struct.unpack('<I', header[4:8])[0]
if switch_magic != MAGIC:
raise Exception("Unexpected magic {}".format(switch_magic))
if switch_version != VERSION:
raise Exception("Unexpected version {}".format(switch_version))
send_data = struct.pack('<IIII', MAGIC, VERSION, bcdUSB, count)
out_ep.write(data=send_data, timeout=0)
def send_file_info(path, in_ep, out_ep):
file_name = Path(path).name
file_size = Path(path).stat().st_size
file_name_len = len(file_name)
send_data = struct.pack('<QQ', file_size, file_name_len)
out_ep.write(data=send_data, timeout=0)
out_ep.write(data=file_name, timeout=0)
def wait_for_input(path, in_ep, out_ep):
buf = None
predicted_off = 0
print("now waiting for intput\n")
with open(path, "rb") as file:
while True:
header = in_ep.read(24, timeout=0)
range_offset = struct.unpack('<Q', header[8:16])[0]
range_size = struct.unpack('<Q', header[16:24])[0]
if (range_offset == 0 and range_size == 0):
break
if (buf != None and range_offset == predicted_off and range_size == len(buf)):
# print("predicted the read off {} size {}".format(predicted_off, len(buf)))
pass
else:
file.seek(range_offset)
buf = file.read(range_size)
if (len(buf) != range_size):
# print("off: {} size: {}".format(range_offset, range_size))
raise ValueError('bad buf size!!!!!')
result = out_ep.write(data=buf, timeout=0)
if (len(buf) != result):
print("off: {} size: {}".format(range_offset, range_size))
raise ValueError('bad result!!!!!')
predicted_off = range_offset + range_size
buf = file.read(range_size)
if __name__ == '__main__':
print("hello world")
# check which mode the user has selected.
args = len(sys.argv)
if (args != 2):
print("either run python usb_total.py game.nsp OR drag and drop the game onto the python file (if python is in your path)")
sys.exit(1)
path = sys.argv[1]
files = []
if os.path.isfile(path) and path.endswith(EXTS):
files.append(path)
elif os.path.isdir(path):
for f in glob.glob(path + "/**/*.*", recursive=True):
if os.path.isfile(f) and f.endswith(EXTS):
files.append(f)
else:
raise ValueError('must be a file!')
# for file in files:
# print("found file: {}".format(file))
# Find the switch
print("waiting for switch...\n")
dev = None
while (dev is None):
dev = usb.core.find(idVendor=0x057E, idProduct=0x3000)
time.sleep(0.5)
print("found the switch!\n")
cfg = None
try:
cfg = dev.get_active_configuration()
print("found active config")
except usb.core.USBError:
print("no currently active config")
cfg = None
if cfg is None:
dev.set_configuration()
cfg = dev.get_active_configuration()
is_out_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_OUT
is_in_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN
out_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_out_ep)
in_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_in_ep)
assert out_ep is not None
assert in_ep is not None
print("iManufacturer: {} iProduct: {} iSerialNumber: {}".format(dev.manufacturer, dev.product, dev.serial_number))
print("bcdUSB: {} bMaxPacketSize0: {}".format(hex(dev.bcdUSB), dev.bMaxPacketSize0))
try:
verify_switch(dev.bcdUSB, len(files), in_ep, out_ep)
for file in files:
print("installing file: {}".format(file))
send_file_info(file, in_ep, out_ep)
wait_for_input(file, in_ep, out_ep)
dev.reset()
except Exception as inst:
print("An exception occurred " + str(inst))