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);
|
||||
|
||||
/**
|
||||
* 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 |
|
||||
\*****************************************************************************/
|
||||
|
@ -11,6 +11,10 @@
|
||||
|
||||
#include "rcheevos/rc_internal.h"
|
||||
|
||||
/* TODO: Is this appropriate? */
|
||||
#include "rhash/aes.h"
|
||||
#include "rhash/sha256.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
@ -18,6 +22,8 @@
|
||||
#include <windows.h>
|
||||
#include <profileapi.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#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 */
|
||||
}
|
||||
|
||||
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 ===== */
|
||||
|
||||
static void rc_client_free_game(rc_client_game_info_t* game)
|
||||
|
@ -41,6 +41,7 @@
|
||||
#include "imgui_internal.h"
|
||||
#include "rc_api_runtime.h"
|
||||
#include "rc_client.h"
|
||||
#include "rc_util.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
@ -123,6 +124,8 @@ struct AchievementProgressIndicator
|
||||
};
|
||||
} // namespace
|
||||
|
||||
using LoginEncryptionKey = std::array<u8, RC_CLIENT_LOGIN_ENCRYPTION_KEY_LENGTH>;
|
||||
|
||||
static void ReportError(std::string_view sv);
|
||||
template<typename... T>
|
||||
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 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 void DestroyClient(rc_client_t** client, std::unique_ptr<HTTPDownloader>* http);
|
||||
static void ClientMessageCallback(const char* message, const rc_client_t* client);
|
||||
@ -466,6 +473,65 @@ void Achievements::UpdateGlyphRanges()
|
||||
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()
|
||||
{
|
||||
#ifdef ENABLE_RAINTEGRATION
|
||||
@ -566,8 +632,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.
|
||||
@ -1878,7 +1954,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();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user