# SAKU · Guide d'intégration SDK

Comment protéger ton application (C++, C#, Python, Rust, Node.js…) avec le système de licences SAKU.

**Backend live** : `https://sakumethod.fun`
**API version** : v3.2
**Authentification client** : par clé de licence (pas de token dev à distribuer)
**Sécurité** : réponses signées HMAC-SHA256 par app + nonce anti-rejeu + HWID lock

---

## Table des matières

1. [Vue d'ensemble](#vue-densemble)
2. [Créer ton app dans le panel admin](#créer-ton-app)
3. [Récupérer tes credentials](#credentials)
4. [SDK C++ (Windows)](#sdk-c-windows)
5. [Intégration C# (.NET)](#c-net)
6. [Intégration Python](#python)
7. [Intégration Rust](#rust)
8. [Intégration Node.js](#nodejs)
9. [Vérifier une signature à la main](#signature)
10. [Codes d'erreur](#codes-derreur)
11. [Bonnes pratiques](#bonnes-pratiques)

---

## Vue d'ensemble

Chaque produit protégé = **une app** dans le panel SAKU. Une app a :
- Un **app_id** (slug court, ex: `valorant-internal`)
- Un **secret** de signature (rotate à volonté)
- Un pool de **clés** (générées via admin panel ou API)

Chaque **client final** achète une clé. La clé est bindée à son HWID au premier `activate`, puis maintenue vivante par un `heartbeat` toutes les 60s.

**Flux typique** :

```
Ton binaire (.exe / .dll)
  │
  ├── 1. saku::activate("SAKU-XXXX-XXXX-XXXX")
  │      ↓
  │      POST /api/v1/{app_id}/activate
  │      { "key": "...", "hwid": "sha256(...)" }
  │      ↓
  │      Réponse signée : { "success": true, "session": "...", "plan": "premium", "expires": 1790... }
  │
  ├── 2. Vérifie la signature avec ton app secret
  │
  ├── 3. Session enregistrée en mémoire → app démarre
  │
  └── 4. saku::heartbeat() en background thread toutes les 60s
         ↓
         POST /api/v1/{app_id}/heartbeat
         { "session": "..." }
         ↓
         Si key bannie / expirée / HWID kick → ferme l'app
```

---

## Créer ton app

1. Login sur https://sakumethod.fun/admin.html
2. Menu **Applications** → **+ Nouveau produit** (ou "Créer une app")
3. Entre un nom (ex: "Valorant Internal") → clique **Créer**
4. Un `app_id` slug est généré automatiquement (`valorant-internal`)

## Credentials

Dans la table Applications :

- **App ID** : colonne "APP ID" (ex: `valorant-internal`)
- **Secret** : colonne "SECRET" → clique le bouton **⧉ Copier** pour révéler la string complète

⚠ **Le secret ne doit JAMAIS être hardcodé dans du code partagé/public.** Il vit uniquement dans ton binaire compilé (obfuscation recommandée). Si tu penses qu'il a leak → onglet App → **Rotate secret** pour en générer un neuf. Attention : ça invalide toutes les builds déjà distribuées.

---

## SDK C++ (Windows)

**Fichier fourni** : `server/static/sdk/saku_auth.hpp` — header-only, C++17, zéro dépendance externe (utilise WinHTTP + BCrypt fournis par Windows).

### Setup

Copie `saku_auth.hpp` dans ton projet.

**Visual Studio** : lien automatique via `#pragma comment(lib, ...)` dans le header.

**MinGW** : `g++ -std=c++17 main.cpp -o app.exe -lwinhttp -lbcrypt -ladvapi32`

### Usage

```cpp
#include "saku_auth.hpp"
#include <iostream>

int main() {
    saku::Client auth(
        "https://sakumethod.fun",   // Base URL
        "valorant-internal",         // Ton app_id
        "2a0e7453abcdef..."          // Ton app secret
    );

    std::string license_key;
    std::cout << "Entre ta cle : ";
    std::getline(std::cin, license_key);

    saku::Result r = auth.activate(license_key);
    if (!r.success) {
        std::cout << "Refuse : " << r.message << "\n";
        return 1;
    }

    std::cout << "Bienvenue. Plan: " << r.plan << "\n";

    // Démarre le heartbeat en background — coupe l'app si la clé devient invalide
    auth.start_heartbeat([](const std::string& reason) {
        MessageBoxA(nullptr, reason.c_str(), "Session terminée", MB_ICONERROR);
        ExitProcess(0);
    });

    // Ton code protégé ici...
    run_main_loop();

    auth.stop_heartbeat();
    return 0;
}
```

### API

| Méthode | Description |
|---|---|
| `Client(url, app_id, secret)` | Crée le client |
| `activate(key)` → `Result` | Active + binde la clé au HWID |
| `start_heartbeat(cb, interval=60s)` | Démarre le keep-alive en thread |
| `stop_heartbeat()` | Arrête le thread proprement |
| `is_active()` → `bool` | Session valide en mémoire ? |
| `plan()` / `expires()` | Infos de la clé |
| `get_hwid()` (namespace) | HWID stable = SHA256(machine_guid + volume_serial) |

Chaque réponse est vérifiée par HMAC-SHA256 avec ton secret. Un attaquant qui intercepte + modifie la réponse casse le HMAC → réponse rejetée.

---

## C# (.NET)

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Security.Cryptography;

public class SakuClient
{
    private readonly string _server, _appId, _secret;
    private readonly HttpClient _http = new();
    public string Session { get; private set; }

    public SakuClient(string server, string appId, string secret) {
        _server = server.TrimEnd('/');
        _appId = appId;
        _secret = secret;
    }

    public async Task<(bool ok, string msg, string plan)> Activate(string key) {
        var hwid = GetHwid();
        var nonce = Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLower();
        var body = JsonSerializer.Serialize(new { key, hwid, nonce });
        var url = $"{_server}/api/v1/{_appId}/activate";
        var res = await _http.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/json"));
        var txt = await res.Content.ReadAsStringAsync();
        var json = JsonDocument.Parse(txt).RootElement;

        // Vérifie la signature HMAC
        if (!VerifySignature(txt, nonce)) return (false, "Signature invalide", "");

        var success = json.GetProperty("success").GetBoolean();
        if (!success) return (false, json.GetProperty("message").GetString(), "");

        Session = json.GetProperty("session").GetString();
        return (true, "OK", json.GetProperty("plan").GetString());
    }

    public async Task<bool> Heartbeat() {
        if (Session == null) return false;
        var nonce = Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLower();
        var body = JsonSerializer.Serialize(new { session = Session, nonce });
        var res = await _http.PostAsync(
            $"{_server}/api/v1/{_appId}/heartbeat",
            new StringContent(body, Encoding.UTF8, "application/json"));
        var txt = await res.Content.ReadAsStringAsync();
        return VerifySignature(txt, nonce)
            && JsonDocument.Parse(txt).RootElement.GetProperty("success").GetBoolean();
    }

    private bool VerifySignature(string json, string nonce) {
        var doc = JsonDocument.Parse(json).RootElement;
        var sig = doc.GetProperty("_sig").GetString();
        var ts = doc.GetProperty("_ts").GetString();
        var echoedNonce = doc.GetProperty("nonce").GetString();
        if (echoedNonce != nonce) return false;

        // Reconstruire le body canonique (tri des clés, sans _sig/_ts)
        var fields = new SortedDictionary<string, JsonElement>();
        foreach (var p in doc.EnumerateObject())
            if (p.Name != "_sig" && p.Name != "_ts") fields[p.Name] = p.Value;
        var canonical = "{" + string.Join(",",
            fields.Select(kv => $"\"{kv.Key}\":{kv.Value.GetRawText()}")) + "}";

        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_secret));
        var expected = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(ts + canonical))).ToLower();
        return expected == sig;
    }

    private string GetHwid() {
        // Combine plusieurs identifiants système en un seul hash
        var mbGuid = new ManagementObject("Win32_ComputerSystemProduct.UUID").GetPropertyValue("UUID").ToString();
        var input = "saku|" + mbGuid + "|" + Environment.MachineName;
        using var sha = SHA256.Create();
        return Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(input))).ToLower();
    }
}
```

**Usage** :
```csharp
var saku = new SakuClient("https://sakumethod.fun", "valorant-internal", "2a0e7453...");
var (ok, msg, plan) = await saku.Activate("SAKU-XXXX-XXXX-XXXX-XXXX");
if (!ok) { MessageBox.Show(msg); Environment.Exit(1); }

// Background heartbeat
_ = Task.Run(async () => {
    while (true) {
        await Task.Delay(60_000);
        if (!await saku.Heartbeat()) { Environment.Exit(0); }
    }
});
```

---

## Python

```python
import hashlib, hmac, json, os, secrets, sys, threading, time
import urllib.request, urllib.parse

class SakuClient:
    def __init__(self, server, app_id, secret):
        self.server = server.rstrip('/')
        self.app_id = app_id
        self.secret = secret.encode()
        self.session = None

    def _post(self, endpoint, extra):
        nonce = secrets.token_hex(16)
        body = json.dumps({**extra, "nonce": nonce})
        url = f"{self.server}/api/v1/{self.app_id}/{endpoint}"
        req = urllib.request.Request(url, body.encode(), {"Content-Type": "application/json"})
        with urllib.request.urlopen(req, timeout=15) as r:
            raw = r.read().decode()
        data = json.loads(raw)
        if not self._verify(data, nonce):
            raise RuntimeError("Signature invalide")
        return data

    def _verify(self, data, nonce):
        if data.get("nonce") != nonce: return False
        sig = data.pop("_sig", "")
        ts = data.pop("_ts", "0")
        canonical = json.dumps(data, sort_keys=True, separators=(",", ":"))
        expected = hmac.new(self.secret, (ts + canonical).encode(), hashlib.sha256).hexdigest()
        # Restore for external use
        data["_sig"] = sig
        data["_ts"] = ts
        return hmac.compare_digest(expected, sig)

    def _hwid(self):
        # Simple HWID basé sur MAC + hostname (adapte selon ton besoin)
        import platform, uuid
        base = f"saku|{uuid.getnode()}|{platform.node()}"
        return hashlib.sha256(base.encode()).hexdigest()

    def activate(self, key):
        r = self._post("activate", {"key": key, "hwid": self._hwid()})
        if r.get("success"):
            self.session = r["session"]
        return r

    def heartbeat(self):
        if not self.session: return None
        return self._post("heartbeat", {"session": self.session})

    def start_heartbeat(self, on_invalid, interval=60):
        def loop():
            while self.session:
                time.sleep(interval)
                try:
                    r = self.heartbeat()
                    if not r or not r.get("success"):
                        on_invalid(r.get("message", "Session terminée") if r else "Serveur injoignable")
                        return
                except Exception as e:
                    on_invalid(str(e))
                    return
        threading.Thread(target=loop, daemon=True).start()
```

**Usage** :
```python
auth = SakuClient("https://sakumethod.fun", "valorant-internal", "2a0e7453...")
r = auth.activate("SAKU-XXXX-XXXX-XXXX-XXXX")
if not r["success"]:
    print("Refuse:", r["message"])
    sys.exit(1)

def kick(reason):
    print("Session terminée:", reason)
    os._exit(0)

auth.start_heartbeat(kick)
# ton code protégé ici
```

---

## Rust

```toml
# Cargo.toml
[dependencies]
reqwest = { version = "0.11", features = ["json", "blocking"] }
hmac = "0.12"
sha2 = "0.10"
serde_json = "1"
rand = "0.8"
hex = "0.4"
```

```rust
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;

pub struct SakuClient {
    server: String,
    app_id: String,
    secret: Vec<u8>,
    session: Option<String>,
    client: reqwest::blocking::Client,
}

impl SakuClient {
    pub fn new(server: &str, app_id: &str, secret: &str) -> Self {
        Self {
            server: server.trim_end_matches('/').to_string(),
            app_id: app_id.to_string(),
            secret: secret.as_bytes().to_vec(),
            session: None,
            client: reqwest::blocking::Client::new(),
        }
    }

    pub fn activate(&mut self, key: &str) -> Result<serde_json::Value, String> {
        let hwid = self.get_hwid();
        let nonce: String = (0..32).map(|_| format!("{:x}", rand::random::<u8>() & 0xf)).collect();
        let body = serde_json::json!({"key": key, "hwid": hwid, "nonce": nonce});
        let url = format!("{}/api/v1/{}/activate", self.server, self.app_id);
        let resp = self.client.post(&url).json(&body).send()
            .map_err(|e| e.to_string())?;
        let data: serde_json::Value = resp.json().map_err(|e| e.to_string())?;
        if !self.verify(&data, &nonce) { return Err("Signature invalide".into()); }
        if data["success"].as_bool() == Some(true) {
            self.session = data["session"].as_str().map(String::from);
        }
        Ok(data)
    }

    fn verify(&self, data: &serde_json::Value, nonce: &str) -> bool {
        let echoed = data["nonce"].as_str().unwrap_or("");
        if echoed != nonce { return false; }
        let sig = data["_sig"].as_str().unwrap_or("");
        let ts = data["_ts"].as_str().unwrap_or("");
        let mut filtered = data.as_object().unwrap().clone();
        filtered.remove("_sig"); filtered.remove("_ts");
        let canonical = serde_json::to_string(&serde_json::Value::Object(filtered)).unwrap();
        let mut mac = HmacSha256::new_from_slice(&self.secret).unwrap();
        mac.update(ts.as_bytes());
        mac.update(canonical.as_bytes());
        let expected = hex::encode(mac.finalize().into_bytes());
        expected == sig
    }

    fn get_hwid(&self) -> String {
        use sha2::{Digest, Sha256};
        let hostname = whoami::hostname();
        let mut h = Sha256::new();
        h.update(format!("saku|{}", hostname));
        hex::encode(h.finalize())
    }
}
```

---

## Node.js

```javascript
const crypto = require('crypto');
const os = require('os');

class SakuClient {
    constructor(server, appId, secret) {
        this.server = server.replace(/\/+$/, '');
        this.appId = appId;
        this.secret = secret;
        this.session = null;
    }

    async _post(endpoint, extra) {
        const nonce = crypto.randomBytes(16).toString('hex');
        const body = JSON.stringify({ ...extra, nonce });
        const r = await fetch(`${this.server}/api/v1/${this.appId}/${endpoint}`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body
        });
        const data = await r.json();
        if (!this._verify(data, nonce)) throw new Error('Signature invalide');
        return data;
    }

    _verify(data, nonce) {
        if (data.nonce !== nonce) return false;
        const sig = data._sig, ts = data._ts;
        const { _sig, _ts, ...rest } = data;
        const canonical = JSON.stringify(rest, Object.keys(rest).sort());
        const expected = crypto.createHmac('sha256', this.secret).update(ts + canonical).digest('hex');
        return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
    }

    _hwid() {
        return crypto.createHash('sha256').update(`saku|${os.hostname()}|${os.userInfo().username}`).digest('hex');
    }

    async activate(key) {
        const r = await this._post('activate', { key, hwid: this._hwid() });
        if (r.success) this.session = r.session;
        return r;
    }

    async heartbeat() {
        if (!this.session) return null;
        return this._post('heartbeat', { session: this.session });
    }

    startHeartbeat(onInvalid, interval = 60000) {
        this._timer = setInterval(async () => {
            try {
                const r = await this.heartbeat();
                if (!r || !r.success) { clearInterval(this._timer); onInvalid(r?.message || 'Session terminée'); }
            } catch (e) {
                clearInterval(this._timer); onInvalid(e.message);
            }
        }, interval);
    }
}

// Usage
const auth = new SakuClient('https://sakumethod.fun', 'valorant-internal', '2a0e7453...');
const r = await auth.activate('SAKU-XXXX-XXXX-XXXX-XXXX');
if (!r.success) { console.log('Refuse:', r.message); process.exit(1); }
auth.startHeartbeat(reason => { console.log(reason); process.exit(0); });
```

---

## Signature

Chaque réponse serveur inclut `_sig` (HMAC-SHA256 hex) et `_ts` (unix timestamp string). L'algorithme :

```
canonical = JSON de la réponse (SANS _sig, SANS _ts), clés triées, séparateurs sans espace
message   = _ts + canonical
_sig      = HMAC_SHA256(app_secret, message) → hex lowercase
```

**Vérifier** :
1. Retire `_sig` et `_ts` de la réponse
2. Sérialise le reste en JSON canonique (clés triées, `separators=(",", ":")`)
3. Calcule `HMAC-SHA256(secret, ts + canonical)` en hex lowercase
4. Compare en constant-time à `_sig`
5. Vérifie que `data.nonce` == le nonce que tu as envoyé (anti-rejeu)

Si un attaquant intercepte et modifie la réponse → HMAC casse → tu rejettes.

---

## Codes d'erreur

Toutes les réponses ont `{"success": bool, "message": "...", ...}`.

Common messages :
- `"Cle invalide."` — clé inexistante dans ta app
- `"Cette cle a ete desactivee."` — bannie par toi
- `"Cette cle a expire."` — date d'expiration dépassée
- `"Cette cle est liee a un autre appareil."` — HWID mismatch (revente / duplication)
- `"Application inconnue."` — mauvais `app_id` dans l'URL
- `"Trop de tentatives."` — rate limit (429), attends 5 min

HTTP status :
- **200** : réponse valide (peut contenir `success: false`)
- **400** : requête mal formée
- **403** : refus (clé, HWID, banni)
- **404** : app/session inconnue
- **429** : rate limit

---

## Bonnes pratiques

### Sécurité binaire

1. **N'expose jamais le secret** : compile-le dans ton binaire. Utilise un obfuscateur ([VMProtect](https://vmpsoft.com/), [Themida](https://www.oreans.com/Themida.php)) pour le protéger de RE.
2. **Vérifie TOUJOURS la signature** — sinon un attaquant MITM peut faker "success:true"
3. **HWID compound** : combine CPU + Mobo + Disk + MAC dans le SHA-256 côté client. Change dans le SDK → 1 seul HWID stable
4. **Ne stocke pas la session en clair** sur disque. Elle est en RAM uniquement — si l'app est kill, refais un `activate`
5. **Heartbeat intervalle 60s** (max 90s). Le serveur drop les sessions après 120s d'inactivité

### Rate limits côté serveur

- **activate** : 10 échecs / 5 min par IP → 429
- **heartbeat** : pas de limit stricte, mais respecte l'intervalle 60s
- **admin API** : 5 échecs login / 15 min par IP

### UX pour l'utilisateur final

- Message clair sur chaque erreur (traduis les `message` server → langue de l'utilisateur)
- Si HWID mismatch → dirige-le vers ton discord pour "reset HWID" (il peut aussi le faire lui-même 1×/24h depuis `sakumethod.fun/compte.html`)
- Si serveur down (network error) → **ne kick pas immédiatement** — laisse quelques retries (le SDK C++ fait déjà ça avec `kMaxNetworkFailures = 4`)

### Distribution

- Utilise **1 app par produit** (Valorant Internal ≠ Warzone Bypass = 2 apps distinctes)
- Génère les clés **par batch** dans le panel admin → Licences ou via revendeur
- Envoie les clés au client par email (dans le success modal → auto envoyé, ou copie manuelle depuis Commandes)

---

## Endpoints publics (pas d'auth requise)

| Méthode | Route | Description |
|---|---|---|
| `POST` | `/api/v1/{app_id}/activate` | Bind une clé au HWID |
| `POST` | `/api/v1/{app_id}/heartbeat` | Keep-alive session |
| `POST` | `/api/v1/{app_id}/self-reset-hwid` | Client reset son HWID (cooldown 24h) |
| `POST` | `/api/orders/mine` | Client fetch ses commandes par email |
| `POST` | `/api/promos/validate` | Valider un code promo |
| `GET`  | `/api/shop/products` | Liste des produits shop |
| `POST` | `/api/balance/get` | Solde d'un client |
| `POST` | `/api/orders/crypto-invoice` | Créer une invoice crypto |
| `POST` | `/api/orders/crypto-status` | Status d'un paiement crypto |
| `GET`  | `/api/status` | Healthcheck |

## Endpoints admin (Bearer token requis)

Header : `Authorization: Bearer {SAKU_ADMIN_TOKEN}`

| Méthode | Route | Description |
|---|---|---|
| `GET` `POST` | `/api/admin/apps` | Liste / crée une app |
| `POST` | `/api/admin/apps/{id}/delete` | Supprime en cascade |
| `POST` | `/api/admin/generate` | Génère un batch de clés |
| `GET`  | `/api/admin/keys` | Liste les clés |
| `POST` | `/api/admin/ban` | Bannir une clé |
| `POST` | `/api/admin/unban` | Débannir |
| `POST` | `/api/admin/reset-hwid` | Reset admin (bypass cooldown avec `force:true`) |
| `POST` | `/api/admin/hwid-history` | Historique HWID d'une clé |
| `POST` | `/api/admin/extend` | Prolonge d'N jours |
| `POST` | `/api/admin/delete` | Supprime une clé |
| `GET`  | `/api/admin/orders` | Toutes les commandes |
| `GET`  | `/api/admin/logs` | Journal (200 dernières) |
| `GET`  | `/api/admin/stats` | Stats agrégées |
| `GET`  | `/api/admin/resellers` | Revendeurs |

---

**Documentation live** : https://sakumethod.fun/docs.html
**Panel admin** : https://sakumethod.fun/admin.html
**Support** : Discord https://discord.gg/8Q6D8GgZNN
