AtlasAuth

AtlasAuth - C# SDK Guide

The official AtlasAuth C# SDK is one file (AtlasAuth.cs) with no dependencies. It follows the frozen wire contract in CONTRACT.md (v1). It talks to https://atlasauth.cc/api/v1, checks the server's ECDSA P-256 / SHA-256 signature on every security-relevant response, and gives you a small async API.

The SDK holds no secret. It only embeds your app_id (a UUID) and your app's

public key (SPKI DER, base64). Shipping both in your binary is safe and required.

A fake or man-in-the-middle server cannot forge a valid response without the private

key, which never leaves the AtlasAuth server.


1. Requirements & install

DSASignatureFormat.IeeeP1363FixedFieldConcatenation, which lets the SDK verify the raw 64-byte IEEE P1363 r||s signature directly, with no DER conversion. On legacy .NET Framework 4.x there is no DSASignatureFormat overload, so you would need BouncyCastle (Org.BouncyCastle.Crypto.Signers.ECDsaSigner) to verify the same payload bytes. The shipped file targets .NET 6+ on purpose and takes no third-party dependency.

System.Text.Json, System.Text, Microsoft.Win32 (registry, for the Windows HWID). No System.Management/WMI, no NuGet packages.

To install, just drop AtlasAuth.cs into your project. That's it.

bash
dotnet new console -o atlasdemo
# copy AtlasAuth.cs into atlasdemo/
# copy this guide's Program.cs into atlasdemo/ (replacing the generated one)
cd atlasdemo
dotnet run

The public namespace is AtlasAuth. The client type is AtlasAuth.AtlasClient.


2. Configure your app id and public key

You have two choices. Hard-code the two constants at the top of AtlasAuth.cs, or pass the values to the constructor (a non-empty constructor argument wins). Both come from your AtlasAuth dashboard.

csharp
// Inside AtlasAuth.cs (the clearly-marked constants):
public const string APP_ID     = "00000000-0000-0000-0000-000000000000";
public const string PUBLIC_KEY = "REPLACE_WITH_BASE64_SPKI_PUBLIC_KEY";

body only, with *no* -----BEGIN PUBLIC KEY----- header and no newlines). This is a public key. Never paste a private key here.

The constructor fails fast. It throws AtlasAuthException if APP_ID is still the all-zero placeholder, if PUBLIC_KEY is the placeholder or is not valid base64, or if the bytes do not import as an SPKI DER ECDSA public key (ImportSubjectPublicKeyInfo).

csharp
using var client = new AtlasClient(APP_ID, PUBLIC_KEY);
// Optional 3rd arg: an HWID seed (see §8):
// using var client = new AtlasClient(APP_ID, PUBLIC_KEY, "my-secret-seed");

3. Security model (what the SDK does for you)

Every signed response is this exact envelope:

json
{ "payload": "<compact-json-string>", "sig": "<base64-p1363-sig>" }

On each call the SDK:

  1. Makes a fresh random nonce (16 bytes turned into lowercase hex) and puts it in the

request body. It holds the nonce until the matching response is verified.

  1. Reads payload as an opaque UTF-8 string. It never re-serializes it (the

signature is over those exact bytes).

  1. base64-decodes sig into exactly 64 bytes (IEEE P1363 r||s, not DER).
  2. Verifies ECDSA-P256-SHA256(sig, utf8(payload), PUBLIC_KEY). If invalid, it throws

AtlasAuthException (hostile; the payload is never parsed).

  1. Parses the payload, then requires payload.nonce == the nonce this request sent

(else it is replay/tamper, so it throws).

  1. Advisory: checks abs(localUnix - payload.t) <= 60s. On drift it records

LastCode = "clock_skew" but does not hard-fail.

A signed ok:false (wrong password, expired key, banned, and so on) is a signed truth. The method returns false and you read LastError / LastCode. Only unverifiable results (bad signature, nonce mismatch, transport-level unsigned {error,code}) throw AtlasAuthException.


4. Session lifecycle

Init()  ->  Login() | Register() | License()  ->  StartHeartbeat(onKicked)  ->  Logout()

Init() must succeed before anything else. All API methods are async and accept an optional CancellationToken.


5. Methods

Task<bool> Init(string? clientVersion = null, CancellationToken ct = default)

POST /init. Sets up the session token and reads app status plus the heartbeat interval. Returns true only if a session was established and AppStatus == "active" and the forced-version check (if any) passes. On a non-active app or version mismatch it returns false and sets LastError/LastCode ("maintenance", "disabled", or "version_mismatch"). Throws on verify/nonce/transport failure.

csharp
if (!await client.Init(clientVersion: "1.0.0"))
{
    Console.Error.WriteLine($"Init failed: {client.LastError} [{client.LastCode}]");
    return;
}
Console.WriteLine($"Connected to '{client.AppName}' " +
                  $"(status: {client.AppStatus}, heartbeat: {client.HeartbeatSeconds}s).");

Task<bool> Login(string username, string password, CancellationToken ct = default)

POST /login. Takes a username and password (the HWID is computed and sent for you). On success it sets Username, Expiry, RemainingSeconds, and Level.

csharp
if (await client.Login("alice", "hunter2"))
    Console.WriteLine($"Welcome, {client.Username}! Level {client.Level}");
else
    Console.WriteLine($"Login failed: {client.LastCode}"); // invalid_credentials, hwid_mismatch, ...

Task<bool> Register(string username, string password, string license, string? email = null, CancellationToken ct = default)

POST /register. Self-signup that uses up a license key (only if your app allows registration). email is optional. The HWID is sent for you.

csharp
if (await client.Register("bob", "s3cret", "ATLAS-XXXX-YYYY", email: "bob@example.com"))
    Console.WriteLine($"Registered as {client.Username}, expiry {client.Expiry}");
else
    Console.WriteLine($"Register failed: {client.LastCode}"); // username_taken, invalid_license, ...

Task<bool> License(string key, CancellationToken ct = default)

POST /license. License-key-only login (no username or password). Sets Expiry, RemainingSeconds, and Level on success.

csharp
if (await client.License("ATLAS-XXXX-YYYY"))
    Console.WriteLine(client.Expiry is null ? "Lifetime key" : $"Expires {client.Expiry}");
else
    Console.WriteLine($"License failed: {client.LastCode}"); // invalid_license, license_expired, ...

Task<string?> Var(string name, CancellationToken ct = default)

POST /var. Fetches an app variable by name. Returns the string value, or null if it is not found or not permitted. In that case LastCode is set (for example "auth_required" for an auth-gated variable fetched on an unauthenticated session, or "not_found").

csharp
string? motd = await client.Var("motd");
if (motd != null) Console.WriteLine($"MOTD: {motd}");
else if (client.LastCode == "auth_required") Console.WriteLine("That variable needs login.");

Task Log(string level, string message, CancellationToken ct = default)

POST /log. Best-effort client-side logging (app log plus an optional Discord webhook). It never throws on a signed response, and transport failures are swallowed so logging can't break your app. level must be "info", "warn", or "error" (anything else is coerced to "info"). It is a no-op if Init() hasn't run.

csharp
await client.Log("info", "user opened the loader");
await client.Log("error", "something went wrong in module X");

void StartHeartbeat(Action<string> onKicked) / void StopHeartbeat()

See §6.

Task Logout(CancellationToken ct = default)

POST /logout. Stops the heartbeat first, ends the session server-side (best-effort), and clears local auth state (Username, Expiry, RemainingSeconds, Level, LoggedIn). Safe to call multiple times.

csharp
await client.Logout();

Task<StatusInfo?> Status(CancellationToken ct = default) / Task<List<NewsItem>> News(CancellationToken ct = default)

These are public, unsigned informational reads (GET /api/v1/status/{app_id} and /api/v1/news/{app_id}). They take no session, send no nonce, and are not signature-verified. Treat them as display/monitoring data only, and keep enforcement on the signed heartbeat. No Init() is required. Status() returns null and sets LastCode = "unknown_app" if the app id is unknown. News() returns an empty list.

csharp
// Live status banner (e.g. on a launcher splash) - no login needed.
var s = await client.Status();
if (s != null)
    Console.WriteLine($"{s.Name}: {s.Status} - {s.StatusMessage} ({s.Online} online)");

// News / changelog feed.
foreach (var item in await client.News())
    Console.WriteLine($"{(item.Pinned ? "[pinned] " : "")}{item.Title}\n{item.Body}");

StatusInfo carries AppId, Name, Status, StatusMessage, Online, and Time. Each NewsItem carries Id, Title, Body, Pinned, CreatedAt, and UpdatedAt (unix seconds). News is ordered pinned-first then newest.

Properties

PropertyTypeMeaning
Usernamestring?Authenticated username (after login/register), else null.
ExpiryDateTimeOffset?Key/subscription expiry. null = lifetime (or not yet known).
AppStatusstring?Last observed "active" / "maintenance" / "disabled" (null pre-init).
LastErrorstring?Human-readable last error.
LastCodestring?Machine-readable last code (e.g. invalid_credentials, rate_limited).
LoggedInboolTrue after a successful login / register / license.
InitializedboolTrue once Init() succeeded and a session token is held.
HeartbeatSecondsintServer-dictated heartbeat interval from /init (default 10).
AppNamestring?App display name from /init.
StatusMessagestring?Message to show the user when the app is not active.
LatestVersionstring?Latest app version advertised by /init.
RemainingSecondslong?Remaining subscription seconds, if reported.
Levelint?User level from /login, if reported.

6. Heartbeat + onKicked

After you authenticate, start the background heartbeat. It calls /check every HeartbeatSeconds seconds. The moment a signed reply says the session is no longer valid (ok==false, valid==false, app_status != "active", key_valid==false, or banned==true), it calls your onKicked(reason) callback exactly once and stops. It also kicks (and reports) if a heartbeat can't be verified, since an unverifiable beat is treated as hostile (reason will be "verify_failed").

csharp
using var exit = new ManualResetEventSlim(false);

client.StartHeartbeat(reason =>
{
    // Invoked on a background thread - marshal to your UI thread if needed.
    // reason ∈ killed | expired | app_disabled | app_maintenance | banned | verify_failed | ...
    Console.Error.WriteLine($"Session ended by server: {reason}. Exiting.");
    exit.Set();
});

exit.Wait();          // block until kicked (or your own signal)
client.StopHeartbeat();

Notes:

LastError and retried on the next beat. Only a verified "no longer valid" reply or an unverifiable beat triggers onKicked.


7. Error handling

There are two separate failure channels:

  1. Signed ok:false: the method returns false (or Var returns null). Read

LastError (human) and LastCode (machine). This is a trustworthy server verdict.

  1. Unverifiable / hostile / transport: the method throws AtlasAuthException. This

covers bad signature, 64-byte length mismatch, nonce mismatch, malformed envelope, unsigned {error,code} bodies (e.g. unknown_app, rate_limited, server_error), timeouts, and network errors. ex.Code carries the machine code when available.

csharp
try
{
    if (!await client.Login(user, pass))
    {
        // signed verdict
        Console.Error.WriteLine($"Login refused: {client.LastError} [{client.LastCode}]");
        return;
    }
}
catch (AtlasAuthException ex)
{
    // could not trust the server's answer - do NOT proceed
    Console.Error.WriteLine($"Cannot trust server: {ex.Message} [{ex.Code}]");
    return;
}

Per-call codes (from CONTRACT.md):

hwid_banned · ip_banned · license_expired · no_subscription · bad_input.

license_used · license_banned · hwid_banned · ip_banned · bad_input.

hwid_mismatch · hwid_banned · ip_banned · bad_input.

banned.

Retry-After header) · server_error.


8. HWID + seed override

The SDK computes an opaque hwid and sends it on /login, /register, and /license. The server binds the first hwid it sees for a key/user and then requires an exact match.

Windows registry MachineGuid (HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid) plus Environment.MachineName plus Environment.ProcessorCount.

base64(SHA256("atlasauth-hwid-seed|" + seed)). This is reproducible on any machine that knows the seed (so the legitimate owner can move their own key) and is unguessable to others. Set it at construction, before Init/Login.

csharp
using var client = new AtlasClient(APP_ID, PUBLIC_KEY, hwidSeed: "owner-portable-seed");
string id = client.ComputeHwid();   // inspect the value if you like

Stronger fingerprint hook: to bind to more hardware (motherboard/disk/MAC serials), gather those strings yourself and feed them through the static AtlasClient.HashIdsToHwid(IEnumerable<string>), which produces the exact canonical base64(SHA256(join("|", ids))) form. Keep the same inputs across runs, or the user will be locked out. Do not pull in System.Management/WMI unless you accept that dependency.


9. Complete runnable Program.cs

Drop this beside AtlasAuth.cs (delete the auto-generated Program.cs), fill in APP_ID and PUBLIC_KEY, then run dotnet run.

csharp
using System;
using System.Threading;
using System.Threading.Tasks;
using AtlasAuth;

internal static class Program
{
    // -------- FILL THESE IN (from the AtlasAuth dashboard) --------------------
    // APP_ID is a UUID. PUBLIC_KEY is base64 of the SPKI DER public key
    // (the base64 body only - no "-----BEGIN PUBLIC KEY-----" header).
    private const string APP_ID     = "00000000-0000-0000-0000-000000000000";
    private const string PUBLIC_KEY = "REPLACE_WITH_BASE64_SPKI_PUBLIC_KEY";
    // -------------------------------------------------------------------------

    private static async Task<int> Main()
    {
        // Optional 3rd arg = HWID seed for a portable key:
        //   new AtlasClient(APP_ID, PUBLIC_KEY, "my-secret-seed");
        using var client = new AtlasClient(APP_ID, PUBLIC_KEY);

        // ---- 1) init -------------------------------------------------------
        Console.WriteLine("Connecting to AtlasAuth...");
        try
        {
            if (!await client.Init(clientVersion: "1.0.0"))
            {
                Console.Error.WriteLine($"Init failed: {client.LastError} [{client.LastCode}]");
                return 1;
            }
        }
        catch (AtlasAuthException ex)
        {
            // Thrown on signature/nonce/transport failure - treat as hostile.
            Console.Error.WriteLine($"Cannot trust server: {ex.Message}");
            return 1;
        }

        Console.WriteLine($"Connected to '{client.AppName}' " +
                          $"(status: {client.AppStatus}, heartbeat: {client.HeartbeatSeconds}s).");

        // ---- 2) login ------------------------------------------------------
        Console.Write("Username: ");
        string user = Console.ReadLine()?.Trim() ?? "";
        Console.Write("Password: ");
        string pass = Console.ReadLine() ?? "";

        bool ok;
        try
        {
            ok = await client.Login(user, pass);
        }
        catch (AtlasAuthException ex)
        {
            Console.Error.WriteLine($"Login could not be verified: {ex.Message}");
            return 1;
        }

        if (!ok)
        {
            Console.Error.WriteLine($"Login failed: {client.LastError} [{client.LastCode}]");
            return 1;
        }

        Console.WriteLine($"Welcome, {client.Username}!");
        Console.WriteLine(client.Expiry is { } exp
            ? $"Subscription expires: {exp.LocalDateTime} " +
              $"(in {client.RemainingSeconds?.ToString() ?? "?"}s)"
            : "Subscription: lifetime");

        // Optional: read an app variable.
        try
        {
            string? motd = await client.Var("motd");
            if (motd != null) Console.WriteLine($"MOTD: {motd}");
        }
        catch (AtlasAuthException) { /* ignore optional var */ }

        // Best-effort log line.
        await client.Log("info", "example client logged in");

        // ---- 3) heartbeat --------------------------------------------------
        using var exit = new ManualResetEventSlim(false);
        client.StartHeartbeat(reason =>
        {
            Console.Error.WriteLine($"\nSession ended by server: {reason}. Exiting.");
            exit.Set();
        });

        Console.WriteLine("Running. Press Ctrl+C to quit, " +
                          "or the app exits if the session is revoked.");
        Console.CancelKeyPress += (_, e) => { e.Cancel = true; exit.Set(); };

        exit.Wait();                 // block until kicked or Ctrl+C

        client.StopHeartbeat();
        await client.Logout();
        return 0;
    }
}