// ============================================================================ // AtlasAuth - Official C++ SDK (C++17) // Header: atlasauth.h // ---------------------------------------------------------------------------- // Implements the frozen AtlasAuth wire contract (v1). See CONTRACT.md. // // Dependencies: libcurl (HTTPS transport) and OpenSSL (ECDSA verify, SHA-256, // base64, RAND). No other third-party libraries are required: the JSON // parser/serializer used by the SDK is self-contained (see atlasauth.cpp). // // =========================================================================== // SECURITY MODEL (read before modifying anything in this SDK) // =========================================================================== // // Every security-relevant server response is SIGNED with the app's ECDSA // P-256 / SHA-256 PRIVATE key. The SDK embeds only the app's PUBLIC key // (SPKI DER, base64) and the app_id (a UUID). There is NO shared secret in // the client; a stolen SDK cannot forge server responses. // // Signed envelope (HTTP 200 body): // { "payload": "", "sig": "" } // // Verification procedure (MUST happen before trusting any field): // 1. Read the envelope's "payload" member as an OPAQUE string. Because it // lives inside JSON, its value is JSON-ESCAPED on the wire. We recover // the ORIGINAL signed bytes by JSON-UNESCAPING that string value ONLY. // We do NOT re-serialize a parsed object - re-serialization would change // byte order / spacing / escaping and break the signature. // 2. base64-decode "sig" -> exactly 64 bytes (IEEE P1363 r||s). // 3. Convert P1363 r||s -> DER ECDSA_SIG, then EVP_DigestVerify with // EVP_sha256 over the EXACT UTF-8 payload bytes from step 1. // 4. Only if the signature is valid do we JSON-parse the payload bytes for // field access. // 5. Require payload.nonce == the fresh random nonce this request sent. // Mismatch => replay/tamper => abort. // 6. (Advisory) Require abs(localUnix - payload.t) <= 60 seconds of skew. // // A fresh cryptographically-random nonce (16 bytes, hex) is generated for // every request and held until that request's response is verified. // // Any non-verifiable response (bad signature, missing envelope, transport // error, nonce mismatch) is treated as a FAILED / HOSTILE call: the relevant // method sets lastError() and returns false/empty. // // TLS is enforced on every request (peer + host verification, HTTPS only). // =========================================================================== // ============================================================================ #ifndef ATLASAUTH_H #define ATLASAUTH_H #include #include #include #include #include #include #include #include #include #include namespace atlas { // ---------------------------------------------------------------------------- // Public, UNSIGNED, informational types (CONTRACT.md section 7). // These come from plain GET endpoints that carry NO signature/nonce - they are // display/monitoring data only. NEVER gate execution on them; the signed // /check heartbeat remains the sole enforcement path. // ---------------------------------------------------------------------------- // GET /status/{app_id} → snapshot of an app's public status. struct StatusInfo { std::string appId; // echoed app_id std::string name; // app display name std::string status; // "active" | "maintenance" | "disabled" std::string statusMessage; // text shown to users when not active int online; // active sessions in the last 5 minutes }; // One published news/changelog entry from GET /news/{app_id}. struct NewsItem { std::string id; std::string title; std::string body; bool pinned; long long createdAt; // unix seconds }; // Maximum tolerated clock skew (seconds) between the local clock and the // server-signed timestamp `payload.t`. Advisory per the contract. constexpr long API_MAX_SKEW_SECONDS = 60; // Base URL for all v1 endpoints (POST JSON). constexpr const char* API_BASE_URL = "https://atlasauth.cc/api/v1"; // ---------------------------------------------------------------------------- // Client - one instance per running application session. // // Lifecycle (per CONTRACT.md section 2): // Client c(APP_ID, PUBLIC_KEY); // c.init(); // required first // c.login(user, pass) | c.register_(...) | c.licenseLogin(key); // c.startHeartbeat(onKicked); // background check() loop // ... app runs ... // c.stopHeartbeat(); // c.logout(); // // Thread-safety: all shared/mutable state is guarded by an internal mutex so // the heartbeat thread and the calling thread can coexist safely. The public // getters take the lock; you may call them from any thread. // ---------------------------------------------------------------------------- class Client { public: // app_id: the app's UUID (paste into the example constant). // base64SpkiPubKey: the app's PUBLIC key, SPKI DER, base64 (standard). Client(std::string app_id, std::string base64SpkiPubKey); ~Client(); Client(const Client&) = delete; Client& operator=(const Client&) = delete; // Optional: set a reproducible HWID seed. When set, the SDK reports // base64( SHA256("atlasauth-hwid-seed|" + seed) ) // instead of the hashed hardware fingerprint. Set BEFORE init/login. void setHwidSeed(std::string seed); // Optional: override the client app version reported to /init (for the // server's forced-version check). Defaults to empty (no version sent). void setVersion(std::string version); // ---- API --------------------------------------------------------------- // POST /init. Required before anything else. Returns false (and sets // lastError) if the app is not "active", a forced version mismatches, or // the response cannot be verified. bool init(); // POST /login (username + password). Returns true only on signed ok:true. bool login(const std::string& user, const std::string& pass); // POST /register (self-signup with a license). `email` is optional. // NOTE: trailing underscore avoids the C++ keyword `register`. bool register_(const std::string& user, const std::string& pass, const std::string& license, const std::string& email = ""); // POST /license (license-key-only login, no username/password). bool licenseLogin(const std::string& key); // POST /var. Returns the variable value on signed ok:true && found:true, // otherwise std::nullopt (auth-required / not found / verify failure). std::optional var(const std::string& name); // ---- Public unsigned informational endpoints (CONTRACT.md §7) ---------- // These are plain HTTPS GETs with NO signature and NO nonce. They are // informational ONLY (status pages, launchers, uptime monitors); never // gate execution on them - enforcement rides on the signed /check beat. // TLS verification is still enforced. May be called without init(). // GET /status/{app_id}. Returns the app's public status snapshot, or // std::nullopt on unknown app / transport / parse failure (sets lastError). std::optional status(); // GET /news/{app_id}. Returns published news (pinned-first, newest-first), // or an empty vector on unknown app / transport / parse failure (sets // lastError). An app with no news also yields an empty vector. std::vector news(); // POST /log. Best-effort; never throws, never blocks meaningfully. // level must be "info" | "warn" | "error". void log(const std::string& level, const std::string& message); // POST /logout. Best-effort; clears local auth state regardless. void logout(); // ---- Heartbeat --------------------------------------------------------- // Spawn a background thread that calls /check every `heartbeat` seconds // (value learned from /init; defaults to 10). If a beat indicates the // session is no longer valid (ok==false || valid==false || // app_status!="active" || key_valid==false || banned==true), onKicked is // invoked once with the reason string and the loop stops. // No-op if already running or not authenticated. void startHeartbeat(std::function onKicked); // Stop and join the heartbeat thread. Safe to call multiple times. void stopHeartbeat(); // ---- Getters (thread-safe) -------------------------------------------- std::string username() const; // authenticated username (if any) // Account expiry as unix seconds. nullopt = lifetime / not set. std::optional expiryUnix() const; std::string appStatus() const; // "active" | "maintenance" | "disabled" std::string lastError() const; // human-readable last failure bool loggedIn() const; // session authenticated & not kicked // Single /check round-trip. Public so callers can poll manually if they // prefer not to use startHeartbeat. Returns true if the session is still // fully valid; sets `reason` (out) and lastError on failure. bool check(std::string& reason); private: // --- networking --- // Performs one POST of `bodyJson` to `endpoint` (e.g. "/login"). // On success, fills `outVerifiedPayloadFields` with the parsed fields of // the VERIFIED signed payload and returns true. On any verification or // transport failure returns false and sets lastError (under lock). // `sentNonce` is the nonce that was placed in the request body. bool postSigned(const std::string& endpoint, const std::string& bodyJson, const std::string& sentNonce, std::map& outVerifiedPayloadFields); // Verify a signed envelope string. On success unescapes + parses the // payload into `outFields` and returns true. Checks nonce == sentNonce and // advisory skew. Sets lastError on failure. bool verifyEnvelope(const std::string& envelopeJson, const std::string& sentNonce, std::map& outFields); // Plain HTTPS GET of `path` (e.g. "/status/") for the UNSIGNED // informational endpoints. Returns the body and HTTP status; no signature // is involved. TLS peer+host verification is enforced exactly as for POST. // Returns false (and sets lastError) on a transport error. bool httpGet(const std::string& path, std::string& response, long& httpCode); // Compute the HWID string per the contract (seed override or hw hash). std::string computeHwid() const; void setError(const std::string& e); // lock-free; caller holds lock void setErrorLocked(const std::string& e); // takes the lock // --- immutable after construction --- const std::string app_id_; const std::string pubKeyB64_; // SPKI DER, base64 // --- guarded state --- mutable std::mutex mtx_; std::string hwidSeed_; // empty => hardware fingerprint std::string version_; // empty => not sent std::string session_; // opaque server session token std::string username_; std::string appStatus_ = "unknown"; std::string statusMessage_; std::string lastError_; std::optional expiry_; // nullopt = lifetime / unknown int heartbeatSeconds_ = 10; bool loggedIn_ = false; bool inited_ = false; // --- heartbeat thread --- std::thread hbThread_; std::atomic hbRunning_{false}; std::atomic hbStop_{false}; }; } // namespace atlas #endif // ATLASAUTH_H