diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a17f52..2521f0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,280 @@ jobs: run: | msbuild .\\library.sln /p:Configuration=Release /p:Platform=x64 + - name: security regression tests + shell: pwsh + run: | + $installation = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + $devcmd = Join-Path $installation 'Common7\Tools\VsDevCmd.bat' + $compilerEnvironment = & cmd.exe /c "`"$devcmd`" -arch=x64 >nul && set" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + foreach ($entry in $compilerEnvironment) { + if ($entry -match '^([^=]+)=(.*)$') { + [Environment]::SetEnvironmentVariable($matches[1], $matches[2], 'Process') + } + } + $source = Join-Path $env:RUNNER_TEMP 'security-regression.cpp' + @' + #define SODIUM_STATIC + #include "auth.hpp" + #include + #include + extern std::string curl_escape(CURL*, const std::string&); + extern std::string get_str_between_two_str(const std::string&, const std::string&, const std::string&); + namespace response_headers_test { + + static void require(bool condition) { + if (!condition) std::abort(); + } + int run() { + keyauth_detail::SignatureHeaders h; + using keyauth_detail::consume_header; + consume_header(h, "HTTP/1.1 200 Connection established\r\n"); + consume_header(h, "X-Signature-Ed25519: stale\r\n"); + consume_header(h, "HTTP/2 200\r\n"); + require(h.signature.empty() && h.timestamp.empty()); + consume_header(h, "X-SIGNATURE-ED25519:\tvalid \t\r\n"); + consume_header(h, "x-signature-timestamp: 1770000000\r\n"); + require(h.signature == "valid" && h.timestamp == "1770000000"); + consume_header(h, "x-signature-ed25519: valid\r\n"); + require(!h.conflicting); + consume_header(h, "x-signature-ed25519: other\r\n"); + require(h.conflicting); + consume_header(h, "HTTP/1.1 103 Early Hints\r\n"); + require(!h.conflicting && h.signature.empty()); + consume_header(h, "x-signature-ed25519: interim\r\n"); + consume_header(h, "HTTP/1.1 503 Service Unavailable\r\n"); + require(h.signature.empty()); + consume_header(h, "server: edge\r\n"); + consume_header(h, "\r\n"); + require(h.signature.empty()); + std::cout << "Response header regression tests passed\n"; + return 0; + } + + } + + namespace payload_verifier_test { + + static void require(bool condition) { if (!condition) std::abort(); } + static std::string hex(const unsigned char* bytes, size_t size) { + std::string out(size * 2 + 1, '\0'); + sodium_bin2hex(out.data(), out.size(), bytes, size); + out.pop_back(); + return out; + } + int run() { + using namespace keyauth_detail; + require(sodium_init() >= 0); + unsigned char pk[crypto_sign_PUBLICKEYBYTES], sk[crypto_sign_SECRETKEYBYTES]; + crypto_sign_keypair(pk, sk); + const auto key = hex(pk, sizeof(pk)); + const std::string body("{\"success\":true}\0tail", 21); + auto sign = [&](const std::string& timestamp) { + const std::string message = timestamp + body; + unsigned char sig[crypto_sign_BYTES]; + crypto_sign_detached(sig, nullptr, + reinterpret_cast(message.data()), message.size(), sk); + return hex(sig, sizeof(sig)); + }; + const std::string timestamp = "1770000000"; + const auto signature = sign(timestamp); + require(verify_payload(signature, timestamp, body, key, 1770000000) == PayloadResult::valid); + require(verify_payload(signature, timestamp, body + "!", key, 1770000000) == PayloadResult::invalid_signature); + require(verify_payload(signature, timestamp, body, key, 1770000121) == PayloadResult::timestamp_skew); + require(verify_payload(signature, timestamp, body, key, 1769999879) == PayloadResult::timestamp_skew); + require(verify_payload(signature, timestamp, body, key, 1770000120) == PayloadResult::valid); + require(verify_payload(sign("1770000000123"), "1770000000123", body, key, 1770000000) == PayloadResult::valid); + require(verify_payload(signature, "1770000000x", body, key, 1770000000) == PayloadResult::timestamp_format); + require(verify_payload("", timestamp, body, key, 1770000000) == PayloadResult::signature_format); + require(verify_payload(signature, timestamp, body, "00", 1770000000) == PayloadResult::public_key_format); + auto damaged = signature; damaged[0] = damaged[0] == '0' ? '1' : '0'; + require(verify_payload(damaged, timestamp, body, key, 1770000000) == PayloadResult::invalid_signature); + sodium_memzero(sk, sizeof(sk)); + std::cout << "Payload signature regression tests passed\n"; + return 0; + } + + } + + namespace file_digest_test { + int run() { + const auto path = std::filesystem::temp_directory_path() / "keyauth-checksum-test.bin"; + { std::ofstream out(path, std::ios::binary); out << "abc"; } + const auto digest = keyauth_detail::file_md5(path); + std::filesystem::remove(path); + if (digest != "900150983cd24fb0d6963f7d28e17f72") return 1; + bool rejected = false; + try { keyauth_detail::file_md5(path); } catch (const std::runtime_error&) { rejected = true; } + if (!rejected) return 1; + std::cout << "File checksum regression tests passed\n"; + return 0; + } + + } + + namespace json_errors_test { + int run() { + bool parse_error = false, type_error = false; + try { nlohmann::json::parse("{broken"); } + catch (const nlohmann::json::parse_error&) { parse_error = true; } + try { nlohmann::json("not a boolean").get(); } + catch (const nlohmann::json::type_error&) { type_error = true; } + if (!parse_error || !type_error) return 1; + if (!nlohmann::json::parse("{broken", nullptr, false).is_discarded()) return 1; + std::cout << "JSON error handling regression tests passed\n"; + return 0; + } + + } + + namespace api_lifecycle_test { + int run() { + if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return 1; + const auto hwid = utils::get_hwid(); + if (hwid.rfind("S-1-", 0) != 0 || utils::get_hwid() != hwid) return 1; + if (curl_escape(nullptr, "a&+= %") != "a%26%2B%3D%20%25") return 1; + if (get_str_between_two_str("?user=test&token=abc", "&token=", "") != "abc") return 1; + for (int i = 0; i < 10; ++i) { + KeyAuth::api first("first", "1234567890", "1.0", "https://keyauth.win/api/1.3/", ""); + KeyAuth::api second("second", "0987654321", "1.0", "https://keyauth.win/api/1.3/", ""); + if (!first.require_signed_downloads) return 1; + first.response.success = true; + if (first.authenticated() || first.authenticated(true)) return 1; + if (!keyauth_detail::auth_window_fresh(10, 10) || + !keyauth_detail::auth_window_fresh(10, 12) || + keyauth_detail::auth_window_fresh(10, 13) || + keyauth_detail::auth_window_fresh(10, 9) || + keyauth_detail::auth_window_fresh(0, 0)) return 1; + first.enable_secure_strings(); + if (!first.name.empty()) return 1; + first.enable_secure_strings(false); + if (first.name != "first" || second.name != "second") return 1; + for (size_t length : {size_t(0), size_t(1), size_t(7), size_t(8), size_t(15), size_t(16), size_t(17), size_t(4096)}) { + const std::string original(length, '\0'); + first.name = original; + first.path = std::string(length, 'x'); + first.enable_secure_strings(); + first.enable_secure_strings(); + if (!first.name.empty() || !first.path.empty()) return 1; + first.enable_secure_strings(false); + first.enable_secure_strings(false); + if (first.name != original || first.path != std::string(length, 'x')) return 1; + } + first.stop_ban_monitor(); + } + std::cout << "API lifecycle and form encoding regression tests passed\n"; + return 0; + } + + } + + + namespace network_policy_test { + static void require(bool ok) { if (!ok) std::abort(); } + int run() { + using namespace keyauth_detail; + require(canonical_host("KEYAUTH.WIN.") == "keyauth.win"); + const auto endpoint = parse_api_endpoint("HTTPS://KEYAUTH.WIN.:443/api/1.3/?check_paid=1"); + require(endpoint.host == "keyauth.win" && endpoint.port == 443); + require(endpoint.url.find("keyauth.win") != std::string::npos); + for (const auto* input : {"http://keyauth.win/api/1.3/", "https://keyauth.win@evil.invalid/", + "https://user:pass@keyauth.win/", "https://keyauth.win:444/", "https://127.0.0.1/", + "https://[::1]/", "https://2130706433/", "https://keyauth.win/#fragment", + "https://keyauth.win\\@evil.invalid/", "https://keyauth..win/"}) { + bool rejected = false; + try { parse_api_endpoint(input); } catch (const std::invalid_argument&) { rejected = true; } + require(rejected); + } + require(parse_api_endpoint("https://auth.customer.example/api/1.3/").host == "auth.customer.example"); + require(hosts_line_overrides("127.0.0.1\tKEYAUTH.WIN. alias # note", "keyauth.win")); + require(hosts_line_overrides("::1 alias keyauth.win", "KEYAUTH.WIN.")); + require(hosts_line_overrides("203.0.113.10 other keyauth.win", "keyauth.win")); + require(!hosts_line_overrides("127.0.0.1 keyauth.win.evil.invalid", "keyauth.win")); + require(!hosts_line_overrides("# 127.0.0.1 keyauth.win", "keyauth.win")); + require(!hosts_line_overrides("127.0.0.1 other # keyauth.win", "keyauth.win")); + require(!hosts_line_overrides("not-an-address keyauth.win", "keyauth.win")); + for (const auto* ip : {"127.0.0.1", "10.0.0.1", "172.16.0.1", "172.31.255.254", + "192.168.1.1", "0.0.0.0", "::", "::1", "fc00::1", "fe80::1", "ff02::1", + "::ffff:127.0.0.1", "::ffff:192.168.1.1", "invalid"}) require(private_address(ip)); + for (const auto* ip : {"1.1.1.1", "8.8.8.8", "172.32.0.1", "2606:4700:4700::1111", + "::ffff:8.8.8.8"}) require(!private_address(ip)); + require(hostname_matches("api.keyauth.win", "*.keyauth.win")); + require(hostname_matches("KEYAUTH.WIN.", "keyauth.win")); + require(!hostname_matches("keyauth.win", "*.keyauth.win")); + require(!hostname_matches("keyauth.win.evil.invalid", "*.keyauth.win")); + require(!connection_address_allowed("127.0.0.1", 443, false, true)); + require(!connection_address_allowed("127.0.0.1", 443, false, false)); + require(!connection_address_allowed("::1", 443, false, false)); + require(connection_address_allowed("10.0.0.1", 443, false, false)); + require(!connection_address_allowed("::ffff:127.0.0.1", 443, false, true)); + require(connection_address_allowed("1.1.1.1", 443, false, true)); + require(connection_address_allowed("8.8.8.8", 443, false, true)); + require(connection_address_allowed("127.0.0.1", 8080, true, true)); + require(!connection_address_allowed("1.1.1.1", 80, false, true)); + CURL* curl = curl_easy_init(); + require(curl != nullptr); + long proxy = 0; + require(curl_easy_getinfo(curl, CURLINFO_USED_PROXY, &proxy) == CURLE_OK); + curl_easy_cleanup(curl); + std::cout << "Hostname and connection policy regression tests passed\n"; + return 0; + } + } + + namespace email_api_test { + static void require(bool value) { if (!value) std::abort(); } + int run() { + using namespace keyauth_detail; + const auto send = email_request("emailOTPSend", "a+b@example.com", "", "", "session&x", "app", "owner", "hwid"); + require(send.find("type=emailOTPSend&email=a%2Bb%40example.com") == 0); + require(send.find("sessionid=session%26x") != std::string::npos); + require(send.find("&hwid=") == std::string::npos); + const auto verify = email_request("emailOTPVerify", "a@example.com", "", "012345", "s", "app", "owner", "hwid"); + require(verify.find("&code=012345") != std::string::npos && verify.find("&hwid=hwid") != std::string::npos); + const auto login = email_request("loginEmail", "a@example.com", "x&+=", "987654", "s", "app", "owner", "h"); + require(login.find("&pass=x%26%2B%3D") != std::string::npos); + require(email_request("magicLinkCheck", "a@example.com", "", "", "s", "app", "owner", "h").find("type=magicLinkCheck&") == 0); + bool rejected = false; + try { email_request("emailOTPVerify", "a@example.com", "", "12xx45", "s", "app", "owner", "h"); } + catch (const std::invalid_argument&) { rejected = true; } + require(rejected); + require(!magic_link_approved(nlohmann::json::parse(R"({"success":true,"message":"Successfully sent magic login link to email."})"))); + require(!magic_link_approved(nlohmann::json::parse(R"({"success":false,"message":"Pending"})"))); + require(magic_link_approved(nlohmann::json::parse(R"({"success":true,"message":"Logged in!"})"))); + require(!magic_link_approved(nlohmann::json::parse(R"({"success":true,"authenticated":false,"message":"Logged in!"})"))); + validate_api_reply(nlohmann::json::parse(R"({"success":false,"message":"Invalid code"})"), "owner"); + rejected = false; + try { validate_api_reply(nlohmann::json::parse(R"({"success":true,"message":"Logged in!","ownerid":"other"})"), "owner"); } + catch (const std::invalid_argument&) { rejected = true; } + require(rejected); + require(json_text(nlohmann::json::parse(R"({"expiry":1770000000})"), "expiry") == "1770000000"); + ResponseBuffer buffer{{}, 4}; + require(append_response(buffer, "test", 4)); + require(!append_response(buffer, "!", 1) && buffer.exceeded && buffer.body == "test"); + require(!connection_address_allowed("127.0.0.1", 8080, true, true, false)); + std::cout << "Email flow and bounded-response regression tests passed\n"; + return 0; + } + } + int main() { + if (email_api_test::run() != 0) return 1; + if (network_policy_test::run() != 0) return 1; + if (response_headers_test::run() != 0) return 1; + if (payload_verifier_test::run() != 0) return 1; + if (file_digest_test::run() != 0) return 1; + if (json_errors_test::run() != 0) return 1; + if (api_lifecycle_test::run() != 0) return 1; + return 0; + } + '@ | Set-Content -LiteralPath $source + $object = Join-Path $env:RUNNER_TEMP 'security-regression.obj' + $executable = Join-Path $env:RUNNER_TEMP 'security-regression.exe' + & cl.exe /nologo /std:c++17 /EHsc /MD "/I$env:GITHUB_WORKSPACE" "/I$env:GITHUB_WORKSPACE\libsodium" $source "/Fo$object" "/Fe$executable" /link "$env:GITHUB_WORKSPACE\x64\Release\library_x64.lib" "$env:GITHUB_WORKSPACE\curl\zlib.lib" user32.lib shell32.lib + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & $executable + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: collect build output shell: pwsh run: | diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 23110ef..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changelog - -## Unreleased -- Network hardening (client-side only): HTTPS-only transport, redirects disabled, and optional host allowlist + public key pinning to reduce the risk of traffic redirection or man-in-the-middle interception. -- Integrity checks: `.text` integrity and page-protection checks, plus non-executable page checks for `.data` and `.rdata` to help detect tampering (transparent, no stealth behavior). - -## Notes -- These protections are defensive and transparent; they do not alter the backend or API and are intended to reduce common redirection and tampering risks. diff --git a/README.md b/README.md index d9edf1a..162739b 100644 --- a/README.md +++ b/README.md @@ -62,80 +62,93 @@ Notes: 2. Use the same `CURL_STATICLIB` define as shown above when statically linking. 3. Rebuild the library after pulling updates to keep everything in sync. -## **Security Features (Built-In)** -The library ships with security checks enabled by default. You do not need to manually call anything beyond `init()` and a normal login/license call. - -What runs automatically: -1. **Integrity checks** (prologue snapshots, function region validation, `.text` hashing, page protections). -2. **Module checks** (core module signature verification + RWX section detection). -3. **Hosts-file checks** for API host tampering. -4. **Timing anomaly checks** to detect time tamper. -5. **Session heartbeat** after successful login/license/upgrade/web login. - -## **Security Overview** -This SDK includes lightweight, client-side defenses that raise the cost of common bypass techniques while keeping normal integrations simple. - -What it protects against: -1. **Inline patching/NOPs**: prologue snapshots and detour heuristics catch modified function entry points. -2. **Code tamper**: `.text` hashing and page‑protection checks detect modified code pages. -3. **API redirection**: hosts‑file checks flag local DNS overrides of the API host. -4. **Time spoofing**: timing anomaly checks reduce abuse of expired keys by system clock changes. -5. **Tampered system DLLs**: core module signature checks reject patched or unsigned system libraries. - -Benefits: -1. **Fail‑closed behavior**: when a check fails, requests are blocked before the API call. -2. **Low integration cost**: no additional calls are required beyond `init()` and a normal login/license flow. -3. **Reduced false positives**: checks are limited to core modules and conservative tamper signals. - -Design notes: -1. These are **client‑side** protections. They complement — not replace — server‑side session validation. -2. If you modify or strip checks, you reduce protection. Keep the SDK updated to inherit fixes. -3. Optional hardening ideas are listed below for advanced users who accept higher false‑positive risk. - -How to keep security enabled: -1. Always call `KeyAuthApp.init()` once before any other API call. -2. Do not remove the built-in checks or tamper with the library internals. -3. Keep your application linked against the updated library after pulling changes. - -How to verify it is running: -1. Use the library normally — the checks are automatic. -2. If a check fails, the library will fail closed with an error message. - -## **Optional Hardening Ideas (Not Enabled)** -These are intentionally **not** enabled in the library to avoid false positives, but you can add them if your app needs them. - -1. **PE header erase**: wipe PE header pages after load to make casual dumping harder. This is not a check; it simply reduces dump quality. -2. **Module allowlists**: require a strict set of loaded modules; this breaks overlays and many legitimate plugins. -3. **System module path checks**: enforce System32/SysWOW64-only paths; can fail on custom Windows installs. -4. **Hypervisor detection**: block VMs; useful for niche threat models but unfriendly to legit users. -5. **IAT validation**: detect import-table hooks for any imported API; can false-positive in some environments. - -## **Security Troubleshooting** -If you see security failures, common causes include: -1. **Modified system DLLs**: non‑Microsoft versions or patched DLLs will be rejected. -2. **Time tampering**: manual clock changes or large time skew can trigger timing checks. -3. **Patched binaries**: inline hooks/NOP patches or modified `.text` will fail integrity checks. - -## **Changelog (Overhaul Summary)** -This list summarizes all changes made in the overhaul: -1. **Integrity checks**: prologue snapshots, function region validation, detour detection, `.text` slice hashing, page protections. -2. **Module trust**: Microsoft signature verification for core DLLs, RWX section detection. -3. **Timing checks**: timing anomaly detection to catch clock tamper. -4. **Import checks**: import address validation. -5. **Network hardening**: hosts‑file override detection for API host. -6. **Session hardening**: session heartbeat after successful login/license/upgrade/web login. -7. **DLL search order**: hardened DLL lookup and removed current‑dir hijacking. -8. **String exposure**: request data zeroized after use; sensitive parameters wiped via `ScopeWipe`. -9. **Debug logging**: minimized request/URL logging to reduce in‑memory exposure. -10. **Parsing hardening**: safer JSON parsing and substring handling to avoid crashes. -11. **Curl safety**: fixed cleanup issues; enforced static libcurl linkage. -12. **Module path APIs**: removed hardcoded System32 paths (uses `GetSystemDirectoryW`). -13. **Example/docs**: added usage section, security feature docs, and troubleshooting guidance. - -Helpful references: -- https://github.com/KeyAuth/KeyAuth-CPP-Example -- https://keyauth.cc/app/ -- https://keyauth.cc/app/?page=forms +## Security and API compatibility + +The SDK retains the API 1.3 endpoint, TLS hostname/certificate verification, +Ed25519 response verification, a 120 second signed timestamp window, and server +session/blacklist checks. It does not automatically replace signing keys or pins. +Compare upstream changes before rebuilding and distributing a new library. + +Removed runtime enforcement: raw disk-versus-loaded PE comparisons, instruction +pattern scanning, process ACL changes, repeated exception handlers, file/registry/ +atom markers, module-signature heuristics, and wall-clock tamper timers. These +can reject legitimate Windows relocation, instrumentation, restricted accounts, +slow initialization, and sleep/resume. They are not reliable evidence that a user +has bypassed authentication. Server authentication and signed responses remain +mandatory for normal API operations. Client-side code can still be patched by a +person controlling the device; sensitive authorization must be enforced server-side. + +The SDK checks HTTP status before checking signature headers, never automatically +replays state-changing requests, and allows normal CDN DNS rotation. It rejects +missing/invalid authentication signatures, conflicting signature headers and +expired signed responses. The file endpoint retains upstream compatibility: +downloads with neither signature header rely on verified HTTPS; partial or invalid +signatures are rejected. Validate publisher signatures or an independently trusted +digest before executing downloaded content. + +API fields are individually URL encoded. Executable/token checksums use Windows +cryptography rather than command shells. MD5 is used only for the existing API +checksum fields; it is not an authentication primitive. Debug logging omits bodies. + +## Handling errors + +Network, HTTP and signature failures now throw `KeyAuth::request_error` rather +than terminating the host process. Unexpected signed response schemas may throw +JSON exceptions. Catch exceptions at your application's API boundary and keep +protected features unavailable when the operation fails: + +```cpp +try { + KeyAuthApp.init(); + if (!KeyAuthApp.response.success) return 1; + KeyAuthApp.license("your_license_key"); + if (!KeyAuthApp.response.success) return 1; +} catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; +} +``` + +An HTTP 429/5xx, gateway/proxy page, or unsigned HTTP success is a failed request, +not proof of user tampering. Do not remove signature validation to work around it. +For timestamp failures, synchronize Windows time. A missing header cannot be +reconstructed by the client; investigate the API/intermediary and sanitized logs. + +## Threads and lifetime + +API operations are serialized per instance, including heartbeat calls. Initialization +and login state belong to the instance. The owned heartbeat stops on destruction; +network errors do not assert a ban. Stop the optional ban monitor before shutdown. +Do not destroy an API instance from its monitor callback, concurrently destroy it +while another thread calls it, or directly read/write public fields concurrently +with background work. Configure hosts/pins before starting API activity. + +The `require_pinning` flag now requires a configured pin. Multiple `sha256//` pins +can support an approved key rotation. TLS certificate/hostname verification stays +enabled alongside pins. `block_private_dns` checks the connected private address; +the legacy `block_custom_ca` field is not an implemented trust-store policy. + +## String visibility and version tags + +The hardcoded internal build tag has been removed. This does not prevent version +fingerprinting or reverse engineering. `enable_secure_strings()` is optional XOR +obfuscation of selected runtime configuration fields, not encryption or a place to +store secrets. Windows imports such as `Sleep` and public API identifiers are not +secrets. No import hiding, packer or anti-analysis mechanism is added by this patch. + +## Build and regression tests + +Rebuild applications against the updated headers and library together; the private +instance layout changed. From a Visual Studio x64 developer prompt: + +```bat +msbuild library.vcxproj /p:Configuration=Release /p:Platform=x64 +``` + +The existing CI workflow runs embedded regression checks for HTTP header block isolation and duplicates, valid/tampered/expired +Ed25519 messages, milliseconds timestamps, binary bodies, checksum vectors, malformed +JSON handling and API object lifetime. They do not replace live application testing. + ## **What is KeyAuth?** diff --git a/auth.cpp b/auth.cpp index 75c3162..747c0dd 100644 --- a/auth.cpp +++ b/auth.cpp @@ -12,13 +12,11 @@ #define _CRT_SECURE_NO_WARNINGS -#ifdef NDEBUG -#define KA_EXIT(code) LI_FN(exit)(1) -#else -#define KA_EXIT(code) LI_FN(exit)(code) -#endif +#define KA_EXIT(code) throw KeyAuth::request_error("KeyAuth operation rejected (code " + std::to_string(code) + ").") #include +#include +#pragma comment(lib, "crypt32.lib") #include #include #include @@ -26,6 +24,8 @@ #include #include +#include +#pragma comment(lib, "shlwapi.lib") #include #include @@ -33,7 +33,6 @@ #include #include #include -#include #include #include @@ -97,18 +96,11 @@ static std::string hexDecode(const std::string& hex); std::string get_str_between_two_str(const std::string& s, const std::string& start_delim, const std::string& stop_delim); int VerifyPayload(std::string signature, std::string timestamp, std::string body); -void checkInit(); + std::string checksum(); -void modify(); -void runChecks(); -void checkAtoms(); -void checkFiles(); -void checkRegistry(); -void error(std::string message); +[[noreturn]] void error(std::string message); std::string generate_random_number(); std::string curl_escape(CURL* curl, const std::string& input); -static std::string build_query_encoded(CURL* curl, const std::string& data); -auto check_section_integrity( const char *section_name, bool fix ) -> bool; void integrity_check(); std::string extract_host(const std::string& url); bool hosts_override_present(const std::string& host); @@ -116,10 +108,10 @@ bool module_has_rwx_section(HMODULE mod); bool core_modules_signed(); static std::wstring get_system_dir(); static std::wstring get_syswow_dir(); -static void security_watchdog(); + bool timing_anomaly_detected(); -void start_heartbeat(KeyAuth::api* instance); -void heartbeat_thread(KeyAuth::api* instance); + + void snapshot_text_hashes(); bool text_hashes_ok(); void snapshot_text_page_protections(); @@ -136,12 +128,8 @@ void cleanUpSeedData(const std::string& seed); thread_local std::string signature; thread_local std::string signatureTimestamp; -static const char* k_build_tag = "KA_BUILD_2026_03_09B"; -struct SignatureHeaders { - std::string signature; - std::string timestamp; -}; +using SignatureHeaders = keyauth_detail::SignatureHeaders; static inline void trim_ws(std::string& s) { while (!s.empty() && std::isspace(static_cast(s.front()))) s.erase(s.begin()); @@ -197,7 +185,7 @@ static bool normalize_signature_header(std::string sig_in, std::string& sig_hex_ } return false; } -bool initialized; + static constexpr uint8_t k_pubkey_xor1 = 0x5A; static constexpr uint8_t k_pubkey_xor2 = 0xA5; static constexpr uint64_t k_pubkey_fnv1a = 0x7553f24ca052d4b1ULL; @@ -217,12 +205,11 @@ static std::atomic pubkey_hash_seen{ 0 }; static std::atomic pubkey_protect_ready{ false }; static DWORD pubkey_protect_baseline = 0; bool KeyAuth::api::debug = false; -std::atomic LoggedIn(false); + std::atomic last_integrity_check{ 0 }; std::atomic integrity_fail_streak{ 0 }; std::atomic last_module_check{ 0 }; std::atomic last_periodic_check{ 0 }; -std::atomic heartbeat_started{ false }; std::atomic text_hashes_ready{ false }; struct TextHash { size_t offset; size_t len; uint32_t hash; }; std::vector text_hashes; @@ -232,7 +219,6 @@ std::atomic data_prot_ready{ false }; std::vector> data_protections; std::atomic heavy_fail_streak{ 0 }; static std::atomic text_crc_baseline{ 0 }; -static std::atomic watchdog_started{ false }; static std::atomic curl_crc_baseline{ 0 }; static std::atomic sodium_crc_baseline{ 0 }; @@ -242,7 +228,6 @@ static inline void secure_zero(std::string& value) noexcept if (!value.empty()) { SecureZeroMemory(value.data(), value.size()); value.clear(); - value.shrink_to_fit(); } } @@ -261,56 +246,76 @@ static uint64_t fnv1a64_bytes(const uint8_t* data, size_t len) return h; } -uint32_t KeyAuth::api::derive_secure_key() const +std::string KeyAuth::api::protect_field(const std::string& in, bool encrypt) const { - LARGE_INTEGER qpc{}; - QueryPerformanceCounter(&qpc); - const uint64_t t = static_cast(qpc.QuadPart); - const uint64_t p = reinterpret_cast(this); - uint32_t k = static_cast(t ^ (t >> 32) ^ p ^ (p >> 32)); - k ^= static_cast(GetCurrentProcessId()); - k ^= static_cast(GetCurrentThreadId() << 16); - k ^= 0x9e3779b9u; - k ^= (k << 6); - k ^= (k >> 2); - if (k == 0) k = 0xA5A5A5A5u; - return k; -} - -std::string KeyAuth::api::xor_crypt_field(const std::string& in) const -{ - if (in.empty()) - return {}; - uint32_t k = secure_strings_key_; - std::string out = in; - for (size_t i = 0; i < out.size(); ++i) { - k ^= (k << 13); - k ^= (k >> 17); - k ^= (k << 5); - out[i] = static_cast(out[i] ^ (k & 0xFF)); + if (in.empty()) return {}; + std::string buffer; + try { + if (encrypt) { + if (in.size() > MAXDWORD - sizeof(uint64_t) - CRYPTPROTECTMEMORY_BLOCK_SIZE) + throw request_error("Protected string exceeds the Windows memory limit."); + const uint64_t length = in.size(); + const size_t bytes = sizeof(length) + in.size(); + buffer.resize((bytes + CRYPTPROTECTMEMORY_BLOCK_SIZE - 1) / + CRYPTPROTECTMEMORY_BLOCK_SIZE * CRYPTPROTECTMEMORY_BLOCK_SIZE, '\0'); + std::memcpy(buffer.data(), &length, sizeof(length)); + std::memcpy(buffer.data() + sizeof(length), in.data(), in.size()); + if (!CryptProtectMemory(buffer.data(), static_cast(buffer.size()), + CRYPTPROTECTMEMORY_SAME_PROCESS)) + throw request_error("Windows memory protection failed."); + return buffer; + } + if (in.size() > MAXDWORD || in.size() % CRYPTPROTECTMEMORY_BLOCK_SIZE != 0) + throw request_error("Invalid protected string size."); + buffer = in; + if (!CryptUnprotectMemory(buffer.data(), static_cast(buffer.size()), + CRYPTPROTECTMEMORY_SAME_PROCESS)) + throw request_error("Windows memory unprotection failed."); + uint64_t length = 0; + std::memcpy(&length, buffer.data(), sizeof(length)); + if (length > buffer.size() - sizeof(length)) + throw request_error("Invalid protected string length."); + std::string result(buffer.data() + sizeof(length), static_cast(length)); + secure_zero(buffer); + return result; + } catch (...) { + secure_zero(buffer); + throw; } - return out; } -std::string KeyAuth::api::get_name() const { return secure_strings_enabled_ ? xor_crypt_field(name_enc_) : name; } -std::string KeyAuth::api::get_ownerid() const { return secure_strings_enabled_ ? xor_crypt_field(ownerid_enc_) : ownerid; } -std::string KeyAuth::api::get_version() const { return secure_strings_enabled_ ? xor_crypt_field(version_enc_) : version; } -std::string KeyAuth::api::get_url() const { return secure_strings_enabled_ ? xor_crypt_field(url_enc_) : url; } -std::string KeyAuth::api::get_path() const { return secure_strings_enabled_ ? xor_crypt_field(path_enc_) : path; } +std::string KeyAuth::api::get_name() const { + std::lock_guard lock(api_mutex_); + return secure_strings_enabled_ ? protect_field(name_enc_, false) : name; +} +std::string KeyAuth::api::get_ownerid() const { + std::lock_guard lock(api_mutex_); + return secure_strings_enabled_ ? protect_field(ownerid_enc_, false) : ownerid; +} +std::string KeyAuth::api::get_version() const { + std::lock_guard lock(api_mutex_); + return secure_strings_enabled_ ? protect_field(version_enc_, false) : version; +} +std::string KeyAuth::api::get_url() const { + std::lock_guard lock(api_mutex_); + return secure_strings_enabled_ ? protect_field(url_enc_, false) : url; +} +std::string KeyAuth::api::get_path() const { + std::lock_guard lock(api_mutex_); + return secure_strings_enabled_ ? protect_field(path_enc_, false) : path; +} void KeyAuth::api::enable_secure_strings(bool enable) { + std::lock_guard api_lock(api_mutex_); if (enable) { if (secure_strings_enabled_) return; - if (secure_strings_key_ == 0) { - secure_strings_key_ = derive_secure_key(); - } - name_enc_ = xor_crypt_field(name); - ownerid_enc_ = xor_crypt_field(ownerid); - version_enc_ = xor_crypt_field(version); - url_enc_ = xor_crypt_field(url); - path_enc_ = xor_crypt_field(path); + name_enc_ = protect_field(name, true); + ownerid_enc_ = protect_field(ownerid, true); + version_enc_ = protect_field(version, true); + url_enc_ = protect_field(url, true); + path_enc_ = protect_field(path, true); secure_zero(name); secure_zero(ownerid); secure_zero(version); @@ -320,17 +325,16 @@ void KeyAuth::api::enable_secure_strings(bool enable) } else { if (!secure_strings_enabled_) return; - name = xor_crypt_field(name_enc_); - ownerid = xor_crypt_field(ownerid_enc_); - version = xor_crypt_field(version_enc_); - url = xor_crypt_field(url_enc_); - path = xor_crypt_field(path_enc_); + name = protect_field(name_enc_, false); + ownerid = protect_field(ownerid_enc_, false); + version = protect_field(version_enc_, false); + url = protect_field(url_enc_, false); + path = protect_field(path_enc_, false); secure_zero(name_enc_); secure_zero(ownerid_enc_); secure_zero(version_enc_); secure_zero(url_enc_); secure_zero(path_enc_); - secure_strings_key_ = 0; secure_strings_enabled_ = false; } } @@ -357,9 +361,12 @@ uint64_t KeyAuth::api::compute_auth_seal(uint64_t nonce, long long window) const material += std::to_string(nonce); material += '|'; material += std::to_string(window); - material += '|'; - material += k_build_tag; - return fnv1a64_bytes(reinterpret_cast(material.data()), material.size()); + uint64_t seal = 0; + const int status = crypto_shorthash(reinterpret_cast(&seal), + reinterpret_cast(material.data()), material.size(), auth_seal_key_.data()); + secure_zero(material); + if (status != 0) throw request_error("Authentication state verification failed."); + return seal; } bool KeyAuth::api::has_active_subscription() const @@ -384,6 +391,7 @@ void KeyAuth::api::reset_auth_runtime() { LoggedIn.store(false); auth_nonce_.store(0); + SecureZeroMemory(auth_seal_key_.data(), auth_seal_key_.size()); auth_window_.store(0); auth_seal_.store(0); response.success = false; @@ -392,10 +400,18 @@ void KeyAuth::api::reset_auth_runtime() void KeyAuth::api::mark_authenticated() { - std::random_device rd; - uint64_t nonce = (static_cast(rd()) << 32) ^ static_cast(rd()); - if (nonce == 0) { - nonce = 0x9e3779b97f4a7c15ULL ^ reinterpret_cast(this); + uint64_t nonce = 0; + do { + if (BCryptGenRandom(nullptr, reinterpret_cast(&nonce), sizeof(nonce), + BCRYPT_USE_SYSTEM_PREFERRED_RNG) < 0) { + reset_auth_runtime(); + throw request_error("Windows cryptographic random generation failed."); + } + } while (nonce == 0); + if (BCryptGenRandom(nullptr, auth_seal_key_.data(), static_cast(auth_seal_key_.size()), + BCRYPT_USE_SYSTEM_PREFERRED_RNG) < 0) { + reset_auth_runtime(); + throw request_error("Windows cryptographic random generation failed."); } const auto window = std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count() / 90; @@ -418,7 +434,13 @@ void KeyAuth::api::refresh_auth_runtime() auth_seal_.store(compute_auth_seal(nonce, window)); } -bool KeyAuth::api::local_auth_valid(bool require_paid) const +bool KeyAuth::api::authenticated(bool require_paid) const +{ + std::lock_guard api_lock(api_mutex_); + return initialized && local_auth_valid(require_paid); +} + +bool KeyAuth::api::local_auth_valid(bool require_paid, bool require_fresh) const { if (!LoggedIn.load()) return false; @@ -431,12 +453,12 @@ bool KeyAuth::api::local_auth_valid(bool require_paid) const if (nonce == 0 || sealed_window == 0 || sealed == 0) return false; + const uint64_t expected = compute_auth_seal(nonce, sealed_window); + if (sodium_memcmp(&expected, &sealed, sizeof(sealed)) != 0) + return false; const auto current_window = std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count() / 90; - if (std::llabs(current_window - sealed_window) > 2) - return false; - - if (compute_auth_seal(nonce, sealed_window) != sealed) + if (require_fresh && !keyauth_detail::auth_window_fresh(sealed_window, current_window)) return false; if (require_paid && !has_active_subscription() && !response.isPaid) @@ -447,14 +469,11 @@ bool KeyAuth::api::local_auth_valid(bool require_paid) const static bool request_bypasses_local_auth(const std::string& data) { - return data.find("type=init") != std::string::npos || - data.find("type=login") != std::string::npos || - data.find("type=license") != std::string::npos || - data.find("type=register") != std::string::npos || - data.find("type=upgrade") != std::string::npos || - data.find("type=forgot") != std::string::npos || - data.find("type=check") != std::string::npos || - data.find("type=logout") != std::string::npos; + const std::string type = data.substr(0, data.find('&')); + return type == "type=init" || type == "type=login" || type == "type=license" || + type == "type=register" || type == "type=upgrade" || type == "type=forgot" || + type == "type=check" || type == "type=logout" || + keyauth_detail::email_request_uses_get(type.substr(5)); } static std::string decode_pubkey_hex(const uint8_t* obf, size_t len, uint8_t key) @@ -481,8 +500,9 @@ static std::string wide_to_utf8(const wchar_t* w) int needed = WideCharToMultiByte(CP_UTF8, 0, w, -1, nullptr, 0, nullptr, nullptr); if (needed <= 0) return {}; - std::string out(static_cast(needed - 1), '\0'); + std::string out(static_cast(needed), '\0'); WideCharToMultiByte(CP_UTF8, 0, w, -1, out.data(), needed, nullptr, nullptr); + out.pop_back(); return out; } @@ -611,33 +631,11 @@ struct ScopeWipe final { void KeyAuth::api::init() { - // harden dll search order to reduce current-dir hijacks - SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32 | LOAD_LIBRARY_SEARCH_USER_DIRS); - SetDllDirectoryW(L""); -#if KEYAUTH_HAVE_SECURITY - LockMemAccess(); -#endif - { - wchar_t exe_path[MAX_PATH] = {}; - GetModuleFileNameW(nullptr, exe_path, MAX_PATH); - std::wstring exe_dir = exe_path; - const auto last_slash = exe_dir.find_last_of(L"\\/"); - if (last_slash != std::wstring::npos) { - exe_dir = exe_dir.substr(0, last_slash); - AddDllDirectory(exe_dir.c_str()); - } - } - std::thread(runChecks).detach(); + std::lock_guard api_lock(api_mutex_); + if (initialized) return; + static const CURLcode curl_init_result = curl_global_init(CURL_GLOBAL_DEFAULT); + if (curl_init_result != CURLE_OK) throw request_error("CURL global initialization failed."); (void)get_public_key_hex(); - seed = generate_random_number(); - std::atexit([]() { cleanUpSeedData(seed); }); - - if (!watchdog_started.exchange(true)) { - std::thread(security_watchdog).detach(); - } - - CreateThread(0, 0, (LPTHREAD_START_ROUTINE)modify, 0, 0, 0); - if (get_ownerid().length() != 10) { MessageBoxA(0, XorStr("Application Not Setup Correctly. Please Watch Video Linked in main.cpp").c_str(), NULL, MB_ICONERROR); @@ -645,18 +643,12 @@ void KeyAuth::api::init() } std::string hash = checksum(); - CURL* curl = curl_easy_init(); auto data = XorStr("type=init") + - XorStr("&ver=") + get_version() + - XorStr("&hash=") + hash + - XorStr("&name=") + curl_escape(curl, get_name().c_str()) + - XorStr("&ownerid=") + get_ownerid(); - if (curl) { - curl_easy_cleanup(curl); // avoid leak from escape helper. -nigel - curl = nullptr; - } - + XorStr("&ver=") + curl_escape(nullptr, get_version()) + + XorStr("&hash=") + curl_escape(nullptr, hash) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); // to ensure people removed secret from main.cpp (some people will forget to) const std::string resolved_path = get_path(); if (resolved_path.find("https") != std::string::npos) { @@ -676,28 +668,11 @@ void KeyAuth::api::init() std::string thash; std::getline(file, token); - auto exec = [&](const char* cmd) -> std::string - { - uint16_t line = -1; - std::array buffer; - std::string result; - std::unique_ptr pipe(_popen(cmd, "r"), _pclose); - if (!pipe) { - throw std::runtime_error(XorStr("popen() failed!")); - } - - while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { - result = buffer.data(); - } - return result; - }; + thash = keyauth_detail::file_md5(std::filesystem::path(resolved_path)); - thash = exec(("certutil -hashfile \"" + resolved_path + XorStr("\" MD5 | find /i /v \"md5\" | find /i /v \"certutil\"")).c_str()); - - data += XorStr("&token=").c_str() + token; - data += XorStr("&thash=").c_str() + resolved_path; + data += XorStr("&token=").c_str() + curl_escape(nullptr, token); + data += XorStr("&thash=").c_str() + curl_escape(nullptr, thash); } - // curl was only used for escape above auto response = req(data, get_url()); @@ -709,20 +684,7 @@ void KeyAuth::api::init() std::hash hasher; int expectedHash = hasher(42); - // 4 lines down, used for debug - /*std::cout << "[DEBUG] Preparing to verify payload..." << std::endl; - std::cout << "[DEBUG] Signature: " << signature << std::endl; - std::cout << "[DEBUG] Timestamp: " << signatureTimestamp << std::endl; - std::cout << "[DEBUG] Raw body: " << response << std::endl;*/ - - if (signature.empty() || signatureTimestamp.empty()) { // used for debug - std::cerr << "[ERROR] Signature or timestamp is empty. Cannot verify." << std::endl; - MessageBoxA(0, "Missing signature headers in response", "KeyAuth", MB_ICONERROR); - exit(99); // Temporary debug exit code - } - - - int result = VerifyPayload(signature, signatureTimestamp, response.data()); + int result = VerifyPayload(signature, signatureTimestamp, response); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { auto json = response_decoder.parse(response); @@ -746,6 +708,7 @@ void KeyAuth::api::init() Sleep(100); } sessionid = json[(XorStr("sessionid"))]; + authenticated_host_ = keyauth_detail::parse_api_endpoint(get_url()).host; initialized = true; load_app_data(json[(XorStr("appinfo"))]); } @@ -773,48 +736,28 @@ void KeyAuth::api::init() } } -size_t write_callback(void* contents, size_t size, size_t nmemb, void* userp) { - ((std::string*)userp)->append((char*)contents, size * nmemb); - return size * nmemb; +size_t write_callback(void* contents, size_t size, size_t nmemb, void* userp) noexcept { + if (size && nmemb > SIZE_MAX / size) return 0; + const size_t bytes = size * nmemb; + try { + return keyauth_detail::append_response(*static_cast(userp), + static_cast(contents), bytes) ? bytes : 0; + } catch (...) { return 0; } } -// Callback function to handle headers -size_t header_callback(char* buffer, size_t size, size_t nitems, void* userdata) { - size_t totalSize = size * nitems; - - std::string header(buffer, totalSize); - if (header.empty()) - return totalSize; - // trim CRLF - while (!header.empty() && (header.back() == '\r' || header.back() == '\n')) { - header.pop_back(); - } - const auto colon = header.find(':'); - if (colon == std::string::npos) - return totalSize; - std::string key = header.substr(0, colon); - std::string value = header.substr(colon + 1); - while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) { - value.erase(value.begin()); - } - std::transform(key.begin(), key.end(), key.begin(), ::tolower); - - auto* sig = static_cast(userdata); - if (sig) { - if (key == "x-signature-ed25519") { - sig->signature = value; - } - if (key == "x-signature-timestamp") { - sig->timestamp = value; - } - } - - return totalSize; +size_t header_callback(char* buffer, size_t size, size_t nitems, void* userdata) noexcept { + if (size && nitems > SIZE_MAX / size) return 0; + const size_t bytes = size * nitems; + try { + return keyauth_detail::consume_header(*static_cast(userdata), + std::string(buffer, bytes)) ? bytes : 0; + } catch (...) { return 0; } } void KeyAuth::api::login(std::string username, std::string password, std::string code) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_user(username); ScopeWipe wipe_pass(password); @@ -823,18 +766,18 @@ void KeyAuth::api::login(std::string username, std::string password, std::string std::string hwid = utils::get_hwid(); auto data = XorStr("type=login") + - XorStr("&username=") + username + - XorStr("&pass=") + password + - XorStr("&code=") + code + - XorStr("&hwid=") + hwid + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&username=") + curl_escape(nullptr, username) + + XorStr("&pass=") + curl_escape(nullptr, password) + + XorStr("&code=") + curl_escape(nullptr, code) + + XorStr("&hwid=") + curl_escape(nullptr, hwid) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); //std::cout << "[DEBUG] Login response: " << response << std::endl; std::hash hasher; int expectedHash = hasher(42); - int result = VerifyPayload(signature, signatureTimestamp, response.data()); + int result = VerifyPayload(signature, signatureTimestamp, response); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { auto json = response_decoder.parse(response); @@ -857,30 +800,11 @@ void KeyAuth::api::login(std::string username, std::string password, std::string else reset_auth_runtime(); - if (api::response.message != XorStr("Initialized").c_str()) { - LI_FN(GlobalAddAtomA)(seed.c_str()); - - std::string file_path = XorStr("C:\\ProgramData\\").c_str() + seed; - std::ofstream file(file_path); - if (file.is_open()) { - file << seed; - file.close(); - } - - std::string regPath = XorStr("Software\\").c_str() + seed; - HKEY hKey; - LONG result = RegCreateKeyExA(HKEY_CURRENT_USER, regPath.c_str(), 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL); - if (result == ERROR_SUCCESS) { - LI_FN(RegSetValueExA)(hKey, seed.c_str(), 0, REG_SZ, reinterpret_cast(seed.c_str()), seed.size() + 1); - LI_FN(RegCloseKey)(hKey); - } - - LI_FN(GlobalAddAtomA)(get_ownerid().c_str()); + if (json[XorStr("success")].get()) { mark_authenticated(); - start_heartbeat(this); - } - else { - KA_EXIT(12); + start_heartbeat(); + } else { + reset_auth_runtime(); } } else { @@ -892,17 +816,150 @@ void KeyAuth::api::login(std::string username, std::string password, std::string } } + +nlohmann::json KeyAuth::api::email_reply(std::string data) +{ + ScopeWipe wipe(data); + const auto raw = req(data, get_url()); + auto reply = nlohmann::json::parse(raw, nullptr, false); + try { + keyauth_detail::validate_api_reply(reply, get_ownerid()); + } catch (const std::exception& e) { + throw request_error(e.what()); + } + load_response_data(reply); + return reply; +} + +bool KeyAuth::api::finish_email_login(const nlohmann::json& reply, const std::string& email) +{ + reset_auth_runtime(); + user_data = {}; + load_response_data(reply); + if (!response.success) return false; + try { + if (reply.contains("info")) { + if (!reply["info"].is_object()) throw request_error("Invalid login profile."); + load_user_data(reply["info"]); + } + if (user_data.username.empty()) user_data.username = email; + const auto status = email_reply("type=check&sessionid=" + curl_escape(nullptr, sessionid) + + "&name=" + curl_escape(nullptr, get_name()) + "&ownerid=" + curl_escape(nullptr, get_ownerid())); + if (!response.success) { + user_data = {}; + return false; + } + response.message = reply["message"].get(); + mark_authenticated(); + start_heartbeat(); + magic_link_email_.clear(); + return true; + } catch (...) { + reset_auth_runtime(); + user_data = {}; + throw; + } +} + +void KeyAuth::api::login_email(std::string email, std::string password, std::string code) +{ + std::lock_guard lock(api_mutex_); + checkInit(); + ScopeWipe wipe_email(email), wipe_password(password), wipe_code(code); + reset_auth_runtime(); + user_data = {}; + magic_link_email_.clear(); + const auto reply = email_reply(keyauth_detail::email_request("loginEmail", email, password, code, + sessionid, get_name(), get_ownerid(), utils::get_hwid())); + finish_email_login(reply, email); +} + +void KeyAuth::api::email_otp_send(std::string email) +{ + std::lock_guard lock(api_mutex_); + checkInit(); + ScopeWipe wipe_email(email); + reset_auth_runtime(); + user_data = {}; + magic_link_email_.clear(); + email_reply(keyauth_detail::email_request("emailOTPSend", email, "", "", sessionid, + get_name(), get_ownerid(), "")); +} + +void KeyAuth::api::email_otp_verify(std::string email, std::string code) +{ + std::lock_guard lock(api_mutex_); + checkInit(); + ScopeWipe wipe_email(email), wipe_code(code); + reset_auth_runtime(); + user_data = {}; + const auto reply = email_reply(keyauth_detail::email_request("emailOTPVerify", email, "", code, + sessionid, get_name(), get_ownerid(), utils::get_hwid())); + finish_email_login(reply, email); +} + +void KeyAuth::api::magic_link_send(std::string email) +{ + std::lock_guard lock(api_mutex_); + checkInit(); + ScopeWipe wipe_email(email); + reset_auth_runtime(); + user_data = {}; + magic_link_email_.clear(); + email_reply(keyauth_detail::email_request("magicLinkSend", email, "", "", sessionid, + get_name(), get_ownerid(), utils::get_hwid())); + if (response.success) { + magic_link_email_ = email; + magic_link_started_ = std::chrono::steady_clock::now(); + magic_link_last_check_ = {}; + } +} + +bool KeyAuth::api::magic_link_pending() const +{ + std::lock_guard lock(api_mutex_); + return !magic_link_email_.empty() && + std::chrono::steady_clock::now() - magic_link_started_ < std::chrono::minutes(15); +} + +bool KeyAuth::api::magic_link_check() +{ + std::lock_guard lock(api_mutex_); + checkInit(); + response.success = false; + if (!magic_link_pending()) { + magic_link_email_.clear(); + response.message = "No active magic link request, or the link has expired."; + return false; + } + const auto now = std::chrono::steady_clock::now(); + if (now - magic_link_last_check_ < std::chrono::seconds(3)) { + response.message = "Waiting for magic link approval."; + return false; + } + magic_link_last_check_ = now; + const auto email = magic_link_email_; + const auto reply = email_reply(keyauth_detail::email_request("magicLinkCheck", email, "", "", + sessionid, get_name(), get_ownerid(), utils::get_hwid())); + if (!keyauth_detail::magic_link_approved(reply)) { + response.success = false; + return false; + } + return finish_email_login(reply, email); +} + void KeyAuth::api::chatget(std::string channel) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_channel(channel); auto data = XorStr("type=chatget") + - XorStr("&channel=") + channel + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&channel=") + curl_escape(nullptr, channel) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); auto json = response_decoder.parse(response); @@ -911,17 +968,18 @@ void KeyAuth::api::chatget(std::string channel) bool KeyAuth::api::chatsend(std::string message, std::string channel) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_message(message); ScopeWipe wipe_channel(channel); auto data = XorStr("type=chatsend") + - XorStr("&message=") + message + - XorStr("&channel=") + channel + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&message=") + curl_escape(nullptr, message) + + XorStr("&channel=") + curl_escape(nullptr, channel) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); auto json = response_decoder.parse(response); @@ -931,20 +989,21 @@ bool KeyAuth::api::chatsend(std::string message, std::string channel) void KeyAuth::api::changeUsername(std::string newusername) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_user(newusername); auto data = XorStr("type=changeUsername") + - XorStr("&newUsername=") + newusername + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&newUsername=") + curl_escape(nullptr, newusername) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); std::hash hasher; int expectedHash = hasher(42); - int result = VerifyPayload(signature, signatureTimestamp, response.data()); + int result = VerifyPayload(signature, signatureTimestamp, response); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { @@ -973,16 +1032,17 @@ void KeyAuth::api::changeUsername(std::string newusername) KeyAuth::api::Tfa& KeyAuth::api::enable2fa(std::string code) { + std::lock_guard api_lock(api_mutex_); checkInit(); KeyAuth::api::activate = true; auto data = XorStr("type=2faenable") + - XorStr("&code=") + code + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&code=") + curl_escape(nullptr, code) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); auto json = response_decoder.parse(response); @@ -1002,6 +1062,7 @@ KeyAuth::api::Tfa& KeyAuth::api::enable2fa(std::string code) KeyAuth::api::Tfa& KeyAuth::api::disable2fa(std::string code) { + std::lock_guard api_lock(api_mutex_); checkInit(); KeyAuth::api::activate = false; @@ -1013,10 +1074,10 @@ KeyAuth::api::Tfa& KeyAuth::api::disable2fa(std::string code) auto data = XorStr("type=2fadisable") + - XorStr("&code=") + code + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&code=") + curl_escape(nullptr, code) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); @@ -1066,11 +1127,12 @@ KeyAuth::api::Tfa& KeyAuth::api::Tfa::handleInput(KeyAuth::api& instance) { instance.disable2fa(code); } - + return *this; } void KeyAuth::api::web_login() { + std::lock_guard api_lock(api_mutex_); checkInit(); // from https://perpetualprogrammers.wordpress.com/2016/05/22/the-http-server-api/ @@ -1285,17 +1347,17 @@ void KeyAuth::api::web_login() std::string hwid = utils::get_hwid(); auto data = XorStr("type=login") + - XorStr("&username=") + user + - XorStr("&token=") + token + - XorStr("&hwid=") + hwid + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&username=") + curl_escape(nullptr, user) + + XorStr("&token=") + curl_escape(nullptr, token) + + XorStr("&hwid=") + curl_escape(nullptr, hwid) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto resp = req(data, get_url()); std::hash hasher; int expectedHash = hasher(42); - int result = VerifyPayload(signature, signatureTimestamp, resp.data()); + int result = VerifyPayload(signature, signatureTimestamp, resp); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { auto json = response_decoder.parse(resp); @@ -1310,30 +1372,13 @@ void KeyAuth::api::web_login() size_t resultCode = hasher(json[(XorStr("code"))]); if (!json[(XorStr("success"))] || (json[(XorStr("success"))] && (resultCode == expectedHash))) { - if (api::response.message != XorStr("Initialized").c_str()) { - LI_FN(GlobalAddAtomA)(seed.c_str()); - - std::string file_path = XorStr("C:\\ProgramData\\").c_str() + seed; - std::ofstream file(file_path); - if (file.is_open()) { - file << seed; - file.close(); - } - - std::string regPath = XorStr("Software\\").c_str() + seed; - HKEY hKey; - LONG result = RegCreateKeyExA(HKEY_CURRENT_USER, regPath.c_str(), 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL); - if (result == ERROR_SUCCESS) { - LI_FN(RegSetValueExA)(hKey, seed.c_str(), 0, REG_SZ, reinterpret_cast(seed.c_str()), seed.size() + 1); - LI_FN(RegCloseKey)(hKey); - } - - LI_FN(GlobalAddAtomA)(get_ownerid().c_str()); + load_response_data(json); + if (json[XorStr("success")].get()) { + load_user_data(json[XorStr("info")]); mark_authenticated(); - start_heartbeat(this); - } - else { - KA_EXIT(12); + start_heartbeat(); + } else { + reset_auth_runtime(); } // Respond to the request. @@ -1405,6 +1450,7 @@ void KeyAuth::api::web_login() void KeyAuth::api::button(std::string button) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_button(button); @@ -1521,6 +1567,7 @@ void KeyAuth::api::button(std::string button) } void KeyAuth::api::regstr(std::string username, std::string password, std::string key, std::string email) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_user(username); ScopeWipe wipe_pass(password); @@ -1530,19 +1577,19 @@ void KeyAuth::api::regstr(std::string username, std::string password, std::strin std::string hwid = utils::get_hwid(); auto data = XorStr("type=register") + - XorStr("&username=") + username + - XorStr("&pass=") + password + - XorStr("&key=") + key + - XorStr("&email=") + email + - XorStr("&hwid=") + hwid + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&username=") + curl_escape(nullptr, username) + + XorStr("&pass=") + curl_escape(nullptr, password) + + XorStr("&key=") + curl_escape(nullptr, key) + + XorStr("&email=") + curl_escape(nullptr, email) + + XorStr("&hwid=") + curl_escape(nullptr, hwid) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); std::hash hasher; int expectedHash = hasher(42); - int result = VerifyPayload(signature, signatureTimestamp, response.data()); + int result = VerifyPayload(signature, signatureTimestamp, response); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { auto json = response_decoder.parse(response); @@ -1564,30 +1611,11 @@ void KeyAuth::api::regstr(std::string username, std::string password, std::strin else reset_auth_runtime(); - if (api::response.message != XorStr("Initialized").c_str()) { - LI_FN(GlobalAddAtomA)(seed.c_str()); - - std::string file_path = XorStr("C:\\ProgramData\\").c_str() + seed; - std::ofstream file(file_path); - if (file.is_open()) { - file << seed; - file.close(); - } - - std::string regPath = XorStr("Software\\").c_str() + seed; - HKEY hKey; - LONG result = RegCreateKeyExA(HKEY_CURRENT_USER, regPath.c_str(), 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL); - if (result == ERROR_SUCCESS) { - LI_FN(RegSetValueExA)(hKey, seed.c_str(), 0, REG_SZ, reinterpret_cast(seed.c_str()), seed.size() + 1); - LI_FN(RegCloseKey)(hKey); - } - - LI_FN(GlobalAddAtomA)(get_ownerid().c_str()); + if (json[XorStr("success")].get()) { mark_authenticated(); - start_heartbeat(this); - } - else { - KA_EXIT(12); + start_heartbeat(); + } else { + reset_auth_runtime(); } } else { @@ -1601,22 +1629,23 @@ void KeyAuth::api::regstr(std::string username, std::string password, std::strin } void KeyAuth::api::upgrade(std::string username, std::string key) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_user(username); ScopeWipe wipe_key(key); auto data = XorStr("type=upgrade") + - XorStr("&username=") + username + - XorStr("&key=") + key + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&username=") + curl_escape(nullptr, username) + + XorStr("&key=") + curl_escape(nullptr, key) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); std::hash hasher; int expectedHash = hasher(42); - int result = VerifyPayload(signature, signatureTimestamp, response.data()); + int result = VerifyPayload(signature, signatureTimestamp, response); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { auto json = response_decoder.parse(response); @@ -1659,6 +1688,7 @@ std::string generate_random_number() { } void KeyAuth::api::license(std::string key, std::string code) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_key(key); ScopeWipe wipe_code(code); @@ -1666,17 +1696,17 @@ void KeyAuth::api::license(std::string key, std::string code) { std::string hwid = utils::get_hwid(); auto data = XorStr("type=license") + - XorStr("&key=") + key + - XorStr("&code=") + code + - XorStr("&hwid=") + hwid + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&key=") + curl_escape(nullptr, key) + + XorStr("&code=") + curl_escape(nullptr, code) + + XorStr("&hwid=") + curl_escape(nullptr, hwid) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); std::hash hasher; int expectedHash = hasher(42); - int result = VerifyPayload(signature, signatureTimestamp, response.data()); + int result = VerifyPayload(signature, signatureTimestamp, response); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { auto json = response_decoder.parse(response); @@ -1697,29 +1727,11 @@ void KeyAuth::api::license(std::string key, std::string code) { else reset_auth_runtime(); - if (api::response.message != XorStr("Initialized").c_str()) { - LI_FN(GlobalAddAtomA)(seed.c_str()); - - std::string file_path = XorStr("C:\\ProgramData\\").c_str() + seed; - std::ofstream file(file_path); - if (file.is_open()) { - file << seed; - file.close(); - } - - std::string regPath = XorStr("Software\\").c_str() + seed; - HKEY hKey; - LONG result = RegCreateKeyExA(HKEY_CURRENT_USER, regPath.c_str(), 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL); - if (result == ERROR_SUCCESS) { - LI_FN(RegSetValueExA)(hKey, seed.c_str(), 0, REG_SZ, reinterpret_cast(seed.c_str()), seed.size() + 1); - LI_FN(RegCloseKey)(hKey); - } - - LI_FN(GlobalAddAtomA)(get_ownerid().c_str()); + if (json[XorStr("success")].get()) { mark_authenticated(); - } - else { - KA_EXIT(12); + start_heartbeat(); + } else { + reset_auth_runtime(); } } else { @@ -1732,37 +1744,39 @@ void KeyAuth::api::license(std::string key, std::string code) { } void KeyAuth::api::setvar(std::string var, std::string vardata) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_var(var); ScopeWipe wipe_data(vardata); auto data = XorStr("type=setvar") + - XorStr("&var=") + var + - XorStr("&data=") + vardata + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&var=") + curl_escape(nullptr, var) + + XorStr("&data=") + curl_escape(nullptr, vardata) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); auto json = response_decoder.parse(response); load_response_data(json); } std::string KeyAuth::api::getvar(std::string var) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_var(var); auto data = XorStr("type=getvar") + - XorStr("&var=") + var + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&var=") + curl_escape(nullptr, var) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); std::hash hasher; int expectedHash = hasher(42); - int result = VerifyPayload(signature, signatureTimestamp, response.data()); + int result = VerifyPayload(signature, signatureTimestamp, response); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { auto json = response_decoder.parse(response); @@ -1790,20 +1804,21 @@ std::string KeyAuth::api::getvar(std::string var) { } void KeyAuth::api::ban(std::string reason) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_reason(reason); auto data = XorStr("type=ban") + - XorStr("&reason=") + reason + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&reason=") + curl_escape(nullptr, reason) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); std::hash hasher; int expectedHash = hasher(42); - int result = VerifyPayload(signature, signatureTimestamp, response.data()); + int result = VerifyPayload(signature, signatureTimestamp, response); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { auto json = response_decoder.parse(response); @@ -1831,20 +1846,21 @@ void KeyAuth::api::ban(std::string reason) { } bool KeyAuth::api::checkblack() { + std::lock_guard api_lock(api_mutex_); checkInit(); std::string hwid = utils::get_hwid(); auto data = XorStr("type=checkblacklist") + - XorStr("&hwid=") + hwid + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&hwid=") + curl_escape(nullptr, hwid) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); std::hash hasher; int expectedHash = hasher(42); - int result = VerifyPayload(signature, signatureTimestamp, response.data()); + int result = VerifyPayload(signature, signatureTimestamp, response); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { auto json = response_decoder.parse(response); @@ -1869,18 +1885,19 @@ bool KeyAuth::api::checkblack() { } void KeyAuth::api::check(bool check_paid) { + std::lock_guard api_lock(api_mutex_); checkInit(); - if (LoggedIn.load() && !local_auth_valid(check_paid)) { + if (LoggedIn.load() && !local_auth_valid(check_paid, false)) { reset_auth_runtime(); return; } auto data = XorStr("type=check") + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); std::string endpoint = get_url(); if (check_paid) { @@ -1891,7 +1908,7 @@ void KeyAuth::api::check(bool check_paid) { std::hash hasher; int expectedHash = hasher(42); - int result = VerifyPayload(signature, signatureTimestamp, response.data()); + int result = VerifyPayload(signature, signatureTimestamp, response); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { auto json = response_decoder.parse(response); @@ -1925,19 +1942,20 @@ void KeyAuth::api::check(bool check_paid) { } std::string KeyAuth::api::var(std::string varid) { + std::lock_guard api_lock(api_mutex_); checkInit(); auto data = XorStr("type=var") + - XorStr("&varid=") + varid + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&varid=") + curl_escape(nullptr, varid) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); std::hash hasher; int expectedHash = hasher(42); - int result = VerifyPayload(signature, signatureTimestamp, response.data()); + int result = VerifyPayload(signature, signatureTimestamp, response); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { auto json = response_decoder.parse(response); @@ -1965,6 +1983,7 @@ std::string KeyAuth::api::var(std::string varid) { } void KeyAuth::api::log(std::string message) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_message(message); @@ -1975,16 +1994,17 @@ void KeyAuth::api::log(std::string message) { auto data = XorStr("type=log") + - XorStr("&pcuser=") + UsernamePC + - XorStr("&message=") + message + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&pcuser=") + curl_escape(nullptr, UsernamePC) + + XorStr("&message=") + curl_escape(nullptr, message) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); req(data, get_url()); } std::vector KeyAuth::api::download(std::string fileid) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_fileid(fileid); @@ -1994,76 +2014,55 @@ std::vector KeyAuth::api::download(std::string fileid) { auto data = XorStr("type=file") + - XorStr("&fileid=") + fileid + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); - - for (int attempt = 0; attempt < 2; ++attempt) { - auto response = req(data, get_url()); - auto json = nlohmann::json::parse(response, nullptr, false); - if (json.is_discarded() || !json.is_object()) { - api::response.success = false; - api::response.message = XorStr("invalid JSON response from download endpoint"); - api::response.message += " ["; - api::response.message += k_build_tag; - api::response.message += "]"; - return {}; - } + XorStr("&fileid=") + curl_escape(nullptr, fileid) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); - load_response_data(json); - const bool success = json.value(XorStr("success"), false); - if (success) { - std::string contents; - const std::string key_contents = XorStr("contents"); - if (json.contains(key_contents) && json[key_contents].is_string()) { - contents = json[key_contents].get(); - } - if (!contents.empty()) { - auto file = hexDecode(contents); - return to_uc_vector(file); - } - if (attempt == 0) { - continue; - } - api::response.message += " ["; - api::response.message += k_build_tag; - api::response.message += "]"; - error(XorStr("download returned empty contents.")); - } - api::response.message += " ["; - api::response.message += k_build_tag; - api::response.message += "]"; - return {}; + auto reply = req(data, get_url()); + auto json = nlohmann::json::parse(reply, nullptr, false); + if (json.is_discarded() || !json.is_object()) { + response.success = false; + throw request_error("Invalid JSON response from download endpoint."); } - return {}; + load_response_data(json); + if (!response.success) return {}; + if (!json.contains("contents") || !json["contents"].is_string()) { + response.success = false; + throw request_error("Download response has no contents field."); + } + const auto contents = json["contents"].get(); + if (contents.size() % 2 || (!contents.empty() && !is_hex_string(contents))) { + response.success = false; + throw request_error("Download contents are not valid hexadecimal data."); + } + return to_uc_vector(hexDecode(contents)); } std::string KeyAuth::api::webhook(std::string id, std::string params, std::string body, std::string contenttype) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_id(id); ScopeWipe wipe_params(params); ScopeWipe wipe_body(body); ScopeWipe wipe_type(contenttype); - CURL *curl = curl_easy_init(); auto data = XorStr("type=webhook") + - XorStr("&webid=") + id + - XorStr("¶ms=") + curl_escape(curl, params) + - XorStr("&body=") + curl_escape(curl, body) + - XorStr("&conttype=") + contenttype + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); - curl_easy_cleanup(curl); + XorStr("&webid=") + curl_escape(nullptr, id) + + XorStr("¶ms=") + curl_escape(nullptr, params) + + XorStr("&body=") + curl_escape(nullptr, body) + + XorStr("&conttype=") + curl_escape(nullptr, contenttype) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); std::hash hasher; int expectedHash = hasher(42); - int result = VerifyPayload(signature, signatureTimestamp, response.data()); + int result = VerifyPayload(signature, signatureTimestamp, response); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { auto json = response_decoder.parse(response); @@ -2093,19 +2092,20 @@ std::string KeyAuth::api::webhook(std::string id, std::string params, std::strin std::string KeyAuth::api::fetchonline() { + std::lock_guard api_lock(api_mutex_); checkInit(); auto data = XorStr("type=fetchOnline") + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); std::hash hasher; int expectedHash = hasher(42); - int result = VerifyPayload(signature, signatureTimestamp, response.data()); + int result = VerifyPayload(signature, signatureTimestamp, response); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { auto json = response_decoder.parse(response); @@ -2141,18 +2141,19 @@ std::string KeyAuth::api::fetchonline() void KeyAuth::api::fetchstats() { + std::lock_guard api_lock(api_mutex_); checkInit(); auto data = XorStr("type=fetchStats") + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); std::hash hasher; int expectedHash = hasher(42); - int result = VerifyPayload(signature, signatureTimestamp, response.data()); + int result = VerifyPayload(signature, signatureTimestamp, response); if ((hasher(result ^ 0xA5A5) & 0xFFFF) == (expectedHash & 0xFFFF)) { @@ -2185,30 +2186,32 @@ void KeyAuth::api::fetchstats() void KeyAuth::api::forgot(std::string username, std::string email) { + std::lock_guard api_lock(api_mutex_); checkInit(); ScopeWipe wipe_user(username); ScopeWipe wipe_email(email); auto data = XorStr("type=forgot") + - XorStr("&username=") + username + - XorStr("&email=") + email + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&username=") + curl_escape(nullptr, username) + + XorStr("&email=") + curl_escape(nullptr, email) + + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); auto json = response_decoder.parse(response); load_response_data(json); } void KeyAuth::api::logout() { + std::lock_guard api_lock(api_mutex_); checkInit(); auto data = XorStr("type=logout") + - XorStr("&sessionid=") + sessionid + - XorStr("&name=") + get_name() + - XorStr("&ownerid=") + get_ownerid(); + XorStr("&sessionid=") + curl_escape(nullptr, sessionid) + + XorStr("&name=") + curl_escape(nullptr, get_name()) + + XorStr("&ownerid=") + curl_escape(nullptr, get_ownerid()); auto response = req(data, get_url()); auto json = response_decoder.parse(response); if (json[(XorStr("success"))]) { @@ -2222,10 +2225,10 @@ void KeyAuth::api::logout() { user_data.subscriptions.clear(); //clear sessionid - sessionid.clear(); + secure_zero(sessionid); //clear enckey - enckey.clear(); + secure_zero(enckey); reset_auth_runtime(); } @@ -2234,40 +2237,48 @@ void KeyAuth::api::logout() { void KeyAuth::api::start_ban_monitor(int interval_seconds, bool check_session, std::function on_ban) { - if (ban_monitor_running_) { - return; - } - - if (interval_seconds < 1) { - interval_seconds = 1; + if (ban_monitor_running_.load()) return; + if (ban_monitor_thread_.joinable()) { + if (ban_monitor_thread_.get_id() == std::this_thread::get_id()) return; + ban_monitor_thread_.join(); } - + interval_seconds = (std::max)(1, interval_seconds); ban_monitor_detected_ = false; ban_monitor_running_ = true; ban_monitor_thread_ = std::thread([this, interval_seconds, check_session, on_ban]() { - while (ban_monitor_running_) { - if (check_session) { - this->check(false); + while (ban_monitor_running_.load()) { + bool blacklisted = false; + try { + std::lock_guard lock(api_mutex_); + if (check_session) check(false); + blacklisted = checkblack(); + } catch (const std::exception& e) { + std::lock_guard lock(api_mutex_); + response.success = false; + response.message = e.what(); } - - if (this->checkblack()) { + if (blacklisted) { ban_monitor_detected_ = true; ban_monitor_running_ = false; - if (on_ban) { - on_ban(); - } + if (on_ban) on_ban(); return; } - - std::this_thread::sleep_for(std::chrono::seconds(interval_seconds)); + std::unique_lock wait_lock(heartbeat_mutex_); + heartbeat_cv_.wait_for(wait_lock, std::chrono::seconds(interval_seconds), + [this] { return !ban_monitor_running_.load(); }); } }); } void KeyAuth::api::stop_ban_monitor() { - ban_monitor_running_ = false; - if (ban_monitor_thread_.joinable()) { + { + std::lock_guard lock(heartbeat_mutex_); + ban_monitor_running_ = false; + } + heartbeat_cv_.notify_all(); + if (ban_monitor_thread_.joinable() && + ban_monitor_thread_.get_id() != std::this_thread::get_id()) { ban_monitor_thread_.join(); } } @@ -2371,139 +2382,21 @@ void KeyAuth::api::reset_lockout(lockout_state& state) int VerifyPayload(std::string signature, std::string timestamp, std::string body) { -#if defined(_DEBUG) - auto dump_sig_debug = [&](const char* reason) { - char temp_path[MAX_PATH] = {}; - char log_path[MAX_PATH] = {}; - if (GetTempPathA(MAX_PATH, temp_path) == 0) { - strcpy_s(temp_path, ".\\"); - } - sprintf_s(log_path, "%skeyauth_sig_debug.log", temp_path); - std::ofstream dbg(log_path, std::ios::out | std::ios::app); - if (!dbg.is_open()) { - return; - } - dbg << "reason=" << (reason ? reason : "unknown") << "\n"; - dbg << "timestamp=" << timestamp << "\n"; - dbg << "signature_len=" << signature.size() << "\n"; - dbg << "signature=" << signature << "\n"; - dbg << "body_len=" << body.size() << "\n"; - dbg << "body=" << body << "\n"; - dbg << "pubkey=" << get_public_key_hex() << "\n"; - dbg << "----\n"; - }; -#endif integrity_check(); - if (timestamp.size() < 10 || timestamp.size() > 13) { -#if defined(_DEBUG) - dump_sig_debug("timestamp_length"); -#endif - MessageBoxA(0, "Signature verification failed (timestamp length)", "KeyAuth", MB_ICONERROR); - exit(2); - } - for (char c : timestamp) { - if (c < '0' || c > '9') { -#if defined(_DEBUG) - dump_sig_debug("timestamp_format"); -#endif - MessageBoxA(0, "Signature verification failed (timestamp format)", "KeyAuth", MB_ICONERROR); - exit(2); - } - } - long long unix_timestamp = 0; - try { - unix_timestamp = std::stoll(timestamp); - } - catch (...) { - std::cerr << "[ERROR] Invalid timestamp format\n"; -#if defined(_DEBUG) - dump_sig_debug("invalid_timestamp"); -#endif - MessageBoxA(0, "Signature verification failed (invalid timestamp)", "KeyAuth", MB_ICONERROR); - exit(2); - } - - auto current_time = std::chrono::system_clock::now(); - long long current_unix_time = std::chrono::duration_cast( - current_time.time_since_epoch()).count(); - - const long long diff = std::llabs(current_unix_time - unix_timestamp); - if (diff > 120) { - std::cerr << "[ERROR] Timestamp too skewed (diff = " - << diff << "s)\n"; -#if defined(_DEBUG) - dump_sig_debug("timestamp_skew"); -#endif - MessageBoxA(0, "Signature verification failed (timestamp skew)", "KeyAuth", MB_ICONERROR); - exit(3); - } - - if (sodium_init() < 0) { - std::cerr << "[ERROR] Failed to initialize libsodium\n"; -#if defined(_DEBUG) - dump_sig_debug("libsodium_init"); -#endif - MessageBoxA(0, "Signature verification failed (libsodium init)", "KeyAuth", MB_ICONERROR); - exit(4); - } - - std::string message = timestamp + body; - - unsigned char sig[64]; - unsigned char pk[32]; - - if (signature.size() != 128) { -#if defined(_DEBUG) - dump_sig_debug("signature_length"); -#endif - MessageBoxA(0, "Signature verification failed (sig length)", "KeyAuth", MB_ICONERROR); - exit(5); - } - if (sodium_hex2bin(sig, sizeof(sig), signature.c_str(), signature.length(), NULL, NULL, NULL) != 0) { - std::cerr << "[ERROR] Failed to parse signature hex.\n"; -#if defined(_DEBUG) - dump_sig_debug("signature_hex_parse"); -#endif - MessageBoxA(0, "Signature verification failed (invalid signature format)", "KeyAuth", MB_ICONERROR); - exit(5); - } - - const std::string pubkey_hex = get_public_key_hex(); - if (sodium_hex2bin(pk, sizeof(pk), pubkey_hex.c_str(), pubkey_hex.length(), NULL, NULL, NULL) != 0) { - std::cerr << "[ERROR] Failed to parse public key hex.\n"; -#if defined(_DEBUG) - dump_sig_debug("public_key_parse"); -#endif - MessageBoxA(0, "Signature verification failed (invalid public key)", "KeyAuth", MB_ICONERROR); - exit(6); + const auto now = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + const auto result = keyauth_detail::verify_payload(signature, timestamp, body, + get_public_key_hex(), now); + if (result == keyauth_detail::PayloadResult::timestamp_skew) { + throw KeyAuth::request_error("Signed response timestamp is outside the 120 second window. Synchronize the system clock and retry."); } - - /*std::cout << "[DEBUG] Timestamp: " << timestamp << std::endl; - std::cout << "[DEBUG] Signature: " << signature << std::endl; - std::cout << "[DEBUG] Body: " << body << std::endl; - std::cout << "[DEBUG] Message (timestamp + body): " << message << std::endl; - std::cout << "[DEBUG] Public Key: " << pubkey_hex << std::endl;*/ - - if (crypto_sign_ed25519_verify_detached(sig, - reinterpret_cast(message.c_str()), - message.length(), - pk) != 0) - { - std::cerr << "[ERROR] Signature verification failed.\n"; -#if defined(_DEBUG) - dump_sig_debug("invalid_signature"); -#endif - MessageBoxA(0, "Signature verification failed (invalid signature)", "KeyAuth", MB_ICONERROR); - exit(7); + if (result != keyauth_detail::PayloadResult::valid) { + throw KeyAuth::request_error("KeyAuth response signature verification failed (reason " + + std::to_string(static_cast(result)) + "). Response rejected."); } - - //std::cout << "[DEBUG] Payload verified successfully.\n"; - - int value = 42 ^ 0xA5A5; - return value & 0xFFFF; + return (42 ^ 0xA5A5) & 0xFFFF; } - // credits https://stackoverflow.com/a/3790661 static std::string hexDecode(const std::string& hex) { @@ -2526,6 +2419,7 @@ std::string get_str_between_two_str(const std::string& s, if (first_delim_pos == std::string::npos) return {}; const auto end_pos_of_first_delim = first_delim_pos + start_delim.length(); + if (stop_delim.empty()) return s.substr(end_pos_of_first_delim); const auto last_delim_pos = s.find(stop_delim, end_pos_of_first_delim); if (last_delim_pos == std::string::npos || last_delim_pos < end_pos_of_first_delim) return {}; @@ -2536,53 +2430,17 @@ std::string get_str_between_two_str(const std::string& s, std::string curl_escape(CURL* curl, const std::string& input) { - if (!curl) - return input; - char* escaped = curl_easy_escape(curl, input.c_str(), 0); - if (!escaped) - return {}; - std::string out(escaped); - curl_free(escaped); - return out; -} - -static std::string build_query_encoded(CURL* curl, const std::string& data) -{ - std::string out; - size_t pos = 0; - while (pos < data.size()) { - size_t amp = data.find('&', pos); - if (amp == std::string::npos) amp = data.size(); - const std::string pair = data.substr(pos, amp - pos); - const size_t eq = pair.find('='); - std::string key = pair.substr(0, eq); - std::string val = (eq == std::string::npos) ? "" : pair.substr(eq + 1); - std::string enc_key = curl_escape(curl, key); - std::string enc_val = curl_escape(curl, val); - if (!out.empty()) out.push_back('&'); - out += enc_key; - if (eq != std::string::npos) { - out.push_back('='); - out += enc_val; - } - pos = amp + 1; - } - return out; + if (input.empty()) return {}; + if (input.size() > INT_MAX) throw KeyAuth::request_error("API field is too large."); + char* encoded = curl_easy_escape(curl, input.data(), static_cast(input.size())); + if (!encoded) throw KeyAuth::request_error("Could not encode API field."); + std::unique_ptr owner(encoded, curl_free); + return std::string(encoded); } std::string extract_host(const std::string& url) { - std::string host = url; - const auto scheme_pos = host.find("://"); - if (scheme_pos != std::string::npos) - host = host.substr(scheme_pos + 3); - const auto slash_pos = host.find('/'); - if (slash_pos != std::string::npos) - host = host.substr(0, slash_pos); - const auto colon_pos = host.find(':'); - if (colon_pos != std::string::npos) - host = host.substr(0, colon_pos); - return host; + return keyauth_detail::parse_api_endpoint(url).host; } static bool is_ip_literal(const std::string& host) @@ -2593,61 +2451,24 @@ static bool is_ip_literal(const std::string& host) inet_pton(AF_INET6, host.c_str(), &sa6.sin6_addr) == 1; } -static bool is_private_or_loopback_ipv4(uint32_t addr_net_order) +static bool is_private_or_loopback_ipv4(uint32_t address) { - const uint32_t a = ntohl(addr_net_order); - const uint8_t b1 = static_cast(a >> 24); - const uint8_t b2 = static_cast((a >> 16) & 0xFF); - if (b1 == 10) return true; - if (b1 == 127) return true; - if (b1 == 0) return true; - if (b1 == 169 && b2 == 254) return true; - if (b1 == 100 && (b2 >= 64 && b2 <= 127)) return true; // 100.64.0.0/10 - if (b1 == 192 && b2 == 168) return true; - if (b1 == 192 && b2 == 0) return true; // 192.0.0.0/24 - if (b1 == 198 && (b2 == 18 || b2 == 19)) return true; // 198.18.0.0/15 - if (b1 == 172) { - const uint8_t b3 = static_cast((a >> 8) & 0xFF); - if (b3 >= 16 && b3 <= 31) return true; - } - if (b1 >= 224) return true; // multicast/reserved - return false; + return keyauth_detail::private_ipv4(address); } -static bool is_loopback_ipv6(const in6_addr& addr) +static bool is_loopback_ipv6(const in6_addr& address) { - static const in6_addr loopback = IN6ADDR_LOOPBACK_INIT; - return std::memcmp(&addr, &loopback, sizeof(loopback)) == 0; + return IN6_IS_ADDR_LOOPBACK(&address) != 0; } -static bool is_private_or_loopback_ipv6(const in6_addr& addr) +static bool is_private_or_loopback_ipv6(const in6_addr& address) { - if (is_loopback_ipv6(addr)) - return true; - const uint8_t b0 = addr.u.Byte[0]; - const uint8_t b1 = addr.u.Byte[1]; - if ((b0 & 0xFE) == 0xFC) // fc00::/7 unique-local - return true; - if (b0 == 0xFE && (b1 & 0xC0) == 0x80) // fe80::/10 link-local - return true; - if ((b0 & 0xFF) == 0xFF) // multicast ff00::/8 - return true; - return false; + return keyauth_detail::private_ipv6(address); } -static bool ip_string_private_or_loopback(const std::string& ip) +static bool ip_string_private_or_loopback(const std::string& address) { - if (ip.empty()) - return false; - sockaddr_in sa4{}; - if (inet_pton(AF_INET, ip.c_str(), &sa4.sin_addr) == 1) { - return is_private_or_loopback_ipv4(sa4.sin_addr.s_addr); - } - sockaddr_in6 sa6{}; - if (inet_pton(AF_INET6, ip.c_str(), &sa6.sin6_addr) == 1) { - return is_private_or_loopback_ipv6(sa6.sin6_addr); - } - return false; + return keyauth_detail::private_address(address); } static bool host_is_keyauth(const std::string& host_lower) @@ -2789,42 +2610,70 @@ static bool dns_fresh_contains_ip(const std::string& host, const std::string& ip bool hosts_override_present(const std::string& host) { - if (host.empty()) - return false; - - std::string host_lower = host; - std::transform(host_lower.begin(), host_lower.end(), host_lower.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - - const char* sysroot = std::getenv("SystemRoot"); - std::string hosts_path = sysroot ? std::string(sysroot) : "C:\\Windows"; - hosts_path += "\\System32\\drivers\\etc\\hosts"; - - // this will block the symlink/reparse tricks and attrs - const DWORD attr = GetFileAttributesA(hosts_path.c_str()); - if (attr == INVALID_FILE_ATTRIBUTES) - return false; - if (attr & FILE_ATTRIBUTE_REPARSE_POINT) // symlink/junction - return true; - - std::ifstream file(hosts_path); - if (!file.good()) - return false; - - std::string line; - while (std::getline(file, line)) { - auto hash_pos = line.find('#'); - if (hash_pos != std::string::npos) - line = line.substr(0, hash_pos); - - std::transform(line.begin(), line.end(), line.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - - // word check example, this can always be improved - if (line.find(" " + host_lower) != std::string::npos || line.find("\t" + host_lower) != std::string::npos) - return true; - } - return false; + if (host.empty()) throw KeyAuth::request_error("Cannot inspect an empty API hostname."); + std::wstring directory(32768, L'\0'); + const auto length = GetSystemWindowsDirectoryW(directory.data(), static_cast(directory.size())); + if (!length || length >= directory.size()) + throw KeyAuth::request_error("Cannot locate the Windows hosts file."); + directory.resize(length); + const auto path = std::filesystem::path(directory) / L"System32" / L"drivers" / L"etc" / L"hosts"; + HANDLE raw = CreateFileW(path.c_str(), GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (raw == INVALID_HANDLE_VALUE) { + const auto code = GetLastError(); + if (code == ERROR_FILE_NOT_FOUND || code == ERROR_PATH_NOT_FOUND) return false; + throw KeyAuth::request_error("Cannot read the Windows hosts file."); + } + struct FileOwner { + HANDLE handle; + ~FileOwner() { CloseHandle(handle); } + } owner{raw}; + std::string pending; + std::array buffer{}; + DWORD count = 0; + while (true) { + if (!ReadFile(raw, buffer.data(), static_cast(buffer.size()), &count, nullptr)) + throw KeyAuth::request_error("Cannot read the Windows hosts file."); + if (!count) break; + pending.append(buffer.data(), count); + size_t newline = 0; + while ((newline = pending.find('\n')) != std::string::npos) { + if (keyauth_detail::hosts_line_overrides(pending.substr(0, newline), host)) return true; + pending.erase(0, newline + 1); + } + if (pending.size() > 65536) throw KeyAuth::request_error("Hosts file contains an oversized line."); + } + return keyauth_detail::hosts_line_overrides(pending, host); +} + +struct RequestConnectionPolicy { + CURL* curl = nullptr; + std::string host; + bool restrict_private = false; + bool block_proxy = false; + bool rejected = false; +}; + +static int validate_request_connection(void* userdata, char* ip, char*, int port, int) noexcept +{ + auto* policy = static_cast(userdata); + try { + long using_proxy = 0; + if (!policy || !policy->curl || !ip || + curl_easy_getinfo(policy->curl, CURLINFO_USED_PROXY, &using_proxy) != CURLE_OK) + return CURL_PREREQFUNC_ABORT; + if (hosts_override_present(policy->host) || + !keyauth_detail::connection_address_allowed(ip, port, using_proxy != 0, + policy->restrict_private, !policy->block_proxy)) { + policy->rejected = true; + return CURL_PREREQFUNC_ABORT; + } + return CURL_PREREQFUNC_OK; + } catch (...) { + if (policy) policy->rejected = true; + return CURL_PREREQFUNC_ABORT; + } } static std::wstring to_lower_ws(std::wstring value) @@ -3257,35 +3106,48 @@ bool data_page_protections_ok() } -void heartbeat_thread(KeyAuth::api* instance) +KeyAuth::api::~api() { - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_int_distribution sleep_seconds(45, 90); - while (true) { - Sleep(static_cast(sleep_seconds(gen) * 1000)); - if (!LoggedIn.load()) - continue; - instance->check(false); - if (!instance->response.success) { - error(XorStr("session check failed.")); + stop_ban_monitor(); + { + std::lock_guard lock(heartbeat_mutex_); + heartbeat_stop_ = true; + } + heartbeat_cv_.notify_all(); + if (heartbeat_thread_.joinable()) heartbeat_thread_.join(); + secure_zero(sessionid); + secure_zero(enckey); + secure_zero(magic_link_email_); + secure_zero(name_enc_); + secure_zero(ownerid_enc_); + secure_zero(version_enc_); + secure_zero(url_enc_); + secure_zero(path_enc_); + reset_auth_runtime(); +} + +void KeyAuth::api::start_heartbeat() +{ + if (heartbeat_thread_.joinable()) return; + heartbeat_thread_ = std::thread([this]() { + std::unique_lock wait_lock(heartbeat_mutex_); + while (!heartbeat_cv_.wait_for(wait_lock, std::chrono::seconds(60), + [this] { return heartbeat_stop_; })) { + wait_lock.unlock(); + { + std::lock_guard lock(api_mutex_); + if (LoggedIn.load()) { + try { + check(false); + } catch (const std::exception& e) { + response.success = false; + response.message = e.what(); + } + } + } + wait_lock.lock(); } - } -} - -void start_heartbeat(KeyAuth::api* instance) -{ - if (heartbeat_started.exchange(true)) - return; - std::thread(heartbeat_thread, instance).detach(); -} - -static void security_watchdog() -{ - while (true) { - Sleep(15000); - checkInit(); - } + }); } void KeyAuth::api::setDebug(bool value) { @@ -3293,6 +3155,8 @@ void KeyAuth::api::setDebug(bool value) { } std::string KeyAuth::api::req(std::string data, const std::string& url) { + ScopeWipe request_wipe(data); + response.success = false; signature.clear(); signatureTimestamp.clear(); // gate requests on integrity checks to reduce bypasses -nigel @@ -3300,170 +3164,144 @@ std::string KeyAuth::api::req(std::string data, const std::string& url) { // usage: keep this in req() so every api call is protected -nigel if (LoggedIn.load() && !request_bypasses_local_auth(data) && !local_auth_valid(false)) { reset_auth_runtime(); - error(XorStr("local auth gate failed.")); - } - if (!is_https_url(url)) { - error(XorStr("API URL must use HTTPS.")); - } - std::string host = extract_host(url); - ScopeWipe host_wipe(host); - // block hosts-file redirects for api host -nigel - if (hosts_override_present(host)) { - error(XorStr("Hosts file override detected for API host.")); - } - // block loopback/private redirects for keyauth domains -nigel - { - std::string host_lower = host; - ScopeWipe host_lower_wipe(host_lower); - std::transform(host_lower.begin(), host_lower.end(), host_lower.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - if (!allowed_hosts.empty()) { - bool allowed = false; - for (const auto& entry : allowed_hosts) { - std::string entry_lower = entry; - std::transform(entry_lower.begin(), entry_lower.end(), entry_lower.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - if (entry_lower.rfind("*.", 0) == 0) { - auto suffix = entry_lower.substr(1); - if (host_lower.size() >= suffix.size() && - host_lower.compare(host_lower.size() - suffix.size(), suffix.size(), suffix) == 0) { - allowed = true; - break; - } - } else if (host_lower == entry_lower) { - allowed = true; - break; - } - } - if (!allowed) { - error(XorStr("API host is not in allowed host list.")); - } - } - if (url_points_to_loopback(url)) { - error(XorStr("loopback or local host detected for API URL.")); - } - if (host_is_keyauth(host_lower)) { - if (is_ip_literal(host_lower)) { - error(XorStr("API host must not be an IP literal.")); - } - bool has_public = false; - if (host_resolves_private_only(host_lower, has_public) && !has_public) { - error(XorStr("API host resolves to private or loopback.")); - } - // disabled: dns cache poisoning check. -nigel - // if (dns_cache_poisoned(host_lower)) { - // error(XorStr("DNS cache poisoning detected for API host.")); - // } - } + throw request_error(XorStr("local auth gate failed.")); } + keyauth_detail::ApiEndpoint endpoint; + try { + endpoint = keyauth_detail::parse_api_endpoint(url); + } catch (const std::exception& e) { + throw request_error(e.what()); + } + const std::string host = endpoint.host; + if (!authenticated_host_.empty() && host != authenticated_host_) + throw request_error("API hostname changed after initialization."); + if (hosts_override_present(host)) + throw request_error("Windows hosts file overrides the configured API hostname."); + if (!allowed_hosts.empty() && std::none_of(allowed_hosts.begin(), allowed_hosts.end(), + [&](const std::string& entry) { return keyauth_detail::hostname_matches(host, entry); })) + throw request_error("API hostname is not allowed."); CURL* curl = curl_easy_init(); if (!curl) { - error(XorStr("CURL Initialization Failed!")); + throw request_error(XorStr("CURL Initialization Failed!")); } - std::string to_return; + std::unique_ptr curl_guard(curl, curl_easy_cleanup); + const std::string operation = data.substr(0, data.find('&')).substr(5); + const bool is_file_request = operation == "file"; + const bool query_request = is_file_request || keyauth_detail::email_request_uses_get(operation); + keyauth_detail::ResponseBuffer response_buffer{{}, is_file_request ? max_download_response_bytes : max_api_response_bytes}; + auto& to_return = response_buffer.body; SignatureHeaders sig_headers; - struct curl_slist* req_headers = nullptr; - - const bool is_file_request = (data.find(XorStr("type=file").c_str()) != std::string::npos); - std::string request_url = url; - if (is_file_request) { - request_url += (url.find('?') == std::string::npos) ? "?" : "&"; - request_url += build_query_encoded(curl, data); - } - - // Set CURL options - curl_easy_setopt(curl, CURLOPT_URL, request_url.c_str()); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); - curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 0L); - curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS); - curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS); - curl_easy_setopt(curl, CURLOPT_CERTINFO, 1L); -#ifdef CURL_SSLVERSION_TLSv1_2 - curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); + std::string request_url = endpoint.url; + ScopeWipe url_wipe(request_url); + if (query_request) { + request_url += (request_url.find('?') == std::string::npos) ? "?" : "&"; + request_url += data; + } + + RequestConnectionPolicy connection_policy{curl, host, block_private_dns || host_is_keyauth(host), block_proxy, false}; + if (curl_easy_setopt(curl, CURLOPT_PREREQFUNCTION, validate_request_connection) != CURLE_OK || + curl_easy_setopt(curl, CURLOPT_PREREQDATA, &connection_policy) != CURLE_OK) + throw request_error("The linked libcurl does not support connection validation."); + + if (curl_easy_setopt(curl, CURLOPT_URL, request_url.c_str()) != CURLE_OK) + throw request_error("Could not apply the required API transport policy."); + if (curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L) != CURLE_OK) + throw request_error("Could not apply the required API transport policy."); + if (curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L) != CURLE_OK) + throw request_error("Could not apply the required API transport policy."); + if (curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L) != CURLE_OK) + throw request_error("Could not apply the required API transport policy."); + if (curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 0L) != CURLE_OK) + throw request_error("Could not apply the required API transport policy."); + if (curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS) != CURLE_OK) + throw request_error("Could not apply the required API transport policy."); + if (curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS) != CURLE_OK) + throw request_error("Could not apply the required API transport policy."); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); +#if LIBCURL_VERSION_NUM >= 0x072200 + if (curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2) != CURLE_OK) + throw request_error("Could not apply the required API transport policy."); #endif - if (is_file_request) { + if (query_request) { curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, nullptr); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0L); } else { curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(data.size())); } curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &to_return); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_buffer); curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback); curl_easy_setopt(curl, CURLOPT_HEADERDATA, &sig_headers); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, req_headers); curl_easy_setopt(curl, CURLOPT_USERAGENT, "KeyAuth"); + if ((require_pinning || block_custom_ca) && pinned_public_keys.empty()) { + curl_guard.reset(); + throw request_error("Public key pinning is required but no pins are configured."); + } if (!pinned_public_keys.empty()) { -#ifdef CURLOPT_PINNEDPUBLICKEY - if (pinned_public_keys.size() > 1) { - error(XorStr("Multiple pinned public keys not supported.")); +#if LIBCURL_VERSION_NUM >= 0x072700 + std::string pins; + for (const auto& pin : pinned_public_keys) { + if (pin.empty() || pin.find_first_of("\r\n") != std::string::npos || pin.find('\0') != std::string::npos) + throw request_error("Configured public key pin is empty or invalid."); + if (pinned_public_keys.size() > 1 && pin.rfind("sha256//", 0) != 0) + throw request_error("Multiple pins require SHA-256 public key hashes."); + if (!pins.empty()) pins += ';'; + pins += pin; + } + if (curl_easy_setopt(curl, CURLOPT_PINNEDPUBLICKEY, pins.c_str()) != CURLE_OK) { + curl_guard.reset(); + throw request_error("Public key pinning is unsupported by the active TLS backend."); } - curl_easy_setopt(curl, CURLOPT_PINNEDPUBLICKEY, pinned_public_keys.at(0).c_str()); #else - error(XorStr("Pinned public key not supported by this libcurl build.")); + throw request_error(XorStr("Pinned public key not supported by this libcurl build.")); #endif } - // Perform the request (retry once if signature headers are missing). - for (int attempt = 0; attempt < 2; ++attempt) { + long http_status = 0; + // Perform once: a missing signature must not replay a state-changing request. + for (int attempt = 0; attempt < 1; ++attempt) { sig_headers.signature.clear(); sig_headers.timestamp.clear(); to_return.clear(); CURLcode code = curl_easy_perform(curl); + if (response_buffer.exceeded) throw request_error("API response exceeds the configured size limit."); if (code != CURLE_OK) { - std::string errorMsg = "CURL Error: " + std::string(curl_easy_strerror(code)); - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(errorMsg); + std::string errorMsg = connection_policy.rejected + ? "API connection rejected by hostname or address policy." + : "CURL Error: " + std::string(curl_easy_strerror(code)); + curl_guard.reset(); + throw request_error(errorMsg); } - // If server reports missing type param, retry as GET with query string (file requests only). - if (is_file_request && to_return.find(XorStr("type parameter was not found").c_str()) != std::string::npos) { - std::string fallback_url = url; - fallback_url += (url.find('?') == std::string::npos) ? "?" : "&"; - fallback_url += build_query_encoded(curl, data); - curl_easy_setopt(curl, CURLOPT_URL, fallback_url.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, nullptr); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0L); - - sig_headers.signature.clear(); - sig_headers.timestamp.clear(); - to_return.clear(); - - code = curl_easy_perform(curl); - if (code != CURLE_OK) { - std::string errorMsg = "CURL Error: " + std::string(curl_easy_strerror(code)); - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(errorMsg); - } + if (curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status) != CURLE_OK || + (http_status < 200 || http_status >= 300) && http_status != 400 && http_status != 401 && http_status != 403) { + curl_guard.reset(); + throw request_error("KeyAuth HTTP error " + std::to_string(http_status) + + ". Check service availability or network/proxy configuration."); + } + if (sig_headers.conflicting) { + curl_guard.reset(); + throw request_error("Conflicting signature headers in API response."); } long ssl_verify = 0; if (curl_easy_getinfo(curl, CURLINFO_SSL_VERIFYRESULT, &ssl_verify) == CURLE_OK) { if (ssl_verify != 0) { - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(XorStr("SSL verify result failed.")); + curl_guard.reset(); + throw request_error(XorStr("SSL verify result failed.")); } } - if (!is_file_request && (sig_headers.signature.empty() || sig_headers.timestamp.empty())) { - if (attempt == 0) { - continue; - } - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(XorStr("missing signature headers.")); + if ((!is_file_request || require_signed_downloads || http_status >= 400) && (sig_headers.signature.empty() || sig_headers.timestamp.empty())) { + curl_guard.reset(); + throw request_error(XorStr("Successful HTTP response is missing KeyAuth signature headers. Check API endpoint or intermediary; response rejected.")); } break; @@ -3471,74 +3309,80 @@ std::string KeyAuth::api::req(std::string data, const std::string& url) { std::string sig_hex; const bool sig_norm_ok = normalize_signature_header(sig_headers.signature, sig_hex); - if (!sig_norm_ok && !is_file_request) { - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(XorStr("invalid signature header format.")); + if (!sig_norm_ok && (!is_file_request || require_signed_downloads || http_status >= 400 || !sig_headers.signature.empty() || !sig_headers.timestamp.empty())) { + curl_guard.reset(); + throw request_error(XorStr("invalid signature header format.")); } std::string ts = sig_headers.timestamp; trim_ws(ts); strip_quotes(ts); - ts.erase(std::remove_if(ts.begin(), ts.end(), - [](unsigned char c) { return c < '0' || c > '9'; }), ts.end()); const bool ts_ok = (ts.size() >= 10 && ts.size() <= 13 && std::all_of(ts.begin(), ts.end(), [](unsigned char c) { return c >= '0' && c <= '9'; })); - if (!ts_ok && !is_file_request) { - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(XorStr("invalid signature timestamp header format.")); + if (!ts_ok && (!is_file_request || require_signed_downloads || http_status >= 400 || !sig_headers.signature.empty() || !sig_headers.timestamp.empty())) { + curl_guard.reset(); + throw request_error(XorStr("invalid signature timestamp header format.")); } const bool sig_ok = sig_norm_ok && ts_ok; // Enforce cryptographic payload verification on every request path. if (sig_ok) { - const int verify_result = VerifyPayload(sig_hex, ts, to_return); + int verify_result; + try { + verify_result = VerifyPayload(sig_hex, ts, to_return); + } catch (...) { + curl_guard.reset(); + throw; + } if ((verify_result & 0xFFFF) != ((42 ^ 0xA5A5) & 0xFFFF)) { - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(XorStr("payload verification marker mismatch.")); + curl_guard.reset(); + throw request_error(XorStr("payload verification marker mismatch.")); } } if (sodium_init() < 0) { - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(XorStr("libsodium init failed in request guard.")); + curl_guard.reset(); + throw request_error(XorStr("libsodium init failed in request guard.")); } unsigned char sig_guard[64] = { 0 }; unsigned char pk_guard[32] = { 0 }; if (sig_ok) { if (sodium_hex2bin(sig_guard, sizeof(sig_guard), sig_hex.c_str(), sig_hex.size(), nullptr, nullptr, nullptr) != 0) { - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(XorStr("signature decode failed in request guard.")); + curl_guard.reset(); + throw request_error(XorStr("signature decode failed in request guard.")); } const std::string pubkey_hex_guard = get_public_key_hex(); if (sodium_hex2bin(pk_guard, sizeof(pk_guard), pubkey_hex_guard.c_str(), pubkey_hex_guard.size(), nullptr, nullptr, nullptr) != 0) { - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(XorStr("public key decode failed in request guard.")); + curl_guard.reset(); + throw request_error(XorStr("public key decode failed in request guard.")); } const std::string signed_message_guard = ts + to_return; if (crypto_sign_ed25519_verify_detached(sig_guard, reinterpret_cast(signed_message_guard.data()), signed_message_guard.size(), pk_guard) != 0) { - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(XorStr("signature verify failed in request guard.")); + curl_guard.reset(); + throw request_error(XorStr("signature verify failed in request guard.")); } } + if (sig_ok) { + const auto envelope = nlohmann::json::parse(to_return, nullptr, false); + if (envelope.is_object() && envelope.contains("ownerid") && + (!envelope["ownerid"].is_string() || envelope["ownerid"] != get_ownerid())) + throw request_error("API response owner does not match the configured application."); + if (http_status >= 400 && (!envelope.is_object() || !envelope.contains("success") || + !envelope["success"].is_boolean() || envelope["success"].get())) + throw request_error("Invalid signed API error response."); + } + char* effective_url = nullptr; if (curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &effective_url) == CURLE_OK && effective_url) { if (!is_https_url(effective_url)) { - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(XorStr("effective url not https.")); + curl_guard.reset(); + throw request_error(XorStr("effective url not https.")); } std::string eff_host = extract_host(effective_url); std::string host_lower = host; @@ -3548,236 +3392,32 @@ std::string KeyAuth::api::req(std::string data, const std::string& url) { std::transform(eff_lower.begin(), eff_lower.end(), eff_lower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (!eff_lower.empty() && eff_lower != host_lower) { - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(XorStr("effective url host mismatch.")); - } - } - - long primary_port = 0; - if (curl_easy_getinfo(curl, CURLINFO_PRIMARY_PORT, &primary_port) == CURLE_OK) { - if (primary_port != 443) { - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(XorStr("api port mismatch.")); - } - } - - char* primary_ip = nullptr; - if (curl_easy_getinfo(curl, CURLINFO_PRIMARY_IP, &primary_ip) == CURLE_OK && primary_ip) { - std::string ip = primary_ip; - std::string host_lower = host; - std::transform(host_lower.begin(), host_lower.end(), host_lower.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - if (host_is_keyauth(host_lower) && ip_string_private_or_loopback(ip)) { - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(XorStr("api host resolved to private or loopback ip.")); - } - if (host_is_keyauth(host_lower) && !dns_fresh_contains_ip(host_lower, ip)) { - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); - error(XorStr("api host ip mismatch vs fresh dns.")); + curl_guard.reset(); + throw request_error(XorStr("effective url host mismatch.")); } } if (KeyAuth::api::debug) { - debugInfo("n/a", "n/a", to_return, "n/a"); + debugInfo("n/a", "n/a", R"({"response_body":"omitted"})", "n/a"); } // no maximum or minimum response size check. -nigel - if (req_headers) curl_slist_free_all(req_headers); - curl_easy_cleanup(curl); + curl_guard.reset(); secure_zero(data); signature = sig_hex; signatureTimestamp = ts; return to_return; } -void error(std::string message) { - for (char& c : message) { - if (c == '&' || c == '|' || c == '\"') c = ' '; // minimize cmd injection surface. -nigel - } - system((XorStr("start cmd /C \"color b && title Error && echo ").c_str() + message + XorStr(" && timeout /t 5\"")).c_str()); - LI_FN(__fastfail)(0); +[[noreturn]] void error(std::string message) { + throw KeyAuth::request_error(message); } -// code submitted in pull request from https://github.com/Roblox932 -auto check_section_integrity( const char *section_name, bool fix = false ) -> bool -{ - const auto map_file = []( HMODULE hmodule ) -> std::tuple - { - wchar_t filename[ MAX_PATH ]; - DWORD size = MAX_PATH; - QueryFullProcessImageName(GetCurrentProcess(), 0, filename, &size); - - - const auto file_handle = CreateFile( filename, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0 ); - if ( !file_handle || file_handle == INVALID_HANDLE_VALUE ) - { - return { 0ull, nullptr }; - } - - const auto file_mapping = CreateFileMapping( file_handle, 0, PAGE_READONLY, 0, 0, 0 ); - if ( !file_mapping ) - { - CloseHandle( file_handle ); - return { 0ull, nullptr }; - } - - return { reinterpret_cast< std::uintptr_t >( MapViewOfFile( file_mapping, FILE_MAP_READ, 0, 0, 0 ) ), file_handle }; - }; - - const auto hmodule = GetModuleHandle( 0 ); - if ( !hmodule ) return true; - - const auto base_0 = reinterpret_cast< std::uintptr_t >( hmodule ); - if ( !base_0 ) return true; - - const auto dos_0 = reinterpret_cast< IMAGE_DOS_HEADER * >( base_0 ); - if ( dos_0->e_magic != IMAGE_DOS_SIGNATURE ) return true; - - const auto nt_0 = reinterpret_cast< IMAGE_NT_HEADERS * >( base_0 + dos_0->e_lfanew ); - if ( nt_0->Signature != IMAGE_NT_SIGNATURE ) return true; - - auto section_0 = IMAGE_FIRST_SECTION( nt_0 ); - - const auto [base_1, file_handle] = map_file( hmodule ); - if ( !base_1 || !file_handle || file_handle == INVALID_HANDLE_VALUE ) return true; - - const auto dos_1 = reinterpret_cast< IMAGE_DOS_HEADER * >( base_1 ); - if ( dos_1->e_magic != IMAGE_DOS_SIGNATURE ) - { - UnmapViewOfFile( reinterpret_cast< void * >( base_1 ) ); - CloseHandle( file_handle ); - return true; - } - - const auto nt_1 = reinterpret_cast< IMAGE_NT_HEADERS * >( base_1 + dos_1->e_lfanew ); - if ( nt_1->Signature != IMAGE_NT_SIGNATURE || - nt_1->FileHeader.TimeDateStamp != nt_0->FileHeader.TimeDateStamp || - nt_1->FileHeader.NumberOfSections != nt_0->FileHeader.NumberOfSections ) - { - UnmapViewOfFile( reinterpret_cast< void * >( base_1 ) ); - CloseHandle( file_handle ); - return true; - } - - auto section_1 = IMAGE_FIRST_SECTION( nt_1 ); - - bool patched = false; - for ( auto i = 0; i < nt_1->FileHeader.NumberOfSections; ++i, ++section_0, ++section_1 ) - { - if ( strcmp( reinterpret_cast< char * >( section_0->Name ), section_name ) || - !( section_0->Characteristics & IMAGE_SCN_MEM_EXECUTE ) ) continue; - - for ( auto i = 0u; i < section_0->SizeOfRawData; ++i ) - { - const auto old_value = *reinterpret_cast< BYTE * >( base_1 + section_1->PointerToRawData + i ); - - if ( *reinterpret_cast< BYTE * >( base_0 + section_0->VirtualAddress + i ) == old_value ) - { - continue; - } - - if ( fix ) - { - DWORD new_protect { PAGE_EXECUTE_READWRITE }, old_protect; - VirtualProtect( ( void * )( base_0 + section_0->VirtualAddress + i ), sizeof( BYTE ), new_protect, &old_protect ); - *reinterpret_cast< BYTE * >( base_0 + section_0->VirtualAddress + i ) = old_value; - VirtualProtect( ( void * )( base_0 + section_0->VirtualAddress + i ), sizeof( BYTE ), old_protect, &new_protect ); - } - - patched = true; - } - - break; - } - - UnmapViewOfFile( reinterpret_cast< void * >( base_1 ) ); - CloseHandle( file_handle ); - - return patched; -} - -void runChecks() { - // Wait before starting checks - int waitTime = 45000; - while (waitTime > 0) { - - if (LoggedIn.load()) { - // If the user is logged in, proceed with the checks immediately - break; - } - std::this_thread::sleep_for(std::chrono::seconds(1)); - waitTime -= 1000; - } - - // Create separate threads for each check - std::thread(checkAtoms).detach(); - std::thread(checkFiles).detach(); - std::thread(checkRegistry).detach(); -} - -void checkAtoms() { - - while (true) { - if (LI_FN(GlobalFindAtomA)(seed.c_str()) == 0) { - KA_EXIT(13); - LI_FN(__fastfail)(0); - } - Sleep(1000); // thread interval - } -} - -void checkFiles() { - - while (true) { - std::string file_path = XorStr("C:\\ProgramData\\").c_str() + seed; - DWORD file_attr = LI_FN(GetFileAttributesA)(file_path.c_str()); - if (file_attr == INVALID_FILE_ATTRIBUTES || (file_attr & FILE_ATTRIBUTE_DIRECTORY)) { - KA_EXIT(14); - LI_FN(__fastfail)(0); - } - Sleep(2000); // thread interval, files are more intensive than Atom tables which use memory - } -} - -void checkRegistry() { - - while (true) { - std::string regPath = XorStr("Software\\").c_str() + seed; - HKEY hKey; - LONG result = LI_FN(RegOpenKeyExA)(HKEY_CURRENT_USER, regPath.c_str(), 0, KEY_READ, &hKey); - if (result != ERROR_SUCCESS) { - KA_EXIT(15); - LI_FN(__fastfail)(0); - } - LI_FN(RegCloseKey)(hKey); - Sleep(1500); // thread interval - } -} - std::string checksum() { - auto exec = [&](const char* cmd) -> std::string - { - uint16_t line = -1; - std::array buffer; - std::string result; - std::unique_ptr pipe(_popen(cmd, "r"), _pclose); - if (!pipe) { - throw std::runtime_error(XorStr("popen() failed!")); - } - - while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { - result = buffer.data(); - } - return result; - }; - - char rawPathName[MAX_PATH]; - GetModuleFileNameA(NULL, rawPathName, MAX_PATH); - - return exec(("certutil -hashfile \"" + std::string(rawPathName) + XorStr( "\" MD5 | find /i /v \"md5\" | find /i /v \"certutil\"") ).c_str()); + std::wstring path(32768, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, path.data(), static_cast(path.size())); + if (!length || length >= path.size()) throw KeyAuth::request_error("Cannot locate executable for API checksum."); + path.resize(length); + return keyauth_detail::file_md5(std::filesystem::path(path)); } std::string getPath() { @@ -3990,165 +3630,16 @@ static bool compare_text_to_disk() return false; } -void checkInit() { +void KeyAuth::api::checkInit() { if (!initialized) { - error(XorStr("You need to run the KeyAuthApp.init(); function before any other KeyAuth functions")); - } - // usage: call init() once at startup; checks run automatically after that -nigel - const auto now = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count(); - const auto last_mod = last_module_check.load(); - if (now - last_mod > 60) { - last_module_check.store(now); - // core module trust check to detect tampered system dlls -nigel - if (!core_modules_signed()) { - error(XorStr("module path check failed, possible side-load detected.")); - } - } - const auto last_periodic = last_periodic_check.load(); - if (now - last_periodic > 30) { - last_periodic_check.store(now); - // detect basic clock tampering to block expired key reuse -nigel - if (timing_anomaly_detected()) { - error(XorStr("timing anomaly detected, possible time tamper.")); - } - // disabled: heavy tamper checks. -nigel - heavy_fail_streak.store(0); - { - std::uintptr_t text_base = 0; - size_t text_size = 0; - if (get_text_section_info(text_base, text_size) && text_base && text_size) { - const auto* text_ptr = reinterpret_cast(text_base); - const uint32_t crc_now = rolling_crc32(text_ptr, text_size); - const uint32_t crc_base = text_crc_baseline.load(); - if (crc_base != 0 && crc_now != crc_base) { - error(XorStr(".text rolling crc mismatch.")); - } - } - } - - // disabled: compare_text_to_disk can false-positive on some systems. -nigel - // if (!compare_text_to_disk()) { - // error(XorStr("memory .text mismatch vs disk image.")); - // } - - // disabled: ntdll syscall stub tamper check can false-positive on some systems. -nigel - // if (ntdll_syscall_stub_tampered("NtQueryInformationProcess") || - // ntdll_syscall_stub_tampered("NtProtectVirtualMemory")) { - // error(XorStr("ntdll syscall stub tampered.")); - // } - - if (!critical_modules_safe()) { - error(XorStr("critical module path violation.")); - } - - HMODULE curl = GetModuleHandleW(L"libcurl.dll"); - if (curl) { - std::wstring p; - if (get_module_path(curl, p)) { - uint32_t now_crc = file_crc32(p); - uint32_t base_crc = curl_crc_baseline.load(); - if (base_crc != 0 && now_crc != base_crc) { - error(XorStr("libcurl checksum mismatch.")); - } - } - } - - HMODULE sodium = GetModuleHandleW(L"libsodium.dll"); - if (sodium) { - std::wstring p; - if (get_module_path(sodium, p)) { - uint32_t now_crc = file_crc32(p); - uint32_t base_crc = sodium_crc_baseline.load(); - if (base_crc != 0 && now_crc != base_crc) { - error(XorStr("libsodium checksum mismatch.")); - } - } - } - -periodic_done: - if (check_section_integrity(XorStr(".text").c_str(), false)) { - const int streak = integrity_fail_streak.fetch_add(1) + 1; - if (streak >= 2) { - error(XorStr("check_section_integrity() failed, don't tamper with the program.")); - } - } else { - integrity_fail_streak.store(0); - } + error(XorStr("You need to run KeyAuthApp.init() before other API functions.")); } - integrity_check(); } void integrity_check() { - const auto now = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count(); - const auto last = last_integrity_check.load(); - if (now - last > 30) { - last_integrity_check.store(now); - if (check_section_integrity(XorStr(".text").c_str(), false)) { - error(XorStr("check_section_integrity() failed, don't tamper with the program.")); - } - } -} - -// code submitted in pull request from https://github.com/BINM7MD -BOOL bDataCompare(const BYTE* pData, const BYTE* bMask, const char* szMask) -{ - for (; *szMask; ++szMask, ++pData, ++bMask) - { - if (*szMask == 'x' && *pData != *bMask) - return FALSE; - } - return (*szMask) == NULL; -} -DWORD64 FindPattern(BYTE* bMask, const char* szMask) -{ - MODULEINFO mi{ }; - GetModuleInformation(GetCurrentProcess(), GetModuleHandleA(NULL), &mi, sizeof(mi)); - - DWORD64 dwBaseAddress = DWORD64(mi.lpBaseOfDll); - const auto dwModuleSize = mi.SizeOfImage; - - for (auto i = 0ul; i < dwModuleSize; i++) - { - if (bDataCompare(PBYTE(dwBaseAddress + i), bMask, szMask)) - return DWORD64(dwBaseAddress + i); - } - return NULL; -} - -DWORD64 Function_Address; -void modify() -{ - check_section_integrity( XorStr( ".text" ).c_str( ), true ); - - while (true) - { - #if KEYAUTH_HAVE_KILLEMU - protection::init(); - #endif - // ^ check for jumps, break points (maybe useless), return address. - - if ( check_section_integrity( XorStr( ".text" ).c_str( ), false ) ) - { - error(XorStr("check_section_integrity() failed, don't tamper with the program.")); - } - // code submitted in pull request from https://github.com/sbtoonz, authored by KeePassXC https://github.com/keepassxreboot/keepassxc/blob/dab7047113c4ad4ffead944d5c4ebfb648c1d0b0/src/core/Bootstrap.cpp#L121 - if(!LockMemAccess()) - { - error(XorStr("LockMemAccess() failed, don't tamper with the program.")); - } - // code submitted in pull request from https://github.com/BINM7MD - if (Function_Address == NULL) { - Function_Address = FindPattern(PBYTE("\x48\x89\x74\x24\x00\x57\x48\x81\xec\x00\x00\x00\x00\x49\x8b\xf0"), XorStr("xxxx?xxxx????xxx").c_str()) - 0x5; - } - BYTE Instruction = *(BYTE*)Function_Address; - - if ((DWORD64)Instruction == 0xE9) { - error(XorStr("Pattern checksum failed, don't tamper with the program.")); - } - Sleep(50); - } + // PE bytes and page permissions change legitimately under the Windows loader, + // instrumentation and OS hotpatching. Authentication uses signed API replies. + (void)get_public_key_hex(); } // Clean up seed data (file and registry key) diff --git a/auth.hpp b/auth.hpp index 487338b..932288d 100644 --- a/auth.hpp +++ b/auth.hpp @@ -1,3 +1,8 @@ +#pragma once +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include #include #include #include @@ -5,9 +10,368 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #define CURL_STATICLIB +#include +#include +#include + +namespace keyauth_detail { +struct SignatureHeaders { + std::string signature; + std::string timestamp; + bool conflicting = false; +}; + +// libcurl may deliver CONNECT, informational, final and trailer header blocks. +// Only the current response's values may authenticate its body. +inline bool consume_header(SignatureHeaders& out, std::string line) { + if (line.rfind("HTTP/", 0) == 0) { + out = {}; + return true; + } + const auto colon = line.find(':'); + if (colon == std::string::npos) return true; + std::string key = line.substr(0, colon); + std::transform(key.begin(), key.end(), key.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + std::string* target = nullptr; + if (key == "x-signature-ed25519") target = &out.signature; + if (key == "x-signature-timestamp") target = &out.timestamp; + if (!target) return true; + std::string value = line.substr(colon + 1); + const auto first = value.find_first_not_of(" \t\r\n"); + const auto last = value.find_last_not_of(" \t\r\n"); + value = first == std::string::npos ? "" : value.substr(first, last - first + 1); + if (!target->empty() && *target != value) out.conflicting = true; + *target = value; + return true; +} +} + +#include +#include +#include +#include + +namespace keyauth_detail { +inline bool auth_window_fresh(long long sealed, long long current) { + return sealed > 0 && current >= sealed && current - sealed <= 2; +} +enum class PayloadResult { valid, timestamp_format, timestamp_skew, sodium_init, + signature_format, public_key_format, invalid_signature }; +inline PayloadResult verify_payload(const std::string& signature, + const std::string& timestamp, const std::string& body, + const std::string& public_key, long long now_seconds) { + if ((timestamp.size() != 10 && timestamp.size() != 13) || + !std::all_of(timestamp.begin(), timestamp.end(), + [](unsigned char c) { return c >= '0' && c <= '9'; })) + return PayloadResult::timestamp_format; + long long seconds = std::stoll(timestamp); + if (timestamp.size() == 13) seconds /= 1000; + if (std::llabs(now_seconds - seconds) > 120) return PayloadResult::timestamp_skew; + if (sodium_init() < 0) return PayloadResult::sodium_init; + unsigned char sig[crypto_sign_BYTES] = {}; + unsigned char pk[crypto_sign_PUBLICKEYBYTES] = {}; + size_t size = 0; + if (signature.size() != sizeof(sig) * 2 || + sodium_hex2bin(sig, sizeof(sig), signature.data(), signature.size(), + nullptr, &size, nullptr) != 0 || size != sizeof(sig)) + return PayloadResult::signature_format; + if (public_key.size() != sizeof(pk) * 2 || + sodium_hex2bin(pk, sizeof(pk), public_key.data(), public_key.size(), + nullptr, &size, nullptr) != 0 || size != sizeof(pk)) + return PayloadResult::public_key_format; + const std::string message = timestamp + body; + return crypto_sign_verify_detached(sig, + reinterpret_cast(message.data()), message.size(), pk) == 0 + ? PayloadResult::valid : PayloadResult::invalid_signature; +} +} + +#include +#include +#include +#include +#include +#include +#include +#pragma comment(lib, "bcrypt.lib") + +namespace keyauth_detail { +// MD5 is retained solely for the API's existing executable/token hash fields. +// Authentication is provided by TLS and Ed25519, not this compatibility digest. +inline std::string file_md5(const std::filesystem::path& path) { + std::ifstream input(path, std::ios::binary); + if (!input) throw std::runtime_error("Cannot open file for API checksum."); + struct Provider { + BCRYPT_ALG_HANDLE handle = nullptr; + ~Provider() { if (handle) BCryptCloseAlgorithmProvider(handle, 0); } + } provider; + struct Hash { + BCRYPT_HASH_HANDLE handle = nullptr; + ~Hash() { if (handle) BCryptDestroyHash(handle); } + } hash; + if (BCryptOpenAlgorithmProvider(&provider.handle, BCRYPT_MD5_ALGORITHM, nullptr, 0) < 0 || + BCryptCreateHash(provider.handle, &hash.handle, nullptr, 0, nullptr, 0, 0) < 0) + throw std::runtime_error("Cannot initialize API checksum."); + std::array buffer{}; + while (input.read(reinterpret_cast(buffer.data()), buffer.size()) || input.gcount()) { + if (BCryptHashData(hash.handle, buffer.data(), static_cast(input.gcount()), 0) < 0) + throw std::runtime_error("Cannot hash API checksum input."); + } + if (!input.eof()) throw std::runtime_error("Cannot read API checksum input."); + std::array digest{}; + if (BCryptFinishHash(hash.handle, digest.data(), static_cast(digest.size()), 0) < 0) + throw std::runtime_error("Cannot finish API checksum."); + const char* hex = "0123456789abcdef"; + std::string out; + for (const auto value : digest) { + out += hex[value >> 4]; out += hex[value & 15]; + } + return out; +} +} + + +namespace keyauth_detail { +inline std::string canonical_host(std::string host) { + std::transform(host.begin(), host.end(), host.begin(), + [](unsigned char c) { return static_cast(c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c); }); + if (!host.empty() && host.back() == '.') host.pop_back(); + return host; +} + +struct ApiEndpoint { + std::string url; + std::string host; + int port = 443; +}; + +inline ApiEndpoint parse_api_endpoint(const std::string& input) { + if (input.empty() || input.find_first_of("\r\n\t\\") != std::string::npos || + input.find('\0') != std::string::npos) + throw std::invalid_argument("Invalid API URL."); + std::unique_ptr handle(curl_url(), curl_url_cleanup); + if (!handle || curl_url_set(handle.get(), CURLUPART_URL, input.c_str(), CURLU_DISALLOW_USER) != CURLUE_OK) + throw std::invalid_argument("Invalid API URL or URL credentials."); + auto part = [&](CURLUPart field, unsigned flags = 0) { + char* raw = nullptr; + const auto code = curl_url_get(handle.get(), field, &raw, flags); + std::unique_ptr value(raw, curl_free); + return code == CURLUE_OK && raw ? std::string(raw) : std::string(); + }; + if (canonical_host(part(CURLUPART_SCHEME)) != "https" || !part(CURLUPART_FRAGMENT).empty()) + throw std::invalid_argument("API URL must use HTTPS without a fragment."); + ApiEndpoint endpoint; + endpoint.host = canonical_host(part(CURLUPART_HOST)); + const auto port = part(CURLUPART_PORT, CURLU_DEFAULT_PORT); + if (endpoint.host.empty() || endpoint.host.size() > 253 || port != "443") + throw std::invalid_argument("API URL requires a hostname and HTTPS port 443."); + size_t label_length = 0; + char previous = 0; + for (unsigned char c : endpoint.host) { + if (c == '.') { + if (!label_length || label_length > 63 || previous == '-') + throw std::invalid_argument("Invalid API hostname."); + label_length = 0; + } else { + if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-') || + (!label_length && c == '-')) + throw std::invalid_argument("Invalid API hostname."); + ++label_length; + } + previous = static_cast(c); + } + if (!label_length || label_length > 63 || previous == '-') + throw std::invalid_argument("Invalid API hostname."); + in_addr ipv4{}; + if (inet_pton(AF_INET, endpoint.host.c_str(), &ipv4) == 1) + throw std::invalid_argument("API URL must use a DNS hostname."); + if (curl_url_set(handle.get(), CURLUPART_HOST, endpoint.host.c_str(), 0) != CURLUE_OK) + throw std::invalid_argument("Invalid API hostname."); + endpoint.url = part(CURLUPART_URL); + if (endpoint.url.empty()) throw std::invalid_argument("Invalid API URL."); + return endpoint; +} + +inline bool hosts_line_overrides(std::string line, const std::string& host) { + const auto comment = line.find('#'); + if (comment != std::string::npos) line.resize(comment); + std::istringstream tokens(line); + std::string address; + if (!(tokens >> address)) return false; + in_addr v4{}; + in6_addr v6{}; + if (inet_pton(AF_INET, address.c_str(), &v4) != 1 && + inet_pton(AF_INET6, address.c_str(), &v6) != 1) return false; + const auto expected = canonical_host(host); + std::string alias; + while (tokens >> alias) { + if (canonical_host(alias) == expected) return true; + } + return false; +} + +inline bool private_ipv4(uint32_t address) { + const uint32_t value = ntohl(address); + const unsigned a = value >> 24; + const unsigned b = (value >> 16) & 255; + return a == 0 || a == 10 || a == 127 || a >= 224 || + (a == 169 && b == 254) || (a == 100 && b >= 64 && b <= 127) || + (a == 172 && b >= 16 && b <= 31) || (a == 192 && b == 168) || + (a == 198 && (b == 18 || b == 19)); +} + +inline bool private_ipv6(const in6_addr& address) { + if (IN6_IS_ADDR_UNSPECIFIED(&address) || IN6_IS_ADDR_LOOPBACK(&address) || + IN6_IS_ADDR_MULTICAST(&address) || IN6_IS_ADDR_LINKLOCAL(&address) || + IN6_IS_ADDR_SITELOCAL(&address) || + (address.u.Byte[0] & 0xfe) == 0xfc) return true; + if (IN6_IS_ADDR_V4MAPPED(&address)) { + uint32_t mapped = 0; + std::memcpy(&mapped, address.u.Byte + 12, sizeof(mapped)); + return private_ipv4(mapped); + } + return false; +} + +inline bool private_address(const std::string& input) { + in_addr v4{}; + if (inet_pton(AF_INET, input.c_str(), &v4) == 1) return private_ipv4(v4.s_addr); + in6_addr v6{}; + if (inet_pton(AF_INET6, input.c_str(), &v6) == 1) return private_ipv6(v6); + return true; +} + +inline bool loopback_address(const std::string& input) { + in_addr v4{}; + if (inet_pton(AF_INET, input.c_str(), &v4) == 1) { + const auto first = ntohl(v4.s_addr) >> 24; + return first == 0 || first == 127; + } + in6_addr v6{}; + if (inet_pton(AF_INET6, input.c_str(), &v6) != 1) return true; + if (IN6_IS_ADDR_UNSPECIFIED(&v6) || IN6_IS_ADDR_LOOPBACK(&v6)) return true; + if (IN6_IS_ADDR_V4MAPPED(&v6)) { + uint32_t mapped = 0; + std::memcpy(&mapped, v6.u.Byte + 12, sizeof(mapped)); + const auto first = ntohl(mapped) >> 24; + return first == 0 || first == 127; + } + return false; +} + +inline bool connection_address_allowed(const std::string& ip, int port, + bool using_proxy, bool restrict_private, bool allow_proxy = true) { + if (using_proxy) return allow_proxy; + return (port == 443 && !loopback_address(ip) && + (!restrict_private || !private_address(ip))); +} + +inline bool hostname_matches(const std::string& host, const std::string& entry) { + const auto expected = canonical_host(entry); + if (expected.rfind("*.", 0) != 0) return canonical_host(host) == expected; + const auto actual = canonical_host(host); + const auto suffix = expected.substr(1); + return actual.size() > suffix.size() && + actual.compare(actual.size() - suffix.size(), suffix.size(), suffix) == 0; +} +} + + +namespace keyauth_detail { +inline std::string json_text(const nlohmann::json& object, const std::string& key, + const std::string& fallback = "") { + if (!object.contains(key) || object[key].is_null()) return fallback; + const auto& value = object[key]; + if (value.is_string()) return value.get(); + if (value.is_number_integer() || value.is_number_unsigned()) return value.dump(); + throw std::invalid_argument("Invalid API field type: " + key); +} + +inline void validate_api_reply(const nlohmann::json& data, const std::string& owner) { + if (!data.is_object() || !data.contains("success") || !data["success"].is_boolean() || + !data.contains("message") || !data["message"].is_string()) + throw std::invalid_argument("Invalid API response structure."); + if (data.contains("ownerid") && (!data["ownerid"].is_string() || data["ownerid"] != owner)) + throw std::invalid_argument("API response owner does not match the application."); + if (data["success"].get() && data.contains("code") && + (!data["code"].is_number_integer() || data["code"] != 68)) + throw std::invalid_argument("Invalid API success code."); +} + +inline bool magic_link_approved(const nlohmann::json& data) { + if (!data.is_object() || !data.contains("success") || !data["success"].is_boolean() || + !data["success"].get()) return false; + if (data.contains("authenticated") && data["authenticated"].is_boolean()) + return data["authenticated"].get(); + if (data.contains("status") && data["status"].is_string()) { + const auto status = data["status"].get(); + if (status == "approved" || status == "authenticated") return true; + if (status == "pending" || status == "sent" || status == "expired" || + status == "denied" || status == "cancelled") return false; + } + if (data.contains("info") && data["info"].is_object() && + data["info"].contains("username") && data["info"]["username"].is_string() && + !data["info"]["username"].get().empty()) return true; + return data.value("message", std::string()) == "Logged in!"; +} + +inline bool email_request_uses_get(const std::string& type) { + return type == "loginEmail" || type == "emailOTPSend" || type == "emailOTPVerify" || + type == "magicLinkSend" || type == "magicLinkCheck"; +} + +inline std::string email_request(const std::string& type, const std::string& email, + const std::string& password, const std::string& code, const std::string& session, + const std::string& name, const std::string& owner, const std::string& hwid) { + if (!email_request_uses_get(type)) throw std::invalid_argument("Invalid email authentication operation."); + if (email.empty()) throw std::invalid_argument("Email is required."); + if (type == "emailOTPVerify" && (code.size() != 6 || !std::all_of(code.begin(), code.end(), + [](unsigned char c) { return c >= '0' && c <= '9'; }))) + throw std::invalid_argument("Email verification code must contain six digits."); + auto encode = [](const std::string& value) { + if (value.empty()) return std::string(); + if (value.size() > INT_MAX) throw std::invalid_argument("API input is too large."); + std::unique_ptr raw( + curl_easy_escape(nullptr, value.data(), static_cast(value.size())), curl_free); + if (!raw) throw std::runtime_error("Could not encode API input."); + return std::string(raw.get()); + }; + std::string request = "type=" + type + "&email=" + encode(email) + "&sessionid=" + encode(session) + + "&name=" + encode(name) + "&ownerid=" + encode(owner); + if (type != "emailOTPSend") request += "&hwid=" + encode(hwid); + if (type == "loginEmail") request += "&pass=" + encode(password) + "&code=" + encode(code); + if (type == "emailOTPVerify") request += "&code=" + encode(code); + return request; +} + +struct ResponseBuffer { + std::string body; + size_t limit; + bool exceeded = false; +}; + +inline bool append_response(ResponseBuffer& output, const char* bytes, size_t count) { + if (count > output.limit || output.body.size() > output.limit - count) { + output.exceeded = true; + return false; + } + output.body.append(bytes, count); + return true; +} +} + struct channel_struct { std::string author; @@ -16,6 +380,13 @@ struct channel_struct }; namespace KeyAuth { + // Network/server/signature errors reject the operation and can be retried by + // the application after correcting the cause. Do not retry mutations blindly. + class request_error : public std::runtime_error { + public: + using std::runtime_error::runtime_error; + }; + class api { public: @@ -28,9 +399,11 @@ namespace KeyAuth { setDebug(debugParameter); } + ~api(); void ban(std::string reason = ""); void init(); void check(bool check_paid = false); + bool authenticated(bool require_paid = false) const; void log(std::string msg); void license(std::string key, std::string code = ""); std::string var(std::string varid); @@ -42,6 +415,12 @@ namespace KeyAuth { void button(std::string value); void upgrade(std::string username, std::string key); void login(std::string username, std::string password, std::string code = ""); + void login_email(std::string email, std::string password, std::string code = ""); + void email_otp_send(std::string email); + void email_otp_verify(std::string email, std::string code); + void magic_link_send(std::string email); + bool magic_link_check(); + bool magic_link_pending() const; std::vector download(std::string fileid); void regstr(std::string username, std::string password, std::string key, std::string email = ""); void chatget(std::string channel); @@ -58,6 +437,10 @@ namespace KeyAuth { bool require_pinning = false; bool block_custom_ca = false; bool block_private_dns = false; + bool block_proxy = false; + bool require_signed_downloads = true; + size_t max_api_response_bytes = 8ULL * 1024 * 1024; + size_t max_download_response_bytes = 512ULL * 1024 * 1024; static std::string expiry_remaining(const std::string& expiry); static constexpr const char* kSavePath = "test.json"; static constexpr int kInitFailSleepMs = 1500; @@ -146,11 +529,25 @@ namespace KeyAuth { void enable_secure_strings(bool enable = true); private: + void checkInit(); + void start_heartbeat(); + bool initialized = false; + std::atomic LoggedIn{false}; + mutable std::recursive_mutex api_mutex_; + std::mutex heartbeat_mutex_; + std::condition_variable heartbeat_cv_; + bool heartbeat_stop_ = false; + std::thread heartbeat_thread_; std::string sessionid, enckey; + std::string authenticated_host_; + std::string magic_link_email_; + std::chrono::steady_clock::time_point magic_link_started_{}; + std::chrono::steady_clock::time_point magic_link_last_check_{}; + nlohmann::json email_reply(std::string data); + bool finish_email_login(const nlohmann::json& data, const std::string& email); std::vector allowed_hosts; std::vector pinned_public_keys; bool secure_strings_enabled_ = false; - uint32_t secure_strings_key_ = 0; std::string name_enc_; std::string ownerid_enc_; std::string version_enc_; @@ -162,12 +559,11 @@ namespace KeyAuth { std::string get_version() const; std::string get_url() const; std::string get_path() const; - std::string xor_crypt_field(const std::string& in) const; - uint32_t derive_secure_key() const; + std::string protect_field(const std::string& in, bool encrypt) const; void reset_auth_runtime(); void mark_authenticated(); void refresh_auth_runtime(); - bool local_auth_valid(bool require_paid = false) const; + bool local_auth_valid(bool require_paid = false, bool require_fresh = true) const; bool has_active_subscription() const; uint64_t compute_auth_seal(uint64_t nonce, long long window) const; @@ -175,6 +571,7 @@ namespace KeyAuth { static void debugInfo(std::string data, std::string url, std::string response, std::string headers); static void setDebug(bool value); std::atomic auth_nonce_{ 0 }; + std::array auth_seal_key_{}; std::atomic auth_window_{ 0 }; std::atomic auth_seal_{ 0 }; @@ -188,16 +585,16 @@ namespace KeyAuth { const std::string key_subs = XorStr("subscriptions"); const std::string key_sub_name = XorStr("subscription"); const std::string key_sub_expiry = XorStr("expiry"); - api::user_data.username = data.value(key_username, ""); - api::user_data.ip = data.value(key_ip, ""); + api::user_data.username = keyauth_detail::json_text(data, key_username); + api::user_data.ip = keyauth_detail::json_text(data, key_ip); if (!data.contains(key_hwid) || data[key_hwid].is_null()) { api::user_data.hwid = XorStr("none"); } else { api::user_data.hwid = data[key_hwid]; } - api::user_data.createdate = data.value(key_created, ""); - api::user_data.lastlogin = data.value(key_lastlogin, ""); + api::user_data.createdate = keyauth_detail::json_text(data, key_created); + api::user_data.lastlogin = keyauth_detail::json_text(data, key_lastlogin); api::user_data.subscriptions.clear(); if (data.contains(key_subs) && data[key_subs].is_array()) { @@ -206,7 +603,7 @@ namespace KeyAuth { if (sub.contains(key_sub_name)) subscriptions.name = sub.value(key_sub_name, ""); if (sub.contains(key_sub_expiry)) - subscriptions.expiry = sub.value(key_sub_expiry, ""); + subscriptions.expiry = keyauth_detail::json_text(sub, key_sub_expiry); api::user_data.subscriptions.emplace_back(subscriptions); } } diff --git a/library.vcxproj b/library.vcxproj index 17661dc..a866126 100644 --- a/library.vcxproj +++ b/library.vcxproj @@ -142,6 +142,8 @@ true true true + Guard + true SODIUM_STATIC;CURL_STATICLIB;_CRT_SECURE_NO_WARNINGS;NDEBUG;_LIB;%(PreprocessorDefinitions) true NotUsing @@ -160,7 +162,7 @@ MachineX64 - libsodium.lib;libcurl.lib;winhttp.lib;%(AdditionalDependencies) + libsodium.lib;libcurl.lib;winhttp.lib;%(AdditionalDependencies) .\curl;.\libsodium;%(AdditionalLibraryDirectories) /NODEFAULTLIB:libcurl.lib %(AdditionalOptions) diff --git a/nlohmann/detail/macro_scope.hpp b/nlohmann/detail/macro_scope.hpp index b3a9cde..8c9f632 100644 --- a/nlohmann/detail/macro_scope.hpp +++ b/nlohmann/detail/macro_scope.hpp @@ -45,8 +45,7 @@ // allow to disable exceptions #if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) - //#define JSON_THROW(exception) throw exception - #define JSON_THROW(exception) + #define JSON_THROW(exception) throw exception #define JSON_TRY try #define JSON_CATCH(exception) catch(exception) #define JSON_INTERNAL_CATCH(exception) catch(exception) diff --git a/utils.cpp b/utils.cpp index 221e5cc..7824a22 100644 --- a/utils.cpp +++ b/utils.cpp @@ -1,14 +1,30 @@ #include "utils.hpp" -#include +#include +#include +#include +#pragma comment(lib, "advapi32.lib") std::string utils::get_hwid() { - ATL::CAccessToken accessToken; - ATL::CSid currentUserSid; - if (accessToken.GetProcessToken(TOKEN_READ | TOKEN_QUERY) && - accessToken.GetUser(¤tUserSid)) - return std::string(CT2A(currentUserSid.Sid())); - return "none"; + HANDLE token = nullptr; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) return "none"; + struct TokenOwner { + HANDLE handle; + ~TokenOwner() { CloseHandle(handle); } + } owner{token}; + DWORD needed = 0; + GetTokenInformation(token, TokenUser, nullptr, 0, &needed); + if (!needed) return "none"; + std::vector buffer(needed); + if (!GetTokenInformation(token, TokenUser, buffer.data(), needed, &needed)) return "none"; + char* sid = nullptr; + if (!ConvertSidToStringSidA(reinterpret_cast(buffer.data())->User.Sid, &sid)) + return "none"; + struct SidOwner { + char* value; + ~SidOwner() { LocalFree(value); } + } sid_owner{sid}; + return std::string(sid); } std::time_t utils::string_to_timet(std::string timestamp) {