Skip to content
← Tous les articles
· Delta1 Labs Licences.NETAnalyse approfondie

Concevoir une clé de licence qu'un humain peut taper : base32, segments groupés et un symbole de contrôle qui attrape les fautes de frappe

Une licence Keyright, ce sont des octets signés, mais ce qu'un client lit sur une facture, tape dans une boîte de dialogue d'activation ou dicte au téléphone est une chaîne de caractères — et base64 est une chaîne exécrable pour ce travail. Elle est sensible à la casse, elle utilise + et / que les clients mail et les shells massacrent, et un seul caractère mal tapé produit un échec de signature sans la moindre indication de l'endroit de l'erreur. Cet article construit l'encodage de transport que Keyright utilise réellement : du base32 Crockford pour que l'alphabet survive à un appel téléphonique et à une touche Maj, des segments de cinq caractères pour que l'œil puisse suivre sa position, et un symbole de contrôle modulo 37 qui attrape chaque erreur d'un caractère et presque chaque transposition avant même de lancer la coûteuse vérification de signature. Il montre le vrai code d'encodage et de décodage en C#, la normalisation qui ramène une clé tapée n'importe comment vers des octets canoniques, et pourquoi le symbole de contrôle est une fonctionnalité d'ergonomie et jamais de sécurité.

Une licence Keyright est, au fond, un blob d’octets signé : une charge utile canonique — édition, droits, verrouillage machine, expiration — plus une signature RSA par-dessus. La cryptographie est traitée ailleurs. Cet article porte sur la partie qui touche un humain : la chaîne que le client lit sur une facture, colle dans une boîte d’activation, ou lit à votre ingénieur support sur une mauvaise ligne téléphonique. Cette chaîne n’est pas la licence. C’est un encodage de transport des octets de la licence, et l’encodage que vous choisissez décide si un client active du premier coup ou ouvre un ticket.

Le choix réflexe est base64, parce que c’est ce que Convert.ToBase64String vous donne gratuitement. C’est le mauvais choix pour tout ce qu’une personne manipule.

// The signed license bytes — payload + RSA signature.
byte[] license = SignLicense(payload, privateKey);   // ~128+ bytes

// The tempting, wrong transport encoding:
string key = Convert.ToBase64String(license);
// "Tw4bQ8K2...e+/9Zg==" — case-sensitive, has + and /, no grouping, no typo check

Tout dans cette sortie se bat contre l’humain qui doit la taper. Base64 est sensible à la casse, donc un client qui se trompe de touche Maj obtient un échec net. Il contient +, / et =, que les clients mail, les encodeurs d’URL et les shells adorent déformer. Il n’a aucune structure, donc une suite de 180 caractères est impossible à suivre à l’œil ou à dicter sans perdre sa place. Et quand un caractère est faux, le seul retour est un échec de signature tout à la fin — sans indication de l’endroit de l’erreur. On peut faire bien mieux, et les pièces sont bien connues.

Choisir un alphabet qui survit à un appel téléphonique

La première décision est l’alphabet. Les contraintes sont humaines : il doit être insensible à la casse (personne ne suit la casse au téléphone), et il ne doit pas contenir de caractères que les gens confondent — 1/I/l, 0/O, U/V. L’alphabet base32 de Douglas Crockford est construit exactement pour cela. Il encode cinq bits par symbole sur 32 caractères, en supprimant entièrement I, L, O et U.

// Crockford base32: 32 symbols, no I, L, O, U.
private const string Alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";

Cinq bits par caractère est légèrement moins dense que les six de base64, mais la densité n’est pas le but — la transcriptibilité l’est. Le bénéfice arrive au décodage, où la même conception vous permet de pardonner les erreurs prévisibles : un O tapé par un client devient 0, un I ou un L devient 1, et les minuscules sont repliées en majuscules avant toute autre chose.

octets signéspayload + sig RSAsymboles base325 bits chacun, sans I L O U+ symbole contrôlevaleur mod 37groupes de 5avec tiretsKETP4 - 9ZQ2M - 7X0R8 - WVND5 - 3insensible à la casse · largeur fixe · le dernier symbole est le contrôle

Encoder les octets

L’encodage est une pompe à bits : on accumule les bits entrants dans un tampon et on émet un symbole chaque fois qu’on en a cinq. Cela gère n’importe quelle longueur d’octets sans tables de remplissage.

public static string EncodeBase32(byte[] data)
{
    var sb = new StringBuilder((data.Length * 8 + 4) / 5);
    int buffer = 0, bitsInBuffer = 0;
    foreach (byte b in data)
    {
        buffer = (buffer << 8) | b;
        bitsInBuffer += 8;
        while (bitsInBuffer >= 5)
        {
            bitsInBuffer -= 5;
            int index = (buffer >> bitsInBuffer) & 0x1F;
            sb.Append(Alphabet[index]);
        }
    }
    if (bitsInBuffer > 0)              // flush the final partial group
    {
        int index = (buffer << (5 - bitsInBuffer)) & 0x1F;
        sb.Append(Alphabet[index]);
    }
    return sb.ToString();
}

Le symbole de contrôle : modulo 37

Voici maintenant la partie qui mérite sa place. On traite les symboles encodés comme les chiffres d’un grand nombre en base 32, on prend ce nombre modulo 37, et on ajoute un symbole qui encode le reste. Trente-sept est choisi parce qu’il est premier et plus grand que la base, ce qui donne au symbole de contrôle son pouvoir de détection d’erreurs : il attrape chaque substitution d’un seul symbole, et l’écrasante majorité des transpositions adjacentes — précisément les deux erreurs qu’une personne commet en tapant.

Vous n’avez pas besoin d’un type grand entier pour calculer le modulo. Un repli séquentiel calcule n mod 37 exactement, un symbole à la fois, parce que (a·32 + b) mod 37 = ((a mod 37)·32 + b) mod 37.

// 37 symbols: the 32 base symbols plus 5 extras for remainders 32..36.
private const string CheckAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U";

public static char ComputeCheckSymbol(string base32)
{
    int acc = 0;
    foreach (char c in base32)
        acc = (acc * 32 + Alphabet.IndexOf(c)) % 37;   // exact n mod 37, no BigInteger
    return CheckAlphabet[acc];
}

L’alphabet étendu ajoute cinq symboles — *, ~, $, =, U — utilisés uniquement pour la position de contrôle, afin qu’un reste de 32 à 36 reste exprimable en un seul caractère. (U peut être réutilisé ici parce qu’il a été exclu de l’alphabet des données.)

Grouper, et normaliser au retour

Le regroupement est de la pure présentation : on découpe les symboles plus le contrôle en suites de largeur fixe et on les joint avec des tirets. La largeur fixe est ce qui permet à l’œil et à l’oreille de garder leur place — « troisième segment, deuxième caractère ».

public static string Format(byte[] license)
{
    string body = EncodeBase32(license);
    string full = body + ComputeCheckSymbol(body);
    var sb = new StringBuilder(full.Length + full.Length / 5);
    for (int i = 0; i < full.Length; i++)
    {
        if (i > 0 && i % 5 == 0) sb.Append('-');
        sb.Append(full[i]);
    }
    return sb.ToString();   // "KETP4-9ZQ2M-7X0R8-WVND5-3"
}

Le décodage doit d’abord défaire ce qu’un humain a fait. Avant toute chose, normaliser : retirer les espaces et les tirets, passer en majuscules, et remplacer les caractères confondables que l’alphabet a délibérément exclus. Ensuite seulement on valide le symbole de contrôle — et seulement si cela passe, on reconstruit les octets et on les transmet à la vérification de signature.

public static bool TryParse(string typed, out byte[] license)
{
    license = Array.Empty<byte>();

    // 1. Normalize what a human typed back toward the canonical form.
    string s = typed.Trim().ToUpperInvariant()
                    .Replace("-", "").Replace(" ", "")
                    .Replace('O', '0').Replace('I', '1').Replace('L', '1');
    if (s.Length < 2) return false;

    // 2. Split body from the trailing check symbol and re-verify it.
    string body = s[..^1];
    char check = s[^1];
    if (ComputeCheckSymbol(body) != check)
        return false;                      // localized: "there is a typo"

    // 3. Only now reconstruct bytes. Signature verification happens after this.
    license = DecodeBase32(body);
    return true;
}

DecodeBase32 est la pompe à bits inverse — accumuler cinq bits par symbole, émettre un octet tous les huit bits — et n’est omise ici que pour la longueur. L’ordre important est dans TryParse : normaliser, puis symbole de contrôle, puis octets, puis signature. Le symbole de contrôle donne au client une erreur précise et instantanée pour l’erreur qu’il a réellement commise ; la signature, qui s’exécute ensuite sur les octets décodés, est la seule chose qui décide si la licence est authentique.

La ligne à ne pas franchir

Il vaut la peine d’être direct sur la frontière, parce que c’est le seul endroit où cette conception tourne mal en pratique. Le symbole de contrôle est calculé par n’importe qui, sur la clé elle-même, sans aucun secret. Un attaquant qui modifie une clé peut trivialement recalculer un symbole de contrôle valide pour n’importe quelle chaîne qu’il produit — le symbole de contrôle ne prouve donc rien quant à l’authenticité. C’est une fonctionnalité d’ergonomie : elle localise les fautes de frappe et vous laisse échouer vite avec un message utile avant de payer pour de la cryptographie à clé publique.

symbole de contrôle — gratuit, sans secretattrape les fautes · erreur précise · en premiersignature RSA — clé publique requisedécide de l'authenticité · en secondpasseune clé forgée peut passer le contrôle et échouer à la signature

La répartition des rôles est tout l’enjeu. Une étape est pour le client honnête qui a mal tapé un caractère ; l’autre est pour le malhonnête qui a modifié la chaîne. Construisez les deux, exécutez-les dans cet ordre, et lisez chaque claim — édition, droits, expiration — depuis les octets vérifiés par la signature, jamais depuis les caractères eux-mêmes. La clé visible doit rester une suite plate de segments d’apparence dénuée de sens, dont le seul travail est d’arriver, intacte, du clavier d’un humain jusqu’à votre TryParse.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.