#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include template static IntType RoundToNextMediaUnit(IntType value) { auto media_unit_mask = IntType{0x1ff}; value += media_unit_mask; value -= value & media_unit_mask; return value; } /** * Fill the file with zeroes such that the difference between the given * base stream position and the new write position is a multiple of the * media unit size. */ template static void ZeroUpToNextMediaUnitFrom(std::ostream& file, StreamPos base) { // TODO: Narrowing cast.. auto diff = file.tellp() - static_cast(base); diff = RoundToNextMediaUnit(diff); ZeroUpTo(file, base + diff); } template T ParseSignedData(Stream& stream) { auto sig = FileFormat::Signature { FileFormat::Load(stream) }; sig.data.resize(FileFormat::GetSignatureSize(sig.type)); stream.Read(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 std::runtime_error("Unknown signature type " + std::to_string(sig.type)); } std::vector zeroes(pad_size, 0); stream.Read(zeroes.data(), zeroes.size()); return T { sig, FileFormat::Load(stream) }; } struct CIA { FileFormat::CIAHeader header; std::array certificates; FileFormat::Ticket ticket; FileFormat::TMD tmd; std::vector ncch; FileFormat::CIAMeta meta; }; CIA ParseCIA(std::ifstream& file) { CIA ret; auto cia_begin = file.tellg(); auto strbuf_it = std::istreambuf_iterator(file.rdbuf()); auto stream = FileFormat::MakeStreamInFromContainer(strbuf_it, decltype(strbuf_it){}); ret.header = FileFormat::Load(stream); auto& cia = ret.header; std::cout << std::hex << "CIA header size: 0x" << cia.header_size << std::endl; std::cout << "Type: 0x" << std::hex << cia.type << std::endl; std::cout << "Version: 0x" << std::hex << cia.version << std::endl; std::cout << "Certificate chain size: 0x" << std::hex << cia.certificate_chain_size << std::endl; std::cout << "Ticket size: 0x" << std::hex << cia.ticket_size << std::endl; std::cout << "TMD size: 0x" << std::hex << cia.tmd_size << std::endl; std::cout << "Meta size: 0x" << std::hex << cia.meta_size << std::endl; std::cout << "Content size: 0x" << std::hex << cia.content_size << std::endl; std::cout << std::endl; // TODO: Handle overflow cases for large content_size values.. auto cert_begin = static_cast(cia_begin) + (unsigned long long){(cia.header_size + 63) & ~UINT32_C(63)}; auto ticket_begin = cert_begin + (unsigned long long){(cia.certificate_chain_size + 63) & ~UINT32_C(63)}; auto tmd_begin = ticket_begin + (unsigned long long){(cia.ticket_size + 63) & ~UINT32_C(63)}; auto content_begin = tmd_begin + (unsigned long long){(cia.tmd_size + 63) & ~UINT32_C(63)}; auto meta_begin = content_begin + (unsigned long long){(cia.content_size + 63) & ~UINT32_C(63)}; file.seekg(cert_begin); strbuf_it = std::istreambuf_iterator(file.rdbuf()); stream = FileFormat::MakeStreamInFromContainer(strbuf_it, decltype(strbuf_it){}); auto& certificates = ret.certificates; for (auto cert_index : { 0, 1, 2}) { auto& certificate = certificates[cert_index]; certificate = ParseSignedData(stream); std::cout << "Certificate " << std::dec << cert_index << ":" << std::endl; std::cout << "Type: " << std::hex << certificate.sig.type << std::endl; std::cout << "Public key type: " << std::hex << certificate.cert.key_type << std::endl; std::cout << "Issuer: "; std::copy(certificate.cert.issuer.begin(), certificate.cert.issuer.end(), std::ostream_iterator(std::cout)); std::cout << std::endl; std::cout << "Name: "; std::copy(certificate.cert.name.begin(), certificate.cert.name.end(), std::ostream_iterator(std::cout)); std::cout << std::endl; if (certificate.cert.key_type == 0) { certificate.pubkey.rsa4096 = FileFormat::Load>(stream); } else if (certificate.cert.key_type == 1) { certificate.pubkey.rsa2048 = FileFormat::Load>(stream); } else if (certificate.cert.key_type == 1) { certificate.pubkey.ecc = FileFormat::Load(stream); } else { throw std::runtime_error("Unknown certificate public key type"); } std::cout << std::endl; } file.seekg(ticket_begin); std::cout << "Ticket: " << std::endl; strbuf_it = std::istreambuf_iterator(file.rdbuf()); stream = FileFormat::MakeStreamInFromContainer(strbuf_it, decltype(strbuf_it){}); ret.ticket = ParseSignedData(stream); auto& ticket = ret.ticket; std::cout << "Signature type: 0x" << std::hex << ticket.sig.type << std::endl; std::cout << "Issuer: "; std::copy(ticket.data.issuer.begin(), ticket.data.issuer.end(), std::ostream_iterator(std::cout)); std::cout << std::endl; std::cout << "Format version: 0x" << std::hex << +ticket.data.version << std::endl; std::cout << "Ticket ID: 0x" << ticket.data.ticket_id << std::endl; std::cout << "Console ID: 0x" << ticket.data.console_id << std::endl; std::cout << "Title ID: 0x" << ticket.data.title_id << std::endl; std::cout << "Sys Access: 0x" << ticket.data.sys_access << std::endl; std::cout << "Title version: 0x" << ticket.data.title_version << std::endl; std::cout << "Permit mask: 0x" << ticket.data.permit_mask << std::endl; std::cout << "Title export: 0x" << +ticket.data.title_export << std::endl; std::cout << "KeyY index: 0x" << +ticket.data.key_y_index << std::endl; for (auto i = 0; i < ticket.data.time_limit.size(); ++i) { if (ticket.data.time_limit[i].enable) { std::cout << "Time limit " << std::dec << i << ": " << ticket.data.time_limit[i].limit_in_seconds << " seconds" << std::endl; } } std::cout << "Unknown @ 0: 0x" << std::hex << +ticket.data.unknown[0] << std::endl; std::cout << "Unknown @ 1: 0x" << std::hex << +ticket.data.unknown[1] << std::endl; std::cout << "Unknown: 0x" << std::hex << +ticket.data.unknown2 << std::endl; for (size_t i = 0; i < ticket.data.unknown3.size(); ++i) { if (ticket.data.unknown3[i]) { std::cout << "Unknown3 @ "<< std::dec << i << ": 0x" << std::hex << +ticket.data.unknown3[i] << std::endl; } } for (size_t i = 0; i < ticket.data.unknown4.size(); ++i) { if (ticket.data.unknown4[i]) { std::cout << "Unknown4 @ "<< std::dec << i << ": 0x" << std::hex << +ticket.data.unknown4[i] << std::endl; } } std::cout << std::endl; // TMD std::cout << "TMD: " << std::endl; std::vector data_buffer; file.seekg(tmd_begin); data_buffer.resize(cia.tmd_size); file.read(data_buffer.data(), cia.tmd_size); auto stream2 = FileFormat::MakeStreamInFromContainer(data_buffer.begin(), data_buffer.end()); ret.tmd = ParseSignedData(stream2); auto& tmd = ret.tmd; std::cout << "Signature type: 0x" << std::hex << tmd.sig.type << std::endl; std::cout << "Issuer: "; std::copy(tmd.data.issuer.begin(), tmd.data.issuer.end(), std::ostream_iterator(std::cout)); std::cout << std::endl; std::cout << "Version: 0x" << std::hex << +tmd.data.version << std::endl; std::cout << "CA revocation list version: 0x" << std::hex << +tmd.data.ca_crl_version << std::endl; std::cout << "Signer CLR version: 0x" << std::hex << +tmd.data.ca_crl_version << std::endl; std::cout << "System version: 0x" << std::hex << +tmd.data.system_version << std::endl; std::cout << "Title id: 0x" << std::hex << +tmd.data.title_id << std::endl; std::cout << "Title type: 0x" << std::hex << +tmd.data.title_type << std::endl; std::cout << "Group id: 0x" << std::hex << +tmd.data.group_id << std::endl; std::cout << "Save data size: 0x" << std::hex << +tmd.data.save_data_size << std::endl; std::cout << "Private save data size for SRL: 0x" << std::hex << +tmd.data.srl_private_data_size << std::endl; std::cout << "SRL flag: 0x" << std::hex << +tmd.data.srl_flag << std::endl; std::cout << "Access rights: 0x" << std::hex << +tmd.data.access_rights << std::endl; std::cout << "Title version: 0x" << std::hex << +tmd.data.title_version << std::endl; std::cout << "Content count: 0x" << std::hex << +tmd.data.content_count << std::endl; std::cout << "Main content: 0x" << std::hex << +tmd.data.main_content << std::endl; std::cout << "Unknown: 0x" << std::hex << +tmd.data.unknown << std::endl; std::cout << "Unknown2: 0x" << std::hex << +tmd.data.unknown2 << std::endl; for (size_t i = 0; i < tmd.data.unknown3.size(); ++i) { if (tmd.data.unknown3[i]) { std::cout << "Unknown3 @ "<< std::dec << i << ": 0x" << std::hex << +tmd.data.unknown3[i] << std::endl; } } std::cout << "Unknown4: 0x" << std::hex << +tmd.data.unknown4 << std::endl; // Read raw content infos into memory for hashing first // TODO: Support more than one content! // TODO: Clean this up.. we are just copying data from one memory buffer into another here, // when instead we could just store a pointer to the current data stream offset... auto content_info_hash_data = Meta::invoke([&] { std::vector data(FileFormat::TMD::ContentInfoHash::Tags::expected_serialized_size * Meta::tuple_size_v); stream2.Read(reinterpret_cast(data.data()), data.size()); return data; }); // 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()); std::cout << "Reference hash: "; for (auto c : ret.tmd.data.content_info_records_sha256) std::cout << std::setw(2) << std::setfill('0') << std::hex << +c; std::cout << std::endl; std::cout << "Computed hash: "; for (auto c : contentinfohash_hash) std::cout << std::setw(2) << std::setfill('0') << std::hex << +c; std::cout << std::endl; auto content_info_hash_info_stream = FileFormat::MakeStreamInFromContainer(content_info_hash_data.begin(), content_info_hash_data.end()); for (auto& content_info_hash_info : ret.tmd.content_info_hashes) content_info_hash_info = FileFormat::Load(content_info_hash_info_stream); // Read raw content infos into memory for hashing first // TODO: Support more than one content! // TODO: Clean this up.. we are just copying data from one memory buffer into another here, // when instead we could just store a pointer to the current data stream offset... tmd.content_infos.resize(tmd.data.content_count); assert(tmd.content_infos.size() == 1); auto content_info_data = Meta::invoke([&] { std::vector data(FileFormat::TMD::ContentInfo::Tags::expected_serialized_size * tmd.content_infos.size()); stream2.Read(reinterpret_cast(data.data()), data.size()); return data; }); auto content_info_stream = FileFormat::MakeStreamInFromContainer(content_info_data.begin(), content_info_data.end()); for (auto& content_info : tmd.content_infos) { content_info = FileFormat::Load(content_info_stream); } std::cout << std::endl; for (size_t index = 0; index < tmd.content_info_hashes.size(); ++index) { auto&& content_info_info = tmd.content_info_hashes[index]; if (content_info_info.chunk_count == 0) continue; assert(index == 0); // TODO: Support more than one metainfo block std::cout << "Content metainfo " << std::dec << index << " index offset: " << std::hex << content_info_info.index_offset << std::endl; std::cout << "Content metainfo " << std::dec << index << " chunk count: " << std::hex << content_info_info.chunk_count << std::endl; // Get content info hash CryptoPP::SHA256 running_hash; CryptoPP::byte chunk_hash[running_hash.DIGESTSIZE]; for (size_t chunk_index = 0; chunk_index < content_info_info.chunk_count; ++chunk_index) { auto size = FileFormat::TMD::ContentInfo::Tags::expected_serialized_size; auto* content_info_buffer = content_info_data.data() + chunk_index * size; running_hash.Update(reinterpret_cast(content_info_buffer), size); } running_hash.Final(chunk_hash); std::cout << "Reference hash: "; for (auto c : content_info_info.sha256) std::cout << std::setw(2) << std::setfill('0') << std::hex << +c; std::cout << std::endl; std::cout << "Computed hash: "; for (auto c : chunk_hash) std::cout << std::setw(2) << std::setfill('0') << std::hex << +c; std::cout << std::endl; } std::cout << std::endl; for (auto& content_info : tmd.content_infos) { assert(tmd.content_infos.size() == 1); if (content_info.type & 1) { throw std::runtime_error("Encrypted CIA contents are unsupported"); } std::cout << "Content index: " << std::dec << content_info.index << std::endl; std::cout << "Content id: " << std::dec << content_info.id << std::endl; std::cout << "Content size: 0x" << std::hex << content_info.size << std::endl; file.seekg(content_begin); std::vector content_data(content_info.size); file.read(content_data.data(), content_data.size()); // Get content hash CryptoPP::byte content_hash[CryptoPP::SHA256::DIGESTSIZE]; CryptoPP::SHA256().CalculateDigest(content_hash, reinterpret_cast(content_data.data()), content_data.size()); std::cout << "Reference hash: "; for (auto c : content_info.sha256) std::cout << std::setw(2) << std::setfill('0') << std::hex << +c; std::cout << std::endl; std::cout << "Computed hash: "; for (auto c : content_hash) std::cout << std::setw(2) << std::setfill('0') << std::hex << +c; std::cout << std::endl; } file.seekg(content_begin); std::cout << std::endl << "Content: " << std::endl; strbuf_it = std::istreambuf_iterator(file.rdbuf()); ret.ncch.resize(cia.content_size); file.read(reinterpret_cast(ret.ncch.data()), ret.ncch.size()); 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; } std::cout << "Finished successfully!" << std::endl; return ret; } int main(int argc, char* argv[]) { std::string in_filename; std::string target_path; bool force_overwrite; { namespace bpo = boost::program_options; bpo::options_description desc; desc.add_options() ("help,h", "Print this help") ("input,i", bpo::value(&in_filename)->required(), "Path to input CIA file") ("base,b", bpo::value(&target_path)->required(), "Path to target NAND tree") ("force,f", bpo::bool_switch(&force_overwrite)->default_value(false), "Force overwrite if any output path already exists"); try { auto positional_arguments = bpo::positional_options_description{}.add("input", -1); bpo::variables_map var_map; bpo::store(bpo::command_line_parser(argc, argv).options(desc).positional(positional_arguments).run(), var_map); if (var_map.count("help") ) { std::cout << desc << std::endl; return 0; } bpo::notify(var_map); } catch (bpo::error error) { std::cout << desc << std::endl; return 1; } } if (!boost::filesystem::exists(target_path)) { std::cout << "Target base directory does not exist!" << std::endl; return 1; } std::cout << "Attempting to open \"" << in_filename << "\" for reading" << std::endl; std::ifstream file; file.exceptions(std::ifstream::badbit | std::ifstream::failbit | std::ifstream::eofbit); file.open(in_filename, std::ios_base::binary); auto cia = ParseCIA(file); auto&& tmd = cia.tmd; uint32_t content_id = 0; // TODO: Should read this from the tmd instead // TODO: Change extension to ".app" once we support this! boost::filesystem::path content_filename = fmt::format("{}/{:08x}/{:08x}/content/{:08x}.cxi", target_path, tmd.data.title_id >> 32, tmd.data.title_id & 0xffffffff, content_id); std::cout << "Attempting to create " << content_filename << "" << std::endl; if (!force_overwrite && boost::filesystem::exists(content_filename)) { std::cout << "File already exists!" << std::endl; return 1; } try { (void)boost::filesystem::create_directories(content_filename.parent_path()); } catch (boost::filesystem::filesystem_error& err) { std::cout << "Couldn't create directory structure: " << err.what() << std::endl; return 1; } std::ofstream ofile; ofile.exceptions(std::ifstream::badbit | std::ifstream::failbit | std::ifstream::eofbit); ofile.open(content_filename, std::ios_base::binary); ofile.write(reinterpret_cast(cia.ncch.data()), cia.ncch.size()); ofile.close(); // TODO: Also copy over tmd once we support that // TODO: Also generate cmd once we support that std::cout << "Success!" << std::endl; }