IOS/ES: Use the correct key for imports/exports
Imports/exports don't always use the title key. Exporting a title and importing it back uses the PRNG key (aka backup key handle or key #5), not the title key (at all). To make things even more fun, some versions of IOS have a bug that causes it to use a zeroed key instead of the PRNG key. When Nintendo decided to fix it, they added checks to keep using the zeroed key only in affected titles to avoid making existing exports useless. (Thanks to tueidj for drawing my attention to this. I missed this edge case during the initial implementation.) This commit implements these checks so we are using the correct key in all of these cases. We now also use IOSC for decryption/encryption since built-in key handles are used. And we now reject any invalid common key index, just like ES.
This commit is contained in:
parent
e608d79f42
commit
fbcc6bbd57
|
@ -13,6 +13,7 @@
|
|||
#include "Core/IOS/Device.h"
|
||||
#include "Core/IOS/ES/Formats.h"
|
||||
#include "Core/IOS/IOS.h"
|
||||
#include "Core/IOS/IOSC.h"
|
||||
|
||||
class PointerWrap;
|
||||
|
||||
|
@ -61,7 +62,7 @@ public:
|
|||
|
||||
bool valid = false;
|
||||
IOS::ES::TMDReader tmd;
|
||||
std::array<u8, 16> key{};
|
||||
IOSC::Handle key_handle = 0;
|
||||
struct ContentContext
|
||||
{
|
||||
bool valid = false;
|
||||
|
|
|
@ -13,12 +13,12 @@
|
|||
#include <mbedtls/sha1.h>
|
||||
|
||||
#include "Common/Align.h"
|
||||
#include "Common/Crypto/AES.h"
|
||||
#include "Common/File.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/NandPaths.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "Core/CommonTitles.h"
|
||||
#include "Core/HW/Memmap.h"
|
||||
#include "Core/IOS/ES/Formats.h"
|
||||
#include "Core/ec_wii.h"
|
||||
|
@ -48,7 +48,7 @@ static ReturnCode WriteTicket(const IOS::ES::TicketReader& ticket)
|
|||
void ES::TitleImportExportContext::DoState(PointerWrap& p)
|
||||
{
|
||||
p.Do(valid);
|
||||
p.Do(key);
|
||||
p.Do(key_handle);
|
||||
tmd.DoState(p);
|
||||
p.Do(content.valid);
|
||||
p.Do(content.id);
|
||||
|
@ -105,11 +105,43 @@ IPCCommandResult ES::ImportTicket(const IOCtlVRequest& request)
|
|||
return GetDefaultReply(ImportTicket(bytes, cert_chain));
|
||||
}
|
||||
|
||||
constexpr std::array<u8, 16> NULL_KEY{};
|
||||
|
||||
// Used for exporting titles and importing them back (ImportTmd and ExportTitleInit).
|
||||
static ReturnCode InitBackupKey(const IOS::ES::TMDReader& tmd, IOSC& iosc, IOSC::Handle* key)
|
||||
{
|
||||
// Some versions of IOS have a bug that causes it to use a zeroed key instead of the PRNG key.
|
||||
// When Nintendo decided to fix it, they added checks to keep using the zeroed key only in
|
||||
// affected titles to avoid making existing exports useless.
|
||||
|
||||
// Ignore the region byte.
|
||||
const u64 title_id = tmd.GetTitleId() | 0xff;
|
||||
const u32 title_flags = tmd.GetTitleFlags();
|
||||
const u32 affected_type = IOS::ES::TITLE_TYPE_0x10 | IOS::ES::TITLE_TYPE_DATA;
|
||||
if (title_id == Titles::SYSTEM_MENU || (title_flags & affected_type) != affected_type ||
|
||||
!(title_id == 0x00010005735841ff || title_id - 0x00010005735a41ff <= 0x700))
|
||||
{
|
||||
*key = IOSC::HANDLE_PRNG_KEY;
|
||||
return IPC_SUCCESS;
|
||||
}
|
||||
|
||||
// Otherwise, use a null key.
|
||||
ReturnCode ret = iosc.CreateObject(key, IOSC::TYPE_SECRET_KEY, IOSC::SUBTYPE_AES128, PID_ES);
|
||||
return ret == IPC_SUCCESS ? iosc.ImportSecretKey(*key, NULL_KEY.data(), PID_ES) : ret;
|
||||
}
|
||||
|
||||
static void ResetTitleImportContext(ES::Context* context, IOSC& iosc)
|
||||
{
|
||||
if (context->title_import_export.key_handle)
|
||||
iosc.DeleteObject(context->title_import_export.key_handle, PID_ES);
|
||||
context->title_import_export = {};
|
||||
}
|
||||
|
||||
ReturnCode ES::ImportTmd(Context& context, const std::vector<u8>& tmd_bytes)
|
||||
{
|
||||
// Ioctlv 0x2b writes the TMD to /tmp/title.tmd (for imports) and doesn't seem to write it
|
||||
// to either /import or /title. So here we simply have to set the import TMD.
|
||||
context.title_import_export = {};
|
||||
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||
context.title_import_export.tmd.SetBytes(tmd_bytes);
|
||||
if (!context.title_import_export.tmd.IsValid())
|
||||
return ES_EINVAL;
|
||||
|
@ -127,11 +159,10 @@ ReturnCode ES::ImportTmd(Context& context, const std::vector<u8>& tmd_bytes)
|
|||
if (!InitImport(context.title_import_export.tmd.GetTitleId()))
|
||||
return ES_EIO;
|
||||
|
||||
// FIXME: ImportTmd does not use the ticket or the title key.
|
||||
const auto ticket = DiscIO::FindSignedTicket(context.title_import_export.tmd.GetTitleId());
|
||||
if (!ticket.IsValid())
|
||||
return ES_NO_TICKET;
|
||||
context.title_import_export.key = ticket.GetTitleKey(m_ios.GetIOSC());
|
||||
ret = InitBackupKey(GetTitleContext().tmd, m_ios.GetIOSC(),
|
||||
&context.title_import_export.key_handle);
|
||||
if (ret != IPC_SUCCESS)
|
||||
return ret;
|
||||
|
||||
context.title_import_export.valid = true;
|
||||
return IPC_SUCCESS;
|
||||
|
@ -150,11 +181,29 @@ IPCCommandResult ES::ImportTmd(Context& context, const IOCtlVRequest& request)
|
|||
return GetDefaultReply(ImportTmd(context, tmd));
|
||||
}
|
||||
|
||||
static ReturnCode InitTitleImportKey(const std::vector<u8>& ticket_bytes, IOSC& iosc,
|
||||
IOSC::Handle* handle)
|
||||
{
|
||||
ReturnCode ret = iosc.CreateObject(handle, IOSC::TYPE_SECRET_KEY, IOSC::SUBTYPE_AES128, PID_ES);
|
||||
if (ret != IPC_SUCCESS)
|
||||
return ret;
|
||||
|
||||
std::array<u8, 16> iv{};
|
||||
std::copy_n(&ticket_bytes[offsetof(IOS::ES::Ticket, title_id)], sizeof(u64), iv.begin());
|
||||
const u8 index = ticket_bytes[offsetof(IOS::ES::Ticket, common_key_index)];
|
||||
if (index > 1)
|
||||
return ES_INVALID_TICKET;
|
||||
|
||||
return iosc.ImportSecretKey(
|
||||
*handle, index == 0 ? IOSC::HANDLE_COMMON_KEY : IOSC::HANDLE_NEW_COMMON_KEY, iv.data(),
|
||||
&ticket_bytes[offsetof(IOS::ES::Ticket, title_key)], PID_ES);
|
||||
}
|
||||
|
||||
ReturnCode ES::ImportTitleInit(Context& context, const std::vector<u8>& tmd_bytes,
|
||||
const std::vector<u8>& cert_chain)
|
||||
{
|
||||
INFO_LOG(IOS_ES, "ImportTitleInit");
|
||||
context.title_import_export = {};
|
||||
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||
context.title_import_export.tmd.SetBytes(tmd_bytes);
|
||||
if (!context.title_import_export.tmd.IsValid())
|
||||
{
|
||||
|
@ -174,8 +223,6 @@ ReturnCode ES::ImportTitleInit(Context& context, const std::vector<u8>& tmd_byte
|
|||
if (!ticket.IsValid())
|
||||
return ES_NO_TICKET;
|
||||
|
||||
context.title_import_export.key = ticket.GetTitleKey(m_ios.GetIOSC());
|
||||
|
||||
std::vector<u8> cert_store;
|
||||
ret = ReadCertStore(&cert_store);
|
||||
if (ret != IPC_SUCCESS)
|
||||
|
@ -186,6 +233,11 @@ ReturnCode ES::ImportTitleInit(Context& context, const std::vector<u8>& tmd_byte
|
|||
if (ret != IPC_SUCCESS)
|
||||
return ret;
|
||||
|
||||
ret = InitTitleImportKey(ticket.GetBytes(), m_ios.GetIOSC(),
|
||||
&context.title_import_export.key_handle);
|
||||
if (ret != IPC_SUCCESS)
|
||||
return ret;
|
||||
|
||||
if (!InitImport(context.title_import_export.tmd.GetTitleId()))
|
||||
return ES_EIO;
|
||||
|
||||
|
@ -300,10 +352,14 @@ ReturnCode ES::ImportContentEnd(Context& context, u32 content_fd)
|
|||
if (!context.title_import_export.valid || !context.title_import_export.content.valid)
|
||||
return ES_EINVAL;
|
||||
|
||||
std::vector<u8> decrypted_data = Common::AES::Decrypt(
|
||||
context.title_import_export.key.data(), context.title_import_export.content.iv.data(),
|
||||
std::vector<u8> decrypted_data(context.title_import_export.content.buffer.size());
|
||||
const ReturnCode decrypt_ret = m_ios.GetIOSC().Decrypt(
|
||||
context.title_import_export.key_handle, context.title_import_export.content.iv.data(),
|
||||
context.title_import_export.content.buffer.data(),
|
||||
context.title_import_export.content.buffer.size());
|
||||
context.title_import_export.content.buffer.size(), decrypted_data.data(), PID_ES);
|
||||
if (decrypt_ret != IPC_SUCCESS)
|
||||
return decrypt_ret;
|
||||
|
||||
IOS::ES::Content content_info;
|
||||
context.title_import_export.tmd.FindContentById(context.title_import_export.content.id,
|
||||
&content_info);
|
||||
|
@ -391,7 +447,7 @@ ReturnCode ES::ImportTitleDone(Context& context)
|
|||
return ES_EIO;
|
||||
|
||||
INFO_LOG(IOS_ES, "ImportTitleDone: title %016" PRIx64, title_id);
|
||||
context.title_import_export = {};
|
||||
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||
return IPC_SUCCESS;
|
||||
}
|
||||
|
||||
|
@ -417,7 +473,7 @@ ReturnCode ES::ImportTitleCancel(Context& context)
|
|||
INFO_LOG(IOS_ES, "ImportTitleCancel: title %016" PRIx64, title_id);
|
||||
}
|
||||
|
||||
context.title_import_export = {};
|
||||
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||
return IPC_SUCCESS;
|
||||
}
|
||||
|
||||
|
@ -579,17 +635,13 @@ ReturnCode ES::ExportTitleInit(Context& context, u64 title_id, u8* tmd_bytes, u3
|
|||
if (!tmd.IsValid())
|
||||
return FS_ENOENT;
|
||||
|
||||
context.title_import_export = {};
|
||||
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||
context.title_import_export.tmd = tmd;
|
||||
|
||||
const auto ticket = DiscIO::FindSignedTicket(context.title_import_export.tmd.GetTitleId());
|
||||
if (!ticket.IsValid())
|
||||
return ES_NO_TICKET;
|
||||
if (ticket.GetTitleId() != context.title_import_export.tmd.GetTitleId())
|
||||
return ES_EINVAL;
|
||||
|
||||
// FIXME: this is wrong. The title key is *not* used here. Key #5 or a null key is.
|
||||
context.title_import_export.key = ticket.GetTitleKey(m_ios.GetIOSC());
|
||||
const ReturnCode ret = InitBackupKey(GetTitleContext().tmd, m_ios.GetIOSC(),
|
||||
&context.title_import_export.key_handle);
|
||||
if (ret != IPC_SUCCESS)
|
||||
return ret;
|
||||
|
||||
const std::vector<u8>& raw_tmd = context.title_import_export.tmd.GetBytes();
|
||||
if (tmd_size != raw_tmd.size())
|
||||
|
@ -633,7 +685,7 @@ ReturnCode ES::ExportContentBegin(Context& context, u64 title_id, u32 content_id
|
|||
const s32 ret = OpenContent(context.title_import_export.tmd, content_info.index, 0);
|
||||
if (ret < 0)
|
||||
{
|
||||
context.title_import_export = {};
|
||||
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||
return static_cast<ReturnCode>(ret);
|
||||
}
|
||||
|
||||
|
@ -671,7 +723,7 @@ ReturnCode ES::ExportContentData(Context& context, u32 content_fd, u8* data, u32
|
|||
if (read_size < 0)
|
||||
{
|
||||
CloseContent(content_fd, 0);
|
||||
context.title_import_export = {};
|
||||
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||
return ES_SHORT_READ;
|
||||
}
|
||||
|
||||
|
@ -679,9 +731,13 @@ ReturnCode ES::ExportContentData(Context& context, u32 content_fd, u8* data, u32
|
|||
// let's just follow IOS here.
|
||||
buffer.resize(Common::AlignUp(buffer.size(), 32));
|
||||
|
||||
const std::vector<u8> output = Common::AES::Encrypt(context.title_import_export.key.data(),
|
||||
context.title_import_export.content.iv.data(),
|
||||
buffer.data(), buffer.size());
|
||||
std::vector<u8> output(buffer.size());
|
||||
const ReturnCode decrypt_ret = m_ios.GetIOSC().Encrypt(
|
||||
context.title_import_export.key_handle, context.title_import_export.content.iv.data(),
|
||||
buffer.data(), buffer.size(), output.data(), PID_ES);
|
||||
if (decrypt_ret != IPC_SUCCESS)
|
||||
return decrypt_ret;
|
||||
|
||||
std::copy(output.cbegin(), output.cend(), data);
|
||||
return IPC_SUCCESS;
|
||||
}
|
||||
|
@ -719,7 +775,7 @@ IPCCommandResult ES::ExportContentEnd(Context& context, const IOCtlVRequest& req
|
|||
|
||||
ReturnCode ES::ExportTitleDone(Context& context)
|
||||
{
|
||||
context.title_import_export = {};
|
||||
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||
return IPC_SUCCESS;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue