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/Device.h"
|
||||||
#include "Core/IOS/ES/Formats.h"
|
#include "Core/IOS/ES/Formats.h"
|
||||||
#include "Core/IOS/IOS.h"
|
#include "Core/IOS/IOS.h"
|
||||||
|
#include "Core/IOS/IOSC.h"
|
||||||
|
|
||||||
class PointerWrap;
|
class PointerWrap;
|
||||||
|
|
||||||
|
@ -61,7 +62,7 @@ public:
|
||||||
|
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
IOS::ES::TMDReader tmd;
|
IOS::ES::TMDReader tmd;
|
||||||
std::array<u8, 16> key{};
|
IOSC::Handle key_handle = 0;
|
||||||
struct ContentContext
|
struct ContentContext
|
||||||
{
|
{
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
|
|
|
@ -13,12 +13,12 @@
|
||||||
#include <mbedtls/sha1.h>
|
#include <mbedtls/sha1.h>
|
||||||
|
|
||||||
#include "Common/Align.h"
|
#include "Common/Align.h"
|
||||||
#include "Common/Crypto/AES.h"
|
|
||||||
#include "Common/File.h"
|
#include "Common/File.h"
|
||||||
#include "Common/FileUtil.h"
|
#include "Common/FileUtil.h"
|
||||||
#include "Common/Logging/Log.h"
|
#include "Common/Logging/Log.h"
|
||||||
#include "Common/NandPaths.h"
|
#include "Common/NandPaths.h"
|
||||||
#include "Common/StringUtil.h"
|
#include "Common/StringUtil.h"
|
||||||
|
#include "Core/CommonTitles.h"
|
||||||
#include "Core/HW/Memmap.h"
|
#include "Core/HW/Memmap.h"
|
||||||
#include "Core/IOS/ES/Formats.h"
|
#include "Core/IOS/ES/Formats.h"
|
||||||
#include "Core/ec_wii.h"
|
#include "Core/ec_wii.h"
|
||||||
|
@ -48,7 +48,7 @@ static ReturnCode WriteTicket(const IOS::ES::TicketReader& ticket)
|
||||||
void ES::TitleImportExportContext::DoState(PointerWrap& p)
|
void ES::TitleImportExportContext::DoState(PointerWrap& p)
|
||||||
{
|
{
|
||||||
p.Do(valid);
|
p.Do(valid);
|
||||||
p.Do(key);
|
p.Do(key_handle);
|
||||||
tmd.DoState(p);
|
tmd.DoState(p);
|
||||||
p.Do(content.valid);
|
p.Do(content.valid);
|
||||||
p.Do(content.id);
|
p.Do(content.id);
|
||||||
|
@ -105,11 +105,43 @@ IPCCommandResult ES::ImportTicket(const IOCtlVRequest& request)
|
||||||
return GetDefaultReply(ImportTicket(bytes, cert_chain));
|
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)
|
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
|
// 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.
|
// 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);
|
context.title_import_export.tmd.SetBytes(tmd_bytes);
|
||||||
if (!context.title_import_export.tmd.IsValid())
|
if (!context.title_import_export.tmd.IsValid())
|
||||||
return ES_EINVAL;
|
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()))
|
if (!InitImport(context.title_import_export.tmd.GetTitleId()))
|
||||||
return ES_EIO;
|
return ES_EIO;
|
||||||
|
|
||||||
// FIXME: ImportTmd does not use the ticket or the title key.
|
ret = InitBackupKey(GetTitleContext().tmd, m_ios.GetIOSC(),
|
||||||
const auto ticket = DiscIO::FindSignedTicket(context.title_import_export.tmd.GetTitleId());
|
&context.title_import_export.key_handle);
|
||||||
if (!ticket.IsValid())
|
if (ret != IPC_SUCCESS)
|
||||||
return ES_NO_TICKET;
|
return ret;
|
||||||
context.title_import_export.key = ticket.GetTitleKey(m_ios.GetIOSC());
|
|
||||||
|
|
||||||
context.title_import_export.valid = true;
|
context.title_import_export.valid = true;
|
||||||
return IPC_SUCCESS;
|
return IPC_SUCCESS;
|
||||||
|
@ -150,11 +181,29 @@ IPCCommandResult ES::ImportTmd(Context& context, const IOCtlVRequest& request)
|
||||||
return GetDefaultReply(ImportTmd(context, tmd));
|
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,
|
ReturnCode ES::ImportTitleInit(Context& context, const std::vector<u8>& tmd_bytes,
|
||||||
const std::vector<u8>& cert_chain)
|
const std::vector<u8>& cert_chain)
|
||||||
{
|
{
|
||||||
INFO_LOG(IOS_ES, "ImportTitleInit");
|
INFO_LOG(IOS_ES, "ImportTitleInit");
|
||||||
context.title_import_export = {};
|
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||||
context.title_import_export.tmd.SetBytes(tmd_bytes);
|
context.title_import_export.tmd.SetBytes(tmd_bytes);
|
||||||
if (!context.title_import_export.tmd.IsValid())
|
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())
|
if (!ticket.IsValid())
|
||||||
return ES_NO_TICKET;
|
return ES_NO_TICKET;
|
||||||
|
|
||||||
context.title_import_export.key = ticket.GetTitleKey(m_ios.GetIOSC());
|
|
||||||
|
|
||||||
std::vector<u8> cert_store;
|
std::vector<u8> cert_store;
|
||||||
ret = ReadCertStore(&cert_store);
|
ret = ReadCertStore(&cert_store);
|
||||||
if (ret != IPC_SUCCESS)
|
if (ret != IPC_SUCCESS)
|
||||||
|
@ -186,6 +233,11 @@ ReturnCode ES::ImportTitleInit(Context& context, const std::vector<u8>& tmd_byte
|
||||||
if (ret != IPC_SUCCESS)
|
if (ret != IPC_SUCCESS)
|
||||||
return ret;
|
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()))
|
if (!InitImport(context.title_import_export.tmd.GetTitleId()))
|
||||||
return ES_EIO;
|
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)
|
if (!context.title_import_export.valid || !context.title_import_export.content.valid)
|
||||||
return ES_EINVAL;
|
return ES_EINVAL;
|
||||||
|
|
||||||
std::vector<u8> decrypted_data = Common::AES::Decrypt(
|
std::vector<u8> decrypted_data(context.title_import_export.content.buffer.size());
|
||||||
context.title_import_export.key.data(), context.title_import_export.content.iv.data(),
|
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.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;
|
IOS::ES::Content content_info;
|
||||||
context.title_import_export.tmd.FindContentById(context.title_import_export.content.id,
|
context.title_import_export.tmd.FindContentById(context.title_import_export.content.id,
|
||||||
&content_info);
|
&content_info);
|
||||||
|
@ -391,7 +447,7 @@ ReturnCode ES::ImportTitleDone(Context& context)
|
||||||
return ES_EIO;
|
return ES_EIO;
|
||||||
|
|
||||||
INFO_LOG(IOS_ES, "ImportTitleDone: title %016" PRIx64, title_id);
|
INFO_LOG(IOS_ES, "ImportTitleDone: title %016" PRIx64, title_id);
|
||||||
context.title_import_export = {};
|
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||||
return IPC_SUCCESS;
|
return IPC_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -417,7 +473,7 @@ ReturnCode ES::ImportTitleCancel(Context& context)
|
||||||
INFO_LOG(IOS_ES, "ImportTitleCancel: title %016" PRIx64, title_id);
|
INFO_LOG(IOS_ES, "ImportTitleCancel: title %016" PRIx64, title_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.title_import_export = {};
|
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||||
return IPC_SUCCESS;
|
return IPC_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -579,17 +635,13 @@ ReturnCode ES::ExportTitleInit(Context& context, u64 title_id, u8* tmd_bytes, u3
|
||||||
if (!tmd.IsValid())
|
if (!tmd.IsValid())
|
||||||
return FS_ENOENT;
|
return FS_ENOENT;
|
||||||
|
|
||||||
context.title_import_export = {};
|
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||||
context.title_import_export.tmd = tmd;
|
context.title_import_export.tmd = tmd;
|
||||||
|
|
||||||
const auto ticket = DiscIO::FindSignedTicket(context.title_import_export.tmd.GetTitleId());
|
const ReturnCode ret = InitBackupKey(GetTitleContext().tmd, m_ios.GetIOSC(),
|
||||||
if (!ticket.IsValid())
|
&context.title_import_export.key_handle);
|
||||||
return ES_NO_TICKET;
|
if (ret != IPC_SUCCESS)
|
||||||
if (ticket.GetTitleId() != context.title_import_export.tmd.GetTitleId())
|
return ret;
|
||||||
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 std::vector<u8>& raw_tmd = context.title_import_export.tmd.GetBytes();
|
const std::vector<u8>& raw_tmd = context.title_import_export.tmd.GetBytes();
|
||||||
if (tmd_size != raw_tmd.size())
|
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);
|
const s32 ret = OpenContent(context.title_import_export.tmd, content_info.index, 0);
|
||||||
if (ret < 0)
|
if (ret < 0)
|
||||||
{
|
{
|
||||||
context.title_import_export = {};
|
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||||
return static_cast<ReturnCode>(ret);
|
return static_cast<ReturnCode>(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -671,7 +723,7 @@ ReturnCode ES::ExportContentData(Context& context, u32 content_fd, u8* data, u32
|
||||||
if (read_size < 0)
|
if (read_size < 0)
|
||||||
{
|
{
|
||||||
CloseContent(content_fd, 0);
|
CloseContent(content_fd, 0);
|
||||||
context.title_import_export = {};
|
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||||
return ES_SHORT_READ;
|
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.
|
// let's just follow IOS here.
|
||||||
buffer.resize(Common::AlignUp(buffer.size(), 32));
|
buffer.resize(Common::AlignUp(buffer.size(), 32));
|
||||||
|
|
||||||
const std::vector<u8> output = Common::AES::Encrypt(context.title_import_export.key.data(),
|
std::vector<u8> output(buffer.size());
|
||||||
context.title_import_export.content.iv.data(),
|
const ReturnCode decrypt_ret = m_ios.GetIOSC().Encrypt(
|
||||||
buffer.data(), buffer.size());
|
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);
|
std::copy(output.cbegin(), output.cend(), data);
|
||||||
return IPC_SUCCESS;
|
return IPC_SUCCESS;
|
||||||
}
|
}
|
||||||
|
@ -719,7 +775,7 @@ IPCCommandResult ES::ExportContentEnd(Context& context, const IOCtlVRequest& req
|
||||||
|
|
||||||
ReturnCode ES::ExportTitleDone(Context& context)
|
ReturnCode ES::ExportTitleDone(Context& context)
|
||||||
{
|
{
|
||||||
context.title_import_export = {};
|
ResetTitleImportContext(&context, m_ios.GetIOSC());
|
||||||
return IPC_SUCCESS;
|
return IPC_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue