Achievements: Add encryption of login tokens in ini
Super simple key derived from the machine's UUID. The idea isn't to provide a ton of security, but prevent users from accidentially exposing their tokens when sharing their ini for debugging purposes. The use of the machine UUID is disabled in portable mode for those who actually move it between computers. Instead, the key is derived from the username alone, which is trivially computable.
This commit is contained in:
parent
5401dc8d52
commit
9970944da2
|
@ -22,6 +22,7 @@
|
|||
#include "common/md5_digest.h"
|
||||
#include "common/path.h"
|
||||
#include "common/scoped_guard.h"
|
||||
#include "common/sha256_digest.h"
|
||||
#include "common/small_string.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common/timer.h"
|
||||
|
@ -144,6 +145,9 @@ static std::string GetLocalImagePath(const std::string_view image_name, int type
|
|||
static void DownloadImage(std::string url, std::string cache_filename);
|
||||
static void UpdateGlyphRanges();
|
||||
|
||||
static TinyString DecryptLoginToken(std::string_view encrypted_token, std::string_view username);
|
||||
static TinyString EncryptLoginToken(std::string_view token, std::string_view username);
|
||||
|
||||
static bool CreateClient(rc_client_t** client, std::unique_ptr<HTTPDownloader>* http);
|
||||
static void DestroyClient(rc_client_t** client, std::unique_ptr<HTTPDownloader>* http);
|
||||
static void ClientMessageCallback(const char* message, const rc_client_t* client);
|
||||
|
@ -569,8 +573,18 @@ bool Achievements::Initialize()
|
|||
if (!username.empty() && !api_token.empty())
|
||||
{
|
||||
INFO_LOG("Attempting login with user '{}'...", username);
|
||||
s_login_request = rc_client_begin_login_with_token(s_client, username.c_str(), api_token.c_str(),
|
||||
ClientLoginWithTokenCallback, nullptr);
|
||||
|
||||
// If we can't decrypt the token, it was an old config and we need to re-login.
|
||||
if (const TinyString decrypted_api_token = DecryptLoginToken(api_token, username); !decrypted_api_token.empty())
|
||||
{
|
||||
s_login_request = rc_client_begin_login_with_token(s_client, username.c_str(), decrypted_api_token.c_str(),
|
||||
ClientLoginWithTokenCallback, nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
WARNING_LOG("Invalid encrypted login token, requesitng a new one.");
|
||||
Host::OnAchievementsLoginRequested(LoginRequestReason::TokenInvalid);
|
||||
}
|
||||
}
|
||||
|
||||
// Hardcore mode isn't enabled when achievements first starts, if a game is already running.
|
||||
|
@ -1884,7 +1898,7 @@ void Achievements::ClientLoginWithPasswordCallback(int result, const char* error
|
|||
|
||||
// Store configuration.
|
||||
Host::SetBaseStringSettingValue("Cheevos", "Username", params->username);
|
||||
Host::SetBaseStringSettingValue("Cheevos", "Token", user->token);
|
||||
Host::SetBaseStringSettingValue("Cheevos", "Token", EncryptLoginToken(user->token, params->username));
|
||||
Host::SetBaseStringSettingValue("Cheevos", "LoginTimestamp", fmt::format("{}", std::time(nullptr)).c_str());
|
||||
Host::CommitBaseSettingChanges();
|
||||
|
||||
|
@ -3335,6 +3349,164 @@ void Achievements::CloseLeaderboard()
|
|||
ImGuiFullscreen::QueueResetFocus(ImGuiFullscreen::FocusResetType::Other);
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include "common/windows_headers.h"
|
||||
#elif !defined(__ANDROID__)
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "common/thirdparty/SmallVector.h"
|
||||
#include "common/thirdparty/aes.h"
|
||||
|
||||
#ifndef __ANDROID__
|
||||
|
||||
static TinyString GetLoginEncryptionMachineKey()
|
||||
{
|
||||
TinyString ret;
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
HKEY hKey;
|
||||
DWORD error;
|
||||
if ((error = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Cryptography", 0, KEY_READ, &hKey)) !=
|
||||
ERROR_SUCCESS)
|
||||
{
|
||||
WARNING_LOG("Open SOFTWARE\\Microsoft\\Cryptography failed for machine key failed: {}", error);
|
||||
return ret;
|
||||
}
|
||||
|
||||
DWORD machine_guid_length;
|
||||
if ((error = RegGetValueA(hKey, NULL, "MachineGuid", RRF_RT_REG_SZ, NULL, NULL, &machine_guid_length)) !=
|
||||
ERROR_SUCCESS)
|
||||
{
|
||||
WARNING_LOG("Get MachineGuid failed: {}", error);
|
||||
RegCloseKey(hKey);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ret.resize(machine_guid_length);
|
||||
if ((error = RegGetValueA(hKey, NULL, "MachineGuid", RRF_RT_REG_SZ, NULL, ret.data(), &machine_guid_length)) !=
|
||||
ERROR_SUCCESS ||
|
||||
machine_guid_length <= 1)
|
||||
{
|
||||
WARNING_LOG("Read MachineGuid failed: {}", error);
|
||||
ret = {};
|
||||
RegCloseKey(hKey);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ret.resize(machine_guid_length);
|
||||
RegCloseKey(hKey);
|
||||
#elif !defined(__ANDROID__)
|
||||
#ifdef __linux__
|
||||
// use /etc/machine-id on Linux
|
||||
std::optional<std::string> machine_id = FileSystem::ReadFileToString("/etc/machine-id");
|
||||
if (machine_id.has_value())
|
||||
ret = std::string_view(machine_id.value());
|
||||
#endif
|
||||
|
||||
if (ret.empty())
|
||||
{
|
||||
WARNING_LOG("Falling back to gethostid()");
|
||||
|
||||
// fallback to POSIX gethostid()
|
||||
const long hostid = gethostid();
|
||||
ret.format("{:08X}", hostid);
|
||||
}
|
||||
#endif
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
static std::array<u8, 32> GetLoginEncryptionKey(std::string_view username)
|
||||
{
|
||||
// super basic key stretching
|
||||
static constexpr u32 EXTRA_ROUNDS = 100;
|
||||
|
||||
SHA256Digest digest;
|
||||
|
||||
#ifndef __ANDROID__
|
||||
// Only use machine key if we're not running in portable mode.
|
||||
if (!EmuFolders::IsRunningInPortableMode())
|
||||
{
|
||||
const TinyString machine_key = GetLoginEncryptionMachineKey();
|
||||
if (!machine_key.empty())
|
||||
digest.Update(machine_key.cbspan());
|
||||
else
|
||||
WARNING_LOG("Failed to get machine key, token will be decipherable.");
|
||||
}
|
||||
#endif
|
||||
|
||||
// salt with username
|
||||
digest.Update(username.data(), username.length());
|
||||
|
||||
std::array<u8, 32> key = digest.Final();
|
||||
|
||||
for (u32 i = 0; i < EXTRA_ROUNDS; i++)
|
||||
key = SHA256Digest::GetDigest(key);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
TinyString Achievements::EncryptLoginToken(std::string_view token, std::string_view username)
|
||||
{
|
||||
TinyString ret;
|
||||
if (token.empty() || username.empty())
|
||||
return ret;
|
||||
|
||||
const auto key = GetLoginEncryptionKey(username);
|
||||
std::array<u32, AES_KEY_SCHEDULE_SIZE> key_schedule;
|
||||
aes_key_setup(&key[0], key_schedule.data(), 128);
|
||||
|
||||
// has to be padded to the block size
|
||||
llvm::SmallVector<u8, 64> data(reinterpret_cast<const u8*>(token.data()),
|
||||
reinterpret_cast<const u8*>(token.data() + token.length()));
|
||||
data.resize(Common::AlignUpPow2(token.length(), AES_BLOCK_SIZE), 0);
|
||||
aes_encrypt_cbc(data.data(), data.size(), data.data(), key_schedule.data(), 128, &key[16]);
|
||||
|
||||
// base64 encode it
|
||||
const std::span<const u8> data_span(data.data(), data.size());
|
||||
ret.resize(static_cast<u32>(StringUtil::EncodedBase64Length(data_span)));
|
||||
StringUtil::EncodeBase64(ret.span(), data_span);
|
||||
return ret;
|
||||
}
|
||||
|
||||
TinyString Achievements::DecryptLoginToken(std::string_view encrypted_token, std::string_view username)
|
||||
{
|
||||
TinyString ret;
|
||||
if (encrypted_token.empty() || username.empty())
|
||||
return ret;
|
||||
|
||||
const size_t encrypted_data_length = StringUtil::DecodedBase64Length(encrypted_token);
|
||||
if (encrypted_data_length == 0 || (encrypted_data_length % AES_BLOCK_SIZE) != 0)
|
||||
return ret;
|
||||
|
||||
const auto key = GetLoginEncryptionKey(username);
|
||||
std::array<u32, AES_KEY_SCHEDULE_SIZE> key_schedule;
|
||||
aes_key_setup(&key[0], key_schedule.data(), 128);
|
||||
|
||||
// has to be padded to the block size
|
||||
llvm::SmallVector<u8, 64> encrypted_data;
|
||||
encrypted_data.resize(encrypted_data_length);
|
||||
if (StringUtil::DecodeBase64(std::span<u8>(encrypted_data.data(), encrypted_data.size()), encrypted_token) !=
|
||||
encrypted_data_length)
|
||||
{
|
||||
WARNING_LOG("Failed to base64 decode encrypted login token.");
|
||||
return ret;
|
||||
}
|
||||
|
||||
aes_decrypt_cbc(encrypted_data.data(), encrypted_data.size(), encrypted_data.data(), key_schedule.data(), 128,
|
||||
&key[16]);
|
||||
|
||||
// remove any trailing null bytes
|
||||
const size_t real_length =
|
||||
StringUtil::Strnlen(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_data_length);
|
||||
ret.append(reinterpret_cast<const char*>(encrypted_data.data()), static_cast<u32>(real_length));
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_RAINTEGRATION
|
||||
|
||||
#include "RA_Consoles.h"
|
||||
|
|
Loading…
Reference in New Issue