Aller au contenu

Code agent dynamique

Le code agent est un identifiant court de quatre caractères affiché à l'étudiant sur la page de confirmation de commande (composant OrderConfirmation.jsx). Il sert de graine individuelle à la réponse de la dernière étape de chaque parcours — l'étape dite DYNAMIC. Cette page décrit la génération du code, les deux formats utilisés selon le niveau, les cinq formules de calcul de la réponse, et le flux de validation associé.

Pourquoi un code agent

Sans code agent, les 100 énigmes de PerfShop partageraient toutes un hash statique, et le premier étudiant ayant terminé un niveau pourrait chuchoter la dernière réponse à ses voisins. Le code agent rend la dernière étape unique pour chaque étudiant : la réponse dépend d'un calcul appliqué au code, et chaque étudiant a le sien.

La contrainte technique est stricte : la bonne réponse ne doit jamais transiter sur le réseau. Elle est calculée une seule fois côté serveur au moment du POST /pedagogique/join, hashée en SHA-256, puis stockée dans la session. L'étudiant fait le calcul mentalement (ou sur papier) en lisant son code agent dans la boutique, tape la réponse dans l'overlay, le frontend la hashe, et le backend compare les deux hashes.

Génération du code

La génération a lieu dans DefaultPedagogiqueSessionService.createSession(), immédiatement après la création du token UUID de la session. Le code dépend du token et du niveau :

String agentCode;
if ("bac1".equals(level) || "bac2".equals(level)) {
    // BAC+1 / BAC+2 : code 100% numérique
    String digitsOnly = session.token.replaceAll("[^0-9]", "");
    agentCode = digitsOnly.substring(0, Math.min(4, digitsOnly.length()));
} else {
    // BAC+3 à BAC+5 : code hexadécimal
    agentCode = session.token.replace("-", "").substring(0, 4).toUpperCase();
}
session.agentCode = agentCode;

Le token de session est un UUID v4 (format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx en minuscules hexadécimales) ; il a 36 caractères, 32 sans les tirets. Le code agent est dérivé par l'un des deux procédés suivants :

Niveau Procédé Exemple (token a8f3e2b1-…)
bac1, bac2 Ne garder que les chiffres décimaux du token, prendre les 4 premiers token a8f3e2b1-4c7d-4a6e-9b0f-3e5a7d1c8b42 → chiffres 83214746903571842 → code 8321
bac3, bac4, bac5 Retirer les tirets, prendre les 4 premiers caractères, mettre en majuscules token a8f3e2b1-…A8F3

Pourquoi deux formats

Le choix du format numérique pour bac1/bac2 est pédagogique et non technique : les étudiants de ces niveaux (lycéens post-bac, BTS débutants) n'ont pas forcément rencontré l'hexadécimal dans leur cursus. Leur imposer un calcul sur des chars AF pour la toute dernière étape d'un parcours de découverte serait un mur injustifié.

À l'inverse, l'hexadécimal pour bac3 et au-dessus est lui-même un objectif pédagogique : savoir lire A3F7, le convertir en décimal, calculer une somme de digits en base 16, appliquer un modulo 256 ou un XOR bit à bit fait partie des compétences attendues à ces niveaux. Le format du code agent prépare l'étudiant à la formule de calcul correspondante.

Unicité

La probabilité que deux étudiants d'une même session obtiennent le même code agent est non nulle mais faible :

  • Format numérique (bac1/bac2) : les 4 premiers chiffres d'un UUID v4 peuvent avoir entre 0 et 4 chiffres décimaux sur leurs 8 premiers hex chars, ce qui rend le codage peu uniforme, mais la collision reste anecdotique pour des classes de moins de 100 étudiants.
  • Format hexadécimal (bac3+) : $16^4 = 65\,536$ codes possibles — la probabilité de collision dans une classe de 30 étudiants est de l'ordre de 0,7 %. En pratique, deux collisions n'empêchent rien : les deux étudiants ont la même réponse finale et aucun autre effet de bord.

Le code agent n'est pas conçu comme un secret cryptographique — c'est un générateur de variabilité pédagogique.

Les cinq formules DYNAMIC

La méthode computeDynamicAnswer(String agentCode, String level) dans DefaultPedagogiqueSessionService dispatch selon le niveau. Chaque formule a été choisie pour que la difficulté du calcul mental corresponde au niveau cible.

Niveau Code Formule de la réponse Exemple (non-spoiler)
bac1 4 chiffres décimaux $R = (\sum_{i=1}^{4} c_i) \times 10$ code 8321 → $(8+3+2+1) \times 10 = 140$
bac2 4 chiffres décimaux $R = \lfloor \text{code} \div \sum c_i \rfloor$ code 8321 → $8321 \div (8+3+2+1) = 8321 \div 14 = 594$
bac3 4 chars hex $R = \sum_{i=1}^{4} h_i$ (valeur décimale de chaque digit hex) code A3F7 → $10 + 3 + 15 + 7 = 35$
bac4 4 chars hex $R = \text{code}_{16} \bmod 256$ code A3F7 → $0xA3F7 = 41975$ ; $41975 \bmod 256 = 247$
bac5 4 chars hex $R = \text{octet}\text{haut} \oplus \text{octet}\text{bas}$ code A3F7 → $0xA3 \oplus 0xF7 = 10100011 \oplus 11110111 = 01010100 = 84$

Ces exemples ne sont pas des réponses

Le code agent affiché à un étudiant dépend de son token de session — il est différent à chaque join. Les exemples ci-dessus illustrent la mécanique avec un code choisi arbitrairement. Aucun étudiant n'obtiendra 8321 ou A3F7 sauf coïncidence.

Implémentation Java (référence)

static String computeDynamicAnswer(String agentCode, String level) {
    return switch (level) {
        case "bac1" -> {
            int sum = 0;
            for (char c : agentCode.toCharArray()) sum += (c - '0');
            yield String.valueOf(sum * 10);
        }
        case "bac2" -> {
            int code = Integer.parseInt(agentCode);
            int sum = 0;
            for (char c : agentCode.toCharArray()) sum += (c - '0');
            yield String.valueOf(sum > 0 ? code / sum : code);
        }
        case "bac3" -> {
            int sum = 0;
            for (char c : agentCode.toCharArray()) {
                sum += (c >= '0' && c <= '9') ? (c - '0') : (c - 'A' + 10);
            }
            yield String.valueOf(sum);
        }
        case "bac4" -> {
            int decimalCode = Integer.parseInt(agentCode, 16);
            yield String.valueOf(decimalCode % 256);
        }
        case "bac5" -> {
            int decimalCode = Integer.parseInt(agentCode, 16);
            int high = (decimalCode >> 8) & 0xFF;
            int low  =  decimalCode       & 0xFF;
            yield String.valueOf(high ^ low);
        }
        default -> "0";
    };
}

La méthode est static et package-private — elle n'est appelée que par createSession() dans le même paquet com.perfshop.service.

Du /join à la validation finale

sequenceDiagram
    autonumber
    actor E as Étudiant
    participant CTR as ChaosStudentController
    participant SVC as PedagogiqueSessionService
    participant DB as MySQL
    participant OC as OrderConfirmation.jsx
    participant OVR as PedagogiqueOverlay

    E->>CTR: POST /pedagogique/join
    CTR->>SVC: createSession(alias, "bac3", 3600)
    SVC->>SVC: token = UUID.randomUUID()
    SVC->>SVC: agentCode = "A3F7" (hex, bac3)
    SVC->>SVC: dynamicAnswer = "35" (somme digits hex)
    SVC->>SVC: extractionAnswerHash = sha256("35")
    SVC->>DB: INSERT agent_code, extraction_answer_hash
    CTR-->>E: {token, alias, totalSteps}

    Note over E: Parcours des 19 premières étapes...

    E->>OVR: Étape 19 (add_to_cart, checkout)
    OVR->>CTR: POST /validate step=19
    CTR-->>OVR: {valid:true, nextEnigme: step 20 DYNAMIC}

    E->>OC: Confirmation de commande
    Note over OC: L'étudiant lit son<br/>code agent : "A3F7"

    E->>E: Calcule mentalement<br/>A+3+F+7 = 10+3+15+7 = 35
    E->>OVR: Tape "35", clique Valider
    OVR->>OVR: sha256hex("35") = "efaa08…"
    OVR->>CTR: POST /validate step=20<br/>{answerHash:"efaa08…"}

    CTR->>CTR: enigme.answerHash() == "DYNAMIC"
    CTR->>CTR: expectedHash = session.extractionAnswerHash
    CTR->>CTR: compare → match
    CTR->>SVC: session.step = 20 ; completedAt = now()
    CTR-->>OVR: {valid:true, completed:true, stars, maxStars}

    Note over E: Navigation auto vers /s/{token}

Ce flux illustre la propriété clé du code agent : la bonne réponse en clair n'existe que dans la tête de l'étudiant et dans le stockage éphémère du service au moment du /join. Une fois hashée, elle ne peut plus être retrouvée sans bruteforce.

Persistance du code agent

Le champ agentCode de MutableSession est volatile et son équivalent en base est la colonne agent_code VARCHAR(10) de la table pedagogique_sessions. Un redémarrage du backend n'invalide pas le code — il est rechargé à l'identique dans toMutableSession(), et le extractionAnswerHash stocké reste aligné puisqu'il dépend uniquement du code + niveau.

Le service expose aussi une méthode getAgentCodeForToken(String token) utilisée par OrderController pour afficher le code sur la page de confirmation de commande sans exposer l'intégralité de la session à l'UI e-commerce.

Variations non implémentées

Les formules ci-dessus sont les seules implémentées à date. La méthode computeDynamicAnswer() a un default -> "0" qui servirait de fallback défensif si un niveau inconnu était demandé — en pratique, le contrôleur rejette déjà tout niveau autre que bac1bac5 en amont avec une réponse HTTP 400. Ajouter un niveau bac6 ou une variation régionale nécessiterait deux modifications :

  1. Ajouter un case "bac6" -> … dans computeDynamicAnswer()
  2. Ajouter le choix de format de code agent dans createSession()
  3. Créer la classe compagne PedagogiqueEnigmeBac6 avec son catalogue

La méthode defaultTimerSeconds() devrait aussi être étendue avec la nouvelle durée.


Pages suivantes : ← Système d'énigmes · Système d'étoiles → · Indices →