Skip to content

Commit 2f90ef0

Browse files
committed
fix unicode paths and locale handling
1 parent a21bc12 commit 2f90ef0

3 files changed

Lines changed: 119 additions & 53 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,11 @@ jobs:
5959
#include "auth-test-access.hpp"
6060
#include <dpapi.h>
6161
#include <iostream>
62+
#include <clocale>
6263
#include <type_traits>
6364
#include <cstdlib>
6465
extern std::string curl_escape(CURL*, const std::string&);
66+
extern std::string checksum();
6567
extern std::string get_str_between_two_str(const std::string&, const std::string&, const std::string&);
6668
namespace KeyAuth {
6769
struct keyauth_test_access {
@@ -366,6 +368,18 @@ jobs:
366368
if (!condition) std::abort();
367369
}
368370
int run() {
371+
const int previous_mode = _configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
372+
const std::string previous_locale = std::setlocale(LC_CTYPE, nullptr);
373+
for (const char* locale : {"C", "tr-TR", "ja-JP", "zh-CN", "pt-BR"}) {
374+
if (!std::setlocale(LC_CTYPE, locale)) return 1;
375+
keyauth_detail::SignatureHeaders localized;
376+
keyauth_detail::consume_header(localized, "X-SIGNATURE-ED25519: valid\r\n");
377+
keyauth_detail::consume_header(localized, "X-SIGNATURE-TIMESTAMP: 1770000000\r\n");
378+
if (localized.signature != "valid" || localized.timestamp != "1770000000") return 1;
379+
if (keyauth_detail::ascii_lower('I') != 'i') return 1;
380+
}
381+
std::setlocale(LC_CTYPE, previous_locale.c_str());
382+
_configthreadlocale(previous_mode);
369383
keyauth_detail::SignatureHeaders h;
370384
using keyauth_detail::consume_header;
371385
consume_header(h, "HTTP/1.1 200 Connection established\r\n");
@@ -459,6 +473,27 @@ jobs:
459473
460474
namespace file_digest_test {
461475
int run() {
476+
// Cover Unicode paths without requiring English language packs. -nigel
477+
const auto directory = std::filesystem::temp_directory_path() /
478+
std::filesystem::path(L"keyauth-\u4e2d\u6587-\u65e5\u672c\u8a9e-a\u00e7\u00e3o");
479+
std::filesystem::create_directories(directory);
480+
const auto unicode_path = directory / L"\u30c6\u30b9\u30c8-\u00e7.bin";
481+
{ std::ofstream output(unicode_path, std::ios::binary); output << "abc"; }
482+
const auto decoded_path = keyauth_detail::native_file_path(unicode_path.u8string());
483+
const auto unicode_digest = keyauth_detail::file_md5(decoded_path);
484+
std::filesystem::remove(unicode_path);
485+
std::filesystem::remove(directory);
486+
if (unicode_digest != "900150983cd24fb0d6963f7d28e17f72") return 1;
487+
bool invalid_path_rejected = false;
488+
try { keyauth_detail::native_file_path(std::string("a\0b", 3)); }
489+
catch (const std::invalid_argument&) { invalid_path_rejected = true; }
490+
if (!invalid_path_rejected) return 1;
491+
std::wstring executable(32768, L'\0');
492+
const DWORD length = GetModuleFileNameW(nullptr, executable.data(),
493+
static_cast<DWORD>(executable.size()));
494+
if (!length || length >= executable.size()) return 1;
495+
executable.resize(length);
496+
if (checksum() != keyauth_detail::file_md5(std::filesystem::path(executable))) return 1;
462497
const auto path = std::filesystem::temp_directory_path() / "keyauth-checksum-test.bin";
463498
{ std::ofstream out(path, std::ios::binary); out << "abc"; }
464499
const auto digest = keyauth_detail::file_md5(path);

‎auth.cpp‎

Lines changed: 36 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ static bool normalize_signature_header(std::string sig_in, std::string& sig_hex_
168168

169169
if (is_hex_string(sig_in) && sig_in.size() == 128) {
170170
std::transform(sig_in.begin(), sig_in.end(), sig_in.begin(),
171-
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
171+
[](unsigned char c) { return keyauth_detail::ascii_lower(c); });
172172
sig_hex_out = sig_in;
173173
return true;
174174
}
@@ -663,7 +663,7 @@ static std::string decode_pubkey_hex(const uint8_t* obf, size_t len, uint8_t key
663663
static std::string to_lower_ascii(std::string v)
664664
{
665665
std::transform(v.begin(), v.end(), v.begin(),
666-
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
666+
[](unsigned char c) { return keyauth_detail::ascii_lower(c); });
667667
return v;
668668
}
669669

@@ -829,20 +829,26 @@ void KeyAuth::api::init()
829829
KA_EXIT(0);
830830
}
831831

832-
if (resolved_path != "" || !resolved_path.empty()) {
832+
if (!resolved_path.empty()) {
833833

834-
if (!std::filesystem::exists(resolved_path)) {
834+
const auto token_path = keyauth_detail::native_file_path(resolved_path);
835+
if (!std::filesystem::exists(token_path)) {
835836
MessageBoxA(0, XorStr("File not found. Please make sure the file exists.").c_str(), NULL, MB_ICONERROR);
836837
KA_EXIT(0);
837838
}
838839
//get the contents of the file
839-
std::ifstream file(resolved_path);
840+
std::ifstream file(token_path);
841+
if (!file) {
842+
throw request_error("Cannot open API token file.");
843+
}
840844
std::string token;
841845
std::string thash;
842846
ScopeWipe wipe_token(token);
843-
std::getline(file, token);
847+
if (!std::getline(file, token)) {
848+
throw request_error("Cannot read API token file.");
849+
}
844850

845-
thash = keyauth_detail::file_md5(std::filesystem::path(resolved_path));
851+
thash = keyauth_detail::file_md5(token_path);
846852

847853
data += XorStr("&token=").c_str() + curl_escape(nullptr, token);
848854
data += XorStr("&thash=").c_str() + curl_escape(nullptr, thash);
@@ -2237,11 +2243,11 @@ void KeyAuth::api::log(std::string message) {
22372243
checkInit();
22382244
ScopeWipe wipe_message(message);
22392245

2240-
char acUserName[UNLEN + 1]{};
2241-
DWORD nUserName = sizeof(acUserName);
2246+
wchar_t acUserName[UNLEN + 1]{};
2247+
DWORD nUserName = static_cast<DWORD>(std::size(acUserName));
22422248
std::string UsernamePC;
2243-
if (GetUserNameA(acUserName, &nUserName) && nUserName > 0 && nUserName <= sizeof(acUserName))
2244-
UsernamePC.assign(acUserName, nUserName - 1);
2249+
if (GetUserNameW(acUserName, &nUserName) && nUserName > 0 && nUserName <= std::size(acUserName))
2250+
UsernamePC = wide_to_utf8(acUserName);
22452251
SecureZeroMemory(acUserName, sizeof(acUserName));
22462252
ScopeWipe wipe_pcuser(UsernamePC);
22472253

@@ -2746,7 +2752,7 @@ static bool is_https_url(const std::string& url)
27462752
if (url.size() < prefix.size())
27472753
return false;
27482754
for (size_t i = 0; i < prefix.size(); ++i) {
2749-
const char c = static_cast<char>(std::tolower(static_cast<unsigned char>(url[i])));
2755+
const char c = keyauth_detail::ascii_lower(static_cast<unsigned char>(url[i]));
27502756
if (c != prefix[i])
27512757
return false;
27522758
}
@@ -3668,9 +3674,9 @@ std::string KeyAuth::api::req(std::string data, const std::string& url) {
36683674
std::string host_lower = host;
36693675
std::string eff_lower = eff_host;
36703676
std::transform(host_lower.begin(), host_lower.end(), host_lower.begin(),
3671-
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
3677+
[](unsigned char c) { return keyauth_detail::ascii_lower(c); });
36723678
std::transform(eff_lower.begin(), eff_lower.end(), eff_lower.begin(),
3673-
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
3679+
[](unsigned char c) { return keyauth_detail::ascii_lower(c); });
36743680
if (!eff_lower.empty() && eff_lower != host_lower) {
36753681
curl_guard.reset();
36763682
throw request_error(XorStr("effective url host mismatch."));
@@ -3701,15 +3707,11 @@ std::string checksum()
37013707
}
37023708

37033709
std::string getPath() {
3704-
const char* programDataPath = std::getenv("ALLUSERSPROFILE");
3705-
3706-
if (programDataPath != nullptr) {
3707-
return std::string(programDataPath);
3708-
}
3709-
else {
3710-
3711-
return std::filesystem::current_path().string();
3710+
const wchar_t* program_data = _wgetenv(L"ALLUSERSPROFILE");
3711+
if (program_data && *program_data) {
3712+
return wide_to_utf8(program_data);
37123713
}
3714+
return std::filesystem::current_path().u8string();
37133715
}
37143716

37153717
void RedactField(nlohmann::json& jsonObject, const std::string& fieldName)
@@ -3780,36 +3782,18 @@ void KeyAuth::api::debugInfo(std::string data, std::string url, std::string resp
37803782
redacted_data = datas.dump();
37813783
}
37823784

3783-
//gets the path
3784-
std::string path = getPath();
3785-
3786-
//fetch filename
3787-
3788-
TCHAR filename[MAX_PATH];
3789-
GetModuleFileName(NULL, filename, MAX_PATH);
3790-
3791-
TCHAR* filename_only = PathFindFileName(filename);
3792-
3793-
std::wstring filenameOnlyString(filename_only);
3794-
3795-
std::string filenameOnly(filenameOnlyString.begin(), filenameOnlyString.end());
3796-
3797-
///////////////////////
3798-
3799-
//creates variables for the paths needed :smile:
3800-
std::string KeyAuthPath = path + "\\KeyAuth";
3801-
std::string logPath = KeyAuthPath + "\\Debug\\" + filenameOnly.substr(0, filenameOnly.size() - 4);
3802-
3803-
//basically loops until we have all the paths
3804-
if (!std::filesystem::exists(KeyAuthPath) || !std::filesystem::exists(KeyAuthPath + "\\Debug") || !std::filesystem::exists(logPath)) {
3805-
3806-
if (!std::filesystem::exists(KeyAuthPath)) { std::filesystem::create_directory(KeyAuthPath); }
3807-
3808-
if (!std::filesystem::exists(KeyAuthPath + "\\Debug")) { std::filesystem::create_directory(KeyAuthPath + "\\Debug"); }
3809-
3810-
if (!std::filesystem::exists(logPath)) { std::filesystem::create_directory(logPath); }
3811-
3785+
// Keep debug paths in UTF-16; narrowing Unicode filenames loses characters. -nigel
3786+
std::wstring executable_path(32768, L'\0');
3787+
const DWORD path_length = GetModuleFileNameW(nullptr, executable_path.data(),
3788+
static_cast<DWORD>(executable_path.size()));
3789+
if (!path_length || path_length >= executable_path.size()) {
3790+
throw request_error("Cannot locate executable for debug log.");
38123791
}
3792+
executable_path.resize(path_length);
3793+
3794+
const auto executable_name = std::filesystem::path(executable_path).filename().stem();
3795+
const auto log_path = keyauth_detail::native_file_path(getPath()) / L"KeyAuth" / L"Debug" / executable_name;
3796+
std::filesystem::create_directories(log_path);
38133797

38143798
if (response.length() >= 500) { return; }
38153799

@@ -3821,7 +3805,7 @@ void KeyAuth::api::debugInfo(std::string data, std::string url, std::string resp
38213805

38223806
std::strftime(time, sizeof(time), "%m-%d-%Y", localTime);
38233807

3824-
std::ofstream logfile(logPath + "\\" + time + ".txt", std::ios::app);
3808+
std::ofstream logfile(log_path / (std::string(time) + ".txt"), std::ios::app);
38253809

38263810
//get time
38273811
int hours = localTime->tm_hour;

‎auth.hpp‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@
2626
#include <cctype>
2727

2828
namespace keyauth_detail {
29+
// HTTP names use ASCII casing regardless of the application's locale. -nigel
30+
inline char ascii_lower(unsigned char value) noexcept {
31+
if (value >= 'A' && value <= 'Z') {
32+
return static_cast<char>(value + ('a' - 'A'));
33+
}
34+
return static_cast<char>(value);
35+
}
2936
inline std::wstring bounded_http_query(const wchar_t* query, size_t bytes,
3037
const void* buffer, size_t buffer_size) {
3138
if (!bytes) return {};
@@ -64,7 +71,7 @@ inline bool consume_header(SignatureHeaders& out, std::string line) {
6471
if (colon == std::string::npos) return true;
6572
std::string key = line.substr(0, colon);
6673
std::transform(key.begin(), key.end(), key.begin(),
67-
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
74+
[](unsigned char c) { return keyauth_detail::ascii_lower(c); });
6875
std::string* target = nullptr;
6976
if (key == "x-signature-ed25519") target = &out.signature;
7077
if (key == "x-signature-timestamp") target = &out.timestamp;
@@ -143,6 +150,46 @@ inline PayloadResult verify_payload(const std::string& signature,
143150
#pragma comment(lib, "bcrypt.lib")
144151

145152
namespace keyauth_detail {
153+
// Prefer UTF-8 paths while retaining existing Windows code-page callers. -nigel
154+
inline std::filesystem::path native_file_path(const std::string& value) {
155+
if (value.empty() || value.find('\0') != std::string::npos || value.size() > INT_MAX) {
156+
throw std::invalid_argument("Invalid API file path.");
157+
}
158+
159+
const int length = static_cast<int>(value.size());
160+
UINT code_page = CP_UTF8;
161+
DWORD flags = MB_ERR_INVALID_CHARS;
162+
int required = MultiByteToWideChar(code_page, flags, value.data(), length, nullptr, 0);
163+
if (!required) {
164+
if (GetLastError() != ERROR_NO_UNICODE_TRANSLATION) {
165+
throw std::runtime_error("Cannot decode API file path.");
166+
}
167+
code_page = CP_ACP;
168+
flags = 0;
169+
required = MultiByteToWideChar(code_page, flags, value.data(), length, nullptr, 0);
170+
}
171+
if (!required) {
172+
throw std::runtime_error("Cannot decode API file path.");
173+
}
174+
175+
std::wstring decoded(static_cast<size_t>(required), L'\0');
176+
if (MultiByteToWideChar(code_page, flags, value.data(), length, decoded.data(), required) != required) {
177+
throw std::runtime_error("Cannot decode API file path.");
178+
}
179+
std::filesystem::path result(decoded);
180+
if (code_page == CP_UTF8 && GetACP() != CP_UTF8) {
181+
std::error_code error;
182+
if (!std::filesystem::exists(result, error)) {
183+
const std::filesystem::path legacy(value);
184+
error.clear();
185+
if (std::filesystem::exists(legacy, error)) {
186+
return legacy;
187+
}
188+
}
189+
}
190+
return result;
191+
}
192+
146193
// MD5 is retained solely for the API's existing executable/token hash fields.
147194
// Authentication is provided by TLS and Ed25519, not this compatibility digest.
148195
inline std::string file_md5(const std::filesystem::path& path) {

0 commit comments

Comments
 (0)