diff --git a/.github/workflows/build-example.yml b/.github/workflows/build-example.yml new file mode 100644 index 0000000..43cf93f --- /dev/null +++ b/.github/workflows/build-example.yml @@ -0,0 +1,110 @@ +name: Build C++ example + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + inputs: + app_name: + description: Optional public KeyAuth application name + required: false + type: string + owner_id: + description: Optional public KeyAuth owner ID + required: false + type: string + app_version: + description: Optional application version + required: false + type: string + +permissions: + contents: read + +env: + KEYAUTH_LIBRARY_COMMIT: e345db830576b7a14cf1a818ffd4a3b368fb228b + KEYAUTH_LIBRARY_SHA256: 93583a96377956b0092b6c037c5c258fc73881e694f57cff305ec658d62a5ab4 + +jobs: + build: + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: ELF-Nigel/keyauth-cpp-library-1.3API + ref: ${{ env.KEYAUTH_LIBRARY_COMMIT }} + path: .keyauth-source + persist-credentials: false + + - name: Verify paired library + shell: pwsh + run: | + $expected = [IO.File]::ReadAllText('.keyauth-source/auth.hpp').Replace("`r`n", "`n") + $actual = [IO.File]::ReadAllText('x64/lib/auth.hpp').Replace("`r`n", "`n") + if ($actual -cne $expected) { throw 'Bundled header does not match the selected library source.' } + $digest = (Get-FileHash -LiteralPath 'x64/library_x64.lib' -Algorithm SHA256).Hash.ToLowerInvariant() + if ($digest -cne $env:KEYAUTH_LIBRARY_SHA256) { throw 'Bundled library does not match the verified build.' } + + - uses: microsoft/setup-msbuild@v2 + + - name: Apply optional public application settings + shell: pwsh + env: + APP_NAME: ${{ inputs.app_name }} + OWNER_ID: ${{ inputs.owner_id }} + APP_VERSION: ${{ inputs.app_version }} + run: | + if ([bool]$env:APP_NAME -ne [bool]$env:OWNER_ID) { + throw 'Provide both application name and owner ID, or leave both blank.' + } + if ($env:OWNER_ID -and $env:OWNER_ID.Length -ne 10) { + throw 'Owner ID must contain 10 characters.' + } + $source = Get-Content -LiteralPath 'x64/main.cpp' -Raw + $settings = @{name = $env:APP_NAME; ownerid = $env:OWNER_ID; version = $env:APP_VERSION} + foreach ($entry in $settings.GetEnumerator()) { + if ([string]::IsNullOrEmpty($entry.Value)) { continue } + $encoded = ([Text.Encoding]::UTF8.GetBytes($entry.Value) | ForEach-Object { '\x{0:x2}' -f $_ }) -join '' + $replacement = 'std::string ' + $entry.Key + ' = skCrypt("' + $encoded + '").decrypt();' + $pattern = '(?m)^std::string ' + $entry.Key + ' = skCrypt\("[^"\r\n]*"\)\.decrypt\(\);' + if (-not [regex]::IsMatch($source, $pattern)) { throw 'Application setting was not found.' } + $source = [regex]::Replace($source, $pattern, [Text.RegularExpressions.MatchEvaluator]{param($match) $replacement}) + } + Set-Content -LiteralPath 'x64/main.cpp' -Value $source -NoNewline + + - name: Build Release x64 + working-directory: x64 + run: msbuild example.vcxproj /t:Build /p:Configuration=Release /p:Platform=x64 /m + + - name: Verify executable mitigations + 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 + $dumpbin = Get-ChildItem -Path (Join-Path $installation 'VC/Tools/MSVC/*/bin/Hostx64/x64/dumpbin.exe') | Sort-Object FullName -Descending | Select-Object -First 1 + if (-not $dumpbin) { throw 'Executable inspection tool was not found.' } + $headers = (& $dumpbin.FullName /headers /loadconfig 'x64/x64/Release/example.exe') -join "`n" + if ($LASTEXITCODE -ne 0) { throw 'Executable inspection failed.' } + foreach ($required in @('CF instrumented', 'FID table present', 'Dynamic base', 'NX compatible', 'High Entropy Virtual Addresses')) { + if ($headers -notmatch [regex]::Escape($required)) { throw "Missing executable mitigation: $required" } + } + + - name: Package runnable example + shell: pwsh + run: | + New-Item -ItemType Directory -Path build-output -Force | Out-Null + Copy-Item -LiteralPath 'x64/x64/Release/example.exe' -Destination build-output + $installation = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + $crt = Get-ChildItem -Path (Join-Path $installation 'VC/Redist/MSVC/*/x64/Microsoft.VC*.CRT') -Directory | Sort-Object FullName -Descending | Select-Object -First 1 + if (-not $crt) { throw 'Visual C++ runtime files were not found.' } + Get-ChildItem -LiteralPath $crt.FullName -Filter '*.dll' | Copy-Item -Destination build-output + Get-FileHash -LiteralPath 'build-output/example.exe' -Algorithm SHA256 | ForEach-Object { $_.Hash.ToLowerInvariant() + ' example.exe' } | Set-Content -LiteralPath 'build-output/SHA256SUMS.txt' + + - uses: actions/upload-artifact@v4 + with: + name: KeyAuth-CPP-Example-x64 + path: build-output/ + if-no-files-found: error + retention-days: 30 diff --git a/x64/auth_guard.hpp b/x64/auth_guard.hpp index f4380f8..ed1d6a9 100644 --- a/x64/auth_guard.hpp +++ b/x64/auth_guard.hpp @@ -1,13 +1,7 @@ #pragma once +#include "auth.hpp" -#include -#include - -inline void checkAuthenticated(const std::string& ownerid) { - while (true) { - if (GlobalFindAtomA(ownerid.c_str()) == 0) { - exit(13); - } - Sleep(1000); - } +inline bool checkAuthenticated(KeyAuth::api& app) { + app.check(); + return app.response.success && app.authenticated(); } diff --git a/x64/example.vcxproj b/x64/example.vcxproj index 16f72c6..5d43f35 100644 --- a/x64/example.vcxproj +++ b/x64/example.vcxproj @@ -125,14 +125,15 @@ Level3 true - CURL_STATICLIB;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + CURL_STATICLIB;SODIUM_STATIC;NDEBUG;_ITERATOR_DEBUG_LEVEL=0;_CONSOLE;%(PreprocessorDefinitions) true - stdcpp20 + stdcpp17 + MultiThreadedDLL Console true - libcurl.lib;libsodium.lib;%(AdditionalDependencies) + library_x64.lib;zlib.lib;winhttp.lib;%(AdditionalDependencies) @@ -141,6 +142,8 @@ true true true + Guard + true CURL_STATICLIB;SODIUM_STATIC;_CRT_SECURE_NO_WARNINGS;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true stdcpp17 @@ -149,10 +152,14 @@ Console + /GUARD:CF %(AdditionalOptions) + true + true + true true true false - libcurl.lib;zlib.lib;libsodium.lib;winhttp.lib;%(AdditionalDependencies) + library_x64.lib;zlib.lib;winhttp.lib;%(AdditionalDependencies) MachineX64 diff --git a/x64/lib/auth.hpp b/x64/lib/auth.hpp index b6b6c78..932288d 100644 --- a/x64/lib/auth.hpp +++ b/x64/lib/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); @@ -56,9 +435,12 @@ namespace KeyAuth { bool ban_monitor_running() const; bool ban_monitor_detected() const; bool require_pinning = false; - bool block_proxy = 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; @@ -147,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_; @@ -163,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; @@ -176,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 }; @@ -189,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()) { @@ -207,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/x64/lib/nlohmann/detail/macro_scope.hpp b/x64/lib/nlohmann/detail/macro_scope.hpp index b3a9cde..8c9f632 100644 --- a/x64/lib/nlohmann/detail/macro_scope.hpp +++ b/x64/lib/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/x64/library_x64.lib b/x64/library_x64.lib index df5e299..71e2bec 100644 Binary files a/x64/library_x64.lib and b/x64/library_x64.lib differ diff --git a/x64/main.cpp b/x64/main.cpp index 27a90f5..92ee0da 100644 --- a/x64/main.cpp +++ b/x64/main.cpp @@ -1,5 +1,5 @@ -#include #include "auth.hpp" +#include #include "auth_guard.hpp" #include "skStr.h" #include "storage.hpp" @@ -26,6 +26,10 @@ std::tm timestamp_to_tm(const std::string& timestamp) { return result; } +std::string readable_timestamp(const std::string& value) { + return value.empty() ? "Not supplied" : tm_to_readable_time(timestamp_to_tm(value)); +} + void init_fail_delay() { Sleep(api::kInitFailSleepMs); } @@ -158,16 +162,16 @@ void print_user_data(const api& app) { std::cout << skCrypt("\n IP address: ") << app.user_data.ip; std::cout << skCrypt("\n Hardware-Id: ") << app.user_data.hwid; std::cout << skCrypt("\n Create date: ") - << tm_to_readable_time(timestamp_to_tm(app.user_data.createdate)); + << readable_timestamp(app.user_data.createdate); std::cout << skCrypt("\n Last login: ") - << tm_to_readable_time(timestamp_to_tm(app.user_data.lastlogin)); + << readable_timestamp(app.user_data.lastlogin); std::cout << skCrypt("\n Subscription(s): "); for (size_t i = 0; i < app.user_data.subscriptions.size(); i++) { const auto& sub = app.user_data.subscriptions.at(i); std::cout << skCrypt("\n name: ") << sub.name; std::cout << skCrypt(" : expiry: ") - << tm_to_readable_time(timestamp_to_tm(sub.expiry)); + << readable_timestamp(sub.expiry); std::cout << skCrypt(" (") << remaining_until(sub.expiry) << skCrypt(")"); } } @@ -175,7 +179,6 @@ void print_user_data(const api& app) { const std::string compilation_date = (std::string)skCrypt(__DATE__); const std::string compilation_time = (std::string)skCrypt(__TIME__); -void sessionStatus(); // copy and paste from https://keyauth.cc/app/ and replace these string variables // Please watch tutorial HERE https://www.youtube.com/watch?v=5x4YkTmFH-U @@ -188,12 +191,17 @@ std::string path = skCrypt("").decrypt(); // (OPTIONAL) see tutorial here https: api KeyAuthApp(name, ownerid, version, url, path); api::lockout_state login_guard{}; -int main() +int main() try { std::string consoleTitle = skCrypt("Loader - Built at: ").decrypt() + compilation_date + " " + compilation_time; SetConsoleTitleA(consoleTitle.c_str()); std::cout << skCrypt("\n\n Connecting.."); + KeyAuthApp.enable_secure_strings(); + for (std::string* setting : {&name, &ownerid, &version, &url, &path}) { + if (!setting->empty()) SecureZeroMemory(setting->data(), setting->size()); + setting->clear(); + } KeyAuthApp.init(); if (!KeyAuthApp.response.success) { @@ -202,7 +210,6 @@ int main() exit(1); } - const std::string ownerid_copy = ownerid; // preserve for auth check thread. -nigel name.clear(); ownerid.clear(); version.clear(); url.clear(); path.clear(); if (lockout_active(login_guard)) { @@ -216,12 +223,15 @@ int main() std::string password; std::string key; std::string TfaCode; + std::string email; + bool email_password = false; + bool passwordless = false; const bool used_saved_creds = try_auto_login(KeyAuthApp, username, password, key); if (!used_saved_creds) { - std::cout << skCrypt("\n\n [1] Login\n [2] Register\n [3] Upgrade\n [4] License key only\n\n Choose option: "); + std::cout << skCrypt("\n\n [1] Login\n [2] Register\n [3] Upgrade\n [4] License key only\n [5] Email and password\n [6] Email verification code\n [7] Email magic link\n\n Choose option: "); int option = 0; if (!read_int(option)) @@ -255,12 +265,48 @@ int main() std::cout << skCrypt("\n Enter license: "); std::cin >> key; KeyAuthApp.upgrade(username, key); + if (KeyAuthApp.response.success) { + std::cout << skCrypt("\n License upgraded. Log in to start a session."); + return 0; + } break; case 4: std::cout << skCrypt("\n Enter license: "); std::cin >> key; KeyAuthApp.license(key, ""); break; + case 5: + email_password = true; + std::cout << "\n Email: "; + std::cin >> email; + std::cout << "\n Password: "; + std::cin >> password; + KeyAuthApp.login_email(email, password); + break; + case 6: + passwordless = true; + std::cout << "\n Email: "; + std::cin >> email; + KeyAuthApp.email_otp_send(email); + if (!KeyAuthApp.response.success) break; + std::cout << "\n Enter the six-digit code from your email: "; + std::cin >> TfaCode; + KeyAuthApp.email_otp_verify(email, TfaCode); + break; + case 7: + passwordless = true; + std::cout << "\n Email: "; + std::cin >> email; + KeyAuthApp.magic_link_send(email); + if (!KeyAuthApp.response.success) break; + std::cout << "\n Open the link in your email. Waiting for approval (up to 15 minutes)..."; + while (KeyAuthApp.magic_link_pending()) { + if (KeyAuthApp.magic_link_check()) break; + std::this_thread::sleep_for(std::chrono::seconds(3)); + } + if (!KeyAuthApp.response.success && !KeyAuthApp.magic_link_pending()) + KeyAuthApp.response.message = "Magic link expired. Request a new link."; + break; default: std::cout << skCrypt("\n\n Status: Failure: Invalid Selection"); bad_input_delay(); @@ -273,8 +319,13 @@ int main() if (!KeyAuthApp.response.success) { - if (KeyAuthApp.response.message == "2FA code required.") { - if (username.empty() || password.empty()) { + if (!passwordless && KeyAuthApp.response.message == "2FA code required.") { + if (email_password) { + std::cout << "\n Enter your six-digit authenticator code: "; + std::cin >> TfaCode; + KeyAuthApp.login_email(email, password, TfaCode); + } + else if (username.empty() || password.empty()) { std::cout << skCrypt("\n Your account has 2FA enabled, please enter 6-digit code:"); std::cin >> TfaCode; KeyAuthApp.license(key, TfaCode); @@ -305,22 +356,24 @@ int main() } reset_lockout(login_guard); - std::cout << skCrypt("\n\n Save credentials to disk for auto-login? [y/N]: "); - const char save_choice = read_choice('n'); // read once to avoid double input. -nigel - const bool save_creds = (save_choice == 'y' || save_choice == 'Y'); - save_or_clear_creds(save_creds, username, password, key); - if (save_creds) - std::cout << skCrypt("Successfully Created File For Auto Login"); - - /* - * Do NOT remove this checkAuthenticated() function. - * It protects you from cracking, it would be NOT be a good idea to remove it - */ - std::thread run(checkAuthenticated, ownerid_copy); - // do NOT remove checkAuthenticated(), it MUST stay for security reasons - std::thread check(sessionStatus); // do NOT remove this function either. - run.detach(); // detach immediately to avoid terminate on early exits. -nigel - check.detach(); // detach immediately to avoid terminate on early exits. -nigel + if (!passwordless && !email_password) { + std::cout << skCrypt("\n\n Save credentials to disk for auto-login? [y/N]: "); + const char save_choice = read_choice('n'); // read once to avoid double input. -nigel + const bool save_creds = (save_choice == 'y' || save_choice == 'Y'); + save_or_clear_creds(save_creds, username, password, key); + if (save_creds) + std::cout << skCrypt("Successfully Created File For Auto Login"); + + } + + for (std::string* credential : {&username, &password, &key, &TfaCode, &email}) { + if (!credential->empty()) SecureZeroMemory(credential->data(), credential->size()); + credential->clear(); + } + if (!checkAuthenticated(KeyAuthApp)) { + std::cout << skCrypt("\n Status: ") << KeyAuthApp.response.message; + return 1; + } //enable 2FA // KeyAuthApp.enable2fa(); you will need to ask for the code @@ -333,6 +386,7 @@ int main() if (KeyAuthApp.user_data.username.empty()) exit(10); + if (!KeyAuthApp.authenticated()) return 1; print_user_data(KeyAuthApp); std::cout << skCrypt("\n\n Status: ") << KeyAuthApp.response.message; @@ -341,22 +395,9 @@ int main() return 0; } - -void sessionStatus() { - KeyAuthApp.check(true); // do NOT specify true usually, it is slower and will get you blocked from API - if (!KeyAuthApp.response.success) { - return; // allow clean exit from thread. -nigel - } - - if (KeyAuthApp.response.isPaid) { - while (true) { - Sleep(20000); // this MUST be included or else you get blocked from API - KeyAuthApp.check(); - if (!KeyAuthApp.response.success) { - return; // allow clean exit from thread. -nigel - } - } - } +catch (const std::exception& error) { + std::cerr << "\n Request failed: " << error.what() << '\n'; + return 1; } std::string tm_to_readable_time(std::tm ctx) { diff --git a/x64/storage.hpp b/x64/storage.hpp index 2ba9919..3a64d1e 100644 --- a/x64/storage.hpp +++ b/x64/storage.hpp @@ -16,7 +16,7 @@ inline std::string ReadFromJson(const std::string& path, const std::string& sect if (!file.good()) return ""; json data = json::parse(file, nullptr, false); - if (data.is_discarded() || !data.contains(section)) + if (data.is_discarded() || !data.is_object() || !data.contains(section) || !data[section].is_string()) return ""; return data[section]; } diff --git a/x64/utils.hpp b/x64/utils.hpp index 87c27c1..7c519ea 100644 --- a/x64/utils.hpp +++ b/x64/utils.hpp @@ -1,67 +1,11 @@ #pragma once -#include "json.hpp" -#include "skStr.h" -#include -#include -#include +#include "storage.hpp" +#include "auth_guard.hpp" -using json = nlohmann::json; // header-only helpers below. -nigel - -inline std::string ReadFromJson(std::string path, std::string section) -{ - if (!std::filesystem::exists(path)) - return ""; // missing file returns empty. -nigel - std::ifstream file(path); - if (!file.good()) - return ""; // failed open returns empty. -nigel - json data = json::parse(file, nullptr, false); - if (data.is_discarded() || !data.contains(section)) - return ""; // invalid or missing key returns empty. -nigel - return data[section]; -} - -inline bool CheckIfJsonKeyExists(std::string path, std::string section) +inline bool CheckIfJsonKeyExists(const std::string& path, const std::string& section) { - if (!std::filesystem::exists(path)) - return false; // missing file means no key. -nigel - std::ifstream file(path); - if (!file.good()) - return false; // failed open means no key. -nigel - json data = json::parse(file, nullptr, false); - if (data.is_discarded()) - return false; // invalid json means no key. -nigel - return data.contains(section); -} - -inline bool WriteToJson(std::string path, std::string name, std::string value, bool userpass, std::string name2, std::string value2) -{ - json file; - if (!userpass) - { - file[name] = value; - } - else - { - file[name] = value; - file[name2] = value2; - } - - std::ofstream jsonfile(path, std::ios::out | std::ios::trunc); - if (!jsonfile.good()) - return false; // failed open means no write. -nigel - jsonfile << file; - jsonfile.flush(); - if (!jsonfile.good() || !std::filesystem::exists(path)) - return false; - - return true; -} - -void checkAuthenticated(std::string ownerid) { - while (true) { - if (GlobalFindAtomA(ownerid.c_str()) == 0) { - exit(13); - } - Sleep(1000); // thread interval - } + std::ifstream file(path); + if (!file.good()) return false; + const auto data = json::parse(file, nullptr, false); + return data.is_object() && data.contains(section); }