Codificación en base64
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:
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.
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.
Los valores irán, por tanto, entre el valor b00000 y b11111 equivalentes al 0 y al 63 respectivamente.
En hexadecimal sería 0x00 ó 0x0000 respectivamente.
Comentarios