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:
Léo Lam 2017-07-02 21:32:03 +02:00
parent e608d79f42
commit fbcc6bbd57
2 changed files with 89 additions and 32 deletions

View file

@ -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;

View file

@ -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;
}