Skip to content

Commit 53cd75e

Browse files
committed
feat: self-ban support, HWID override support, release 1.0.2
1 parent 23470f7 commit 53cd75e

4 files changed

Lines changed: 134 additions & 5 deletions

File tree

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ int main() {
6262
| `onFailure` | `std::function<void(const std::string&, const std::exception*)>` | no | `nullptr` | Failure callback; if null, `std::exit(1)` |
6363
| `requestTimeout` | `int` | no | `15` | HTTP timeout (seconds) |
6464
| `ttlSeconds` | `int` | no | `0` (server default: 86400) | Requested session token lifetime. `0` means "server default". Server clamps to `[3600, 604800]`; preserved across heartbeat refreshes. |
65+
| `hwidOverride` | `std::string` | no | `""` | Optional custom HWID/subject string. When non-empty (for example `tg:123456789`), the SDK sends it instead of generating a machine fingerprint. |
66+
67+
For Telegram/Discord bot flows, prefer immutable IDs (`tg:<user_id>`, `discord:<user_id>`) instead of usernames.
6568

6669
## Billing model
6770

README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,24 @@ target_link_libraries(yourapp PRIVATE AuthForge::authforge_sdk)
7878
| `onFailure` | std::function | `nullptr` | Callback `(const string&, const exception*)` on auth failure |
7979
| `requestTimeout` | int | `15` | HTTP request timeout in seconds |
8080
| `ttlSeconds` | int | `0` (server default: 86400) | Requested session token lifetime. `0` means "server default". Server clamps to `[3600, 604800]`; preserved across heartbeat refreshes. |
81+
| `hwidOverride` | string | `""` | Optional custom hardware/subject identifier. When non-empty, the SDK uses this value instead of generated device fingerprint data. |
82+
83+
### Identity-based binding example (Telegram/Discord)
84+
85+
```cpp
86+
authforge::AuthForgeClient client(
87+
"YOUR_APP_ID",
88+
"YOUR_APP_SECRET",
89+
"YOUR_PUBLIC_KEY",
90+
"SERVER",
91+
900,
92+
authforge::AuthForgeClient::kDefaultApiBaseUrl,
93+
onFailure,
94+
15,
95+
0,
96+
"tg:" + std::to_string(telegramUserId) // or "discord:" + std::to_string(discordUserId)
97+
);
98+
```
8199

82100
## Billing
83101

@@ -91,6 +109,7 @@ Any heartbeat interval is safe economically: a desktop app running 6h/day at a 1
91109
| Method | Returns | Description |
92110
|---|---|---|
93111
| `Login(const std::string&)` | `bool` | Validates key and stores signed session (`sessionToken`, `expiresIn`, `appVariables`, `licenseVariables`) |
112+
| `SelfBan(...)` | `bool` | Requests `/auth/selfban` to blacklist HWID/IP and optionally revoke (session-authenticated only) |
94113
| `Logout()` | `void` | Stops heartbeat and clears all session/auth state |
95114
| `IsAuthenticated()` | `bool` | True when an active authenticated session exists |
96115
| `GetSessionDataJson()` | `std::optional<std::string>` | Full decoded payload JSON |
@@ -108,7 +127,7 @@ Any heartbeat interval is safe economically: a desktop app running 6h/day at a 1
108127
If authentication fails, the SDK calls your `onFailure` callback if one is provided. If no callback is set, **the SDK calls `std::exit(1)` to terminate the process.** This is intentional — it prevents your app from running without a valid license.
109128

110129
Recognized server errors:
111-
`invalid_app`, `invalid_key`, `expired`, `revoked`, `hwid_mismatch`, `no_credits`, `app_burn_cap_reached`, `blocked`, `rate_limited`, `replay_detected`, `app_disabled`, `session_expired`, `bad_request`, `system_error`
130+
`invalid_app`, `invalid_key`, `expired`, `revoked`, `hwid_mismatch`, `no_credits`, `app_burn_cap_reached`, `blocked`, `rate_limited`, `replay_detected`, `app_disabled`, `session_expired`, `revoke_requires_session`, `bad_request`, `system_error`
112131

113132
Request retries are automatic inside the internal HTTP layer:
114133
- `rate_limited`: retry after 2s, then 5s (max 3 attempts total)
@@ -131,9 +150,29 @@ authforge::AuthForgeClient client(
131150
);
132151
```
133152

153+
## Self-ban (tamper response)
154+
155+
Use `SelfBan(...)` when anti-tamper checks trigger:
156+
157+
```cpp
158+
// Post-session (authenticated): defaults to revoke + HWID/IP blacklist.
159+
client.SelfBan();
160+
161+
// Pre-session: pass license key, SDK automatically disables revokeLicense.
162+
client.SelfBan("AF-XXXX-XXXX-XXXX");
163+
164+
// Custom flags:
165+
client.SelfBan("", "", false, true, true);
166+
```
167+
168+
`SelfBan(...)` chooses request mode automatically:
169+
- Uses post-session mode when a session token is available (`sessionToken` arg or current SDK session).
170+
- Falls back to pre-session mode with `licenseKey` + nonce + app secret.
171+
- In pre-session mode, revoke is always disabled client-side to avoid unsafe key revocations.
172+
134173
## How It Works
135174

136-
1. **Login**Collects a hardware fingerprint (MAC, CPU, disk serial), generates a random nonce, and sends everything to the AuthForge API. The server validates the license key, binds the HWID, deducts a credit, and returns a signed payload. The SDK verifies the Ed25519 signature and nonce to prevent replay attacks.
175+
1. **Login**Uses `hwidOverride` when non-empty; otherwise collects a hardware fingerprint (MAC, CPU, disk serial). It then generates a random nonce and sends everything to the AuthForge API. The server validates the license key, binds the HWID, deducts a credit, and returns a signed payload. The SDK verifies the Ed25519 signature and nonce to prevent replay attacks.
137176

138177
2. **Heartbeat** — A detached background thread checks in at the configured interval. In SERVER mode, it sends a fresh nonce and verifies the response. In LOCAL mode, it re-verifies the stored signature and checks expiry without network calls.
139178

authforge_sdk.cpp

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ AuthForgeClient::AuthForgeClient(
181181
std::string apiBaseUrl,
182182
std::function<void(const std::string &, const std::exception *)> onFailure,
183183
int requestTimeout,
184-
int ttlSeconds)
184+
int ttlSeconds,
185+
std::string hwidOverride)
185186
: appId_(std::move(appId)),
186187
appSecret_(std::move(appSecret)),
187188
publicKey_(std::move(publicKey)),
@@ -221,7 +222,8 @@ AuthForgeClient::AuthForgeClient(
221222
throw std::invalid_argument("public_key must be 32 bytes (base64 Ed25519 raw key)");
222223
}
223224

224-
hwid_ = GetHwid();
225+
const std::string trimmedOverride = Trim(hwidOverride);
226+
hwid_ = trimmedOverride.empty() ? GetHwid() : trimmedOverride;
225227
}
226228

227229
bool AuthForgeClient::Login(const std::string &licenseKey) {
@@ -242,6 +244,84 @@ bool AuthForgeClient::Login(const std::string &licenseKey) {
242244
}
243245
}
244246

247+
bool AuthForgeClient::SelfBan(
248+
const std::string &licenseKey,
249+
const std::string &sessionToken,
250+
bool revokeLicense,
251+
bool blacklistHwid,
252+
bool blacklistIp) {
253+
try {
254+
std::string resolvedSessionToken;
255+
std::string resolvedLicenseKey;
256+
std::string hwid;
257+
{
258+
std::lock_guard<std::mutex> guard(lock_);
259+
resolvedSessionToken = Trim(sessionToken.empty() ? sessionToken_ : sessionToken);
260+
resolvedLicenseKey = Trim(licenseKey.empty() ? licenseKey_ : licenseKey);
261+
hwid = hwid_;
262+
}
263+
264+
auto appendFlags = [](std::string body,
265+
bool revoke,
266+
bool includeRevoke,
267+
bool hwidFlag,
268+
bool ipFlag) {
269+
if (body.size() < 2 || body.back() != '}') {
270+
return body;
271+
}
272+
body.pop_back();
273+
if (includeRevoke) {
274+
body += ",\"revokeLicense\":";
275+
body += revoke ? "true" : "false";
276+
}
277+
body += ",\"blacklistHwid\":";
278+
body += hwidFlag ? "true" : "false";
279+
body += ",\"blacklistIp\":";
280+
body += ipFlag ? "true" : "false";
281+
body.push_back('}');
282+
return body;
283+
};
284+
285+
std::string response;
286+
if (!resolvedSessionToken.empty()) {
287+
std::string body = BuildJsonBody({
288+
{"appId", appId_},
289+
{"sessionToken", resolvedSessionToken},
290+
{"hwid", hwid},
291+
});
292+
body = appendFlags(body, revokeLicense, true, blacklistHwid, blacklistIp);
293+
response = PostJson("/auth/selfban", body);
294+
} else {
295+
if (resolvedLicenseKey.empty()) {
296+
throw std::runtime_error("missing_license_key");
297+
}
298+
std::string body = BuildJsonBody({
299+
{"appId", appId_},
300+
{"appSecret", appSecret_},
301+
{"licenseKey", resolvedLicenseKey},
302+
{"hwid", hwid},
303+
{"nonce", GenerateNonceHex32()},
304+
});
305+
// Pre-session self-ban cannot revoke licenses.
306+
body = appendFlags(body, false, true, blacklistHwid, blacklistIp);
307+
response = PostJson("/auth/selfban", body);
308+
}
309+
310+
JsonValue status;
311+
ExtractJsonValue(response, "status", status);
312+
if (!IsSuccessStatus(status)) {
313+
throw std::runtime_error(ExtractServerError(response));
314+
}
315+
return true;
316+
} catch (const std::exception &exc) {
317+
Fail("selfban_failed", &exc);
318+
return false;
319+
} catch (...) {
320+
Fail("selfban_failed", nullptr);
321+
return false;
322+
}
323+
}
324+
245325
void AuthForgeClient::StartHeartbeatOnce() {
246326
std::lock_guard<std::mutex> guard(lock_);
247327
if (heartbeatStarted_) {

authforge_sdk.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@ class AuthForgeClient {
2525
std::string apiBaseUrl = kDefaultApiBaseUrl,
2626
std::function<void(const std::string &, const std::exception *)> onFailure = nullptr,
2727
int requestTimeout = 15,
28-
int ttlSeconds = 0);
28+
int ttlSeconds = 0,
29+
std::string hwidOverride = "");
2930

3031
bool Login(const std::string &licenseKey);
32+
bool SelfBan(const std::string &licenseKey = "",
33+
const std::string &sessionToken = "",
34+
bool revokeLicense = true,
35+
bool blacklistHwid = true,
36+
bool blacklistIp = true);
3137
void Logout();
3238
bool IsAuthenticated() const;
3339
std::optional<std::string> GetSessionDataJson() const;
@@ -124,6 +130,7 @@ class AuthForgeClient {
124130
"replay_detected",
125131
"app_disabled",
126132
"session_expired",
133+
"revoke_requires_session",
127134
"bad_request",
128135
"system_error",
129136
};

0 commit comments

Comments
 (0)