Skip to content
← Todas las publicaciones
· Delta1 Labs Licenciamiento.NETAnálisis a fondo

Diseñar una clave de licencia que un humano pueda teclear: base32, agrupación en segmentos y un dígito de control que atrapa los errores de tecleo

Una licencia de Keyright son bytes firmados, pero lo que un cliente lee en una factura, teclea en un diálogo de activación o dicta por teléfono es una cadena, y base64 es una cadena pésima para ese trabajo. Distingue mayúsculas de minúsculas, usa + y / que se corrompen en el correo y en los shells, y un solo carácter mal tecleado produce un fallo de firma sin ninguna pista de dónde estuvo el error. Esta entrada construye la codificación de transporte que Keyright usa realmente: base32 de Crockford para que el alfabeto sobreviva a una llamada telefónica y a la tecla de mayúsculas, segmentos de cinco caracteres para que el ojo pueda seguir la posición, y un símbolo de control módulo 37 que atrapa todos los errores de un solo carácter y casi todas las transposiciones antes de ejecutar siquiera la costosa verificación de firma. Muestra la codificación y decodificación reales en C#, la normalización que convierte una clave tecleada con descuido de vuelta en bytes canónicos, y por qué el dígito de control es una característica de ergonomía y nunca de seguridad.

Una licencia de Keyright es, en el fondo, un blob de bytes firmado: un payload canónico (edición, entitlements, bloqueo por equipo, caducidad) más una firma RSA sobre él. La criptografía está resuelta en otro sitio. Esta entrada trata de la parte que toca a un humano: la cadena que ese cliente lee en una factura, pega en un cuadro de activación o le lee a tu ingeniero de soporte por una línea telefónica con mala cobertura. Esa cadena no es la licencia. Es una codificación de transporte de los bytes de la licencia, y la codificación que elijas decide si un cliente activa a la primera o abre un ticket.

La elección refleja es base64, porque es lo que Convert.ToBase64String te da gratis. Es la elección equivocada para cualquier cosa que maneje una persona.

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

Todo en esa salida lucha contra el humano que tiene que teclearla. Base64 distingue mayúsculas de minúsculas, así que un cliente que se equivoca con la tecla de mayúsculas obtiene un fallo irrecuperable. Contiene +, / y =, que a los clientes de correo, a los codificadores de URL y a los shells les encanta corromper. No tiene estructura, de modo que una secuencia de 180 caracteres es imposible de seguir con la vista o de dictar sin perder el hilo. Y cuando un carácter está mal, la única respuesta es un fallo de firma al final del todo, sin ninguna pista de dónde estuvo el error. Podemos hacerlo mucho mejor, y las piezas son bien conocidas.

Elige un alfabeto que sobreviva a una llamada telefónica

La primera decisión es el alfabeto. Las restricciones son humanas: debe ser insensible a mayúsculas (nadie lleva la cuenta de las mayúsculas por teléfono) y no debe contener caracteres que la gente confunda: 1/I/l, 0/O, U/V. El alfabeto base32 de Douglas Crockford está construido exactamente para esto. Codifica cinco bits por símbolo sobre 32 caracteres, eliminando por completo I, L, O y U.

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

Cinco bits por carácter es ligeramente menos denso que los seis de base64, pero la densidad no es el objetivo; la transcribibilidad sí. La recompensa llega al decodificar, donde el mismo diseño te permite perdonar los errores previsibles: una O que el cliente tecleó se convierte en 0, una I o una L se convierten en 1, y las minúsculas se pliegan a mayúsculas antes de que ocurra nada más.

bytes firmadospayload + firma RSAsímbolos base325 bits cada uno, sin I L O U+ símbolo controlvalor mod 37agrupar de 5con guionesKETP4 - 9ZQ2M - 7X0R8 - WVND5 - 3insensible a mayúsculas · ancho fijo · el último símbolo es el control

Codificar los bytes

La codificación es una bomba de bits: acumulas los bits entrantes en un búfer y emites un símbolo cada vez que tienes cinco. Esto maneja cualquier longitud de bytes sin tablas de relleno.

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

El símbolo de control: módulo 37

Ahora la parte que se gana el sueldo. Trata los símbolos codificados como los dígitos de un gran número en base 32, calcula ese número módulo 37 y añade un símbolo que codifique el resto. Se elige treinta y siete porque es primo y mayor que la base, y eso es lo que da al símbolo de control su poder de detección de errores: atrapa todas las sustituciones de un solo símbolo y la inmensa mayoría de las transposiciones adyacentes, precisamente los dos errores que comete una persona al teclear.

No necesitas un tipo de enteros grandes para calcular el módulo. Un plegado acumulativo calcula n mod 37 de forma exacta, símbolo a símbolo, porque (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];
}

El alfabeto extendido añade cinco símbolos (*, ~, $, =, U) que se usan solo en la posición de control, de modo que un resto de 32 a 36 sigue siendo expresable en un único carácter. (U queda libre para reutilizarse aquí porque fue excluida del alfabeto de datos.)

Agrúpala, y normaliza al recibirla de vuelta

La agrupación es pura presentación: corta los símbolos más el control en tramos de ancho fijo y únelos con guiones. El ancho fijo es lo que permite al ojo y al oído no perder el sitio: «segmento tres, segundo carácter».

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

La decodificación tiene que deshacer primero lo que hizo un humano. Antes que nada, normaliza: elimina espacios y guiones, pasa a mayúsculas y mapea los caracteres confundibles que el alfabeto dejó fuera a propósito. Solo entonces validas el símbolo de control, y solo si eso pasa reconstruyes los bytes y se los entregas a la verificación de firma.

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 es la bomba de bits inversa (acumula cinco bits por símbolo, emite un byte cada ocho bits) y se omite aquí solo por brevedad. La secuencia importante está en TryParse: normalizar, luego símbolo de control, luego bytes, luego firma. El símbolo de control le da al cliente un error preciso e instantáneo por el error que de verdad cometió; la firma, que se ejecuta después sobre los bytes decodificados, es lo único que decide si la licencia es genuina.

La línea que no debes cruzar

Vale la pena ser contundente sobre el límite, porque es el único punto en el que este diseño sale mal en la práctica. El símbolo de control lo calcula cualquiera, sobre la propia clave, sin ningún secreto de por medio. Un atacante que edite una clave puede recalcular trivialmente un símbolo de control válido para la cadena que haya producido, así que el símbolo de control no demuestra nada sobre la autenticidad. Es una característica de ergonomía: localiza los errores de tecleo y te permite fallar rápido con un mensaje útil antes de pagar por criptografía de clave pública.

símbolo de control — gratis, sin secretoatrapa erratas · error preciso · va primerofirma RSA — requiere clave públicadecide la autenticidad · va segundapasauna clave falsificada puede pasar el control y aun así fallar la firma

La división del trabajo es todo el sentido del diseño. Una etapa es para el cliente honesto que se equivocó al teclear un carácter; la otra es para el deshonesto que editó la cadena. Construye ambas, ejecútalas en ese orden y lee cada claim (edición, entitlements, caducidad) de los bytes con firma verificada, nunca de los propios caracteres. La clave visible debe seguir siendo una secuencia plana de segmentos de aspecto carente de significado cuyo único trabajo es llegar, intacta, del teclado de un humano a tu TryParse.

Prueba Nebula.NET

Endurece tu código .NET en minutos — empieza con la edición gratuita.