add basic support for title installing

This commit is contained in:
ITotalJustice
2025-04-20 14:12:12 +01:00
parent 651d9fa495
commit 89e82927ee
40 changed files with 3661 additions and 316 deletions

View File

@@ -1008,6 +1008,10 @@ void App::Draw() {
this->queue.presentImage(this->swapchain, slot);
}
auto App::GetApp() -> App* {
return g_app;
}
auto App::GetVg() -> NVGcontext* {
return g_app->vg;
}

View File

@@ -132,9 +132,9 @@ void log_file_write(const char* msg) {
}
void log_file_fwrite(const char* fmt, ...) {
std::va_list v{};
va_list v{};
va_start(v, fmt);
log_write_arg(fmt, v);
log_write_arg(fmt, &v);
va_end(v);
}

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

@@ -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

@@ -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,6 +54,10 @@ constexpr std::string_view VIDEO_EXTENSIONS[] = {
constexpr std::string_view IMAGE_EXTENSIONS[] = {
"png", "jpg", "jpeg", "bmp", "gif",
};
constexpr std::string_view INSTALL_EXTENSIONS[] = {
"nsp", "xci", "nsz", "xcz",
};
struct RomDatabaseEntry {
std::string_view folder;
@@ -294,6 +300,8 @@ Menu::Menu(const std::vector<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 +414,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 +429,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 +467,32 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
}));
}
// if install is enabled, check if all currently selected files are installable.
if (m_entries_current.size() && App::GetInstallEnable()) {
bool should_install = true;
if (!m_selected_count) {
should_install = IsExtension(GetEntry().GetExtension(), INSTALL_EXTENSIONS);
} else {
const auto entries = GetSelectedEntries();
for (auto&e : entries) {
if (!IsExtension(e.GetExtension(), INSTALL_EXTENSIONS)) {
should_install = false;
break;
}
}
}
if (should_install) {
options->Add(std::make_shared<SidebarEntryCallback>("Install"_i18n, [this](){
if (!m_selected_count) {
InstallFile(GetEntry());
} else {
InstallFiles(GetSelectedEntries());
}
}));
}
}
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
@@ -628,6 +662,9 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
icon = ThemeEntryID_ICON_VIDEO;
} else if (IsExtension(ext, IMAGE_EXTENSIONS)) {
icon = ThemeEntryID_ICON_IMAGE;
} else if (IsExtension(ext, INSTALL_EXTENSIONS)) {
// todo: maybe replace this icon with something else?
icon = ThemeEntryID_ICON_NRO;
} else if (IsExtension(ext, "zip")) {
icon = ThemeEntryID_ICON_ZIP;
} else if (IsExtension(ext, "nro")) {
@@ -775,6 +812,34 @@ void Menu::InstallForwarder() {
));
}
void Menu::InstallFile(const FileEntry& target) {
std::vector<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()) {

View File

@@ -323,10 +323,6 @@ MainMenu::MainMenu() {
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);
@@ -340,21 +336,94 @@ 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>("Patch ticket"_i18n, App::GetApp()->m_patch_ticket.Get(), [this](bool& enable){
App::GetApp()->m_patch_ticket.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [this](bool& enable){
App::GetApp()->m_skip_base.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip Patch"_i18n, App::GetApp()->m_skip_patch.Get(), [this](bool& enable){
App::GetApp()->m_skip_patch.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip addon"_i18n, App::GetApp()->m_skip_addon.Get(), [this](bool& enable){
App::GetApp()->m_skip_addon.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [this](bool& enable){
App::GetApp()->m_skip_data_patch.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [this](bool& enable){
App::GetApp()->m_skip_ticket.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [this](bool& enable){
App::GetApp()->m_skip_nca_hash_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [this](bool& enable){
App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [this](bool& enable){
App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [this](bool& enable){
App::GetApp()->m_ignore_distribution_bit.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [this](bool& enable){
App::GetApp()->m_convert_to_standard_crypto.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [this](bool& enable){
App::GetApp()->m_lower_master_key.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [this](bool& enable){
App::GetApp()->m_lower_system_version.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
}));
}})
);

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,67 @@
#include "yati/container/nsp.hpp"
#include "defines.hpp"
#include "log.hpp"
#include <memory>
namespace sphaira::yati::container {
namespace {
#define PFS0_MAGIC 0x30534650
struct Pfs0Header {
u32 magic;
u32 total_files;
u32 string_table_size;
u32 padding;
};
struct Pfs0FileTableEntry {
u64 data_offset;
u64 data_size;
u32 name_offset;
u32 padding;
};
} // namespace
Result Nsp::Validate(source::Base* source) {
u32 magic;
u64 bytes_read;
R_TRY(source->Read(std::addressof(magic), 0, sizeof(magic), std::addressof(bytes_read)));
R_UNLESS(magic == PFS0_MAGIC, 0x1);
R_SUCCEED();
}
Result Nsp::GetCollections(Collections& out) {
u64 bytes_read;
s64 off = 0;
// get header
Pfs0Header header{};
R_TRY(m_source->Read(std::addressof(header), off, sizeof(header), std::addressof(bytes_read)));
R_UNLESS(header.magic == PFS0_MAGIC, 0x1);
off += bytes_read;
// get file table
std::vector<Pfs0FileTableEntry> file_table(header.total_files);
R_TRY(m_source->Read(file_table.data(), off, file_table.size() * sizeof(Pfs0FileTableEntry), std::addressof(bytes_read)))
off += bytes_read;
// get string table
std::vector<char> string_table(header.string_table_size);
R_TRY(m_source->Read(string_table.data(), off, string_table.size(), std::addressof(bytes_read)))
off += bytes_read;
out.reserve(header.total_files);
for (u32 i = 0; i < header.total_files; i++) {
CollectionEntry entry;
entry.name = string_table.data() + file_table[i].name_offset;
entry.offset = off + file_table[i].data_offset;
entry.size = file_table[i].data_size;
out.emplace_back(entry);
}
R_SUCCEED();
}
} // namespace sphaira::yati::container

View File

@@ -0,0 +1,95 @@
#include "yati/container/xci.hpp"
#include "defines.hpp"
#include "log.hpp"
namespace sphaira::yati::container {
namespace {
#define XCI_MAGIC std::byteswap(0x48454144)
#define HFS0_MAGIC 0x30534648
#define HFS0_HEADER_OFFSET 0xF000
struct Hfs0Header {
u32 magic;
u32 total_files;
u32 string_table_size;
u32 padding;
};
struct Hfs0FileTableEntry {
u64 data_offset;
u64 data_size;
u32 name_offset;
u32 hash_size;
u64 padding;
u8 hash[0x20];
};
struct Hfs0 {
Hfs0Header header{};
std::vector<Hfs0FileTableEntry> file_table{};
std::vector<std::string> string_table{};
s64 data_offset{};
};
Result Hfs0GetPartition(source::Base* source, s64 off, Hfs0& out) {
u64 bytes_read;
// get header
R_TRY(source->Read(std::addressof(out.header), off, sizeof(out.header), std::addressof(bytes_read)));
R_UNLESS(out.header.magic == HFS0_MAGIC, 0x1);
off += bytes_read;
// get file table
out.file_table.resize(out.header.total_files);
R_TRY(source->Read(out.file_table.data(), off, out.file_table.size() * sizeof(Hfs0FileTableEntry), std::addressof(bytes_read)))
off += bytes_read;
// get string table
std::vector<char> string_table(out.header.string_table_size);
R_TRY(source->Read(string_table.data(), off, string_table.size(), std::addressof(bytes_read)))
off += bytes_read;
for (u32 i = 0; i < out.header.total_files; i++) {
out.string_table.emplace_back(string_table.data() + out.file_table[i].name_offset);
}
out.data_offset = off;
R_SUCCEED();
}
} // namespace
Result Xci::Validate(source::Base* source) {
u32 magic;
u64 bytes_read;
R_TRY(source->Read(std::addressof(magic), 0x100, sizeof(magic), std::addressof(bytes_read)));
R_UNLESS(magic == XCI_MAGIC, 0x1);
R_SUCCEED();
}
Result Xci::GetCollections(Collections& out) {
Hfs0 root{};
R_TRY(Hfs0GetPartition(m_source, HFS0_HEADER_OFFSET, root));
for (u32 i = 0; i < root.header.total_files; i++) {
if (root.string_table[i] == "secure") {
Hfs0 secure{};
R_TRY(Hfs0GetPartition(m_source, root.data_offset + root.file_table[i].data_offset, secure));
for (u32 i = 0; i < secure.header.total_files; i++) {
CollectionEntry entry;
entry.name = secure.string_table[i];
entry.offset = secure.data_offset + secure.file_table[i].data_offset;
entry.size = secure.file_table[i].data_size;
out.emplace_back(entry);
}
R_SUCCEED();
}
}
return 0x1;
}
} // namespace sphaira::yati::container

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, bool convert_personalised) {
TicketData data;
R_TRY(GetTicketData(ticket, &data));
if (data.title_key_type == es::TicketTitleKeyType_Common) {
// todo: verify common signature
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized && convert_personalised) {
}
R_SUCCEED();
}
} // namespace sphaira::es

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 char* path) {
m_file = std::fopen(path, "rb");
if (!m_file) {
m_open_result = fsdevGetLastResult();
}
}
Stdio::~Stdio() {
if (R_SUCCEEDED(GetOpenResult())) {
std::fclose(m_file);
}
}
Result Stdio::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
R_TRY(GetOpenResult());
std::fseek(m_file, off, SEEK_SET);
R_TRY(fsdevGetLastResult());
*bytes_read = std::fread(buf, 1, size, m_file);
return fsdevGetLastResult();
}
} // namespace sphaira::yati::source

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

File diff suppressed because it is too large Load Diff