#include #include #include #include #include #include #include #include #include #include #include #include #include namespace HLE::PXI { std::array GenerateAESKey(const std::array& key_x, const std::array& key_y); } template T ParseSignedData(Stream& reader) { auto sig = FileFormat::Signature { FileFormat::LoadValue(reader), {} }; sig.data.resize(FileFormat::GetSignatureSize(sig.type)); reader(reinterpret_cast(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 zeroes(pad_size, 0); reader(zeroes.data(), zeroes.size()); return T { sig, FileFormat::SerializationInterface::Load(reader) }; } struct CIA { FileFormat::CIAHeader header; std::array certificates; FileFormat::Ticket ticket; FileFormat::TMD tmd; std::vector> 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(size), HLE::PXI::FS::FileBufferInHostMemory { dest, static_cast(size) }); cia_offset += size; }; auto header = FileFormat::SerializationInterface::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 content_mask(0x2000); read_file(reinterpret_cast(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(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>::Load(read_file); } else if (certificate.cert.key_type == 1) { certificate.pubkey.rsa2048 = FileFormat::SerializationInterface>::Load(read_file); } else if (certificate.cert.key_type == 1) { certificate.pubkey.ecc = FileFormat::SerializationInterface::Load(read_file); } else { throw std::runtime_error("Unknown certificate public key type"); } } // Ticket logger.info("Ticket:"); cia_offset = ticket_begin; ret.ticket = ParseSignedData(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(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 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(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::Load(stream); } } { std::vector 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(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::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 ncch(content_info.size); read_file(reinterpret_cast(ncch.data()), ncch.size()); if (content_info.type & 1) { CryptoPP::CBC_Mode::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 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(title_key.data()), reinterpret_cast(title_key.data()), title_key.size()); } // Decrypt content using decrypted title key { // IV is the content ID encoded as big-endian std::array 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(ncch.data()), reinterpret_cast(ncch.data()), ncch.size()); } } CryptoPP::byte content_hash[CryptoPP::SHA256::DIGESTSIZE]; CryptoPP::SHA256().CalculateDigest(content_hash, reinterpret_cast(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(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(file.rdbuf()); // stream = FileFormat::MakeStreamInFromContainer(strbuf_it, decltype(strbuf_it){}); // ret.meta = FileFormat::Load(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(""); }