diff --git a/Source/Core/Core/IOS/ES/ES.cpp b/Source/Core/Core/IOS/ES/ES.cpp index 6023325681..85eb87de8a 100644 --- a/Source/Core/Core/IOS/ES/ES.cpp +++ b/Source/Core/Core/IOS/ES/ES.cpp @@ -514,11 +514,12 @@ IPCCommandResult ES::IOCtlV(const IOCtlVRequest& request) case IOCTL_ES_GET_TICKET_FROM_VIEW: return GetTicketFromView(request); + case IOCTL_ES_SET_UP_STREAM_KEY: + return SetUpStreamKey(*context, request); case IOCTL_ES_DELETE_STREAM_KEY: return DeleteStreamKey(request); case IOCTL_ES_VERIFYSIGN: - case IOCTL_ES_UNKNOWN_3C: case IOCTL_ES_UNKNOWN_41: case IOCTL_ES_UNKNOWN_42: PanicAlert("IOS-ES: Unimplemented ioctlv 0x%x (%zu in vectors, %zu io vectors)", @@ -646,6 +647,118 @@ s32 ES::DIVerify(const IOS::ES::TMDReader& tmd, const IOS::ES::TicketReader& tic return IPC_SUCCESS; } +constexpr u32 FIRST_PPC_UID = 0x1000; + +ReturnCode ES::CheckStreamKeyPermissions(const u32 uid, const u8* ticket_view, + const IOS::ES::TMDReader& tmd) const +{ + const u32 title_flags = tmd.GetTitleFlags(); + // Only allow using this function with some titles (WFS titles). + // The following is the exact check from IOS. Unfortunately, other than knowing that the + // title type is what IOS checks, we don't know much about the constants used here. + constexpr u32 WFS_AND_0x4_FLAG = IOS::ES::TITLE_TYPE_0x4 | IOS::ES::TITLE_TYPE_WFS_MAYBE; + if ((!(title_flags & IOS::ES::TITLE_TYPE_0x4) && ~(title_flags >> 5) & 1) || + (title_flags & WFS_AND_0x4_FLAG) == WFS_AND_0x4_FLAG) + { + return ES_EINVAL; + } + + // This function can only be used by specific UIDs, depending on the title type. + // It cannot be used at all internally, unless the request comes from the WFS process. + // It can only be used from the PPC for some title types. + // Note: PID 0x19 is used by the WFS modules. + if (uid < FIRST_PPC_UID && uid != PID_UNKNOWN) + return ES_EINVAL; + + // If the title type is of this specific type, then this function is limited to WFS. + if (title_flags & IOS::ES::TITLE_TYPE_WFS_MAYBE && uid != PID_UNKNOWN) + return ES_EINVAL; + + const u64 view_title_id = Common::swap64(ticket_view + offsetof(IOS::ES::TicketView, title_id)); + if (view_title_id != tmd.GetTitleId()) + return ES_EINVAL; + + // More permission checks. + const u32 permitted_title_mask = + Common::swap32(ticket_view + offsetof(IOS::ES::TicketView, permitted_title_mask)); + const u32 permitted_title_id = + Common::swap32(ticket_view + offsetof(IOS::ES::TicketView, permitted_title_id)); + if ((uid == PID_UNKNOWN && (~permitted_title_mask & 0x13) != permitted_title_id) || + !IsActiveTitlePermittedByTicket(ticket_view)) + { + return ES_EACCES; + } + + return IPC_SUCCESS; +} + +ReturnCode ES::SetUpStreamKey(const u32 uid, const u8* ticket_view, const IOS::ES::TMDReader& tmd, + u32* handle) +{ + ReturnCode ret = CheckStreamKeyPermissions(uid, ticket_view, tmd); + if (ret != IPC_SUCCESS) + return ret; + + // TODO (for the future): signature checks. + + // Find a signed ticket from the view. + const u64 ticket_id = Common::swap64(&ticket_view[offsetof(IOS::ES::TicketView, ticket_id)]); + const u64 title_id = Common::swap64(&ticket_view[offsetof(IOS::ES::TicketView, title_id)]); + const IOS::ES::TicketReader installed_ticket = DiscIO::FindSignedTicket(title_id); + // Unlike the other "get ticket from view" function, this returns a FS error, not ES_NO_TICKET. + if (!installed_ticket.IsValid()) + return FS_ENOENT; + const std::vector ticket_bytes = installed_ticket.GetRawTicket(ticket_id); + if (ticket_bytes.empty()) + return ES_NO_TICKET; + + // Create the handle and return it. + std::array iv{}; + std::memcpy(iv.data(), &title_id, sizeof(title_id)); + ret = m_ios.GetIOSC().CreateObject(handle, IOSC::ObjectType::TYPE_SECRET_KEY, + IOSC::ObjectSubType::SUBTYPE_AES128, PID_ES); + if (ret != IPC_SUCCESS) + return ret; + + const u32 owner_uid = uid >= FIRST_PPC_UID ? PID_PPCBOOT : uid; + ret = m_ios.GetIOSC().SetOwnership(*handle, 1 << owner_uid, PID_ES); + if (ret != IPC_SUCCESS) + return ret; + + const u8 index = ticket_bytes[offsetof(IOS::ES::Ticket, common_key_index)]; + if (index > 1) + return ES_INVALID_TICKET; + + auto common_key_handle = index == 0 ? IOSC::HANDLE_COMMON_KEY : IOSC::HANDLE_NEW_COMMON_KEY; + return m_ios.GetIOSC().ImportSecretKey(*handle, common_key_handle, iv.data(), + &ticket_bytes[offsetof(IOS::ES::Ticket, title_key)], + PID_ES); +} + +IPCCommandResult ES::SetUpStreamKey(const Context& context, const IOCtlVRequest& request) +{ + if (!request.HasNumberOfValidVectors(2, 1) || + request.in_vectors[0].size != sizeof(IOS::ES::TicketView) || + !IOS::ES::IsValidTMDSize(request.in_vectors[1].size) || + request.io_vectors[0].size != sizeof(u32)) + { + return GetDefaultReply(ES_EINVAL); + } + + std::vector tmd_bytes(request.in_vectors[1].size); + Memory::CopyFromEmu(tmd_bytes.data(), request.in_vectors[1].address, tmd_bytes.size()); + const IOS::ES::TMDReader tmd{std::move(tmd_bytes)}; + + if (!tmd.IsValid()) + return GetDefaultReply(ES_EINVAL); + + u32 handle; + const ReturnCode ret = + SetUpStreamKey(context.uid, Memory::GetPointer(request.in_vectors[0].address), tmd, &handle); + Memory::Write_U32(handle, request.io_vectors[0].address); + return GetDefaultReply(ret); +} + IPCCommandResult ES::DeleteStreamKey(const IOCtlVRequest& request) { if (!request.HasNumberOfValidVectors(1, 0) || request.in_vectors[0].size != sizeof(u32)) @@ -654,6 +767,19 @@ IPCCommandResult ES::DeleteStreamKey(const IOCtlVRequest& request) const u32 handle = Memory::Read_U32(request.in_vectors[0].address); return GetDefaultReply(m_ios.GetIOSC().DeleteObject(handle, PID_ES)); } + +bool ES::IsActiveTitlePermittedByTicket(const u8* ticket_view) const +{ + if (!GetTitleContext().active) + return false; + + const u32 title_identifier = static_cast(GetTitleContext().tmd.GetTitleId()); + const u32 permitted_title_mask = + Common::swap32(ticket_view + offsetof(IOS::ES::TicketView, permitted_title_mask)); + const u32 permitted_title_id = + Common::swap32(ticket_view + offsetof(IOS::ES::TicketView, permitted_title_id)); + return title_identifier && (title_identifier & ~permitted_title_mask) == permitted_title_id; +} } // namespace Device } // namespace HLE } // namespace IOS diff --git a/Source/Core/Core/IOS/ES/ES.h b/Source/Core/Core/IOS/ES/ES.h index b56ac57b69..51ad6dffa7 100644 --- a/Source/Core/Core/IOS/ES/ES.h +++ b/Source/Core/Core/IOS/ES/ES.h @@ -121,6 +121,9 @@ public: ReturnCode GetV0TicketFromView(const u8* ticket_view, u8* ticket) const; ReturnCode GetTicketFromView(const u8* ticket_view, u8* ticket, u32* ticket_size) const; + ReturnCode SetUpStreamKey(u32 uid, const u8* ticket_view, const IOS::ES::TMDReader& tmd, + u32* handle); + private: enum { @@ -183,7 +186,7 @@ private: IOCTL_ES_DIGETTMDSIZE = 0x39, IOCTL_ES_DIGETTMD = 0x3A, IOCTL_ES_DIVERIFY_WITH_VIEW = 0x3B, - IOCTL_ES_UNKNOWN_3C = 0x3C, + IOCTL_ES_SET_UP_STREAM_KEY = 0x3C, IOCTL_ES_DELETE_STREAM_KEY = 0x3D, IOCTL_ES_DELETE_CONTENT = 0x3E, IOCTL_ES_INVALID_3F = 0x3F, @@ -234,6 +237,7 @@ private: IPCCommandResult Launch(const IOCtlVRequest& request); IPCCommandResult LaunchBC(const IOCtlVRequest& request); IPCCommandResult DIVerify(const IOCtlVRequest& request); + IPCCommandResult SetUpStreamKey(const Context& context, const IOCtlVRequest& request); IPCCommandResult DeleteStreamKey(const IOCtlVRequest& request); // Title contents @@ -283,6 +287,10 @@ private: bool LaunchIOS(u64 ios_title_id); bool LaunchPPCTitle(u64 title_id, bool skip_reload); static TitleContext& GetTitleContext(); + bool IsActiveTitlePermittedByTicket(const u8* ticket_view) const; + + ReturnCode CheckStreamKeyPermissions(u32 uid, const u8* ticket_view, + const IOS::ES::TMDReader& tmd) const; static const DiscIO::NANDContentLoader& AccessContentDevice(u64 title_id); diff --git a/Source/Core/Core/IOS/ES/Formats.cpp b/Source/Core/Core/IOS/ES/Formats.cpp index ba99dc77a6..66ebb0dff4 100644 --- a/Source/Core/Core/IOS/ES/Formats.cpp +++ b/Source/Core/Core/IOS/ES/Formats.cpp @@ -154,6 +154,11 @@ u64 TMDReader::GetTitleId() const return Common::swap64(m_bytes.data() + offsetof(TMDHeader, title_id)); } +u32 TMDReader::GetTitleFlags() const +{ + return Common::swap32(m_bytes.data() + offsetof(TMDHeader, title_flags)); +} + u16 TMDReader::GetTitleVersion() const { return Common::swap16(m_bytes.data() + offsetof(TMDHeader, title_version)); diff --git a/Source/Core/Core/IOS/ES/Formats.h b/Source/Core/Core/IOS/ES/Formats.h index 6d53baf1fc..77db4ec3b3 100644 --- a/Source/Core/Core/IOS/ES/Formats.h +++ b/Source/Core/Core/IOS/ES/Formats.h @@ -39,6 +39,20 @@ bool IsTitleType(u64 title_id, TitleType title_type); bool IsDiscTitle(u64 title_id); bool IsChannel(u64 title_id); +enum TitleFlags : u32 +{ + // All official titles have this flag set. + TITLE_TYPE_DEFAULT = 0x1, + // Unknown. + TITLE_TYPE_0x4 = 0x4, + // Used for DLC titles. + TITLE_TYPE_DATA = 0x8, + // Appears to be used for WFS titles. + TITLE_TYPE_WFS_MAYBE = 0x20, + // Unknown. + TITLE_TYPE_CT = 0x40, +}; + #pragma pack(push, 4) struct TMDHeader { @@ -51,7 +65,7 @@ struct TMDHeader u8 signer_crl_version; u64 ios_id; u64 title_id; - u32 title_type; + u32 title_flags; u16 group_id; u16 zero; u16 region; @@ -154,6 +168,7 @@ public: u64 GetIOSId() const; DiscIO::Region GetRegion() const; u64 GetTitleId() const; + u32 GetTitleFlags() const; u16 GetTitleVersion() const; u16 GetGroupId() const;