IOS/ES: Expose title management ioctlvs

This exposes all ES title management ioctlvs to avoid duplicating IOS
code everywhere and to make it easier to reuse (since this way it's
not unnecessarily tied to the PPC IPC mechanism anymore) and unit test.

Some functions were also renamed for consistency with the other names,
*and* with official names.
This commit is contained in:
Léo Lam 2017-05-13 21:39:53 +02:00
parent f8ffcb2483
commit 5587342ca1
3 changed files with 358 additions and 270 deletions

View File

@ -383,21 +383,21 @@ IPCCommandResult ES::IOCtlV(const IOCtlVRequest& request)
switch (request.request)
{
case IOCTL_ES_ADDTICKET:
return AddTicket(request);
return ImportTicket(request);
case IOCTL_ES_ADDTMD:
return AddTMD(*context, request);
return ImportTmd(*context, request);
case IOCTL_ES_ADDTITLESTART:
return AddTitleStart(*context, request);
return ImportTitleInit(*context, request);
case IOCTL_ES_ADDCONTENTSTART:
return AddContentStart(*context, request);
return ImportContentBegin(*context, request);
case IOCTL_ES_ADDCONTENTDATA:
return AddContentData(*context, request);
return ImportContentData(*context, request);
case IOCTL_ES_ADDCONTENTFINISH:
return AddContentFinish(*context, request);
return ImportContentEnd(*context, request);
case IOCTL_ES_ADDTITLEFINISH:
return AddTitleFinish(*context, request);
return ImportTitleDone(*context, request);
case IOCTL_ES_ADDTITLECANCEL:
return AddTitleCancel(*context, request);
return ImportTitleCancel(*context, request);
case IOCTL_ES_GETDEVICEID:
return GetConsoleID(request);
case IOCTL_ES_OPENTITLECONTENT:

View File

@ -55,7 +55,65 @@ public:
ReturnCode Close(u32 fd) override;
IPCCommandResult IOCtlV(const IOCtlVRequest& request) override;
struct OpenedContent
{
u64 m_title_id;
IOS::ES::Content m_content;
u32 m_position;
};
struct TitleImportContext
{
IOS::ES::TMDReader tmd;
u32 content_id = 0xFFFFFFFF;
std::vector<u8> content_buffer;
};
// TODO: merge this with TitleImportContext. Also reuse the global content table.
struct TitleExportContext
{
struct ExportContent
{
OpenedContent content;
std::array<u8, 16> iv{};
};
bool valid = false;
IOS::ES::TMDReader tmd;
std::vector<u8> title_key;
std::map<u32, ExportContent> contents;
};
struct Context
{
void DoState(PointerWrap& p);
u16 gid = 0;
u32 uid = 0;
TitleImportContext title_import;
TitleExportContext title_export;
bool active = false;
// We use this to associate an IPC fd with an ES context.
u32 ipc_fd = -1;
};
// Title management
ReturnCode ImportTicket(const std::vector<u8>& ticket_bytes);
ReturnCode ImportTmd(Context& context, const std::vector<u8>& tmd_bytes);
ReturnCode ImportTitleInit(Context& context, const std::vector<u8>& tmd_bytes);
ReturnCode ImportContentBegin(Context& context, u64 title_id, u32 content_id);
ReturnCode ImportContentData(Context& context, u32 content_fd, const u8* data, u32 data_size);
ReturnCode ImportContentEnd(Context& context, u32 content_fd);
ReturnCode ImportTitleDone(Context& context);
ReturnCode ImportTitleCancel(Context& context);
ReturnCode ExportTitleInit(Context& context, u64 title_id, u8* tmd, u32 tmd_size);
ReturnCode ExportContentBegin(Context& context, u64 title_id, u32 content_id);
ReturnCode ExportContentData(Context& context, u32 content_fd, u8* data, u32 data_size);
ReturnCode ExportContentEnd(Context& context, u32 content_fd);
ReturnCode ExportTitleDone(Context& context);
ReturnCode DeleteTitle(u64 title_id);
ReturnCode DeleteTitleContent(u64 title_id) const;
ReturnCode DeleteTicket(const u8* ticket_view);
private:
enum
@ -131,67 +189,26 @@ private:
IOCTL_ES_CHECKKOREAREGION = 0x45,
};
struct OpenedContent
{
u64 m_title_id;
IOS::ES::Content m_content;
u32 m_position;
};
struct TitleImportContext
{
IOS::ES::TMDReader tmd;
u32 content_id = 0xFFFFFFFF;
std::vector<u8> content_buffer;
};
// TODO: merge this with TitleImportContext. Also reuse the global content table.
struct TitleExportContext
{
struct ExportContent
{
OpenedContent content;
std::array<u8, 16> iv{};
};
bool valid = false;
IOS::ES::TMDReader tmd;
std::vector<u8> title_key;
std::map<u32, ExportContent> contents;
};
struct Context
{
void DoState(PointerWrap& p);
u16 gid = 0;
u32 uid = 0;
TitleImportContext title_import;
TitleExportContext title_export;
bool active = false;
// We use this to associate an IPC fd with an ES context.
u32 ipc_fd = -1;
};
// ES can only have 3 contexts at one time.
using ContextArray = std::array<Context, 3>;
// Title management
IPCCommandResult AddTicket(const IOCtlVRequest& request);
IPCCommandResult AddTMD(Context& context, const IOCtlVRequest& request);
IPCCommandResult AddTitleStart(Context& context, const IOCtlVRequest& request);
IPCCommandResult AddContentStart(Context& context, const IOCtlVRequest& request);
IPCCommandResult AddContentData(Context& context, const IOCtlVRequest& request);
IPCCommandResult AddContentFinish(Context& context, const IOCtlVRequest& request);
IPCCommandResult AddTitleFinish(Context& context, const IOCtlVRequest& request);
IPCCommandResult AddTitleCancel(Context& context, const IOCtlVRequest& request);
IPCCommandResult ImportTicket(const IOCtlVRequest& request);
IPCCommandResult ImportTmd(Context& context, const IOCtlVRequest& request);
IPCCommandResult ImportTitleInit(Context& context, const IOCtlVRequest& request);
IPCCommandResult ImportContentBegin(Context& context, const IOCtlVRequest& request);
IPCCommandResult ImportContentData(Context& context, const IOCtlVRequest& request);
IPCCommandResult ImportContentEnd(Context& context, const IOCtlVRequest& request);
IPCCommandResult ImportTitleDone(Context& context, const IOCtlVRequest& request);
IPCCommandResult ImportTitleCancel(Context& context, const IOCtlVRequest& request);
IPCCommandResult ExportTitleInit(Context& context, const IOCtlVRequest& request);
IPCCommandResult ExportContentBegin(Context& context, const IOCtlVRequest& request);
IPCCommandResult ExportContentData(Context& context, const IOCtlVRequest& request);
IPCCommandResult ExportContentEnd(Context& context, const IOCtlVRequest& request);
IPCCommandResult ExportTitleDone(Context& context, const IOCtlVRequest& request);
IPCCommandResult DeleteTitle(const IOCtlVRequest& request);
IPCCommandResult DeleteTicket(const IOCtlVRequest& request);
IPCCommandResult DeleteTitleContent(const IOCtlVRequest& request);
IPCCommandResult DeleteTicket(const IOCtlVRequest& request);
// Device identity and encryption
IPCCommandResult GetConsoleID(const IOCtlVRequest& request);

View File

@ -30,17 +30,11 @@ namespace HLE
{
namespace Device
{
IPCCommandResult ES::AddTicket(const IOCtlVRequest& request)
ReturnCode ES::ImportTicket(const std::vector<u8>& ticket_bytes)
{
if (!request.HasNumberOfValidVectors(3, 0))
return GetDefaultReply(ES_EINVAL);
std::vector<u8> bytes(request.in_vectors[0].size);
Memory::CopyFromEmu(bytes.data(), request.in_vectors[0].address, request.in_vectors[0].size);
IOS::ES::TicketReader ticket{std::move(bytes)};
IOS::ES::TicketReader ticket{ticket_bytes};
if (!ticket.IsValid())
return GetDefaultReply(ES_EINVAL);
return ES_EINVAL;
const u32 ticket_device_id = ticket.GetDeviceId();
const u32 device_id = EcWii::GetInstance().GetNGID();
@ -49,59 +43,67 @@ IPCCommandResult ES::AddTicket(const IOCtlVRequest& request)
if (device_id != ticket_device_id)
{
WARN_LOG(IOS_ES, "Device ID mismatch: ticket %08x, device %08x", ticket_device_id, device_id);
return GetDefaultReply(ES_DEVICE_ID_MISMATCH);
return ES_DEVICE_ID_MISMATCH;
}
const s32 ret = ticket.Unpersonalise();
const ReturnCode ret = static_cast<ReturnCode>(ticket.Unpersonalise());
if (ret < 0)
{
ERROR_LOG(IOS_ES, "AddTicket: Failed to unpersonalise ticket for %016" PRIx64 " (ret = %d)",
ERROR_LOG(IOS_ES, "ImportTicket: Failed to unpersonalise ticket for %016" PRIx64 " (%d)",
ticket.GetTitleId(), ret);
return GetDefaultReply(ret);
return ret;
}
}
if (!DiscIO::AddTicket(ticket))
return GetDefaultReply(ES_EIO);
return ES_EIO;
INFO_LOG(IOS_ES, "AddTicket: Imported ticket for title %016" PRIx64, ticket.GetTitleId());
return GetDefaultReply(IPC_SUCCESS);
INFO_LOG(IOS_ES, "ImportTicket: Imported ticket for title %016" PRIx64, ticket.GetTitleId());
return IPC_SUCCESS;
}
IPCCommandResult ES::AddTMD(Context& context, const IOCtlVRequest& request)
IPCCommandResult ES::ImportTicket(const IOCtlVRequest& request)
{
if (!request.HasNumberOfValidVectors(3, 0))
return GetDefaultReply(ES_EINVAL);
std::vector<u8> bytes(request.in_vectors[0].size);
Memory::CopyFromEmu(bytes.data(), request.in_vectors[0].address, request.in_vectors[0].size);
return GetDefaultReply(ImportTicket(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
// to either /import or /title. So here we simply have to set the import TMD.
context.title_import.tmd.SetBytes(tmd_bytes);
// TODO: validate TMDs and return the proper error code (-1027) if the signature type is invalid.
if (!context.title_import.tmd.IsValid())
return ES_EINVAL;
if (!IOS::ES::InitImport(context.title_import.tmd.GetTitleId()))
return ES_EIO;
return IPC_SUCCESS;
}
IPCCommandResult ES::ImportTmd(Context& context, const IOCtlVRequest& request)
{
if (!request.HasNumberOfValidVectors(1, 0))
return GetDefaultReply(ES_EINVAL);
std::vector<u8> tmd(request.in_vectors[0].size);
Memory::CopyFromEmu(tmd.data(), request.in_vectors[0].address, request.in_vectors[0].size);
// 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.tmd.SetBytes(std::move(tmd));
// TODO: validate TMDs and return the proper error code (-1027) if the signature type is invalid.
if (!context.title_import.tmd.IsValid())
return GetDefaultReply(ES_EINVAL);
if (!IOS::ES::InitImport(context.title_import.tmd.GetTitleId()))
return GetDefaultReply(FS_EIO);
return GetDefaultReply(IPC_SUCCESS);
return GetDefaultReply(ImportTmd(context, tmd));
}
IPCCommandResult ES::AddTitleStart(Context& context, const IOCtlVRequest& request)
ReturnCode ES::ImportTitleInit(Context& context, const std::vector<u8>& tmd_bytes)
{
if (!request.HasNumberOfValidVectors(4, 0))
return GetDefaultReply(ES_EINVAL);
INFO_LOG(IOS_ES, "IOCTL_ES_ADDTITLESTART");
std::vector<u8> tmd(request.in_vectors[0].size);
Memory::CopyFromEmu(tmd.data(), request.in_vectors[0].address, request.in_vectors[0].size);
context.title_import.tmd.SetBytes(tmd);
INFO_LOG(IOS_ES, "ImportTitleInit");
context.title_import.tmd.SetBytes(tmd_bytes);
if (!context.title_import.tmd.IsValid())
{
ERROR_LOG(IOS_ES, "Invalid TMD while adding title (size = %zd)", tmd.size());
return GetDefaultReply(ES_EINVAL);
ERROR_LOG(IOS_ES, "Invalid TMD while adding title (size = %zd)", tmd_bytes.size());
return ES_EINVAL;
}
// Finish a previous import (if it exists).
@ -111,43 +113,47 @@ IPCCommandResult ES::AddTitleStart(Context& context, const IOCtlVRequest& reques
FinishImport(previous_tmd);
if (!IOS::ES::InitImport(context.title_import.tmd.GetTitleId()))
return GetDefaultReply(FS_EIO);
return ES_EIO;
// TODO: check and use the other vectors.
return GetDefaultReply(IPC_SUCCESS);
return IPC_SUCCESS;
}
IPCCommandResult ES::AddContentStart(Context& context, const IOCtlVRequest& request)
IPCCommandResult ES::ImportTitleInit(Context& context, const IOCtlVRequest& request)
{
if (!request.HasNumberOfValidVectors(2, 0))
if (!request.HasNumberOfValidVectors(4, 0))
return GetDefaultReply(ES_EINVAL);
u64 title_id = Memory::Read_U64(request.in_vectors[0].address);
u32 content_id = Memory::Read_U32(request.in_vectors[1].address);
std::vector<u8> tmd(request.in_vectors[0].size);
Memory::CopyFromEmu(tmd.data(), request.in_vectors[0].address, request.in_vectors[0].size);
return GetDefaultReply(ImportTitleInit(context, tmd));
}
ReturnCode ES::ImportContentBegin(Context& context, u64 title_id, u32 content_id)
{
if (context.title_import.content_id != 0xFFFFFFFF)
{
ERROR_LOG(IOS_ES, "Trying to add content when we haven't finished adding "
"another content. Unsupported.");
return GetDefaultReply(ES_EINVAL);
return ES_EINVAL;
}
context.title_import.content_id = content_id;
context.title_import.content_buffer.clear();
INFO_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTSTART: title id %016" PRIx64 ", "
"content id %08x",
title_id, context.title_import.content_id);
INFO_LOG(IOS_ES, "ImportContentBegin: title %016" PRIx64 ", content ID %08x", title_id,
context.title_import.content_id);
if (!context.title_import.tmd.IsValid())
return GetDefaultReply(ES_EINVAL);
return ES_EINVAL;
if (title_id != context.title_import.tmd.GetTitleId())
{
ERROR_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTSTART: title id %016" PRIx64 " != "
ERROR_LOG(IOS_ES, "ImportContentBegin: title id %016" PRIx64 " != "
"TMD title id %016" PRIx64 ", ignoring",
title_id, context.title_import.tmd.GetTitleId());
return ES_EINVAL;
}
// We're supposed to return a "content file descriptor" here, which is
@ -155,24 +161,36 @@ IPCCommandResult ES::AddContentStart(Context& context, const IOCtlVRequest& requ
// no known content installer which performs content addition concurrently.
// Instead we just log an error (see above) if this condition is detected.
s32 content_fd = 0;
return GetDefaultReply(content_fd);
return static_cast<ReturnCode>(content_fd);
}
IPCCommandResult ES::AddContentData(Context& context, const IOCtlVRequest& request)
IPCCommandResult ES::ImportContentBegin(Context& context, const IOCtlVRequest& request)
{
if (!request.HasNumberOfValidVectors(2, 0))
return GetDefaultReply(ES_EINVAL);
u64 title_id = Memory::Read_U64(request.in_vectors[0].address);
u32 content_id = Memory::Read_U32(request.in_vectors[1].address);
return GetDefaultReply(ImportContentBegin(context, title_id, content_id));
}
ReturnCode ES::ImportContentData(Context& context, u32 content_fd, const u8* data, u32 data_size)
{
INFO_LOG(IOS_ES, "ImportContentData: content fd %08x, size %d", content_fd, data_size);
context.title_import.content_buffer.insert(context.title_import.content_buffer.end(), data,
data + data_size);
return IPC_SUCCESS;
}
IPCCommandResult ES::ImportContentData(Context& context, const IOCtlVRequest& request)
{
if (!request.HasNumberOfValidVectors(2, 0))
return GetDefaultReply(ES_EINVAL);
u32 content_fd = Memory::Read_U32(request.in_vectors[0].address);
INFO_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTDATA: content fd %08x, "
"size %d",
content_fd, request.in_vectors[1].size);
u8* data_start = Memory::GetPointer(request.in_vectors[1].address);
u8* data_end = data_start + request.in_vectors[1].size;
context.title_import.content_buffer.insert(context.title_import.content_buffer.end(), data_start,
data_end);
return GetDefaultReply(IPC_SUCCESS);
return GetDefaultReply(
ImportContentData(context, content_fd, data_start, request.in_vectors[1].size));
}
static bool CheckIfContentHashMatches(const std::vector<u8>& content, const IOS::ES::Content& info)
@ -187,34 +205,27 @@ static std::string GetImportContentPath(u64 title_id, u32 content_id)
return Common::GetImportTitlePath(title_id) + StringFromFormat("/content/%08x.app", content_id);
}
IPCCommandResult ES::AddContentFinish(Context& context, const IOCtlVRequest& request)
ReturnCode ES::ImportContentEnd(Context& context, u32 content_fd)
{
if (!request.HasNumberOfValidVectors(1, 0))
return GetDefaultReply(ES_EINVAL);
INFO_LOG(IOS_ES, "ImportContentEnd: content fd %08x", content_fd);
if (context.title_import.content_id == 0xFFFFFFFF)
return GetDefaultReply(ES_EINVAL);
u32 content_fd = Memory::Read_U32(request.in_vectors[0].address);
INFO_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTFINISH: content fd %08x", content_fd);
return ES_EINVAL;
if (!context.title_import.tmd.IsValid())
return GetDefaultReply(ES_EINVAL);
return ES_EINVAL;
// Try to find the title key from a pre-installed ticket.
IOS::ES::TicketReader ticket = DiscIO::FindSignedTicket(context.title_import.tmd.GetTitleId());
if (!ticket.IsValid())
{
return GetDefaultReply(ES_NO_TICKET);
}
return ES_NO_TICKET;
// The IV for title content decryption is the lower two bytes of the
// content index, zero extended.
IOS::ES::Content content_info;
if (!context.title_import.tmd.FindContentById(context.title_import.content_id, &content_info))
{
return GetDefaultReply(ES_EINVAL);
}
return ES_EINVAL;
u8 iv[16] = {0};
iv[0] = (content_info.index >> 8) & 0xFF;
iv[1] = content_info.index & 0xFF;
@ -223,8 +234,8 @@ IPCCommandResult ES::AddContentFinish(Context& context, const IOCtlVRequest& req
context.title_import.content_buffer.size());
if (!CheckIfContentHashMatches(decrypted_data, content_info))
{
ERROR_LOG(IOS_ES, "AddContentFinish: Hash for content %08x doesn't match", content_info.id);
return GetDefaultReply(ES_HASH_MISMATCH);
ERROR_LOG(IOS_ES, "ImportContentEnd: Hash for content %08x doesn't match", content_info.id);
return ES_HASH_MISMATCH;
}
std::string content_path;
@ -248,56 +259,82 @@ IPCCommandResult ES::AddContentFinish(Context& context, const IOCtlVRequest& req
File::IOFile file(temp_path, "wb");
if (!file.WriteBytes(decrypted_data.data(), content_info.size))
{
ERROR_LOG(IOS_ES, "AddContentFinish: Failed to write to %s", temp_path.c_str());
return GetDefaultReply(ES_EIO);
ERROR_LOG(IOS_ES, "ImportContentEnd: Failed to write to %s", temp_path.c_str());
return ES_EIO;
}
}
if (!File::Rename(temp_path, content_path))
{
ERROR_LOG(IOS_ES, "AddContentFinish: Failed to move content to %s", content_path.c_str());
return GetDefaultReply(ES_EIO);
ERROR_LOG(IOS_ES, "ImportContentEnd: Failed to move content to %s", content_path.c_str());
return ES_EIO;
}
context.title_import.content_id = 0xFFFFFFFF;
return GetDefaultReply(IPC_SUCCESS);
return IPC_SUCCESS;
}
IPCCommandResult ES::AddTitleFinish(Context& context, const IOCtlVRequest& request)
IPCCommandResult ES::ImportContentEnd(Context& context, const IOCtlVRequest& request)
{
if (!request.HasNumberOfValidVectors(0, 0) || !context.title_import.tmd.IsValid())
if (!request.HasNumberOfValidVectors(1, 0))
return GetDefaultReply(ES_EINVAL);
u32 content_fd = Memory::Read_U32(request.in_vectors[0].address);
return GetDefaultReply(ImportContentEnd(context, content_fd));
}
ReturnCode ES::ImportTitleDone(Context& context)
{
if (!context.title_import.tmd.IsValid())
return ES_EINVAL;
if (!WriteImportTMD(context.title_import.tmd))
return GetDefaultReply(ES_EIO);
return ES_EIO;
if (!FinishImport(context.title_import.tmd))
return GetDefaultReply(FS_EIO);
return ES_EIO;
INFO_LOG(IOS_ES, "IOCTL_ES_ADDTITLEFINISH");
INFO_LOG(IOS_ES, "ImportTitleDone: title %016" PRIx64, context.title_import.tmd.GetTitleId());
context.title_import.tmd.SetBytes({});
return GetDefaultReply(IPC_SUCCESS);
return IPC_SUCCESS;
}
IPCCommandResult ES::AddTitleCancel(Context& context, const IOCtlVRequest& request)
IPCCommandResult ES::ImportTitleDone(Context& context, const IOCtlVRequest& request)
{
if (!request.HasNumberOfValidVectors(0, 0) || !context.title_import.tmd.IsValid())
if (!request.HasNumberOfValidVectors(0, 0))
return GetDefaultReply(ES_EINVAL);
return GetDefaultReply(ImportTitleDone(context));
}
ReturnCode ES::ImportTitleCancel(Context& context)
{
if (!context.title_import.tmd.IsValid())
return ES_EINVAL;
const IOS::ES::TMDReader original_tmd =
IOS::ES::FindInstalledTMD(context.title_import.tmd.GetTitleId());
if (!original_tmd.IsValid())
{
// This should never happen unless someone messed with the installed TMD directly.
// Still, let's check for this case and return an error instead of potentially crashing.
return GetDefaultReply(FS_ENOENT);
return FS_ENOENT;
}
if (!FinishImport(original_tmd))
return GetDefaultReply(FS_EIO);
return ES_EIO;
INFO_LOG(IOS_ES, "ImportTitleCancel: title %016" PRIx64, context.title_import.tmd.GetTitleId());
context.title_import.tmd.SetBytes({});
return GetDefaultReply(IPC_SUCCESS);
return IPC_SUCCESS;
}
IPCCommandResult ES::ImportTitleCancel(Context& context, const IOCtlVRequest& request)
{
if (!request.HasNumberOfValidVectors(0, 0) || !context.title_import.tmd.IsValid())
return GetDefaultReply(ES_EINVAL);
return GetDefaultReply(ImportTitleCancel(context));
}
static bool CanDeleteTitle(u64 title_id)
@ -306,51 +343,47 @@ static bool CanDeleteTitle(u64 title_id)
return static_cast<u32>(title_id >> 32) != 0x00000001 || static_cast<u32>(title_id) > 0x101;
}
ReturnCode ES::DeleteTitle(u64 title_id)
{
if (!CanDeleteTitle(title_id))
return ES_EINVAL;
const std::string title_dir = Common::GetTitlePath(title_id, Common::FROM_SESSION_ROOT);
if (!File::IsDirectory(title_dir))
return FS_ENOENT;
if (!File::DeleteDirRecursively(title_dir))
{
ERROR_LOG(IOS_ES, "DeleteTitle: Failed to delete title directory: %s", title_dir.c_str());
return FS_EACCESS;
}
// XXX: ugly, but until we drop CNANDContentManager everywhere, this is going to be needed.
DiscIO::CNANDContentManager::Access().ClearCache();
return IPC_SUCCESS;
}
IPCCommandResult ES::DeleteTitle(const IOCtlVRequest& request)
{
if (!request.HasNumberOfValidVectors(1, 0) || request.in_vectors[0].size != 8)
return GetDefaultReply(ES_EINVAL);
const u64 title_id = Memory::Read_U64(request.in_vectors[0].address);
if (!CanDeleteTitle(title_id))
return GetDefaultReply(ES_EINVAL);
const std::string title_dir = Common::GetTitlePath(title_id, Common::FROM_SESSION_ROOT);
if (!File::IsDirectory(title_dir))
return GetDefaultReply(FS_ENOENT);
if (!File::DeleteDirRecursively(title_dir))
{
ERROR_LOG(IOS_ES, "DeleteTitle: Failed to delete title directory: %s", title_dir.c_str());
return GetDefaultReply(FS_EACCESS);
}
// XXX: ugly, but until we drop CNANDContentManager everywhere, this is going to be needed.
DiscIO::CNANDContentManager::Access().ClearCache();
return GetDefaultReply(IPC_SUCCESS);
return GetDefaultReply(DeleteTitle(title_id));
}
IPCCommandResult ES::DeleteTicket(const IOCtlVRequest& request)
ReturnCode ES::DeleteTicket(const u8* ticket_view)
{
if (!request.HasNumberOfValidVectors(1, 0) ||
request.in_vectors[0].size != sizeof(IOS::ES::TicketView))
{
return GetDefaultReply(ES_EINVAL);
}
const u64 title_id =
Memory::Read_U64(request.in_vectors[0].address + offsetof(IOS::ES::TicketView, title_id));
const u64 title_id = Common::swap64(ticket_view + offsetof(IOS::ES::TicketView, title_id));
if (!CanDeleteTitle(title_id))
return GetDefaultReply(ES_EINVAL);
return ES_EINVAL;
auto ticket = DiscIO::FindSignedTicket(title_id);
if (!ticket.IsValid())
return GetDefaultReply(FS_ENOENT);
return FS_ENOENT;
const u64 ticket_id =
Memory::Read_U64(request.in_vectors[0].address + offsetof(IOS::ES::TicketView, ticket_id));
const u64 ticket_id = Common::swap64(ticket_view + offsetof(IOS::ES::TicketView, ticket_id));
ticket.DeleteTicket(ticket_id);
const std::vector<u8>& new_ticket = ticket.GetRawTicket();
@ -358,7 +391,7 @@ IPCCommandResult ES::DeleteTicket(const IOCtlVRequest& request)
{
File::IOFile ticket_file(ticket_path, "wb");
if (!ticket_file || !ticket_file.WriteBytes(new_ticket.data(), new_ticket.size()))
return GetDefaultReply(ES_EIO);
return ES_EIO;
}
// Delete the ticket file if it is now empty.
@ -373,7 +406,17 @@ IPCCommandResult ES::DeleteTicket(const IOCtlVRequest& request)
if (ticket_parent_dir_entries.children.empty())
File::DeleteDir(ticket_parent_dir);
return GetDefaultReply(IPC_SUCCESS);
return IPC_SUCCESS;
}
IPCCommandResult ES::DeleteTicket(const IOCtlVRequest& request)
{
if (!request.HasNumberOfValidVectors(1, 0) ||
request.in_vectors[0].size != sizeof(IOS::ES::TicketView))
{
return GetDefaultReply(ES_EINVAL);
}
return GetDefaultReply(DeleteTicket(Memory::GetPointer(request.in_vectors[0].address)));
}
ReturnCode ES::DeleteTitleContent(u64 title_id) const
@ -401,37 +444,82 @@ IPCCommandResult ES::DeleteTitleContent(const IOCtlVRequest& request)
return GetDefaultReply(DeleteTitleContent(Memory::Read_U64(request.in_vectors[0].address)));
}
IPCCommandResult ES::ExportTitleInit(Context& context, const IOCtlVRequest& request)
ReturnCode ES::ExportTitleInit(Context& context, u64 title_id, u8* tmd_bytes, u32 tmd_size)
{
if (!request.HasNumberOfValidVectors(1, 1) || request.in_vectors[0].size != 8)
return GetDefaultReply(ES_EINVAL);
// No concurrent title import/export is allowed.
if (context.title_export.valid)
return GetDefaultReply(ES_EINVAL);
return ES_EINVAL;
const auto tmd = IOS::ES::FindInstalledTMD(Memory::Read_U64(request.in_vectors[0].address));
const auto tmd = IOS::ES::FindInstalledTMD(title_id);
if (!tmd.IsValid())
return GetDefaultReply(FS_ENOENT);
return FS_ENOENT;
context.title_export.tmd = tmd;
const auto ticket = DiscIO::FindSignedTicket(context.title_export.tmd.GetTitleId());
if (!ticket.IsValid())
return GetDefaultReply(ES_NO_TICKET);
return ES_NO_TICKET;
if (ticket.GetTitleId() != context.title_export.tmd.GetTitleId())
return GetDefaultReply(ES_EINVAL);
return ES_EINVAL;
context.title_export.title_key = ticket.GetTitleKey();
const auto& raw_tmd = context.title_export.tmd.GetRawTMD();
if (request.io_vectors[0].size != raw_tmd.size())
return GetDefaultReply(ES_EINVAL);
if (tmd_size != raw_tmd.size())
return ES_EINVAL;
Memory::CopyToEmu(request.io_vectors[0].address, raw_tmd.data(), raw_tmd.size());
std::copy_n(raw_tmd.cbegin(), raw_tmd.size(), tmd_bytes);
context.title_export.valid = true;
return GetDefaultReply(IPC_SUCCESS);
return IPC_SUCCESS;
}
IPCCommandResult ES::ExportTitleInit(Context& context, const IOCtlVRequest& request)
{
if (!request.HasNumberOfValidVectors(1, 1) || request.in_vectors[0].size != 8)
return GetDefaultReply(ES_EINVAL);
const u64 title_id = Memory::Read_U64(request.in_vectors[0].address);
u8* tmd_bytes = Memory::GetPointer(request.io_vectors[0].address);
const u32 tmd_size = request.io_vectors[0].size;
return GetDefaultReply(ExportTitleInit(context, title_id, tmd_bytes, tmd_size));
}
ReturnCode ES::ExportContentBegin(Context& context, u64 title_id, u32 content_id)
{
if (!context.title_export.valid || context.title_export.tmd.GetTitleId() != title_id)
{
ERROR_LOG(IOS_ES, "Tried to use ExportContentBegin with an invalid title export context.");
return ES_EINVAL;
}
const auto& content_loader = AccessContentDevice(title_id);
if (!content_loader.IsValid())
return FS_ENOENT;
const auto* content = content_loader.GetContentByID(content_id);
if (!content)
return ES_EINVAL;
OpenedContent entry;
entry.m_position = 0;
entry.m_content = content->m_metadata;
entry.m_title_id = title_id;
content->m_Data->Open();
u32 cfd = 0;
while (context.title_export.contents.find(cfd) != context.title_export.contents.end())
cfd++;
TitleExportContext::ExportContent content_export;
content_export.content = std::move(entry);
content_export.iv[0] = (content->m_metadata.index >> 8) & 0xFF;
content_export.iv[1] = content->m_metadata.index & 0xFF;
context.title_export.contents.emplace(cfd, content_export);
// IOS returns a content ID which is passed to further content calls.
return static_cast<ReturnCode>(cfd);
}
IPCCommandResult ES::ExportContentBegin(Context& context, const IOCtlVRequest& request)
@ -443,38 +531,44 @@ IPCCommandResult ES::ExportContentBegin(Context& context, const IOCtlVRequest& r
const u64 title_id = Memory::Read_U64(request.in_vectors[0].address);
const u32 content_id = Memory::Read_U32(request.in_vectors[1].address);
if (!context.title_export.valid || context.title_export.tmd.GetTitleId() != title_id)
return GetDefaultReply(ExportContentBegin(context, title_id, content_id));
}
ReturnCode ES::ExportContentData(Context& context, u32 content_fd, u8* data, u32 data_size)
{
const auto iterator = context.title_export.contents.find(content_fd);
if (!context.title_export.valid || iterator == context.title_export.contents.end() ||
iterator->second.content.m_position >= iterator->second.content.m_content.size)
{
ERROR_LOG(IOS_ES, "Tried to use ExportContentBegin with an invalid title export context.");
return GetDefaultReply(ES_EINVAL);
return ES_EINVAL;
}
const auto& content_loader = AccessContentDevice(title_id);
if (!content_loader.IsValid())
return GetDefaultReply(FS_ENOENT);
auto& metadata = iterator->second.content;
const auto* content = content_loader.GetContentByID(content_id);
if (!content)
return GetDefaultReply(ES_EINVAL);
OpenedContent entry;
entry.m_position = 0;
entry.m_content = content->m_metadata;
entry.m_title_id = title_id;
const auto& content_loader = AccessContentDevice(metadata.m_title_id);
const auto* content = content_loader.GetContentByID(metadata.m_content.id);
content->m_Data->Open();
u32 cid = 0;
while (context.title_export.contents.find(cid) != context.title_export.contents.end())
cid++;
const u32 length =
std::min(static_cast<u32>(metadata.m_content.size - metadata.m_position), data_size);
std::vector<u8> buffer(length);
TitleExportContext::ExportContent content_export;
content_export.content = std::move(entry);
content_export.iv[0] = (content->m_metadata.index >> 8) & 0xFF;
content_export.iv[1] = content->m_metadata.index & 0xFF;
if (!content->m_Data->GetRange(metadata.m_position, length, buffer.data()))
{
ERROR_LOG(IOS_ES, "ExportContentData: ES_SHORT_READ");
return ES_SHORT_READ;
}
context.title_export.contents.emplace(cid, content_export);
// IOS returns a content ID which is passed to further content calls.
return GetDefaultReply(cid);
// IOS aligns the buffer to 32 bytes. Since we also need to align it to 16 bytes,
// let's just follow IOS here.
buffer.resize(Common::AlignUp(buffer.size(), 32));
const std::vector<u8> output =
Common::AES::Encrypt(context.title_export.title_key.data(), iterator->second.iv.data(),
buffer.data(), buffer.size());
std::copy_n(output.cbegin(), output.size(), data);
metadata.m_position += length;
return IPC_SUCCESS;
}
IPCCommandResult ES::ExportContentData(Context& context, const IOCtlVRequest& request)
@ -485,57 +579,20 @@ IPCCommandResult ES::ExportContentData(Context& context, const IOCtlVRequest& re
return GetDefaultReply(ES_EINVAL);
}
const u32 content_id = Memory::Read_U32(request.in_vectors[0].address);
const u32 content_fd = Memory::Read_U32(request.in_vectors[0].address);
u8* data = Memory::GetPointer(request.io_vectors[0].address);
const u32 bytes_to_read = request.io_vectors[0].size;
const auto iterator = context.title_export.contents.find(content_id);
if (!context.title_export.valid || iterator == context.title_export.contents.end() ||
iterator->second.content.m_position >= iterator->second.content.m_content.size)
{
return GetDefaultReply(ES_EINVAL);
}
auto& metadata = iterator->second.content;
const auto& content_loader = AccessContentDevice(metadata.m_title_id);
const auto* content = content_loader.GetContentByID(metadata.m_content.id);
content->m_Data->Open();
const u32 length =
std::min(static_cast<u32>(metadata.m_content.size - metadata.m_position), bytes_to_read);
std::vector<u8> buffer(length);
if (!content->m_Data->GetRange(metadata.m_position, length, buffer.data()))
{
ERROR_LOG(IOS_ES, "ExportContentData: ES_SHORT_READ");
return GetDefaultReply(ES_SHORT_READ);
}
// IOS aligns the buffer to 32 bytes. Since we also need to align it to 16 bytes,
// let's just follow IOS here.
buffer.resize(Common::AlignUp(buffer.size(), 32));
const std::vector<u8> output =
Common::AES::Encrypt(context.title_export.title_key.data(), iterator->second.iv.data(),
buffer.data(), buffer.size());
Memory::CopyToEmu(request.io_vectors[0].address, output.data(), output.size());
metadata.m_position += length;
return GetDefaultReply(IPC_SUCCESS);
return GetDefaultReply(ExportContentData(context, content_fd, data, bytes_to_read));
}
IPCCommandResult ES::ExportContentEnd(Context& context, const IOCtlVRequest& request)
ReturnCode ES::ExportContentEnd(Context& context, u32 content_fd)
{
if (!request.HasNumberOfValidVectors(1, 0) || request.in_vectors[0].size != 4)
return GetDefaultReply(ES_EINVAL);
const u32 content_id = Memory::Read_U32(request.in_vectors[0].address);
const auto iterator = context.title_export.contents.find(content_id);
const auto iterator = context.title_export.contents.find(content_fd);
if (!context.title_export.valid || iterator == context.title_export.contents.end() ||
iterator->second.content.m_position != iterator->second.content.m_content.size)
{
return GetDefaultReply(ES_EINVAL);
return ES_EINVAL;
}
// XXX: Check the content hash, as IOS does?
@ -544,16 +601,30 @@ IPCCommandResult ES::ExportContentEnd(Context& context, const IOCtlVRequest& req
content_loader.GetContentByID(iterator->second.content.m_content.id)->m_Data->Close();
context.title_export.contents.erase(iterator);
return GetDefaultReply(IPC_SUCCESS);
return IPC_SUCCESS;
}
IPCCommandResult ES::ExportContentEnd(Context& context, const IOCtlVRequest& request)
{
if (!request.HasNumberOfValidVectors(1, 0) || request.in_vectors[0].size != 4)
return GetDefaultReply(ES_EINVAL);
const u32 content_fd = Memory::Read_U32(request.in_vectors[0].address);
return GetDefaultReply(ExportContentEnd(context, content_fd));
}
ReturnCode ES::ExportTitleDone(Context& context)
{
if (!context.title_export.valid)
return ES_EINVAL;
context.title_export.valid = false;
return IPC_SUCCESS;
}
IPCCommandResult ES::ExportTitleDone(Context& context, const IOCtlVRequest& request)
{
if (!context.title_export.valid)
return GetDefaultReply(ES_EINVAL);
context.title_export.valid = false;
return GetDefaultReply(IPC_SUCCESS);
return GetDefaultReply(ExportTitleDone(context));
}
} // namespace Device
} // namespace HLE