mirror of
https://github.com/stenzek/duckstation.git
synced 2024-11-23 05:49:43 +00:00
Achievements: Add encryption of login tokens
This commit is contained in:
parent
23e9bf0f28
commit
116a98fd1d
@ -217,6 +217,47 @@ typedef struct rc_client_user_game_summary_t {
|
|||||||
*/
|
*/
|
||||||
RC_EXPORT void RC_CCONV rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary);
|
RC_EXPORT void RC_CCONV rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple encryption that can be used to protect API tokens in configuration. We do not provide
|
||||||
|
* any cryptographic guarantees other than a significantly increased difficulty of getting the
|
||||||
|
* API token if a user shares their configuration file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define RC_CLIENT_LOGIN_ENCRYPTION_KEY_LENGTH 32 /* 128-bit key, 128-bit IV */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the AES key to use for encryption API tokens.
|
||||||
|
* This key is based on a UUID that is unique to the computer that it is running on, with an
|
||||||
|
* optional salt. One possible option for a salt is the username.
|
||||||
|
*/
|
||||||
|
RC_EXPORT int RC_CCONV rc_client_get_login_encryption_key(uint8_t key[RC_CLIENT_LOGIN_ENCRYPTION_KEY_LENGTH],
|
||||||
|
const void* salt, size_t salt_length);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the length of a given API token after it has been encrypted. AES operates in blocks
|
||||||
|
* of 128 bits/16 bytes, therefore the encrypted token must be a multiple of 16 bytes in size.
|
||||||
|
*/
|
||||||
|
RC_EXPORT size_t RC_CCONV rc_client_get_encrypted_login_length(size_t plaintext_length);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts an API token, protecting it against accidental exposure by sharing configurations.
|
||||||
|
* The token is encrypted using AES-128. You should convert the returned ciphertext to hex before
|
||||||
|
* storing it in a configuration file, as it will contain non-printable characters. ciphertext
|
||||||
|
* should be sized according to rc_client_get_encrypted_login_length().
|
||||||
|
*/
|
||||||
|
RC_EXPORT int RC_CCONV rc_client_encrypt_login(const uint8_t key[RC_CLIENT_LOGIN_ENCRYPTION_KEY_LENGTH],
|
||||||
|
const char* plaintext, size_t plaintext_length, uint8_t* ciphertext,
|
||||||
|
size_t ciphertext_length);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts a previously-encrypted API token.
|
||||||
|
* plaintext should be at least ciphertext_length in size. If the token is not a multiple of 16
|
||||||
|
* bytes in size, the actual size of the token will be returned in plaintext_length.
|
||||||
|
*/
|
||||||
|
RC_EXPORT int RC_CCONV rc_client_decrypt_login(const uint8_t key[RC_CLIENT_LOGIN_ENCRYPTION_KEY_LENGTH],
|
||||||
|
const uint8_t* ciphertext, size_t ciphertext_length, char* plaintext,
|
||||||
|
size_t* plaintext_length);
|
||||||
|
|
||||||
/*****************************************************************************\
|
/*****************************************************************************\
|
||||||
| Game |
|
| Game |
|
||||||
\*****************************************************************************/
|
\*****************************************************************************/
|
||||||
|
@ -11,6 +11,10 @@
|
|||||||
|
|
||||||
#include "rcheevos/rc_internal.h"
|
#include "rcheevos/rc_internal.h"
|
||||||
|
|
||||||
|
/* TODO: Is this appropriate? */
|
||||||
|
#include "rhash/aes.h"
|
||||||
|
#include "rhash/sha256.h"
|
||||||
|
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
@ -18,6 +22,8 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <profileapi.h>
|
#include <profileapi.h>
|
||||||
#else
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -901,6 +907,168 @@ void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_g
|
|||||||
rc_mutex_unlock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */
|
rc_mutex_unlock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int rc_client_get_machine_uuid_string(const void** uuid, size_t* uuid_length)
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
HKEY hKey;
|
||||||
|
DWORD machine_guid_length;
|
||||||
|
const char* machine_guid;
|
||||||
|
|
||||||
|
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Cryptography", 0, KEY_READ, &hKey) != ERROR_SUCCESS)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (RegGetValueA(hKey, NULL, "MachineGuid", RRF_RT_REG_SZ, NULL, NULL, &machine_guid_length) != ERROR_SUCCESS)
|
||||||
|
{
|
||||||
|
RegCloseKey(hKey);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
machine_guid = (char*)malloc(machine_guid_length);
|
||||||
|
if (machine_guid == NULL ||
|
||||||
|
RegGetValueA(hKey, NULL, "MachineGuid", RRF_RT_REG_SZ, NULL, machine_guid, &machine_guid_length) !=
|
||||||
|
ERROR_SUCCESS ||
|
||||||
|
machine_guid_length <= 1)
|
||||||
|
{
|
||||||
|
free(machine_guid);
|
||||||
|
RegCloseKey(hKey);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegCloseKey(hKey);
|
||||||
|
*uuid = machine_guid;
|
||||||
|
*uuid_length = machine_guid_length - 1; /* drop terminating zero */
|
||||||
|
return 1;
|
||||||
|
#else
|
||||||
|
long hostid;
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
/* use /etc/machine-id on Linux */
|
||||||
|
int fd;
|
||||||
|
struct stat sd;
|
||||||
|
char* machine_id;
|
||||||
|
|
||||||
|
fd = open("/etc/machine-id", O_RDONLY);
|
||||||
|
if (fd >= 0)
|
||||||
|
{
|
||||||
|
if (fstat(fd, &sd) == 0 && sd.st_size > 0)
|
||||||
|
{
|
||||||
|
machine_id = (char*)malloc(sd.st_size);
|
||||||
|
if (machine_id != NULL && read(fd, machine_id, sd.st_size) == sd.st_size)
|
||||||
|
{
|
||||||
|
*uuid = machine_id;
|
||||||
|
*uuid_length = sd.st_size;
|
||||||
|
close(fd);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(machine_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// fallback to POSIX gethostid()
|
||||||
|
hostid = gethostid();
|
||||||
|
*uuid = malloc(sizeof(hostid));
|
||||||
|
memcpy((void*)uuid, &hostid, sizeof(hostid));
|
||||||
|
return 1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc_client_get_login_encryption_key(uint8_t key[RC_CLIENT_LOGIN_ENCRYPTION_KEY_LENGTH], const void* salt,
|
||||||
|
size_t salt_length)
|
||||||
|
{
|
||||||
|
/* super basic key stretching */
|
||||||
|
static const int EXTRA_ROUNDS = 100;
|
||||||
|
|
||||||
|
const void* machine_uuid;
|
||||||
|
size_t machine_uuid_length;
|
||||||
|
SHA256_CTX sha256_ctx;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (!rc_client_get_machine_uuid_string(&machine_uuid, &machine_uuid_length))
|
||||||
|
return RC_API_FAILURE;
|
||||||
|
|
||||||
|
sha256_init(&sha256_ctx);
|
||||||
|
sha256_update(&sha256_ctx, (const uint8_t*)machine_uuid, machine_uuid_length);
|
||||||
|
if (salt_length > 0)
|
||||||
|
sha256_update(&sha256_ctx, (const uint8_t*)salt, salt_length);
|
||||||
|
sha256_final(&sha256_ctx, key);
|
||||||
|
|
||||||
|
for (int i = 0; i < EXTRA_ROUNDS; i++)
|
||||||
|
{
|
||||||
|
sha256_init(&sha256_ctx);
|
||||||
|
sha256_update(&sha256_ctx, key, RC_CLIENT_LOGIN_ENCRYPTION_KEY_LENGTH);
|
||||||
|
sha256_final(&sha256_ctx, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(machine_uuid);
|
||||||
|
|
||||||
|
return RC_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t rc_client_get_encrypted_login_length(size_t plaintext_length)
|
||||||
|
{
|
||||||
|
/* ciphertext should be aligned to block size */
|
||||||
|
return (((plaintext_length + (AES_BLOCKLEN - 1)) / AES_BLOCKLEN) * AES_BLOCKLEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc_client_encrypt_login(const uint8_t key[RC_CLIENT_LOGIN_ENCRYPTION_KEY_LENGTH], const char* plaintext,
|
||||||
|
size_t plaintext_length, uint8_t* ciphertext, size_t ciphertext_length)
|
||||||
|
{
|
||||||
|
struct AES_ctx aes_ctx;
|
||||||
|
size_t padding_size;
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
/* ciphertext should be aligned to block size */
|
||||||
|
if (ciphertext_length < (((plaintext_length + (AES_BLOCKLEN - 1)) / AES_BLOCKLEN) * AES_BLOCKLEN) ||
|
||||||
|
(ciphertext_length % AES_BLOCKLEN) != 0)
|
||||||
|
{
|
||||||
|
return RC_INSUFFICIENT_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(ciphertext, plaintext, plaintext_length);
|
||||||
|
padding_size = ciphertext_length - plaintext_length;
|
||||||
|
if (padding_size > 0)
|
||||||
|
memset(&ciphertext[plaintext_length], 0, padding_size);
|
||||||
|
|
||||||
|
AES_init_ctx(&aes_ctx, key);
|
||||||
|
AES_ctx_set_iv(&aes_ctx, key + 16); /* second part of 256-bit key is the IV */
|
||||||
|
AES_CBC_encrypt_buffer(&aes_ctx, ciphertext, ciphertext_length);
|
||||||
|
return RC_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc_client_decrypt_login(const uint8_t key[RC_CLIENT_LOGIN_ENCRYPTION_KEY_LENGTH], const uint8_t* ciphertext,
|
||||||
|
size_t ciphertext_length, char* plaintext, size_t* plaintext_length)
|
||||||
|
{
|
||||||
|
struct AES_ctx aes_ctx;
|
||||||
|
size_t len;
|
||||||
|
|
||||||
|
/* ciphertext should be aligned to block size */
|
||||||
|
if ((ciphertext_length % AES_BLOCKLEN) != 0)
|
||||||
|
return RC_INSUFFICIENT_BUFFER;
|
||||||
|
|
||||||
|
/* decrypt_buffer is an inout parameter, need to copy */
|
||||||
|
memcpy(plaintext, ciphertext, ciphertext_length);
|
||||||
|
|
||||||
|
AES_init_ctx(&aes_ctx, key);
|
||||||
|
AES_ctx_set_iv(&aes_ctx, key + 16); /* second part of 256-bit key is the IV */
|
||||||
|
AES_CBC_decrypt_buffer(&aes_ctx, plaintext, ciphertext_length);
|
||||||
|
|
||||||
|
/* may not be null terminated, and C89 has no safe strlen... */
|
||||||
|
len = 0;
|
||||||
|
while (len < ciphertext_length)
|
||||||
|
{
|
||||||
|
if (plaintext[len] == '\0')
|
||||||
|
break;
|
||||||
|
len++;
|
||||||
|
}
|
||||||
|
|
||||||
|
*plaintext_length = len;
|
||||||
|
return RC_OK;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Game ===== */
|
/* ===== Game ===== */
|
||||||
|
|
||||||
static void rc_client_free_game(rc_client_game_info_t* game)
|
static void rc_client_free_game(rc_client_game_info_t* game)
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
#include "imgui_internal.h"
|
#include "imgui_internal.h"
|
||||||
#include "rc_api_runtime.h"
|
#include "rc_api_runtime.h"
|
||||||
#include "rc_client.h"
|
#include "rc_client.h"
|
||||||
|
#include "rc_util.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
@ -123,6 +124,8 @@ struct AchievementProgressIndicator
|
|||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
using LoginEncryptionKey = std::array<u8, RC_CLIENT_LOGIN_ENCRYPTION_KEY_LENGTH>;
|
||||||
|
|
||||||
static void ReportError(std::string_view sv);
|
static void ReportError(std::string_view sv);
|
||||||
template<typename... T>
|
template<typename... T>
|
||||||
static void ReportFmtError(fmt::format_string<T...> fmt, T&&... args);
|
static void ReportFmtError(fmt::format_string<T...> fmt, T&&... args);
|
||||||
@ -144,6 +147,10 @@ static std::string GetLocalImagePath(const std::string_view image_name, int type
|
|||||||
static void DownloadImage(std::string url, std::string cache_filename);
|
static void DownloadImage(std::string url, std::string cache_filename);
|
||||||
static void UpdateGlyphRanges();
|
static void UpdateGlyphRanges();
|
||||||
|
|
||||||
|
static LoginEncryptionKey GetLoginEncryptionKey(std::string_view username);
|
||||||
|
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 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 DestroyClient(rc_client_t** client, std::unique_ptr<HTTPDownloader>* http);
|
||||||
static void ClientMessageCallback(const char* message, const rc_client_t* client);
|
static void ClientMessageCallback(const char* message, const rc_client_t* client);
|
||||||
@ -466,6 +473,65 @@ void Achievements::UpdateGlyphRanges()
|
|||||||
ImGuiManager::SetEmojiFontRange(ImGuiManager::CompactFontRange(sorted_codepoints));
|
ImGuiManager::SetEmojiFontRange(ImGuiManager::CompactFontRange(sorted_codepoints));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Achievements::LoginEncryptionKey Achievements::GetLoginEncryptionKey(std::string_view username)
|
||||||
|
{
|
||||||
|
LoginEncryptionKey ret;
|
||||||
|
|
||||||
|
const int res = rc_client_get_login_encryption_key(ret.data(), username.data(), username.length());
|
||||||
|
if (res != RC_OK)
|
||||||
|
{
|
||||||
|
WARNING_LOG("rc_client_get_login_encryption_key() failed: {}, assuming zero.", res);
|
||||||
|
ret = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
TinyString Achievements::EncryptLoginToken(std::string_view token, std::string_view username)
|
||||||
|
{
|
||||||
|
TinyString ret;
|
||||||
|
if (token.empty())
|
||||||
|
return ret;
|
||||||
|
|
||||||
|
const size_t len = rc_client_get_encrypted_login_length(token.length());
|
||||||
|
DynamicHeapArray<u8> encrypted(len);
|
||||||
|
const int res = rc_client_encrypt_login(GetLoginEncryptionKey(username).data(), token.data(), token.length(),
|
||||||
|
encrypted.data(), len);
|
||||||
|
if (res != RC_OK)
|
||||||
|
{
|
||||||
|
ERROR_LOG("rc_client_encrypt_login() failed: {}", res);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.append_hex(encrypted.data(), encrypted.size());
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
TinyString Achievements::DecryptLoginToken(std::string_view encrypted_token, std::string_view username)
|
||||||
|
{
|
||||||
|
TinyString ret;
|
||||||
|
if (encrypted_token.empty())
|
||||||
|
return ret;
|
||||||
|
|
||||||
|
const std::optional<std::vector<u8>> ciphertext = StringUtil::DecodeHex(encrypted_token);
|
||||||
|
if (!ciphertext.has_value() || ciphertext->size() >= std::numeric_limits<u32>::max())
|
||||||
|
return ret;
|
||||||
|
|
||||||
|
size_t token_length = ciphertext->size();
|
||||||
|
ret.reserve(static_cast<u32>(token_length));
|
||||||
|
|
||||||
|
const int res = rc_client_decrypt_login(GetLoginEncryptionKey(username).data(), ciphertext->data(),
|
||||||
|
ciphertext->size(), ret.data(), &token_length);
|
||||||
|
if (res != RC_OK)
|
||||||
|
{
|
||||||
|
ERROR_LOG("rc_client_decrypt_login() failed: {}", res);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.set_size(static_cast<u32>(token_length));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
bool Achievements::IsActive()
|
bool Achievements::IsActive()
|
||||||
{
|
{
|
||||||
#ifdef ENABLE_RAINTEGRATION
|
#ifdef ENABLE_RAINTEGRATION
|
||||||
@ -566,8 +632,18 @@ bool Achievements::Initialize()
|
|||||||
if (!username.empty() && !api_token.empty())
|
if (!username.empty() && !api_token.empty())
|
||||||
{
|
{
|
||||||
INFO_LOG("Attempting login with user '{}'...", username);
|
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.
|
// Hardcore mode isn't enabled when achievements first starts, if a game is already running.
|
||||||
@ -1878,7 +1954,7 @@ void Achievements::ClientLoginWithPasswordCallback(int result, const char* error
|
|||||||
|
|
||||||
// Store configuration.
|
// Store configuration.
|
||||||
Host::SetBaseStringSettingValue("Cheevos", "Username", params->username);
|
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::SetBaseStringSettingValue("Cheevos", "LoginTimestamp", fmt::format("{}", std::time(nullptr)).c_str());
|
||||||
Host::CommitBaseSettingChanges();
|
Host::CommitBaseSettingChanges();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user