Aller au contenu
bdkshop

Guide des créateurs

Protéger un script avec une licence

Chaque acheteur reçoit sa propre clé. Au démarrage, votre script la fait vérifier par BDKSHOP et s’arrête si elle est refusée : une copie partagée ou une commande remboursée ne fonctionne plus. Aucun serveur à installer de votre côté.

Comment ça marche

  1. Vous activez la clé de licence sur votre annonce et collez quelques lignes de code dans votre script.
  2. À chaque vente, BDKSHOP crée une clé du type BDK-XXXX-XXXX-XXXX-XXXX, affichée sur la page de commande de l’acheteur.
  3. L’acheteur la copie dans la configuration de votre produit (par exemple son server.cfg ou son config.yml).
  4. Au démarrage, votre script envoie la clé et un identifiant du serveur à BDKSHOP, qui répond si la licence est valide.

La clé fonctionne sur un nombre limité de serveurs, que vous choisissez. L’acheteur qui change de serveur clique sur « Changer de serveur » depuis sa commande, une fois par semaine au plus ; vous pouvez aussi réinitialiser une licence vous-même.

1. Activer la protection sur une annonce

  1. Dans Mon compte › Mes annonces, modifiez l’annonce, rubrique Protection anti-fuite.
  2. Choisissez Clé de licence et indiquez le nombre d’installations par licence (0 = illimité, 50 au plus).
  3. Enregistrez : le lien Code d’intégration et licences émises apparaît. Il donne l’identifiant produit de l’annonce (prd_…), le code prêt à copier et la liste des clés vendues.

Indépendamment de la clé, chaque archive .zip ou .jar téléchargée porte l’empreinte invisible de la commande. Si votre création circule, la page Anti-fuite retrouve la commande d’origine à partir du fichier ; vous signalez alors la fuite en un clic et l’équipe révoque la licence correspondante.

2. Ajouter la vérification à votre script

Remplacez prd_votre_identifiant par l’identifiant produit de votre annonce. Sur la page de vos licences, le même code est déjà rempli avec le bon identifiant.

-- server.lua : vérification de la licence BDKSHOP au démarrage de la ressource.
-- Dans le server.cfg de l'acheteur : set bdkshop_licence "BDK-XXXX-XXXX-XXXX-XXXX"
local PRODUIT = 'prd_votre_identifiant'

CreateThread(function()
    Wait(5000)
    local cle = GetConvar('bdkshop_licence', '')
    local serveur = GetConvar('web_baseUrl', '')
    if serveur == '' then serveur = GetConvar('sv_hostname', 'serveur') end
    local ressource = GetCurrentResourceName()

    PerformHttpRequest('https://bdkshop.fr/api/v1/licenses/verify', function(code, body)
        local ok, data = pcall(json.decode, body or '')
        if code ~= 200 or not ok or type(data) ~= 'table' then
            -- BDKSHOP injoignable : on n'arrête pas le serveur de l'acheteur pour autant.
            print(('^3[%s] Vérification de licence impossible pour le moment (%s).^0'):format(ressource, tostring(code)))
            return
        end
        if not data.valid then
            print(('^1[%s] Licence refusée : %s^0'):format(ressource, data.message))
            StopResource(ressource)
            return
        end
        print(('^2[%s] Licence valide.^0'):format(ressource))
    end, 'POST', json.encode({
        product = PRODUIT, key = cle, identifier = serveur, label = GetConvar('sv_hostname', 'Serveur FiveM'),
    }), { ['Content-Type'] = 'application/json', ['Accept'] = 'application/json' })
end)
// Plugin Paper / Spigot (Java 17+). Dans config.yml de l'acheteur : licence: "BDK-XXXX-XXXX-XXXX-XXXX"
private static final String PRODUIT = "prd_votre_identifiant";

@Override
public void onEnable() {
    saveDefaultConfig();
    verifierLicence();
}

private void verifierLicence() {
    String cle = getConfig().getString("licence", "").replace("\"", "");
    String json = "{\"product\":\"" + PRODUIT + "\",\"key\":\"" + cle + "\",\"identifier\":\"" + instance() + "\",\"label\":\"Serveur Minecraft\"}";
    HttpRequest requete = HttpRequest.newBuilder(URI.create("https://bdkshop.fr/api/v1/licenses/verify"))
            .header("Content-Type", "application/json").header("Accept", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(json)).build();

    HttpClient.newHttpClient().sendAsync(requete, HttpResponse.BodyHandlers.ofString()).thenAccept(reponse -> {
        if (reponse.statusCode() != 200) {
            getLogger().warning("Vérification de licence impossible pour le moment (" + reponse.statusCode() + ").");
            return;
        }
        if (!reponse.body().contains("\"valid\":true")) {
            getLogger().severe("Licence refusée : le plugin est désactivé.");
            getServer().getScheduler().runTask(this, () -> getServer().getPluginManager().disablePlugin(this));
        }
    }).exceptionally(erreur -> {
        getLogger().warning("BDKSHOP injoignable : " + erreur.getMessage());
        return null;
    });
}

/** Identifiant stable de l'installation, créé au premier démarrage. */
private String instance() {
    File fichier = new File(getDataFolder(), ".instance");
    try {
        if (!fichier.exists()) {
            getDataFolder().mkdirs();
            Files.writeString(fichier.toPath(), UUID.randomUUID().toString());
        }
        return Files.readString(fichier.toPath()).trim();
    } catch (IOException e) {
        return "port-" + getServer().getPort();
    }
}
// Node.js 18+ (bot Discord, back-end de site). Clé de l'acheteur dans la variable BDKSHOP_LICENCE.
const os = require('node:os');
const PRODUIT = 'prd_votre_identifiant';

async function verifierLicence() {
  try {
    const reponse = await fetch('https://bdkshop.fr/api/v1/licenses/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
      body: JSON.stringify({
        product: PRODUIT,
        key: process.env.BDKSHOP_LICENCE ?? '',
        identifier: os.hostname(),
        label: 'Mon application',
      }),
    });
    if (!reponse.ok) return console.warn(`Vérification de licence impossible pour le moment (${reponse.status}).`);
    const data = await reponse.json();
    if (!data.valid) {
      console.error(`Licence refusée : ${data.message}`);
      process.exit(1);
    }
  } catch (erreur) {
    console.warn('BDKSHOP injoignable :', erreur.message);
  }
}

verifierLicence();
curl -X POST https://bdkshop.fr/api/v1/licenses/verify \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  -d '{"product":"prd_votre_identifiant","key":"BDK-XXXX-XXXX-XXXX-XXXX","identifier":"mon-serveur-1","nonce":"a1b2c3"}'

# Réponse
{"valid":true,"status":"active","message":"Licence valide.","licensee":"pseudo","activations":1,"max_activations":1,"product":"prd_votre_identifiant","nonce":"a1b2c3","timestamp":1760000000,"signature":"…"}

Une règle d’or : si BDKSHOP ne répond pas (coupure réseau, code HTTP différent de 200), laissez le script fonctionner. Vos acheteurs ne doivent jamais être pénalisés par une panne. Seule une réponse "valid": false doit arrêter le produit.

3. Référence de l’API

POST https://bdkshop.fr/api/v1/licenses/verify, corps en JSON (Content-Type: application/json). Aucune authentification : l’identifiant produit et la clé suffisent.

ParamètreObligatoireDescription
productouiIdentifiant produit de l’annonce (prd_…).
keyouiClé de l’acheteur. Les majuscules et les espaces autour ne comptent pas.
identifierouiIdentifiant stable du serveur ou de l’installation (3 à 200 caractères). Il n’est jamais stocké en clair, seulement son empreinte.
labelnonNom de l’installation montré à l’acheteur et à vous (80 caractères au plus).
noncenonValeur aléatoire renvoyée telle quelle, pour vérifier la signature (64 caractères au plus).

La réponse est toujours un code HTTP 200 avec ce contenu (sauf requête mal formée : 422, ou trop de requêtes : 429) :

curl -X POST https://bdkshop.fr/api/v1/licenses/verify \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  -d '{"product":"prd_votre_identifiant","key":"BDK-XXXX-XXXX-XXXX-XXXX","identifier":"mon-serveur-1","nonce":"a1b2c3"}'

# Réponse
{"valid":true,"status":"active","message":"Licence valide.","licensee":"pseudo","activations":1,"max_activations":1,"product":"prd_votre_identifiant","nonce":"a1b2c3","timestamp":1760000000,"signature":"…"}
statusvalidSignificationCe que fait votre script
activetrueLicence valide, installation enregistrée.Fonctionne normalement.
protection_disabledtrueVous avez désactivé la clé sur l’annonce : toutes les clés sont acceptées.Fonctionne normalement.
invalid_keyfalseClé inconnue pour ce produit (faute de frappe, clé inventée).S’arrête et affiche le message.
revokedfalseCommande remboursée, ou licence révoquée après une fuite.S’arrête.
expiredfalseProduit vendu par abonnement et abonnement terminé.S’arrête et invite à se réabonner.
activation_limitfalseClé déjà utilisée sur le nombre maximum de serveurs.S’arrête et invite à « Changer de serveur » depuis la commande.
unknown_productfalseIdentifiant produit inconnu (erreur dans votre code).À corriger avant de publier.

Limites : 60 vérifications par minute et par adresse IP, et 20 par minute pour une même clé. Une vérification au démarrage, puis toutes les quelques heures au plus, suffit largement.

4. Vérifier la signature (avancé)

Un acheteur malin pourrait détourner l’adresse de BDKSHOP vers un faux serveur qui répond toujours « valide ». Pour l’en empêcher, chaque réponse est signée : signature vaut le HMAC-SHA256 du texte valide|statut|produit|nonce|horodatage (valide vaut 1 ou 0), calculé avec le secret de votre annonce. Envoyez un nonce aléatoire, recalculez la signature et vérifiez que l’horodatage est récent.

// Node.js 18+ : vérification de la signature (protège contre une fausse réponse).
const crypto = require('node:crypto');
const PRODUIT = 'prd_votre_identifiant';
const SECRET = 'votre_secret'; // Mon compte › Anti-fuite › Licences de l'annonce › « Vérifier la signature des réponses »

async function verifierLicence(cle, identifiant) {
  const nonce = crypto.randomBytes(16).toString('hex');
  const reponse = await fetch('https://bdkshop.fr/api/v1/licenses/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
    body: JSON.stringify({ product: PRODUIT, key: cle, identifier: identifiant, nonce }),
  });
  const data = await reponse.json();

  const texte = [data.valid ? '1' : '0', data.status, data.product, data.nonce ?? '', String(data.timestamp)].join('|');
  const attendue = crypto.createHmac('sha256', SECRET).update(texte).digest('hex');
  const recente = Math.abs(Date.now() / 1000 - data.timestamp) < 300;
  if (data.nonce !== nonce || data.signature !== attendue || !recente) {
    throw new Error('Réponse de licence non authentique.');
  }

  return data.valid;
}

Le secret se trouve sur la page des licences de l’annonce, rubrique « Vérifier la signature des réponses (avancé) ». Il est lisible par quiconque ouvre votre script : obfusquez la partie qui le contient, et générez-en un nouveau si vous pensez qu’il a fuité.

Bonnes pratiques

  • Lisez la clé dans la configuration de l’acheteur, jamais écrite en dur dans le code.
  • Utilisez un identifiant qui ne change pas à chaque redémarrage (nom du serveur, fichier créé au premier lancement), sinon chaque démarrage compte comme une nouvelle installation.
  • Expliquez dans la description de l’annonce où l’acheteur doit coller sa clé.
  • Affichez le message de la réponse dans la console : il est clair pour l’acheteur.
  • Ne bloquez jamais le serveur en cas d’erreur réseau.

Questions fréquentes

Que se passe-t-il en cas de remboursement ? La licence est révoquée automatiquement : la vérification suivante répond revoked.

Et pour un abonnement ? La clé reste la même à chaque renouvellement. À la fin de l’abonnement, elle répond expired après quelques jours de grâce.

Je retire la protection de mon annonce ? L’API répond alors « valide » à toutes les clés : les serveurs de vos acheteurs continuent de fonctionner.

Mon script est en Python, PHP ou autre ? Toute technologie capable d’envoyer une requête HTTP POST en JSON convient : inspirez-vous de la requête brute.

Une question qui n’est pas ici ? Utilisez « Signaler un bug ou une idée » en bas de page, ou contactez-nous.

Signaler un bug ou une idée

Votre message arrive directement à l’équipe BDKSHOP. Pour une commande ou un paiement, ouvrez plutôt un litige ou contactez-nous depuis l’aide.

Type

Nous enregistrons la page concernée et votre navigateur pour reproduire le problème. Ne mettez ni mot de passe ni donnée bancaire.