Skip to content
← Alle Beiträge
· Delta1 Labs Lizenzierung.NETTiefenanalyse

Einen Lizenzschlüssel entwerfen, den Menschen tippen können: Base32, Segmentgruppierung und eine Prüfziffer, die Tippfehler fängt

Eine Keyright-Lizenz besteht aus signierten Bytes, aber was ein Kunde von einer Rechnung abliest, in einen Aktivierungsdialog tippt oder am Telefon diktiert, ist eine Zeichenkette — und Base64 ist eine furchtbare Zeichenkette für diese Aufgabe. Sie unterscheidet Groß- und Kleinschreibung, sie verwendet + und /, die in E-Mails und Shells verstümmelt werden, und ein einziges falsch getipptes Zeichen erzeugt einen Signaturfehler ohne jeden Hinweis darauf, wo der Fehler lag. Dieser Beitrag baut die Transportkodierung, die Keyright tatsächlich verwendet: Crockford-Base32, damit das Alphabet ein Telefonat und eine Umschalttaste übersteht, Segmente aus fünf Zeichen, damit das Auge die Position verfolgen kann, und ein Mod-37-Prüfsymbol, das jeden Einzelzeichenfehler und fast jede Vertauschung fängt, bevor Sie je die teure Signaturprüfung ausführen. Er zeigt das echte Kodieren und Dekodieren in C#, die Normalisierung, die einen schlampig getippten Schlüssel wieder in kanonische Bytes verwandelt, und warum die Prüfziffer ein Ergonomie-Feature ist und niemals ein Sicherheitsmerkmal.

Eine Keyright-Lizenz ist im Kern ein signierter Byte-Blob: eine kanonische Payload — Edition, Berechtigungen, Gerätebindung, Ablaufdatum — plus eine RSA-Signatur darüber. Die Kryptografie ist an anderer Stelle geklärt. In diesem Beitrag geht es um den Teil, der einen Menschen berührt: die Zeichenkette, die ein Kunde von einer Rechnung abliest, in ein Aktivierungsfeld einfügt oder Ihrem Support-Techniker über eine schlechte Telefonleitung vorliest. Diese Zeichenkette ist nicht die Lizenz. Sie ist eine Transportkodierung der Lizenz-Bytes, und die Kodierung, die Sie wählen, entscheidet darüber, ob ein Kunde beim ersten Versuch aktiviert oder ein Ticket eröffnet.

Die reflexartige Wahl ist Base64, weil Convert.ToBase64String das kostenlos liefert. Für alles, was ein Mensch in die Hand nimmt, ist das die falsche Wahl.

// 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

Alles an dieser Ausgabe arbeitet gegen den Menschen, der sie tippen muss. Base64 unterscheidet Groß- und Kleinschreibung, sodass ein Kunde, der die falsche Umschaltstellung erwischt, einen harten Fehler bekommt. Es enthält +, / und =, die E-Mail-Clients, URL-Encoder und Shells liebend gern verstümmeln. Es hat keine Struktur, sodass eine Folge von 180 Zeichen mit dem Auge unmöglich zu verfolgen oder zu diktieren ist, ohne die Stelle zu verlieren. Und wenn ein Zeichen falsch ist, ist die einzige Rückmeldung ein Signaturfehler ganz am Ende — ohne Hinweis darauf, wo der Fehler lag. Wir können es deutlich besser machen, und die Bausteine sind gut verstanden.

Ein Alphabet wählen, das ein Telefonat übersteht

Die erste Entscheidung ist das Alphabet. Die Einschränkungen sind menschlich: Es darf keine Groß-/Kleinschreibung unterscheiden (niemand verfolgt am Telefon die Schreibweise), und es darf keine Zeichen enthalten, die Menschen verwechseln — 1/I/l, 0/O, U/V. Douglas Crockfords Base32-Alphabet ist genau dafür gebaut. Es kodiert fünf Bit pro Symbol über 32 Zeichen und lässt I, L, O und U vollständig weg.

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

Fünf Bit pro Zeichen sind etwas weniger dicht als die sechs von Base64, aber Dichte ist nicht das Ziel — Abschreibbarkeit ist es. Der Gewinn kommt beim Dekodieren, wo dasselbe Design es erlaubt, die vorhersehbaren Fehler zu verzeihen: Ein O, das ein Kunde getippt hat, wird zu 0, ein I oder L zu 1, und Kleinbuchstaben werden zu Großbuchstaben, bevor irgendetwas anderes passiert.

signierte BytesPayload + RSA-Sig.Base32-Symboleje 5 Bit, ohne I L O U+ PrüfsymbolWert mod 375er-Gruppenmit BindestrichenKETP4 - 9ZQ2M - 7X0R8 - WVND5 - 3ohne Groß-/Kleinschreibung · feste Breite · letztes Symbol ist die Prüfung

Die Bytes kodieren

Das Kodieren ist eine Bit-Pumpe: Eingehende Bits werden in einem Puffer gesammelt, und jedes Mal, wenn fünf beisammen sind, wird ein Symbol ausgegeben. Das verarbeitet jede Byte-Länge ohne Padding-Tabellen.

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();
}

Das Prüfsymbol: mod 37

Jetzt der Teil, der seinen Platz verdient. Man behandelt die kodierten Symbole als Ziffern einer großen Zahl zur Basis 32, nimmt diese Zahl modulo 37 und hängt ein Symbol an, das den Rest kodiert. Siebenunddreißig wird gewählt, weil es eine Primzahl und größer als die Basis ist — genau das verleiht dem Prüfsymbol seine Fehlererkennungskraft: Es fängt jede Ersetzung eines einzelnen Symbols und die überwältigende Mehrheit der Vertauschungen benachbarter Symbole — exakt die beiden Fehler, die ein tippender Mensch macht.

Man braucht keinen Big-Integer-Typ, um den Modulus zu berechnen. Eine laufende Faltung berechnet n mod 37 exakt, ein Symbol nach dem anderen, denn (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];
}

Das erweiterte Alphabet fügt fünf Symbole hinzu — *, ~, $, =, U —, die nur an der Prüfposition verwendet werden, sodass ein Rest von 32–36 weiterhin in einem Zeichen ausdrückbar ist. (U kann hier frei wiederverwendet werden, weil es aus dem Datenalphabet ausgeschlossen wurde.)

Gruppieren, und beim Einlesen normalisieren

Die Gruppierung ist reine Darstellung: Die Symbole plus Prüfsymbol werden in Folgen fester Breite zerschnitten und mit Bindestrichen verbunden. Die feste Breite ist es, die Auge und Ohr die Stelle behalten lässt — „Segment drei, zweites Zeichen“.

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"
}

Das Dekodieren muss zuerst einen Menschen rückgängig machen. Vor allem anderen: normalisieren — Leerzeichen und Bindestriche entfernen, in Großbuchstaben wandeln und die verwechselbaren Zeichen abbilden, die das Alphabet absichtlich weggelassen hat. Erst dann prüft man das Prüfsymbol — und nur wenn das besteht, rekonstruiert man die Bytes und übergibt sie der Signaturprüfung.

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 ist die umgekehrte Bit-Pumpe — fünf Bit pro Symbol sammeln, alle acht Bit ein Byte ausgeben — und wird hier nur aus Platzgründen weggelassen. Die wichtige Reihenfolge steckt in TryParse: normalisieren, dann Prüfsymbol, dann Bytes, dann Signatur. Das Prüfsymbol gibt dem Kunden eine präzise, sofortige Fehlermeldung für den Fehler, den er tatsächlich gemacht hat; die Signatur, die danach auf den dekodierten Bytes läuft, ist das Einzige, das entscheidet, ob die Lizenz echt ist.

Die Linie, die Sie nicht überschreiten dürfen

Es lohnt sich, bei dieser Grenze deutlich zu werden, denn sie ist die eine Stelle, an der dieses Design in der Praxis schiefgeht. Das Prüfsymbol wird von jedem berechnet, über den Schlüssel selbst, ohne jedes Geheimnis. Ein Angreifer, der einen Schlüssel bearbeitet, kann trivial ein gültiges Prüfsymbol für jede beliebige von ihm erzeugte Zeichenkette neu berechnen — das Prüfsymbol beweist also nichts über Echtheit. Es ist ein Ergonomie-Feature: Es lokalisiert Tippfehler und lässt Sie mit einer nützlichen Meldung früh scheitern, bevor Sie für Public-Key-Kryptografie bezahlen.

Prüfsymbol — kostenlos, kein Geheimnisfängt Tippfehler · präziser Fehler · läuft zuerstRSA-Signatur — braucht öffentl. Schlüsselentscheidet Echtheit · läuft als Zweitesbestandenein gefälschter Schlüssel kann die Prüfung bestehen und trotzdem an der Signatur scheitern

Die Arbeitsteilung ist der ganze Punkt. Die eine Stufe ist für den ehrlichen Kunden, der sich bei einem Zeichen vertippt hat; die andere für den unehrlichen, der die Zeichenkette bearbeitet hat. Bauen Sie beide, führen Sie sie in dieser Reihenfolge aus und lesen Sie jeden Anspruch — Edition, Berechtigungen, Ablaufdatum — aus den signaturgeprüften Bytes, niemals aus den Zeichen selbst. Der sichtbare Schlüssel sollte eine flache, bedeutungslos aussehende Folge von Segmenten bleiben, deren einzige Aufgabe es ist, intakt von der Tastatur eines Menschen bei Ihrem TryParse anzukommen.

Nebula.NET testen

Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.