Files
Xenia-Canary/src/xenia/kernel/xam/profile_manager.cc

614 lines
18 KiB
C++

/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2024 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include <regex>
#include "xenia/kernel/xam/profile_manager.h"
#include "xenia/base/logging.h"
#include "xenia/emulator.h"
#include "xenia/hid/input_system.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/util/crypto_utils.h"
#include "xenia/vfs/devices/host_path_device.h"
DEFINE_string(logged_profile_slot_0_xuid, "",
"XUID of the profile to load on boot in slot 0", "Profiles");
DEFINE_string(logged_profile_slot_1_xuid, "",
"XUID of the profile to load on boot in slot 1", "Profiles");
DEFINE_string(logged_profile_slot_2_xuid, "",
"XUID of the profile to load on boot in slot 2", "Profiles");
DEFINE_string(logged_profile_slot_3_xuid, "",
"XUID of the profile to load on boot in slot 3", "Profiles");
namespace xe {
namespace kernel {
namespace xam {
bool ProfileManager::DecryptAccountFile(const uint8_t* data,
X_XAMACCOUNTINFO* output, bool devkit) {
const uint8_t* key = util::GetXeKey(0x19, devkit);
if (!key) {
return false; // this shouldn't happen...
}
// Generate RC4 key from data hash
uint8_t rc4_key[0x14];
util::HmacSha(key, 0x10, data, 0x10, 0, 0, 0, 0, rc4_key, 0x14);
uint8_t dec_data[sizeof(X_XAMACCOUNTINFO) + 8];
// Decrypt data
util::RC4(rc4_key, 0x10, data + 0x10, sizeof(dec_data), dec_data,
sizeof(dec_data));
// Verify decrypted data against hash
uint8_t data_hash[0x14];
util::HmacSha(key, 0x10, dec_data, sizeof(dec_data), 0, 0, 0, 0, data_hash,
0x14);
if (std::memcmp(data, data_hash, 0x10) == 0) {
// Copy account data to output
std::memcpy(output, dec_data + 8, sizeof(X_XAMACCOUNTINFO));
return true;
}
return false;
}
void ProfileManager::EncryptAccountFile(const X_XAMACCOUNTINFO* input,
uint8_t* output, bool devkit) {
const uint8_t* key = util::GetXeKey(0x19, devkit);
if (!key) {
return; // this shouldn't happen...
}
X_XAMACCOUNTINFO* output_acct =
reinterpret_cast<X_XAMACCOUNTINFO*>(output + 0x18);
std::memcpy(output_acct, input, sizeof(X_XAMACCOUNTINFO));
// Set confounder, should be random but meh
std::memset(output + 0x10, 0xFD, 8);
// Encrypted data = xam account info + 8 byte confounder
uint32_t enc_data_size = sizeof(X_XAMACCOUNTINFO) + 8;
// Set data hash
uint8_t data_hash[0x14];
util::HmacSha(key, 0x10, output + 0x10, enc_data_size, 0, 0, 0, 0, data_hash,
0x14);
std::memcpy(output, data_hash, 0x10);
// Generate RC4 key from data hash
uint8_t rc4_key[0x14];
util::HmacSha(key, 0x10, data_hash, 0x10, 0, 0, 0, 0, rc4_key, 0x14);
// Encrypt data
util::RC4(rc4_key, 0x10, output + 0x10, enc_data_size, output + 0x10,
enc_data_size);
}
ProfileManager::ProfileManager(KernelState* kernel_state,
UserTracker* user_tracker)
: kernel_state_(kernel_state), user_tracker_(user_tracker) {
logged_profiles_.clear();
accounts_.clear();
for (const auto account_xuid : FindProfiles()) {
LoadAccount(account_xuid);
}
if (!cvars::logged_profile_slot_0_xuid.empty()) {
Login(xe::string_util::from_string<uint64_t>(
cvars::logged_profile_slot_0_xuid, true),
0);
}
if (!cvars::logged_profile_slot_1_xuid.empty()) {
Login(xe::string_util::from_string<uint64_t>(
cvars::logged_profile_slot_1_xuid, true),
1);
}
if (!cvars::logged_profile_slot_2_xuid.empty()) {
Login(xe::string_util::from_string<uint64_t>(
cvars::logged_profile_slot_2_xuid, true),
2);
}
if (!cvars::logged_profile_slot_3_xuid.empty()) {
Login(xe::string_util::from_string<uint64_t>(
cvars::logged_profile_slot_3_xuid, true),
3);
}
}
void ProfileManager::ReloadProfile(const uint64_t xuid) {
if (accounts_.contains(xuid)) {
accounts_.erase(xuid);
}
LoadAccount(xuid);
}
void ProfileManager::ReloadProfiles() {
for (const auto account_xuid : FindProfiles()) {
LoadAccount(account_xuid);
}
}
UserProfile* ProfileManager::GetProfile(const uint64_t xuid) const {
const uint8_t user_index = GetUserIndexAssignedToProfile(xuid);
if (user_index >= XUserMaxUserCount) {
return nullptr;
}
return GetProfile(user_index);
}
UserProfile* ProfileManager::GetProfile(uint8_t user_index) const {
if (user_index == XUserIndexLatest) {
user_index = kernel_state_->emulator()->input_system()->GetLastUsedSlot();
}
if (user_index == XUserIndexNone) {
return nullptr;
}
if (user_index == XUserIndexAny) {
for (uint8_t i = 0; i < XUserMaxUserCount; i++) {
if (!logged_profiles_.count(i)) {
continue;
}
return logged_profiles_.at(i).get();
}
}
if (!logged_profiles_.count(user_index)) {
return nullptr;
}
return logged_profiles_.at(user_index).get();
}
bool ProfileManager::LoadAccount(const uint64_t xuid) {
const std::string xuid_as_string = fmt::format("{:016X}", xuid);
XELOGI("{}: Loading Account: {}", __func__, xuid_as_string);
MountProfile(xuid);
const std::string guest_path =
fmt::format(kDefaultMountFormat, xuid) + ":\\Account";
xe::vfs::File* output_file;
xe::vfs::FileAction action = {};
auto status = kernel_state_->file_system()->OpenFile(
nullptr, guest_path, xe::vfs::FileDisposition::kOpen,
xe::vfs::FileAccess::kFileReadData, false, true, &output_file, &action);
if (XFAILED(status) || !output_file || !output_file->entry()) {
XELOGI("{}: Failed to open Account file: {:08X}", __func__, status);
DismountProfile(xuid);
return false;
}
std::vector<uint8_t> file_data;
file_data.resize(output_file->entry()->size());
size_t bytes_read = 0;
output_file->ReadSync(
std::span<uint8_t>(file_data.data(), output_file->entry()->size()), 0,
&bytes_read);
output_file->Destroy();
if (bytes_read < sizeof(X_XAMACCOUNTINFO)) {
DismountProfile(xuid);
return false;
}
X_XAMACCOUNTINFO tmp_acct;
if (!ProfileManager::DecryptAccountFile(file_data.data(), &tmp_acct)) {
if (!ProfileManager::DecryptAccountFile(file_data.data(), &tmp_acct,
true)) {
XELOGW("Failed to decrypt account data file for XUID: {}",
xuid_as_string);
return false;
}
}
// We don't need profile to be mounted anymore, so for now let's close it.
// We need it only when we want to login into this account!
DismountProfile(xuid);
accounts_.insert_or_assign(xuid, tmp_acct);
return true;
}
bool ProfileManager::MountProfile(const uint64_t xuid, std::string mount_path) {
std::filesystem::path profile_path = GetProfilePath(xuid);
if (mount_path.empty()) {
mount_path = fmt::format(kDefaultMountFormat, xuid);
}
mount_path += ':';
auto device =
std::make_unique<vfs::HostPathDevice>(mount_path, profile_path, false);
if (!device->Initialize()) {
XELOGE(
"MountProfile: Unable to mount {} profile; file not found or "
"corrupted.",
profile_path);
return false;
}
return kernel_state_->file_system()->RegisterDevice(std::move(device));
}
bool ProfileManager::DismountProfile(const uint64_t xuid) {
return kernel_state_->file_system()->UnregisterDevice(
fmt::format(kDefaultMountFormat, xuid) + ':');
}
void ProfileManager::Login(const uint64_t xuid, const uint8_t user_index,
bool notify) {
if (logged_profiles_.size() >= XUserMaxUserCount) {
XELOGE(
"Cannot login account with XUID: {:016X} due to lack of free slots "
"(Max 4 accounts at once)",
xuid);
return;
}
if (user_index < XUserMaxUserCount) {
const auto& profile = logged_profiles_.find(user_index);
if (profile != logged_profiles_.cend()) {
if (profile->second && profile->second->xuid() == xuid) {
// Do nothing! User is already signed in to that slot.
return;
}
}
}
// Find if xuid is already logged in. We might want to logout.
auto it = std::find_if(
logged_profiles_.begin(), logged_profiles_.end(),
[xuid](const auto& entry) { return entry.second->xuid() == xuid; });
if (it != logged_profiles_.end()) {
Logout(it->first);
}
if (!accounts_.count(xuid)) {
return;
}
auto& profile = accounts_[xuid];
const uint8_t assigned_user_slot =
user_index < XUserMaxUserCount ? user_index : FindFirstFreeProfileSlot();
XELOGI("Loaded {} (GUID: {:016X}) to slot {}", profile.GetGamertagString(),
xuid, assigned_user_slot);
MountProfile(xuid);
logged_profiles_[assigned_user_slot] =
std::make_unique<UserProfile>(xuid, &profile);
user_tracker_->AddUser(xuid);
if (notify) {
kernel_state_->BroadcastNotification(kXNotificationSystemSignInChanged,
GetUsedUserSlots().to_ulong());
}
UpdateConfig(xuid, assigned_user_slot);
}
void ProfileManager::Logout(const uint8_t user_index, bool notify) {
auto profile = logged_profiles_.find(user_index);
if (profile == logged_profiles_.cend()) {
return;
}
kernel_state_->xam_state()->user_tracker()->RemoveUser(
profile->second->xuid());
DismountProfile(profile->second->xuid());
logged_profiles_.erase(profile);
if (notify) {
kernel_state_->BroadcastNotification(kXNotificationSystemSignInChanged,
GetUsedUserSlots().to_ulong());
}
UpdateConfig(0, user_index);
}
void ProfileManager::LoginMultiple(
const std::map<uint8_t, uint64_t>& profiles) {
int slots_mask = 0;
for (auto [slot, xuid] : profiles) {
Login(xuid, slot, false);
slots_mask |= (1 << slot);
}
kernel_state_->BroadcastNotification(kXNotificationSystemSignInChanged,
slots_mask);
}
std::vector<uint64_t> ProfileManager::FindProfiles() const {
// Info: Profile directory name is also it's offline xuid
std::vector<uint64_t> profiles_xuids;
auto profiles_directory = xe::filesystem::FilterByName(
xe::filesystem::ListDirectories(
kernel_state_->emulator()->content_root()),
std::regex("[0-9A-F]{16}"));
for (const auto& profile : profiles_directory) {
const std::string profile_xuid = xe::path_to_utf8(profile.name);
if (profile_xuid == fmt::format("{:016X}", 0)) {
continue;
}
if (!std::filesystem::exists(
profile.path / profile.name / kDashboardStringID /
fmt::format("{:08X}",
static_cast<uint32_t>(XContentType::kProfile)) /
profile.name)) {
XELOGE("Profile {} doesn't have profile package!", profile_xuid);
continue;
}
XELOGI("{}: Adding profile {} to profile list", __func__, profile_xuid);
profiles_xuids.push_back(
xe::string_util::from_string<uint64_t>(profile_xuid, true));
}
XELOGI("ProfileManager: Found {} Profiles", profiles_xuids.size());
return profiles_xuids;
}
uint8_t ProfileManager::FindFirstFreeProfileSlot() const {
if (!IsAnyProfileSignedIn()) {
return 0;
}
std::bitset<XUserMaxUserCount> used_slots = {};
for (const auto& [index, entry] : logged_profiles_) {
used_slots.set(index);
}
for (uint8_t i = 0; i < used_slots.size(); ++i) {
if (!used_slots[i]) {
return i;
}
}
return XUserIndexAny;
}
std::bitset<XUserMaxUserCount> ProfileManager::GetUsedUserSlots() const {
std::bitset<XUserMaxUserCount> used_slots = {};
for (const auto& [index, entry] : logged_profiles_) {
if (!entry) {
continue;
}
used_slots.set(index);
}
return used_slots;
}
uint8_t ProfileManager::GetUserIndexAssignedToProfile(
const uint64_t xuid) const {
for (const auto& [index, entry] : logged_profiles_) {
if (!entry) {
continue;
}
if (entry->xuid() != xuid) {
continue;
}
return index;
}
return XUserIndexAny;
}
std::filesystem::path ProfileManager::GetProfileContentPath(
const uint64_t xuid, const uint32_t title_id,
const XContentType content_type) const {
std::filesystem::path profile_content_path =
kernel_state_->emulator()->content_root() / fmt::format("{:016X}", xuid);
if (title_id != -1 && title_id != 0) {
profile_content_path =
profile_content_path / fmt::format("{:08X}", title_id);
if (content_type != XContentType::kInvalid) {
profile_content_path =
profile_content_path /
fmt::format("{:08X}", static_cast<uint32_t>(content_type));
}
}
return profile_content_path;
}
std::filesystem::path ProfileManager::GetProfilePath(
const uint64_t xuid) const {
return GetProfilePath(fmt::format("{:016X}", xuid));
}
std::filesystem::path ProfileManager::GetProfilePath(
const std::string xuid) const {
return kernel_state_->emulator()->content_root() / xuid / kDashboardStringID /
fmt::format("{:08X}", static_cast<uint32_t>(XContentType::kProfile)) /
xuid;
}
bool ProfileManager::CreateProfile(const std::string gamertag, bool autologin,
bool default_xuid) {
const auto xuid = !default_xuid ? GenerateXuid() : 0xB13EBABEBABEBABE;
if (!std::filesystem::create_directories(GetProfilePath(xuid))) {
return false;
}
if (!MountProfile(xuid)) {
return false;
}
const bool is_account_created = CreateAccount(xuid, gamertag);
if (is_account_created && autologin) {
Login(xuid);
}
return is_account_created;
}
bool ProfileManager::CreateProfile(const X_XAMACCOUNTINFO* account_info,
uint64_t xuid) {
if (!xuid) {
xuid = GenerateXuid();
}
if (!std::filesystem::create_directories(GetProfilePath(xuid))) {
return false;
}
if (!MountProfile(xuid)) {
return false;
}
return CreateAccount(xuid, account_info);
}
const X_XAMACCOUNTINFO* ProfileManager::GetAccount(const uint64_t xuid) {
if (!accounts_.count(xuid)) {
return nullptr;
}
return &accounts_[xuid];
}
bool ProfileManager::CreateAccount(const uint64_t xuid,
const std::string gamertag) {
X_XAMACCOUNTINFO account = {};
const std::u16string gamertag_u16 = xe::to_utf16(gamertag);
string_util::copy_and_swap_truncating(account.gamertag, gamertag_u16,
sizeof(account.gamertag));
const bool result = UpdateAccount(xuid, &account);
DismountProfile(xuid);
if (result) {
accounts_.insert({xuid, account});
}
return result;
}
bool ProfileManager::CreateAccount(const uint64_t xuid,
const X_XAMACCOUNTINFO* account) {
const bool result = UpdateAccount(xuid, account);
if (result) {
accounts_.insert({xuid, *account});
}
return result;
}
bool ProfileManager::UpdateAccount(const uint64_t xuid,
const X_XAMACCOUNTINFO* account) {
const std::string guest_path =
fmt::format(kDefaultMountFormat, xuid) + ":\\Account";
xe::vfs::File* output_file;
xe::vfs::FileAction action = {};
auto status = kernel_state_->file_system()->OpenFile(
nullptr, guest_path, xe::vfs::FileDisposition::kOpenIf,
xe::vfs::FileAccess::kFileWriteData, false, true, &output_file, &action);
if (XFAILED(status) || !output_file || !output_file->entry()) {
XELOGI("{}: Failed to open Account file for creation: {:08X}", __func__,
status);
return false;
}
std::vector<uint8_t> encrypted_data;
encrypted_data.resize(sizeof(X_XAMACCOUNTINFO) + 0x18);
EncryptAccountFile(account, encrypted_data.data());
size_t written_bytes = 0;
output_file->WriteSync(
std::span<uint8_t>(encrypted_data.data(), encrypted_data.size()), 0,
&written_bytes);
output_file->Destroy();
// Refresh the in-memory account data
accounts_.insert_or_assign(xuid, *account);
return true;
}
void ProfileManager::UpdateConfig(const uint64_t xuid, const uint8_t slot) {
if (slot >= XUserMaxUserCount) {
return;
}
const std::string hex_xuid = xe::string_util::to_hex_string(xuid);
switch (slot) {
case 0:
OVERRIDE_string(logged_profile_slot_0_xuid, hex_xuid);
break;
case 1:
OVERRIDE_string(logged_profile_slot_1_xuid, hex_xuid);
break;
case 2:
OVERRIDE_string(logged_profile_slot_2_xuid, hex_xuid);
break;
case 3:
OVERRIDE_string(logged_profile_slot_3_xuid, hex_xuid);
break;
default:
break;
}
return;
}
bool ProfileManager::DeleteProfile(const uint64_t xuid) {
const uint8_t user_index = GetUserIndexAssignedToProfile(xuid);
if (user_index < XUserMaxUserCount) {
Logout(user_index);
}
DismountProfile(xuid);
if (accounts_.count(xuid)) {
accounts_.erase(xuid);
}
std::error_code ec;
std::filesystem::remove_all(GetProfileContentPath(xuid), ec);
if (ec) {
XELOGE("Cannot remove profile: {}", ec.message());
return false;
}
return true;
}
bool ProfileManager::IsGamertagValid(const std::string gamertag) {
std::regex pattern(R"(^[A-Za-z][A-Za-z0-9]*( [A-Za-z0-9]+)*$)");
if (gamertag.length() < 1 || gamertag.length() > 15) {
return false;
}
return std::regex_match(gamertag, pattern);
}
} // namespace xam
} // namespace kernel
} // namespace xe