// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include #include #include #include #include "common/alignment.h" #include "core/file_sys/certificate.h" #include "core/file_sys/otp.h" #include "core/file_sys/signature.h" #include "core/file_sys/ticket.h" #include "core/hle/service/am/am.h" #include "core/hw/aes/key.h" #include "core/hw/ecc.h" #include "core/hw/unique_data.h" #include "core/loader/loader.h" namespace FileSys { Loader::ResultStatus Ticket::DoTitlekeyFixup() { if (ticket_body.console_id == 0u) { // Common ticket, no need to fix up return Loader::ResultStatus::Success; } auto& otp = HW::UniqueData::GetOTP(); auto& ct_cert = HW::UniqueData::GetCTCert(); if (!otp.Valid() || !ct_cert.IsValid()) { LOG_ERROR(HW_AES, "Tried to fixup a ticket without a valid OTP/CTCert"); return Loader::ResultStatus::Error; } if (ticket_body.console_id != otp.GetDeviceID()) { // Ticket does not correspond to this console, cannot fixup LOG_ERROR(HW_AES, "Tried to fixup a ticket that does not correspond to this console"); return Loader::ResultStatus::Error; } HW::ECC::PublicKey ticket_ecc_public = HW::ECC::CreateECCPublicKey(ticket_body.ecc_public_key); auto agreement = ct_cert.ECDHAgree(ticket_ecc_public); if (agreement.empty()) { LOG_ERROR(HW_AES, "Failed to perform ECDH agreement"); return Loader::ResultStatus::Error; } CryptoPP::SHA1 hash; u8 digest[CryptoPP::SHA1::DIGESTSIZE]; hash.CalculateDigest(digest, agreement.data(), agreement.size()); std::vector key(0x10); memcpy(key.data(), digest, key.size()); std::vector iv(0x10); *reinterpret_cast(iv.data()) = ticket_body.ticket_id; CryptoPP::CBC_Mode::Decryption{key.data(), key.size(), iv.data()}.ProcessData( ticket_body.title_key.data(), ticket_body.title_key.data(), ticket_body.title_key.size()); return Loader::ResultStatus::Success; } Loader::ResultStatus Ticket::Load(std::span file_data, std::size_t offset) { std::size_t total_size = static_cast(file_data.size() - offset); serialized_size = total_size; if (total_size < sizeof(u32)) return Loader::ResultStatus::Error; std::memcpy(&signature_type, &file_data[offset], sizeof(u32)); // Signature lengths are variable, and the body follows the signature u32 signature_size = GetSignatureSize(signature_type); if (signature_size == 0) { return Loader::ResultStatus::Error; } // The ticket body start position is rounded to the nearest 0x40 after the signature std::size_t body_start = Common::AlignUp(signature_size + sizeof(u32), 0x40); std::size_t body_end = body_start + sizeof(Body); if (total_size < body_end) return Loader::ResultStatus::Error; // Read signature + ticket body ticket_signature.resize(signature_size); std::memcpy(ticket_signature.data(), &file_data[offset + sizeof(u32)], signature_size); std::memcpy(&ticket_body, &file_data[offset + body_start], sizeof(Body)); std::size_t content_index_start = body_end; if (total_size < content_index_start + (2 * sizeof(u32))) return Loader::ResultStatus::Error; // Read content index size from the second u32 into it. Actual format is undocumented. const size_t content_index_size = static_cast(static_cast(*reinterpret_cast( &file_data[offset + content_index_start + 1 * sizeof(u32)]))); const size_t content_index_end = content_index_start + content_index_size; if (total_size < content_index_end) return Loader::ResultStatus::Error; std::vector content_index_vec; content_index_vec.resize(content_index_size); std::memcpy(content_index_vec.data(), &file_data[offset + content_index_start], content_index_size); content_index.Load(this, content_index_vec); return Loader::ResultStatus::Success; } Loader::ResultStatus Ticket::Load(u64 title_id, u64 ticket_id) { FileUtil::IOFile f(Service::AM::GetTicketPath(title_id, ticket_id), "rb"); if (!f.IsOpen()) { return Loader::ResultStatus::Error; } std::vector ticket_data(f.GetSize()); f.ReadBytes(ticket_data.data(), ticket_data.size()); return Load(ticket_data, 0); } std::vector Ticket::Serialize() const { std::vector ret(sizeof(u32_be)); *reinterpret_cast(ret.data()) = signature_type; ret.insert(ret.end(), ticket_signature.begin(), ticket_signature.end()); u32 padding = 0x40 - (ret.size() % 0x40); ret.insert(ret.end(), padding, 0); std::span body_span{reinterpret_cast(&ticket_body), reinterpret_cast(&ticket_body) + sizeof(ticket_body)}; ret.insert(ret.end(), body_span.begin(), body_span.end()); ret.insert(ret.end(), content_index.GetRaw().cbegin(), content_index.GetRaw().cend()); return ret; } Loader::ResultStatus Ticket::Save(const std::string& file_path) const { FileUtil::IOFile file(file_path, "wb"); if (!file.IsOpen()) return Loader::ResultStatus::Error; auto serialized = Serialize(); if (file.WriteBytes(serialized.data(), serialized.size()) != serialized.size()) { return Loader::ResultStatus::Error; } return Loader::ResultStatus::Success; } std::optional> Ticket::GetTitleKey() const { HW::AES::InitKeys(); std::array ctr{}; std::memcpy(ctr.data(), &ticket_body.title_id, sizeof(u64)); HW::AES::SelectCommonKeyIndex(ticket_body.common_key_index); if (!HW::AES::IsNormalKeyAvailable(HW::AES::KeySlotID::TicketCommonKey)) { LOG_ERROR(Service_FS, "CommonKey {} missing", ticket_body.common_key_index); return {}; } auto key = HW::AES::GetNormalKey(HW::AES::KeySlotID::TicketCommonKey); auto title_key = ticket_body.title_key; CryptoPP::CBC_Mode::Decryption{key.data(), key.size(), ctr.data()}.ProcessData( title_key.data(), title_key.data(), title_key.size()); return title_key; } bool Ticket::IsPersonal() const { if (ticket_body.console_id == 0u) { // Common ticket return false; } auto& otp = HW::UniqueData::GetOTP(); if (!otp.Valid()) { LOG_ERROR(HW_AES, "Invalid OTP"); return false; } return ticket_body.console_id == otp.GetDeviceID(); } void Ticket::ContentIndex::Initialize() { if (!parent || initialized) { return; } if (content_index.size() < sizeof(MainHeader)) { LOG_ERROR(Service_FS, "Ticket content index is too small"); return; } MainHeader* main_header = reinterpret_cast(content_index.data()); if (main_header->always1 != 1 || main_header->header_size != sizeof(MainHeader) || main_header->context_index_size != content_index.size() || main_header->index_header_size != sizeof(IndexHeader)) { u16 always1 = main_header->always1; u16 header_size = main_header->header_size; u32 context_index_size = main_header->context_index_size; u16 index_header_size = main_header->index_header_size; LOG_ERROR(Service_FS, "Ticket content index has unexpected parameters title_id={}, ticket_id={}, " "always1={}, header_size={}, " "size={}, index_header_size={}", parent->GetTitleID(), parent->GetTicketID(), always1, header_size, context_index_size, index_header_size); return; } for (u32 i = 0; i < main_header->index_headers_count; i++) { IndexHeader* curr_header = reinterpret_cast( content_index.data() + main_header->index_headers_offset + main_header->index_header_size * i); if (curr_header->type != 3 || curr_header->entry_size != sizeof(RightsField)) { u16 type = curr_header->type; LOG_WARNING(Service_FS, "Found unsupported index header type, skiping... title_id={}, " "ticket_id={}, type={}", parent->GetTitleID(), parent->GetTicketID(), type); continue; } for (u32 j = 0; j < curr_header->entry_count; j++) { RightsField* field = reinterpret_cast( content_index.data() + curr_header->data_offset + curr_header->entry_size * j); rights.push_back(*field); } } initialized = true; } bool Ticket::ContentIndex::HasRights(u16 content_index) { if (!initialized) { Initialize(); if (!initialized) return false; } // From: // https://github.com/d0k3/GodMode9/blob/4424c37a89337ffb074c80807da1e80f358779b7/arm9/source/game/ticket.c#L198 if (rights.empty()) { return content_index < 256; // when no fields, true if below 256 } bool has_right = false; // it loops until one of these happens: // - we run out of bit fields // - at the first encounter of an index offset field that's bigger than index // - at the first encounter of a positive indicator of content rights for (u32 i = 0; i < rights.size(); i++) { u16 start_index = rights[i].start_index; if (content_index < start_index) { break; } u16 bit_pos = content_index - start_index; if (bit_pos >= 1024) { continue; // not in this field } if (rights[i].rights[bit_pos / 8] & (1 << (bit_pos % 8))) { has_right = true; break; } } return has_right; } } // namespace FileSys