Notxor tiene un blog

Defenestrando la vida

Codificación en base64

2022-08-03

Una de las cosas que siempre me han llamado la atención es la capacidad de convertir datos binarios a cadenas de texto. Lo vemos constantemente en el correo electrónico1, o en cómo se guardan las claves de encriptado. Ha habido varios sistemas para convertir datos binarios en cadenas. La idea es poder trasmitir esa información a través de la red sin las dificultades que plantearía el enviar datos binarios que, seguramente, contendrán valores especiales para la conexión, como el de fin de fichero o fin de transmisión. Eso provocaría una interrupción y no se finalizaría el traspaso de los datos de un sitio a otro.

El asunto es que algunas veces, incluso el texto plano puede contener datos binarios. Recordemos que los sistemas se inventaron para transmitir ASCII puro y el UTF se les puede indigestar. Por ejemplo, si queremos transmitir el siguiente texto:

Esto es un texto de prueba. No tiene ninguna importancia lo que ponga, pero necesito escribir algunos caracteres especiales. Pueden ser los caracteres acentuados y letras como la ñ (Ñ), o la ç (Ç). O las áéíóú àèìòù äëïöü.

Si quiero enviar esto directamente por una conexión es posible que tenga algún signo indigesto para el socket. Para convertirlo en algo más digerible tengo dos opciones. La primera, y más a mano, es seleccionar el texto y llamar a la función de Emacs base64-encode-region y me devolverá una cadena como la siguiente:

RXN0byBlcyB1biB0ZXh0byBkZSBwcnVlYmEuIE5vIHRpZW5lIG5pbmd1bmEgaW1wb3J0YW5jaWEg
bG8gcXVlCnBvbmdhLCBwZXJvIG5lY2VzaXRvIGVzY3JpYmlyIGFsZ3Vub3MgY2FyYWN0ZXJlcyBl
c3BlY2lhbGVzLiBQdWVkZW4Kc2VyIGxvcyBjYXJhY3RlcmVzIGFjZW50dWFkb3MgeSBsZXRyYXMg
Y29tbyBsYSDxICjRKSwgbyBsYSDnICjHKS4gTwpsYXMg4ent8/og4Ojs8vkg5Ovv9vwu

Éste método tiene un inconveniente y es que los caracteres multibyte que se manejan en unicode no los traga bien, se le indigestan. Si intentamos utilizar ese tipo de caracteres, por ejemplo los del Esperanto contenidos en eĥoŝanĝo ĉiuĵaŭde2, no podría con ellos.

La otra alternativa es guardar el texto en un fichero y llamar al comando del sistema base64, ya que lo provee el propio sistema operativo, tiene más cuidado con los caracteres unicode. Para la demostración he añadido la expresión anterior al final del texto, por lo que quedará como:

Esto es un texto de prueba. No tiene ninguna importancia lo que ponga, pero necesito escribir algunos caracteres especiales. Pueden ser los caracteres acentuados y letras como la ñ (Ñ), o la ç (Ç). O las áéíóú àèìòù äëïöü. «Eĥoŝanĝo ĉiuĵaŭde»

La operación es sencilla:

$ base64 texto-plano.txt 
RXN0byBlcyB1biB0ZXh0byBkZSBwcnVlYmEuIE5vIHRpZW5lIG5pbmd1bmEgaW1wb3J0YW5jaWEg
bG8gcXVlCnBvbmdhLCBwZXJvIG5lY2VzaXRvIGVzY3JpYmlyIGFsZ3Vub3MgY2FyYWN0ZXJlcyBl
c3BlY2lhbGVzLiBQdWVkZW4Kc2VyIGxvcyBjYXJhY3RlcmVzIGFjZW50dWFkb3MgeSBsZXRyYXMg
Y29tbyBsYSDDsSAow5EpLCBvIGxhIMOnICjDhykuIE8KbGFzIMOhw6nDrcOzw7ogw6DDqMOsw7LD
uSDDpMOrw6/DtsO8LiDCq0XEpW/FnWFuxJ1vIMSJaXXEtWHFrWRlwrsK

El comando es sencillo, le pasamos un nombre de fichero y devuelve una cadena con el contenido convertido a base64. Si quieres guardarlo en otro archivo tienes que aplicar un poco del conocimiento fontanero propio de Unix/Linux.

¿Cómo funciona base64?

En el trasfondo de base64 está la conversión de bytes de 8 bits en grupos de 6 bits de tal manera que se pueden utilizar los caracteres de [A-Z][a-z][0-9][+/], es decir:

ABCDEFGHIJKLMNOPQRSTUWXYZabcdefghijklmnopqrstuwxyz0123456789+/

Es decir, tomando un valor de 6 bits3 como índice de la cadena anterior.

Hay que señalar que hay varios sistemas base64. La totalidad, o casi la totalidad coinciden en la utilización del abecedario en mayúsculas, en minúsculas y los números. Para completar el número de 64 se deben emplear otros dos caracteres. La mayoría de los sistemas utilizan el + y la barra /, pero puedes encontrar que utilicen otros.

También se utilizan otros caracteres especiales, como por ejemplo el signo =, que se utiliza para rellenar espacios si la conversión no es congruente al pasar de 8 a 6 bits y debemos añadir bits. Los bits que se añaden al final son ceros, pero marcamos en la cadena final cuántos ceros hemos añadido con uno o dos signos de igual:

- Hola mundo -->  SG9sYSBtdW5kbw==
- Hola mund  -->  SG9sYSBtdW5k
- Hola mun   -->  SG9sYSBtdW4=

Otro aspecto a tener en cuenta son los rellenos. En el código se hacen añadiendo ceros al final4. Por ejemplo, la cadena Hola podemos descomponerla en sus correspondientes bits:

Hola         --> 01001000011011110110110001100001
base64(Hola) --> SG9sYQ==
Hol          --> 010010000110111101101100
base64(Hol)  --> SG9s
Ho           --> 0100100001101111
base64(Ho)   --> SG8=
H            --> 01001000
base64(H)    --> SA==

El proceso consiste en deshacer los octetos y convertirlos en sextetos, tal que así:

   48 (H)   6f (o)   6c (l)   61 (a)
  01001000 01101111 01101100 01100001

  010010-000110-111101-101100-011000-01(00 00)
     S      G      9      s      Y    Q =  =
--------------------------------------------------
   48 (H)   6f (o)   6c (l)
  01001000 01101111 01101100

  010010-000110-111101-101100
     S      G      9      s
--------------------------------------------------
   48 (H)   6f (o)
  01001000 01101111

  010010-000110-1111(00)
     S      G     8  =
--------------------------------------------------
   48 (H)
  01001000

  010010-00(00 00)
     S   A  =  =

En algunos sistemas es obligatorio marcar los ceros añadidos (2 ó 4) con uno o dos caracteres = respectivamente al final de la cadena generada. Sin embargo, es una pista importante a la hora de decodificar la cadena para devolverlo a binario, saber cuántos ceros debemos quitar del final.

La tabla de conversión sería la siguiente:

A 000000 B 000001 C 000010 D 000011 E 000100 F 000101 G 000110 H 000111
I 001000 J 001001 K 001010 L 001011 M 001100 N 001101 O 001110 P 001111
Q 010000 R 010001 S 010010 T 010011 U 010100 V 010101 W 010110 X 010111
Y 011000 Z 011001 a 011010 b 011011 c 011100 d 011101 e 011110 f 011111
g 100000 h 100001 i 100010 j 100011 k 100100 l 100101 m 100110 n 100111
o 101000 p 101001 q 101010 r 101011 s 101100 t 101101 u 101110 v 101111
w 110000 x 110001 y 110010 z 110011 0 110100 1 110101 2 110110 3 110111
4 111000 5 111001 6 111010 7 111011 8 111100 9 111101 + 111110 / 111111

Otras consideraciones

Podemos encontrarnos también con otros caracteres que no son los especificados por la tabla anterior. Por ejemplo, en algunos sistemas es obligatorio dividir la cadena en líneas insertando un par CL+CF. El largo de las líneas puede variar según el sistema de codificación, en algún de esos sistemas el largo es 64 en general, pudiendo ser menor la última línea. Pero en la mayoría de sistemas sólo se especifica que no debe ser más largo de 76 columnas y, por último, en algunos otros no se especifica nada con respecto separar la cadena en líneas.

Otro carácter que podemos encontrarnos es el *. Se utiliza para separar el texto cifrado del que está en claro, aunque esté también codificado en base64.

Aunque para nosotros las cadenas «Hola mundo!» y «¡Hola mundo!» sean muy similares, por el pequeño cambio de utilizar la exclamación del principio, los resultados pueden ser muy dispares:

 Hola mundo! --> SG9sYSBtdW5kbyE=
¡Hola mundo! --> oUhvbGEgbXVuZG8h

Conclusiones

base64 es un sistema de codificación muy interesante para transmitir información binaria a través de la red. Estamos muy acostumbrados a verlo, tanto, que quizá no reparamos en él. Lo tenemos asociado a claves de encriptado porque es el formato en el que solemos ver la información de esas claves. En realidad, no es un formato para ocultar nada, sino para transmitirlo de forma segura.

Si os preguntáis si he hecho pruebas... pues sí, las he hecho. Si alguno queréis trastear un poco os pongo aquí el código fuente en Tcl:

package provide b64 1.0

namespace eval ::b64 {
    namespace export encode decode separar-lineas juntar-lineas
}

#
# Codifica la `cadena`
#
proc b64::encode {cadena} {
    set cad [encoding convertto utf-8 $cadena]
    binary scan $cad B* bits
    switch [expr {[string length $bits] % 6}] {
        0 {set final {}}
        2 {append bits 0000; set final ==}
        4 {append bits 00; set final =}
    }
    set devolver [string map {
        000000 A 000001 B 000010 C 000011 D 000100 E 000101 F 000110 G 000111 H
        001000 I 001001 J 001010 K 001011 L 001100 M 001101 N 001110 O 001111 P
        010000 Q 010001 R 010010 S 010011 T 010100 U 010101 V 010110 W 010111 X
        011000 Y 011001 Z 011010 a 011011 b 011100 c 011101 d 011110 e 011111 f
        100000 g 100001 h 100010 i 100011 j 100100 k 100101 l 100110 m 100111 n
        101000 o 101001 p 101010 q 101011 r 101100 s 101101 t 101110 u 101111 v
        110000 w 110001 x 110010 y 110011 z 110100 0 110101 1 110110 2 110111 3
        111000 4 111001 5 111010 6 111011 7 111100 8 111101 9 111110 + 111111 /
    } $bits]$final
    return [separar-lineas $devolver]
}

#
# Decodifica la `cadena`
#
proc b64::decode {cadena} {
    set cadena [juntar-lineas $cadena]
    set num [string trimright $cadena =]
    set str [string map {
        A 000000 B 000001 C 000010 D 000011 E 000100 F 000101 G 000110 H 000111
        I 001000 J 001001 K 001010 L 001011 M 001100 N 001101 O 001110 P 001111
        Q 010000 R 010001 S 010010 T 010011 U 010100 V 010101 W 010110 X 010111
        Y 011000 Z 011001 a 011010 b 011011 c 011100 d 011101 e 011110 f 011111
        g 100000 h 100001 i 100010 j 100011 k 100100 l 100101 m 100110 n 100111
        o 101000 p 101001 q 101010 r 101011 s 101100 t 101101 u 101110 v 101111
        w 110000 x 110001 y 110010 z 110011 0 110100 1 110101 2 110110 3 110111
        4 111000 5 111001 6 111010 7 111011 8 111100 9 111101 + 111110 / 111111
    } $num]
    switch [expr {[string length $cadena]-[string length $num]}] {
        0 {}
        1 {set str [string range $str 0 {end-2}]}
        2 {set str [string range $str 0 {end-4}]}
    }
    return [encoding convertfrom utf-8 [binary format B* $str]]
}

#
# Separa la `cadena` en líneas de `ancho`
#
proc b64::separar-lineas {cadena {ancho 76}} {
    set lista {}
    while {[string length $cadena] > $ancho} {
        lappend lista [string range $cadena 0 [expr {$ancho - 1}]]
        set cadena [string range $cadena [expr {$ancho}] [expr {[string length $cadena] - 1}]]
    }
    if {[string length $cadena] > 0} {lappend lista $cadena}
    return [join $lista "\n"]
}

#
# Elimina los saltos de línea de `cadena`
#
proc b64::juntar-lineas {cadena} {
    return [string map {"\n" {}} $cadena]
}

#
# Algunas pruebas de conversión
#
proc prueba {} {
    set cadena "¡Hola mundo!"
    set bcadena [b64::encode $cadena]
    set ncadena [b64::decode $bcadena]

    set cadena1 "eĥoŝanĝo ĉiuĵaŭde"
    set bcadena1 [b64::encode $cadena1]
    set ncadena1 [b64::decode $bcadena1]

    set cadena2 "Hola"
    set bcadena2 [b64::encode $cadena2]
    set ncadena2 [b64::decode $bcadena2]

    puts "$cadena: $ncadena | $bcadena"
    puts "$cadena1: $ncadena1 | $bcadena1"
    puts "$cadena2: $ncadena2 | $bcadena2"

    set c [b64::juntar-lineas "RXN0byBlcyB1biB0ZXh0byBkZSBwcnVlYmEuIE5vIHRpZW5lIG5pbmd1bmEgaW1wb3J0YW5jaWEg
bG8gcXVlCnBvbmdhLCBwZXJvIG5lY2VzaXRvIGVzY3JpYmlyIGFsZ3Vub3MgY2FyYWN0ZXJlcyBl
c3BlY2lhbGVzLiBQdWVkZW4Kc2VyIGxvcyBjYXJhY3RlcmVzIGFjZW50dWFkb3MgeSBsZXRyYXMg
Y29tbyBsYSDDsSAow5EpLCBvIGxhIMOnICjDhykuIE8KbGFzIMOhw6nDrcOzw7ogw6DDqMOsw7LD
uSDDpMOrw6/DtsO8LiDCq0XEpW/FnWFuxJ1vIMSJaXXEtWHFrWRlwrsK"]
    puts $c

    puts [b64::decode $c]

    set c [b64::separar-lineas "RXN0byBlcyB1biB0ZXh0byBkZSBwcnVlYmEuIE5vIHRpZW5lIG5pbmd1bmEgaW1wb3J0YW5jaWEgbG8gcXVlCnBvbmdhLCBwZXJvIG5lY2VzaXRvIGVzY3JpYmlyIGFsZ3Vub3MgY2FyYWN0ZXJlcyBlc3BlY2lhbGVzLiBQdWVkZW4Kc2VyIGxvcyBjYXJhY3RlcmVzIGFjZW50dWFkb3MgeSBsZXRyYXMgY29tbyBsYSDDsSAow5EpLCBvIGxhIMOnICjDhykuIE8KbGFzIMOhw6nDrcOzw7ogw6DDqMOsw7LDuSDDpMOrw6/DtsO8LiDCq0XEpW/FnWFuxJ1vIMSJaXXEtWHFrWRlwrsK" 72]
    puts $c
    puts [b64::decode $c]
}

#
# Si se llama desde línea de comandos con la opción `-test` hace las pruebas
#
if {$argc > 0 & [lindex $argv 0] == "-test" } {prueba}

Footnotes:

1

Un medio con un protocolo que se inventó para enviar mensajes de texto y al que se pueden añadir datos binarios como imágenes u otros archivos.

2

Es una expresión sin sentido. Se podría traducir como cambio de eco todos los jueves, pero tiene la particularidad de contener todos los caracteres especiales del idioma condensados en una sencilla expresión de dos palabras.

3

Los valores irán, por tanto, entre el valor b00000 y b11111 equivalentes al 0 y al 63 respectivamente.

4

En hexadecimal sería 0x00 ó 0x0000 respectivamente.

Categoría: criptografia

Comentarios

Debido a algunos ataques mailintencionados a través de la herramienta de comentarios, he decidido no proporcionar dicha opción en el Blog. Si alguien quiere comentar algo, me puede encontrar en Mastodon y en Diaspora con el nick de Notxor.

También se ha abierto un grupo en Telegram, para usuarios de esa red.

Disculpen las molestias.