mirror of
https://github.com/mikage-emu/mikage-dev.git
synced 2025-01-09 06:50:59 +01:00
393 lines
18 KiB
C++
393 lines
18 KiB
C++
#include <processes/pxi_fs.hpp>
|
|
|
|
#include <platform/file_formats/cia.hpp>
|
|
#include <platform/crypto.hpp>
|
|
|
|
#include <framework/exceptions.hpp>
|
|
#include <framework/formats.hpp>
|
|
#include <framework/ranges.hpp>
|
|
|
|
#include <cryptopp/aes.h>
|
|
#include <cryptopp/modes.h>
|
|
#include <cryptopp/osrng.h>
|
|
#include <cryptopp/rsa.h>
|
|
|
|
#include <range/v3/algorithm/equal.hpp>
|
|
#include <range/v3/view/zip.hpp>
|
|
|
|
#include <filesystem>
|
|
|
|
namespace HLE::PXI {
|
|
std::array<uint8_t, 16> GenerateAESKey(const std::array<uint8_t, 16>& key_x, const std::array<uint8_t, 16>& key_y);
|
|
}
|
|
|
|
template<typename T, typename SubType, typename Stream>
|
|
T ParseSignedData(Stream& reader) {
|
|
auto sig = FileFormat::Signature { FileFormat::LoadValue<uint32_t, boost::endian::order::big>(reader), {} };
|
|
|
|
sig.data.resize(FileFormat::GetSignatureSize(sig.type));
|
|
reader(reinterpret_cast<char*>(sig.data.data()), sig.data.size() * sizeof(sig.data[0]));
|
|
size_t pad_size;
|
|
switch (sig.type) {
|
|
case 0x10000:
|
|
case 0x10001:
|
|
case 0x10003:
|
|
case 0x10004:
|
|
pad_size = 0x3c;
|
|
break;
|
|
|
|
case 0x10002:
|
|
case 0x10005:
|
|
pad_size = 0x40;
|
|
break;
|
|
|
|
default:
|
|
throw Mikage::Exceptions::Invalid("Unknown signature type {:#x}", sig.type);
|
|
}
|
|
std::vector<char> zeroes(pad_size, 0);
|
|
reader(zeroes.data(), zeroes.size());
|
|
|
|
return T { sig, FileFormat::SerializationInterface<SubType>::Load(reader) };
|
|
}
|
|
|
|
struct CIA {
|
|
FileFormat::CIAHeader header;
|
|
|
|
std::array<FileFormat::Certificate, 3> certificates;
|
|
FileFormat::Ticket ticket;
|
|
FileFormat::TMD tmd;
|
|
|
|
std::vector<std::vector<uint8_t>> contents;
|
|
|
|
FileFormat::CIAMeta meta;
|
|
};
|
|
|
|
void InstallCIA(std::filesystem::path content_dir, spdlog::logger& logger, const KeyDatabase& keydb, HLE::PXI::FS::FileContext& file_context, HLE::PXI::FS::File& file) {
|
|
auto cia_offset = uint64_t{0};
|
|
auto read_file = [&](char* dest, size_t size) {
|
|
file.Read(file_context, cia_offset, static_cast<uint32_t>(size), HLE::PXI::FS::FileBufferInHostMemory { dest, static_cast<uint32_t>(size) });
|
|
cia_offset += size;
|
|
};
|
|
|
|
auto header = FileFormat::SerializationInterface<FileFormat::CIAHeader>::Load(read_file);
|
|
|
|
logger.info("CIA header size: {:#x}", header.header_size);
|
|
logger.info(" Type: {:#x}", header.type);
|
|
logger.info(" Version: {:#x}", header.version);
|
|
logger.info(" Certificate chain size: {:#x}", header.certificate_chain_size);
|
|
logger.info(" Ticket size: {:#x}", header.ticket_size);
|
|
logger.info(" TMD size: {:#x}", header.tmd_size);
|
|
logger.info(" Meta size: {:#x}", header.meta_size);
|
|
logger.info(" Content size: {:#x}", header.content_size);
|
|
|
|
if (header.header_size != FileFormat::CIAHeader::Tags::expected_serialized_size + 0x2000 /* content mask size */) {
|
|
throw Mikage::Exceptions::NotImplemented("Unexpected CIA header size {:#x}", header.header_size);
|
|
}
|
|
if (header.version != 0) {
|
|
throw Mikage::Exceptions::NotImplemented("Unexpected CIA version {:#x}", header.version);
|
|
}
|
|
|
|
std::vector<uint8_t> content_mask(0x2000);
|
|
read_file(reinterpret_cast<char*>(content_mask.data()), content_mask.size());
|
|
logger.info(" Content indexes:");
|
|
for (auto byte_index : ranges::views::indexes(content_mask)) {
|
|
for (int bit_index = 0; bit_index < 8; ++bit_index) {
|
|
if (content_mask[byte_index] & (0x80 >> bit_index)) {
|
|
logger.info(" {}", byte_index * 8 + bit_index);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Data sections are aligned to 64 bytes
|
|
const auto cert_begin = (header.header_size + 63) & ~uint64_t { 63 };
|
|
const auto ticket_begin = cert_begin + ((header.certificate_chain_size + 63) & ~uint64_t { 63 });
|
|
const auto tmd_begin = ticket_begin + ((header.ticket_size + 63) & ~uint64_t { 63 });
|
|
const auto content_begin = tmd_begin + ((header.tmd_size + 63) & ~uint64_t { 63 });
|
|
const auto meta_begin = content_begin + ((header.content_size + 63) & ~uint64_t { 63 });
|
|
static_assert(sizeof(meta_begin) == 8);
|
|
|
|
CIA ret;
|
|
|
|
// Certificates
|
|
cia_offset = cert_begin;
|
|
auto& certificates = ret.certificates;
|
|
for (auto cert_index : { 0, 1, 2}) {
|
|
auto& certificate = certificates[cert_index];
|
|
certificate = ParseSignedData<FileFormat::Certificate, FileFormat::Certificate::Data>(read_file);
|
|
logger.info("Certificate {}:", cert_index);
|
|
logger.info(" Type: {:#x}", certificate.sig.type);
|
|
logger.info(" Public key type: {:#x}", certificate.cert.key_type);
|
|
logger.info(" Issuer: {:c}", fmt::join(certificate.cert.issuer, ""));
|
|
logger.info(" Name: {:c}", fmt::join(certificate.cert.name, ""));
|
|
|
|
if (certificate.cert.key_type == 0) {
|
|
certificate.pubkey.rsa4096 = FileFormat::SerializationInterface<FileFormat::CertificatePublicKey::RSAKey<4096/8>>::Load(read_file);
|
|
} else if (certificate.cert.key_type == 1) {
|
|
certificate.pubkey.rsa2048 = FileFormat::SerializationInterface<FileFormat::CertificatePublicKey::RSAKey<2048/8>>::Load(read_file);
|
|
} else if (certificate.cert.key_type == 1) {
|
|
certificate.pubkey.ecc = FileFormat::SerializationInterface<FileFormat::CertificatePublicKey::ECCKey>::Load(read_file);
|
|
} else {
|
|
throw std::runtime_error("Unknown certificate public key type");
|
|
}
|
|
}
|
|
|
|
// Ticket
|
|
logger.info("Ticket:");
|
|
cia_offset = ticket_begin;
|
|
ret.ticket = ParseSignedData<FileFormat::Ticket, FileFormat::Ticket::Data>(read_file);
|
|
auto& ticket = ret.ticket;
|
|
logger.info(" Signature type: {:#x}", ticket.sig.type);
|
|
logger.info(" Issuer: {:c}", fmt::join(ticket.data.issuer, ""));
|
|
logger.info(" Format version: {:#x}", ticket.data.version);
|
|
logger.info(" Ticket ID: {:#x}", ticket.data.ticket_id);
|
|
logger.info(" Console ID: {:#x}", ticket.data.console_id);
|
|
logger.info(" Title ID: {:#x}", ticket.data.title_id);
|
|
logger.info(" Sys Access: {:#x}", ticket.data.sys_access);
|
|
logger.info(" Title version: {:#x}", ticket.data.title_version);
|
|
logger.info(" Permit mask: {:#x}", ticket.data.permit_mask);
|
|
logger.info(" Title export: {:#x}", ticket.data.title_export);
|
|
logger.info(" KeyY index: {:#x}", ticket.data.key_y_index);
|
|
for (uint32_t i = 0; i < ticket.data.time_limit.size(); ++i) {
|
|
if (ticket.data.time_limit[i].enable) {
|
|
logger.info(" Time limit {}: {} seconds", i, ticket.data.time_limit[i].limit_in_seconds);
|
|
}
|
|
}
|
|
logger.info(" Unknown @ 0: {:#x}", ticket.data.unknown[0]);
|
|
logger.info(" Unknown @ 1: {:#x}", ticket.data.unknown[1]);
|
|
logger.info(" Unknown: {:#x}", ticket.data.unknown2);
|
|
for (size_t i = 0; i < ticket.data.unknown3.size(); ++i) {
|
|
if (ticket.data.unknown3[i]) {
|
|
logger.info(" Unknown3 @ {}: {:#x}", i, ticket.data.unknown3[i]);
|
|
}
|
|
}
|
|
for (size_t i = 0; i < ticket.data.unknown4.size(); ++i) {
|
|
if (ticket.data.unknown4[i]) {
|
|
logger.info(" Unknown4 @ {}: {:#x}", i, ticket.data.unknown4[i]);
|
|
}
|
|
}
|
|
|
|
// TMD
|
|
logger.info("TMD: ");
|
|
|
|
cia_offset = tmd_begin;
|
|
|
|
ret.tmd = ParseSignedData<FileFormat::TMD, FileFormat::TMD::Data>(read_file);
|
|
auto& tmd = ret.tmd;
|
|
|
|
logger.info(" Signature type: {:#x}", tmd.sig.type);
|
|
logger.info(" Issuer: {:c}", fmt::join(tmd.data.issuer, ""));
|
|
logger.info(" Version: {:#x}", tmd.data.version);
|
|
logger.info(" CA revocation list version: {:#x}", tmd.data.ca_crl_version);
|
|
logger.info(" Signer CLR version: {:#x}", tmd.data.ca_crl_version);
|
|
logger.info(" System version: {:#x}", tmd.data.system_version);
|
|
logger.info(" Title id: {:#x}", tmd.data.title_id);
|
|
logger.info(" Title type: {:#x}", tmd.data.title_type);
|
|
logger.info(" Group id: {:#x}", tmd.data.group_id);
|
|
logger.info(" Save data size: {:#x}", tmd.data.save_data_size);
|
|
logger.info(" Private save data size for SRL: {:#x}", tmd.data.srl_private_data_size);
|
|
logger.info(" SRL flag: {:#x}", tmd.data.srl_flag);
|
|
logger.info(" Access rights: {:#x}", tmd.data.access_rights);
|
|
logger.info(" Title version: {:#x}", tmd.data.title_version);
|
|
logger.info(" Content count: {:#x}", tmd.data.content_count);
|
|
logger.info(" Main content: {:#x}", tmd.data.main_content);
|
|
|
|
logger.info(" Unknown: {:#x}", tmd.data.unknown);
|
|
logger.info(" Unknown2: {:#x}", tmd.data.unknown2);
|
|
for (size_t i = 0; i < tmd.data.unknown3.size(); ++i) {
|
|
if (tmd.data.unknown3[i]) {
|
|
logger.info(" Unknown3 @ {}: {:#x}", i, tmd.data.unknown3[i]);
|
|
}
|
|
}
|
|
logger.info(" Unknown4: {:#x}", tmd.data.unknown4);
|
|
|
|
// TMD content info
|
|
{
|
|
// Read raw content infos into memory for hashing first
|
|
std::vector<char> content_info_hash_data(FileFormat::TMD::ContentInfoHash::Tags::expected_serialized_size * tmd.content_info_hashes.size());
|
|
read_file(content_info_hash_data.data(), content_info_hash_data.size());
|
|
|
|
// Get content info hash ... hash
|
|
CryptoPP::byte contentinfohash_hash[CryptoPP::SHA256::DIGESTSIZE];
|
|
CryptoPP::SHA256().CalculateDigest(contentinfohash_hash,
|
|
reinterpret_cast<const CryptoPP::byte*>(content_info_hash_data.data()), content_info_hash_data.size());
|
|
logger.info(" Reference hash: {:02x}", fmt::join(ret.tmd.data.content_info_records_sha256, ""));
|
|
logger.info(" Computed hash: {:02x}", fmt::join(contentinfohash_hash, ""));
|
|
if (!ranges::equal(ret.tmd.data.content_info_records_sha256, contentinfohash_hash)) {
|
|
throw std::runtime_error("Hash over TMD content info hashes doesn't match reference");
|
|
}
|
|
|
|
auto stream = FileFormat::MakeStreamInFromContainer(content_info_hash_data);
|
|
for (auto& content_info_hash : tmd.content_info_hashes) {
|
|
content_info_hash = FileFormat::SerializationInterface<FileFormat::TMD::ContentInfoHash>::Load(stream);
|
|
}
|
|
}
|
|
|
|
{
|
|
std::vector<char> content_info_data(FileFormat::TMD::ContentInfo::Tags::expected_serialized_size * tmd.data.content_count);
|
|
read_file(content_info_data.data(), content_info_data.size());
|
|
|
|
{
|
|
std::size_t content_info_index = 0;
|
|
for (auto& content_info_hash : tmd.content_info_hashes) {
|
|
if (content_info_hash.chunk_count == 0) {
|
|
continue;
|
|
}
|
|
|
|
// TODO: Should the given index_offset be used instead of content_info_index?
|
|
if (content_info_hash.index_offset != content_info_index) {
|
|
throw Mikage::Exceptions::Invalid("Unknown behavior for TMD content info offset");
|
|
}
|
|
|
|
// Hash "chunk_count" content infos
|
|
CryptoPP::byte contentinfo_hash[CryptoPP::SHA256::DIGESTSIZE];
|
|
auto data_ptr_for_hash = &content_info_data[content_info_index];
|
|
uint64_t bytes_to_hash = content_info_hash.chunk_count * FileFormat::TMD::ContentInfo::Tags::expected_serialized_size;
|
|
CryptoPP::SHA256().CalculateDigest(contentinfo_hash,
|
|
reinterpret_cast<const CryptoPP::byte*>(data_ptr_for_hash), bytes_to_hash);
|
|
logger.info(" Hashes covering content info {}-{}:", content_info_hash.index_offset, content_info_hash.index_offset + content_info_hash.chunk_count - 1);
|
|
logger.info(" Reference: {:02x}", fmt::join(content_info_hash.sha256, ""));
|
|
logger.info(" Computed: {:02x}", fmt::join(contentinfo_hash, ""));
|
|
if (!ranges::equal(content_info_hash.sha256, contentinfo_hash)) {
|
|
throw std::runtime_error("Hash over TMD content info doesn't match reference");
|
|
}
|
|
|
|
content_info_index += content_info_hash.chunk_count;
|
|
}
|
|
}
|
|
|
|
tmd.content_infos.resize(tmd.data.content_count);
|
|
auto stream = FileFormat::MakeStreamInFromContainer(content_info_data);
|
|
uint64_t total_content_size = 0;
|
|
for (const auto& content_info_index : ranges::views::indexes(tmd.content_infos)) {
|
|
logger.info(" Content info {}", content_info_index);
|
|
auto& content_info = tmd.content_infos[content_info_index];
|
|
content_info = FileFormat::SerializationInterface<FileFormat::TMD::ContentInfo>::Load(stream);
|
|
|
|
logger.info(" Index: {}", content_info.index);
|
|
logger.info(" Id: {}", content_info.id);
|
|
logger.info(" Size: {:#x}", content_info.size);
|
|
logger.info(" Flags:{}", content_info.type == 0 ? " (none)" : "");
|
|
if (content_info.type & 1) {
|
|
logger.info(" encrypted");
|
|
}
|
|
if (content_info.type > 1) {
|
|
logger.info(" unknown ({:#x})", content_info.type);
|
|
}
|
|
|
|
if ((content_info.index / 8 > content_mask.size()) ||
|
|
(content_mask[content_info.index / 8] & (~content_info.index % 8)) == 0) {
|
|
throw Mikage::Exceptions::Invalid("TMD content index not present in CIA content mask");
|
|
}
|
|
|
|
total_content_size += content_info.size;
|
|
}
|
|
|
|
if (total_content_size != header.content_size) {
|
|
throw Mikage::Exceptions::Invalid("Sum of TMD content sizes doesn't match CIA content size");
|
|
}
|
|
}
|
|
|
|
if (cia_offset != tmd_begin + header.tmd_size) {
|
|
throw Mikage::Exceptions::Invalid("TMD ended at unexpected offset");
|
|
}
|
|
|
|
|
|
// Content
|
|
logger.info("Content:");
|
|
cia_offset = content_begin;
|
|
|
|
for (const auto& content_info : ret.tmd.content_infos) {
|
|
std::vector<uint8_t> ncch(content_info.size);
|
|
read_file(reinterpret_cast<char*>(ncch.data()), ncch.size());
|
|
|
|
if (content_info.type & 1) {
|
|
CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption dec;
|
|
|
|
// Decrypt title key using common key
|
|
auto title_key = ret.ticket.data.title_key_encrypted;
|
|
{
|
|
const auto& common_key_x = keydb.aes_slots[0x3d].x.value();
|
|
const auto& common_key_y = keydb.common_y.at(ret.ticket.data.key_y_index).value();
|
|
auto common_key = HLE::PXI::GenerateAESKey(common_key_x, common_key_y);
|
|
|
|
// IV is the title ID encoded as big-endian
|
|
std::array<uint8_t, 0x10> iv {};
|
|
memcpy(iv.data(), &ret.ticket.data.title_id, sizeof(ret.ticket.data.title_id));
|
|
std::reverse(iv.begin(), iv.begin() + sizeof(ret.ticket.data.title_id));
|
|
dec.SetKeyWithIV(common_key.data(), sizeof(common_key), iv.data());
|
|
dec.ProcessData(reinterpret_cast<CryptoPP::byte*>(title_key.data()), reinterpret_cast<const CryptoPP::byte*>(title_key.data()), title_key.size());
|
|
}
|
|
|
|
// Decrypt content using decrypted title key
|
|
{
|
|
// IV is the content ID encoded as big-endian
|
|
std::array<uint8_t, 0x10> iv {};
|
|
memcpy(iv.data(), &content_info.index, sizeof(content_info.index));
|
|
std::reverse(iv.begin(), iv.begin() + sizeof(content_info.index));
|
|
dec.SetKeyWithIV(title_key.data(), sizeof(title_key), iv.data());
|
|
dec.ProcessData(reinterpret_cast<CryptoPP::byte*>(ncch.data()), reinterpret_cast<const CryptoPP::byte*>(ncch.data()), ncch.size());
|
|
}
|
|
}
|
|
|
|
CryptoPP::byte content_hash[CryptoPP::SHA256::DIGESTSIZE];
|
|
CryptoPP::SHA256().CalculateDigest(content_hash,
|
|
reinterpret_cast<const CryptoPP::byte*>(ncch.data()), ncch.size());
|
|
|
|
logger.info(" Reference content hash: {:02x}", fmt::join(content_info.sha256, ""));
|
|
logger.info(" Computed content hash: {:02x}", fmt::join(content_hash, ""));
|
|
|
|
if (!ranges::equal(content_info.sha256, content_hash)) {
|
|
throw std::runtime_error("Content hash doesn't match TMD");
|
|
}
|
|
|
|
ret.contents.push_back(std::move(ncch));
|
|
}
|
|
|
|
logger.info("Beginning install...\n");
|
|
|
|
uint32_t title_id_high = ret.ticket.data.title_id >> 32;
|
|
uint32_t title_id_low = ret.ticket.data.title_id & 0xffffffff;
|
|
content_dir /= fmt::format("{:08x}/{:08x}/content", title_id_high, title_id_low);
|
|
std::filesystem::create_directories(content_dir);
|
|
|
|
// // TODO: Implement TMD writing
|
|
// {
|
|
// // TODO: If a TMD already exists, remove it and use its incremented number here
|
|
// std::ofstream file(content_dir / fmt::format("{:x}.tmd", 0));
|
|
// }
|
|
|
|
for (auto content_index = 0; content_index < ret.contents.size(); ++content_index) {
|
|
const auto& ncch = ret.contents[content_index];
|
|
const auto& content_info = ret.tmd.content_infos.at(content_index);
|
|
|
|
std::ofstream out_file(content_dir / fmt::format("{:08x}.cxi", content_info.id));
|
|
out_file.write(reinterpret_cast<const char*>(ncch.data()), ncch.size());
|
|
|
|
// To simplify title launching, we always boot from 00000000.cxi currently.
|
|
// Copy the main title to that location hence
|
|
// TODO: Read TMD when launching titles instead
|
|
if (content_index == ret.tmd.data.main_content) {
|
|
if (content_info.id != 0) {
|
|
std::filesystem::copy(content_dir / fmt::format("{:08x}.cxi", content_info.id), content_dir / fmt::format("00000000.cxi"), std::filesystem::copy_options::overwrite_existing);
|
|
}
|
|
} else if (content_info.id == 0) {
|
|
throw Mikage::Exceptions::NotImplemented("Content id 0 expected to be the main content");
|
|
}
|
|
}
|
|
|
|
// TODO: If title id is native firm, extract its files...
|
|
|
|
// // Meta
|
|
// file.seekg(meta_begin);
|
|
// strbuf_it = std::istreambuf_iterator<char>(file.rdbuf());
|
|
// stream = FileFormat::MakeStreamInFromContainer(strbuf_it, decltype(strbuf_it){});
|
|
// ret.meta = FileFormat::Load<FileFormat::CIAMeta>(stream);
|
|
// for (auto& title_id : ret.meta.dependencies) {
|
|
// if (!title_id)
|
|
// continue;
|
|
|
|
// std::cout << "Dependency: " << std::hex << std::setw(16) << std::setfill('0') << title_id << std::endl;
|
|
// }
|
|
|
|
logger.info("");
|
|
}
|