¿Tiene que ocuparse del formato Base64? Entonces esta página es perfecta para Ud. Utilice nuestra práctica herramienta en línea para codificar o decodificar sus datos.

Decodificación Base64 en R: una guía completa

Una cadena aterriza en tu sesión de R. Tiene pinta de una sopa de letras barajadas: letras, dígitos, el ocasional signo más o barra, y un par de signos de igual colgando del final. La persona que te la envió jura que en su día fue una frase perfectamente corriente, un JPEG o un documento JSON. Esa cadena es Base64, y esta página es la receta para convertirla de nuevo en lo que era.

Un repaso rápido, porque la página de inicio de este sitio explica el formato a fondo: Base64 escribe tres bytes de entrada como cuatro caracteres tomados de un alfabeto de 64 símbolos, y uno o dos caracteres = al final marcan dónde acabó el dato real. Ese intercambio de cuatro por tres es la razón de que el texto codificado sea un tercio más grande que el original, y decodificar es simplemente correr el intercambio en reversa. Con la forma del problema en la cabeza, abramos algunos sobres.

Aquí está el giro que hace que R sea un poco diferente de muchos otros lenguajes: R base no tiene Base64 de ningún tipo. No hay ningún base64_decode() escondido en un paquete base, y no hay ningún builtin de una línea al que puedas recurrir. Tienes que traer un paquete. La buena noticia es que el ecosistema ofrece varios, cada uno con su propia personalidad, y al final de este artículo sabrás exactamente cuál coger y a cuál desconfiar.

Los decodificadores de un vistazo

Cinco paquetes hacen el trabajo pesado, y se dividen en dos grandes bandos: los permisivos, que se encogen de hombros ante la entrada sucia, y los estrictos, que tratan el RFC como un contrato. Aquí está el reparto, vigente a fecha de 2026:

Paquete Versión (2026) Puntos de entrada para decodificar Personalidad
base64enc 0.1-6 base64decode() Permisivo por defecto, ganó un modo strict en febrero de 2026
openssl 2.4.2 base64_decode() Salta los espacios en blanco, pero falla en silencio de formas que merecen una mirada seria
b64 0.1.7 decode(), decode_as_string() Estricto, vectorizado, escrito en Rust, rápido
base64 2.0.2 decode() Envoltorio cómodo de archivo a archivo sobre openssl
base64url 1.4 base64_urldecode() Alfabeto URL-safe, sin padding, indulgente en silencio

Tres finalistas merecen una mención. El paquete jsonlite exporta sus propios helpers, base64_enc, base64_dec y el par URL-safe, así que si ya analizas JSON, puedes tener un decodificador a mano. El paquete jose incluye base64url_decode() para el trabajo con JWT. Y el añejo RCurl sigue llevando una función base64() que envuelve libcurl: funciona, está orientada a caracteres, y tiene el aire de un abuelo fiel en vez de una herramienta para código nuevo.

Instalando el reparto

Si R aún no está en la máquina, tu sistema operativo lo incluye: r-base en Debian y Ubuntu, R en Fedora, un paquete o instalador en macOS y Windows. Y luego los paquetes, directos de CRAN:

install.packages("base64enc")
install.packages("openssl")
install.packages("b64")
install.packages("base64url")

Dos notas de compilación, porque ahí es donde las instalaciones se tuercen. El paquete openssl se compila contra el OpenSSL de tu sistema, así que una máquina Linux desnuda puede querer primero las cabeceras de desarrollo:

sudo apt install libssl-dev

El paquete b64 es un motor Rust envuelto con extendr, así que compilarlo desde fuente pide el toolchain de Rust (sudo apt install cargo trae también rustc). En Windows y macOS recibes binarios precompilados de CRAN y nada de esto aplica. Si prefieres un gestor de paquetes, pak::pkg("base64enc") o remotes::install_cran("b64") hacen el mismo trabajo con menos opiniones sobre los repositorios.

Tu primera decodificación: el rito de tres líneas

El noventa por ciento de la vida de decodificación cabe en tres líneas. Aquí está la prueba de humo canónica, usando la célebre cadena TWFu:

library(base64enc)
packed <- "TWFu"
bytes <- base64decode(packed)
bytes
#> [1] 4d 61 6e
rawToChar(bytes)
#> [1] "Man"

Tres cosas merecen la pena notar en esa pequeña ceremonia. Primera, base64decode() siempre te entrega un vector raw, nunca una cadena. Es una característica, no un accidente: Base64 puede llevar una frase, un JPEG o un certificado, y ninguno debería tratarse de forma diferente antes de saber qué tienes entre manos. Segunda, el salto de vuelta de los bytes al texto es un paso aparte, deliberado, a través de rawToChar(), y ese paso es donde vive la decisión del charset (volveremos a ello). Tercera, TWFu se decodifica a la palabra "Man": tres letras, cero drama. Guarda esa cadena en el bolsillo. Si un trozo de código de decodificación convierte TWFu en Man, la máquina es honesta.

Porque siempre acabarás decodificando algo que tú mismo codificaste, aquí está el viaje de ida y vuelta completo que demuestra que las dos direcciones coinciden:

text <- "Hello, world!"
packed <- base64encode(charToRaw(text))
packed
#> [1] "SGVsbG8sIHdvcmxkIQ=="
identical(text, rawToChar(base64decode(packed)))
#> [1] TRUE

La frontera del vector raw

R cruza la frontera bytes/texto con dos funciones diminutas, y una vez que las conoces el resto de este artículo se siente obvio. charToRaw() convierte una cadena en sus bytes, rawToChar() hace lo contrario, y entre medias tienes todo el kit raw: length() para contar bytes, head() para echar un vistazo, writeBin() y readBin() para moverlos hacia y desde archivos. Cada decodificador de este artículo se detiene en esa frontera a propósito.

bytes <- base64decode("SGVsbG8s")
length(bytes)
#> [1] 6
head(bytes)
#> [1] 48 65 6c 6c 6f 2c
rawToChar(bytes)
#> [1] "Hello,"

Así que cuando veas un resultado como [1] 48, léelo como "un byte, valor 72, la letra H", y para ahí hasta que sepas qué charset llevan puesto los bytes. Esa pausa es toda la disciplina de la decodificación.

Entrada sucia: cómo discrepan los decodificadores

Esta es la sección que te salva a las 2 de la madrugada, porque los decodificadores no están de acuerdo sobre qué pasa cuando la entrada está un poco sucia. El Base64 del mundo real llega con espacios por dentro, padding arriado por un copy-paste nervioso, un símbolo errante de un portapapeles que se lo comió, o basura colgando al final. Así responde cada decodificador, ante la misma familia de culpables:

Entrada base64enc (por defecto) base64enc (strict = TRUE) openssl b64
"SGVs bG8s IHdvcmxkIQ==" (un espacio por dentro) "Hello, world!" (espacio saltado) error: carácter inválido, se indica la posición "Hello, world!" (espacio saltado) error
"SGVsbG8sIHdvcmxkIQ" (padding arriado) "Hello, world!" (padding faltante tolerado) error: padding faltante error: fallo al decodificar error: padding inválido
"SGVsbG8s!IHdvcmxkIQ==" (un "!" por dentro) "Hello, world!" (carácter malo saltado) error: carácter inválido vector raw vacío, sin error error
"SGVsbG8sIHdvcmxkIQ==xx" (basura al final) "Hello, world!" (basura ignorada) error: contenido sobrante vector raw vacío, sin error error
"TQ==" (limpio) M M M M

Lee esa tabla dos veces, porque contiene toda la historia. El base64decode() por defecto es el aduanero amable: salta los caracteres fuera del alfabeto, tolera el padding faltante e ignora el contenido sobrante, así que las cadenas con forma de email o de portapapeles simplemente pasan. El modo strict = TRUE, que llegó con la release 0.1-5 en febrero de 2026 tras un largo silencio, es el perito forense: valida la cadena entera, nombra la posición exacta del culpable y rechaza lo que no sea de manual. b64 es estricto por defecto y nunca medio funciona. Y openssl se queda en el incómodo centro: salta los espacios en blanco con gusto y da error correctamente con el padding faltante, pero cuando se topa con un carácter realmente ilegal o con basura al final devuelve un vector raw vacío y no dice absolutamente nada. Ese resultado vacío en silencio es el comportamiento más peligroso de este artículo entero, así que miremos cómo ocurre:

dirty <- "SGVsbG8s!IHdvcmxkIQ=="
openssl::base64_decode(dirty)
#> raw(0)   # vacío. sin error. sin aviso. sin pista.

Si escribes código contra openssl, comprueba la longitud de lo que recibes antes de confiar en ello. Es un pequeño hábito que evita toda una clase de misterios de "¿dónde se fue mi dato?".

Para el bando estricto, aquí está base64enc siendo preciso, con los mensajes de error exactos que verás en tu propia consola:

base64enc::base64decode("TWF u", strict = TRUE)
#> v=30000, pad=0, org='u'
#> Error: Invalid character (' ') at position 4 in base64 string (not allowed in strict mode)
base64enc::base64decode("TWF", strict = TRUE)
#> v=10000, pad=3, org=''
#> Error: Missing padding (1 characters) at the end of the base64 string (not allowed in strict mode)
base64enc::base64decode("TWF=xx", strict = TRUE)
#> v=20000, pad=0, org='xx'
#> Error: Trailing content 'xx' after padding at position 4 in base64 string (not allowed in strict mode)

Una rareza que esperar: cada llamada estricta, con éxito o con fallo, charlea una línea de estado a nivel C por la consola, algo como v=30000, pad=0, org='u'. Es una escritura directa del código compilado, no un aviso de R, así que suppressWarnings() no le tocará. Es inofensiva, pero si tus pruebas comparan la salida de la consola, ya sabes por qué hay una línea extra.

Costumbres de R: NA, vectores y concatenación silenciosa

Base64 tiene sus rarezas; R añade las suyas. La primera muerde a través de NA. Cuando pasas NA_character_ a un decodificador, R lo convierte en silencio a la cadena "NA" antes de que el decodificador lo vea, y "NA" es un grupo Base64 perfectamente válido. El resultado no es un error ni un vector vacío. Es el byte 0x34, la letra "4":

base64decode(NA_character_)
#> [1] 34
rawToChar(base64decode(NA_character_))
#> [1] "4"

Así que una columna de valores que faltan no se decodifica a valores que faltan. Se decodifica a una columna de la letra 4, y tu código aguas abajo la procesa con alegría. Si tu entrada puede contener NA, filtra primero:

values <- c("TWFu", NA_character_, "TQ==")
clean <- values[!is.na(values)]
out <- vapply(clean, function(x) rawToChar(base64decode(x)), character(1))
unname(out)
#> [1] "Man" "M"

La segunda rareza es la entrada vectorial, y los decodificadores toman giros completamente distintos. base64decode() trata un vector de caracteres como varios trozos de una misma cadena y los concatena, así que dos elementos vuelven como un solo vector raw. openssl::base64_decode() ni siquiera llega hasta ahí: comprime su argumento y luego tropieza con una prueba lógica de dos elementos, que produce uno de los mensajes de error más honestos de R. Y b64::decode() es el único que está verdaderamente vectorizado, devolviendo un bloque decodificado por cada entrada:

base64decode(c("TWFu", "TQ=="))
#> [1] 4d 61 6e 4d
openssl::base64_decode(c("TWFu", "TQ=="))
#> Error in if (is.na(text)) : the condition has length > 1
out <- b64::decode(c("TWFu", "TQ=="))
rawToChar(out[[1]])
#> [1] "Man"
rawToChar(out[[2]])
#> [1] "M"

Moral: itera o vectoriza con deliberación, y nunca asumas que un vector de entradas te da un vector de salidas.

El alfabeto URL-safe

El Base64 estándar gasta las dos últimas plazas de su alfabeto en + y /, y esos son exactamente los caracteres que a las URLs no les encantan. El más se vuelve %2B, la barra se vuelve %2F, y el padding se vuelve %3D, así que un token que debería pegarse en cualquier sitio empieza a llevar signos de porcentaje. La variante URL-safe, definida en la sección 5 del RFC 4648, cambia esos dos caracteres por - y _ y normalmente también suelta el padding. R tiene tres puertas hacia ese mundo.

La puerta uno son los motores b64. Un motor es simplemente un alfabeto y una política de padding configurados, y el paquete incluye los cuatro que necesitas:

library(b64)
std <- encode(as.raw(c(0xfb, 0xef, 0xbe)))
std
#> [1] "++++"
url <- encode(as.raw(c(0xfb, 0xef, 0xbe)), engine("url_safe"))
url
#> [1] "----"
decoded <- decode(url, engine("url_safe"))[[1]]
toString(decoded)
#> [1] "fb, ef, be"

Los motores son "standard" (el por defecto), "standard_no_pad", "url_safe" y "url_safe_no_pad", y el mismo objeto motor funciona en ambas direcciones, lo que mantiene tu código simétrico. También son estrictos con su alfabeto: dale "----" al motor estándar y responde "Invalid byte 45, offset 0.", lo cual es un alivio.

La puerta dos es el paquete base64url dedicado, pequeño y de un solo propósito: alfabeto URL-safe, sin padding, siempre. Una advertencia: su decodificador es permisivo, así que decodificará con gusto el prefijo válido de una cadena corrupta y se parará ahí sin quejarse. Si un valor URL-safe viene de una fuente no confiable, prefiere b64.

base64url::base64_urlencode("hello world")
#> [1] "aGVsbG8gd29ybGQ"
base64url::base64_urldecode("aGVsbG8g!!!")
#> [1] "hello "   # el resto se descarta en silencio

La puerta tres es jose, que exporta base64url_encode() y base64url_decode() para el trabajo con JWT, y devuelve vectores raw como uno esperaría.

Y cuando solo necesitas el caso puntual de una vez y quieres quedarte dentro de base64enc, cirugía sobre la cadena más una corrección de padding es suficiente:

url <- "----"
fixed <- gsub("_", "/", gsub("-", "+", url), fixed = TRUE)
padded <- switch(as.character(nchar(fixed) %% 4L),
  "0" = fixed,
  "2" = paste0(fixed, "=="),
  "3" = paste0(fixed, "="))
rawToChar(base64decode(padded))
#> [1] "\xfb\xef\xbe"

JWTs: asomarse y confiar

La razón por la que la mayoría se topa con el Base64 URL-safe es el JSON Web Token. Un JWT son tres partes base64url unidas por puntos: un header que describe cómo se firmó, un payload de claims, y una firma que hace que todo sea digno de confianza. Decodificar las dos primeras partes en R es asunto de cinco líneas. Fíjate en el fixed = TRUE de strsplit(): el punto es un comodín de regex, y sin esa bandera R divide alegremente tu token en caracteres sueltos, que es el bug Base64 más común en el código R del mundo.

jwt <- jose::jwt_encode_hmac(jose::jwt_claim(sub = "1234567890", iat = 1516239022, name = "John Doe"), "0123456789abcdef")
parts <- strsplit(jwt, ".", fixed = TRUE)[[1]]
header <- base64url::base64_urldecode(parts[1])
header
#> [1] "{\"typ\":\"JWT\",\"alg\":\"HS256\"}"
payload <- jsonlite::fromJSON(base64url::base64_urldecode(parts[2]))
payload$name
#> [1] "John Doe"

Dos advertencias honestas. Primera, decodificar un JWT es asomarse, no confiar: la tercera parte es la firma, y solo significa algo cuando se comprueba contra la clave del emisor. Para eso, el paquete jose maneja toda la familia. Su release 2.0 (abril de 2026) añade soporte ED25519 y hace opcional el header typ; los tutoriales antiguos que muestran los helpers jwk_read()/jwk_write() de la 1.x (o sus alias read_jwk()/write_jwk()) siguen siendo válidos, porque la 2.0 sigue incluyendo esos mismos helpers de lectura y escritura JWK:

library(jose)
claims <- jwt_decode_hmac(jwt, "0123456789abcdef")
claims$name
#> [1] "John Doe"
claims$sub
#> [1] "1234567890"
sp <- jwt_split(jwt)
sp$header
#> $alg
#> [1] "HS256"
#>
#> $typ
#> [1] "JWT"
sp$payload$name
#> [1] "John Doe"

jwt_decode_hmac() verifica la firma y además aplica los claims exp y nbf, lanzando un error si el token ha caducado. Y segunda: base64url::base64_urldecode() y compañía decodifican el header y el payload sin problema, pero la parte de firma es binaria, así que decodifícala con una función que devuelve raw (como b64::decode() con el motor "url_safe_no_pad") y no a una cadena.

Charsets: ¿qué texto son esos bytes?

Cada decodificador de este artículo se detiene en la frontera del vector raw a propósito, porque la respuesta a "¿qué texto era ese?" depende del charset en el que se empaquetaron los bytes. La buena noticia de entrada: si el emisor usó UTF-8, que es la mayor parte de la web moderna, tu vida es corta y feliz. base64enc incluso incluye una puerta para ello:

packed <- base64encode(charToRaw("caf\u00e9"))
bytes <- base64decode(packed)
checkUTF8(bytes, quiet = TRUE)
#> [1] TRUE
rawToChar(bytes)
#> [1] "café"

checkUTF8() pregunta "¿se pueden leer estos bytes como UTF-8?" sin comprometerse. Con quiet = TRUE devuelve TRUE o FALSE; por defecto es aún más directo y lanza un error con bytes inválidos, nombrando al culpable, como en INVALID byte 0xff at 0x0 (0, line 1): invalid start byte (FE/FF). También informa FALSE para cadenas que contienen bytes NUL, que nunca pueden ser parte de una cadena R válida en UTF-8. Úsalo como puerta, y el resto viene solo.

Aquí es donde la puerta importa. En un locale UTF-8, rawToChar() no lanza un error cuando los bytes no son UTF-8 válido. Te entrega una cadena que R marca con Encoding() == "unknown", y según el locale y la build de R la misma llamada puede morir en su lugar con un error de "invalid multibyte sequence", que al menos es honesto. En cualquier caso, un rawToChar() sin protección sobre bytes extraños es cómo nace el mojibake:

broken <- as.raw(c(0xc3, 0x28))   # un par UTF-8 truncado
checkUTF8(broken, quiet = TRUE)
#> [1] FALSE
rawToChar(broken)                  # sin error. ese es el problema.
#> [1] "\xc3("

Y cuando el emisor usó otra cosa por completo, Latin-1 o Windows-1252 o Shift-JIS, la receta es decodificar a raw y luego traducir con iconv(), que entiende los charsets nombrados:

latin1_bytes <- as.raw(c(0x63, 0x61, 0x66, 0xe9))   # "café" en Latin-1
iconv(rawToChar(latin1_bytes), from = "latin1", to = "UTF-8")
#> [1] "café"

Los emoji merecen una palabra, porque R tiene una historia aquí. El R moderno almacena los puntos de código por encima de U+FFFF, donde viven los emoji, como UTF-8 de verdad, así que el viaje de ida y vuelta funciona y el recuento de bytes coincide con lo que cualquier otro lenguaje espera. Las releases antiguas de R almacenaban esos mismos caracteres como pares de surrogados, un esquema a menudo llamado CESU-8, así que un solo emoji cruzaba la frontera como seis bytes de UTF-8 inválido. Si heredas código R antiguo cuyos emoji llegan rotos por el cable, ese es el principal sospechoso: comprueba nchar(x, type = "bytes") y compáralo con lo que el lenguaje del emisor produciría.

emoji <- "\U0001F600"
nchar(emoji, type = "bytes")
#> [1] 4
round_trip <- base64decode(base64encode(charToRaw(emoji)))
identical(emoji, rawToChar(round_trip))
#> [1] TRUE

La regla práctica: asume UTF-8, verifica con checkUTF8() y solo acude a iconv() cuando tengas conocimiento positivo de otro charset. Nunca adivines.

Archivos: del texto envolvente a los bytes de verdad

Las cadenas son fáciles; los archivos son donde Base64 gana su sustento. Empieza con la cadena de trabajo manual que funciona con cualquier paquete: lee el texto codificado, decodifica, escribe los bytes.

cat("SGVsbG8sIHdvcmxkIQ==", file = "hello.b64")
bytes <- base64decode(paste(readLines("hello.b64"), collapse = ""))
writeBin(bytes, "hello.txt")
readLines("hello.txt")
#> [1] "Hello, world!"

Luego la capa de comodidad. base64enc puede leer y escribir por ti: el argumento file apunta a un archivo con texto Base64, y output apunta a donde deben caer los bytes decodificados. El valor de retorno es el número de bytes escritos, una comprobación de cordura gratis y encantadora:

n <- base64decode(file = "hello.b64", output = "hello.txt")
n
#> [1] 13
readLines("hello.txt")
#> [1] "Hello, world!"

La entrada envolvente, la que ha sobrevivido al email o a la armadura PEM, no es problema para los decodificadores permisivos: miran de largo a través de los saltos de línea donde caigan, mientras lo que quede sea Base64 válido. MIME envuelve a 76 caracteres con CRLF entre líneas; los bloques PEM envuelven a 64. El motor estricto, por supuesto, rechaza el envoltorio de plano.

wrapped <- paste0("VGhlIHF1aWNrIGJ", "\r\n",
  "yb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==")
rawToChar(base64decode(wrapped))
#> [1] "The quick brown fox jumps over the lazy dog"
rawToChar(openssl::base64_decode(wrapped))
#> [1] "The quick brown fox jumps over the lazy dog"
b64::decode(wrapped)
#> Error: Invalid byte 13, offset 15.

El paquete b64 tiene el par de archivos con los nombres más claros, y como es rápido a nivel Rust es al que hay que acudir cuando el archivo es grande. Hay un filo cortante, sin embargo: decode_file() lee el archivo byte a byte y no perdona. Un salto de línea final - el tipo que writeLines() añade pero cat() nunca - hace que el motor Rust entre en pánico, que se manifiesta como un error atrapable de la forma User function panicked: decode_file_. Escribe el texto codificado con cat() o writeBin(charToRaw(enc), path) y el filo se queda sordo.

writeBin(charToRaw("file payload bytes"), "payload.bin")
enc <- b64::encode_file("payload.bin")
cat(enc, file = "payload.b64")
bytes <- b64::decode_file("payload.b64")
rawToChar(bytes)
#> [1] "file payload bytes"

El paquete base64 es la tercera puerta de entrada: puramente orientado a archivos, con un par de funciones a juego:

base64::encode("payload.bin", "payload.b64")
base64::decode("payload.b64", "payload.out")
readLines("payload.out")
#> [1] "file payload bytes"

Una forma más de archivo que te encontrarás en la práctica: una columna de data frame llena de bloques codificados, muy común en volcados de APIs. Para unas pocas centenas de filas, una llamada vectorizada simple es suficiente:

df <- data.frame(content = c(b64::encode("alpha"), b64::encode("beta"),
  b64::encode("gamma")))
decoded <- b64::decode(df$content)
df$decoded <- vapply(decoded, function(x) rawToChar(x), character(1))
df$decoded
#> [1] "alpha" "beta"  "gamma"

APIs y respuestas web

A las APIs JSON les encanta meter binario dentro de texto, y Base64 es su maleta favorita. El patrón en R es el mismo cada vez: descarga, analiza, decodifica y decide qué son los bytes. El cliente HTTP moderno es httr2, y el lado JSON es jsonlite:

library(httr2)
library(jsonlite)
res <- req_perform(request("https://httpbin.org/get?attachment=TWFuIGlzIGhlcmU%3D&name=man.txt"))
data <- fromJSON(rawToChar(res$body))
bytes <- base64decode(data$args$attachment)
writeBin(bytes, data$args$name)
length(bytes)
#> [1] 11

Tres notas para el camino. El constructor de peticiones es request(), y lleva ese nombre desde la primera release de httr2. El cuerpo de la respuesta llega como vector raw, así que rawToChar() antes de analizarlo como JSON. Y la API puede hablar un dialecto: comprueba si su Base64 es URL-safe o está desprovisto de padding antes de dárselo a un decodificador que espera la versión de manual. Algunas APIs incluso doble-codifican, Base64 de Base64, que reconocerás cuando una decodificación te dé más Base64 en vez de los bytes esperados.

Data URIs y documentos autocontenidos

El esquema de URI data: (RFC 2397) deja que un documento lleve su propio contenido: un tipo MIME, la palabra "base64" cuando el payload está codificado, y el payload en sí. Te topas con estos constantemente en el HTML que raspas, y decodificar uno es una operación de cadena en dos pasos seguida de una decodificación corriente:

img_tag <- "<img src=\"data:image/png;base64,iVBORw0KGgoAAA\" />"
uri <- regmatches(img_tag, regexec("src=\"([^\"]*)\"", img_tag))[[1]][2]
uri
#> [1] "data:image/png;base64,iVBORw0KGgoAAA"
payload <- sub("^data:[^,]*,", "", uri)
bytes <- base64decode(payload)
length(bytes)
#> [1] 10

La misma forma aparece en CSS, en anotaciones PDF y en informes R Markdown autocontenidos, donde un plot ha sido aplanado dentro de una etiqueta <img> para que el HTML viaje sin archivos aparte. Si renderizas documentos así y quieres extraer las imágenes incrustadas, este es todo el truco: encuentra la cadena data:, corta en la primera coma, decodifica.

Bases de datos, email y configuración

Base64 aparece en bases de datos cada vez que alguien quiso binario dentro de una columna de texto, y el lado de decodificación es una columna de cadenas de vuelta a bytes. Aquí está el viaje de ida y vuelta contra SQLite a través de DBI y RSQLite: guarda el valor codificado, consúltalo de vuelta, decodifícalo, y tienes tus bytes:

library(DBI)
library(RSQLite)
db <- dbConnect(SQLite(), ":memory:")
dbExecute(db, "CREATE TABLE files (name TEXT, payload TEXT)")
stored <- base64encode(charToRaw("stored in a database"))
dbExecute(db, paste0("INSERT INTO files VALUES ('note.txt', '", stored, "')"))
row <- dbGetQuery(db, "SELECT * FROM files")
rawToChar(base64decode(row$payload))
#> [1] "stored in a database"

SQLite también puede almacenar binario de forma nativa como BLOB, en cuyo caso no hace falta Base64 para nada y la columna vuelve a R como vector raw, como en class(dbGetQuery(db, "SELECT payload FROM blobs")$payload[[1]]) == "raw". La variante Base64-en-TEXT existe por portabilidad: puedes inspeccionarla con un editor de texto, y cualquier otro lenguaje puede leerla sin drivers binarios.

El email es de donde viene el Base64 envolvente. Las partes MIME envuelven a 76 caracteres con CRLF entre líneas, y cualquier sistema de correo que hayas leído ha transportado adjuntos exactamente así. R no tiene un cliente de correo de primera clase, pero cuando recibes un archivo .eml, la parte base64 de un adjunto es solo texto envolvente: los decodificadores permisivos lo desenvolverán por ti mientras decodifican. El paquete mime te ayuda a identificar qué tienes entre manos:

mime::guess_type("photo.jpg")
#> [1] "image/jpeg"
part <- "SGVsbG8s\r\nCnRoaXMgaXMgYW4g\r\nZW1haWwgYXR0YWNobWVudA=="
rawToChar(base64decode(part))
#> [1] "Hello,\nthis is an email attachment"

Los archivos de configuración completan el recorrido. Cuando un certificado o un blob está almacenado codificado en Base64 en una configuración YAML o JSON, o en una variable de entorno, R lo lee como una cadena corriente y tú decodificas bajo demanda:

config_line <- "api_cert: TFRTU0VDUkVU"
cert_b64 <- sub("^api_cert: ", "", config_line)
raws <- base64decode(cert_b64)
rawToChar(raws)
#> [1] "LTSSECRET"

Payloads grandes

Las cadenas R tienen un techo duro de 2^31 - 1 bytes, y una cadena Base64 lo bastante grande puede chocar con él, porque la forma codificada es un tercio más grande que el original. El paquete base64enc lo maneja desde su release de 2022: dale a base64encode() un ancho de línea y te devuelve un vector de líneas en vez de una cadena enorme, y en el lado de decodificación el argumento file = lee el archivo como líneas y decodifica sin forzarte a sostener una cadena gigante en una variable.

Para archivos de cientos de megabytes, el patrón práctico es leer en fragmentos alineados. Los grupos Base64 son independientes, así que cualquier fragmento cuya longitud sea múltiplo de cuatro caracteres decodifica por sí solo; solo necesitas un pequeño buffer para llevar el extremo desaliñado:

chunk <- 65536L
con <- file("huge.b64", "rb")
on.exit(close(con))
leftover <- ""
out <- raw(0)
while (TRUE) {
  piece <- readChar(con, chunk, useBytes = TRUE)
  if (length(piece) == 0) break
  piece <- gsub("[\r\n]", "", piece)
  ready <- paste0(leftover, piece)
  take <- floor(nchar(ready) / 4) * 4
  if (take > 0) {
    out <- c(out, base64decode(substr(ready, 1, take)))
    leftover <- substr(ready, take + 1, nchar(ready))
  } else {
    leftover <- ready
  }
}
if (nchar(leftover) > 0) out <- c(out, base64decode(leftover))
length(out)

El paquete b64 es el campeón de velocidad en este territorio: su motor Rust decodifica un archivo de 50 megabytes en una fracción de segundo, y su decode() vectorizado maneja una columna de muchos valores codificados en una sola llamada, que es una diferencia dramática frente a iterar fila a fila. Si tienes muchas cadenas, corre tú mismo una comparación rápida con system.time(); la brecha entre un bucle por fila y una llamada vectorizada suele ser bastante grande como para importar.

La línea de comandos

No todo necesita una sesión R completa. Las herramientas Unix clásicas hablan Base64 nativamente, y R puede darles trabajo o cogerles trabajo. En Linux, base64 -d decodifica (macOS y otros sistemas BSD usan -D):

echo "SGVsbG8sIHdvcmxkIQ==" | base64 -d
#> Hello, world!

Y un Rscript de una línea hace el mismo trabajo con los mismos paquetes que usas en tus scripts:

Rscript -e 'library(base64enc); writeLines(rawToChar(base64decode("TWFu")))'
#> Man

Usa la shell para comprobaciones rápidas y tuberías; usa R cuando el resultado necesite vivir en un data frame, un archivo o un informe. Una advertencia: los argumentos de línea de comandos tienen un límite de tamaño (ARG_MAX), así que no pegues cadenas de varios megabytes en el terminal. Pásalas por un archivo en su lugar.

Trampas que conviene conocer

Aquí está la lista corta de las formas en que esto muerde a los desarrolladores de R, todas nativas del ecosistema y no de Base64 en general:

  • openssl falla en silencio. base64_decode() devuelve un vector raw vacío, sin error ni aviso, cuando se topa con un carácter fuera del alfabeto o con basura al final. Comprueba siempre length() del resultado antes de creerlo.
  • NA no es un valor que falta para un decodificador. NA_character_ se convierte a la cadena "NA", que se decodifica al byte 0x34. Filtra NA antes de decodificar.
  • Los vectores no se comportan como esperas. base64decode() concatena su entrada en un solo vector raw; openssl::base64_decode() muere con the condition has length > 1 ante entrada multi-elemento; solo b64::decode() está verdaderamente vectorizado.
  • rawToChar es un corruptor silencioso. Los bytes UTF-8 inválidos no lanzan un error en un locale UTF-8; se convierten en una cadena marcada "unknown". Pasa la puerta con checkUTF8() primero.
  • El punto de un JWT es un comodín de regex. strsplit(jwt, ".") sin fixed = TRUE parte en cada carácter. Este es el bug Base64 más común en código R.
  • Los archivos de b64 no perdonan. Un salto de línea final en el archivo codificado hace que b64::decode_file() entre en pánico. Escribe con cat(), no con writeLines().
  • Permisivo no es lo suficientemente permisivo para la confianza. El modo por defecto de base64decode() salta caracteres malos donde sea, así que una cadena corrupta puede decodificarse a basura con pinta verosímil y sin error. Usa strict = TRUE en fronteras de confianza.
  • Los mensajes de error de b64 hablan Rust. Espera frases como Both cases of Either errored cuando le pases un tipo que no quiere. El mensaje es inútil a propósito; es un sistema de tipos de Rust encogiéndose de hombros.

Prácticas recomendadas

  • Por defecto, usa base64enc::base64decode() para el trabajo de cada día, y activa strict = TRUE donde la entrada cruce una frontera de confianza: APIs no confiables, subidas de usuarios, lo que sea firmado.
  • Acude a b64 cuando necesites velocidad, vectorización de verdad o los motores URL-safe y sin padding, y acepta que es estricto por diseño.
  • Si openssl ya está en tu proyecto, úsalo, pero trata cada resultado como sospechoso hasta que hayas comprobado su longitud.
  • Decide el charset con deliberación: asume UTF-8, verifica con checkUTF8() y solo usa iconv() cuando el emisor te haya dicho lo contrario.
  • Para JWTs, asómate con strsplit() más fixed = TRUE, pero confía solo después de que jose verifique la firma.
  • Mantén TWFu en tus pruebas: son tres bytes de prueba de humo para cualquier ruta de decodificación que escribas.
  • Recuerda lo que Base64 no es: no es cifrado, y no es compresión. Es cinta de empaquetar, y cualquiera con este artículo puede revertir todo lo que hace. Decodifica libremente, confía selectivamente.

Una breve historia de Base64 en R

El formato es viejo. Fue estandarizado para el protocolo Privacy-Enhanced Mail en 1987 (RFC 989), adoptado por MIME en 1993 (RFC 1521, y luego el definitivo RFC 2045 en 1996), ordenado en RFC 3548 en 2003, y dado su forma moderna, incluido el alfabeto URL-safe, en RFC 4648 en 2006. La historia de R es mucho más corta y mucho más veloz. El paquete base64enc, de Simon Urbanek, aterrizó por primera vez en CRAN en septiembre de 2012 y ha sido el caballo de batalla desde entonces, ganando checkUTF8() en 2015 y el soporte de vectores largos en 2022. En octubre de 2024 el viejo paquete base64 se reeditó explícitamente como envoltorio de compatibilidad, con su propia descripción señalando ahora a las aplicaciones nuevas hacia base64enc, openssl o jsonlite. Luego llegó b64 a principios de 2024 (release actual 0.1.7, julio de 2025), un motor Rust construido con extendr que trajo vectorización de verdad y una cuadra de alfabetos. En febrero de 2026, base64enc publicó su largamente esperado modo strict, y en abril de 2026 el paquete jose se rediseñó en torno a las funciones jwt_*. Diez años por lo que otros lenguajes empaquetaron en una sola biblioteca, pero el resultado es un arcón de herramientas donde cada decodificador tiene un trabajo claro y una personalidad clara.

Datos curiosos

Porque una guía completa debería acabar con una sonrisa:

  • base64decode(NA_character_) devuelve la letra "4", porque NA_character_ viaja hacia el decodificador como la cadena "NA", y "NA" es un grupo Base64 válido. La sorpresa de un byte más propia de R en todo el lenguaje.
  • Dale a openssl::base64_decode() una cadena con un carácter ilegal y devuelve raw(0) en silencio absoluto. El silencio más peligroso del ecosistema.
  • Cada llamada estricta a base64decode() charlea una línea de estado a nivel C como v=30000, pad=0, org='u'. No es un aviso. Es el código C aclarándose la garganta.
  • b64 puede decodificar alfabetos que nunca has conocido: BinHex, UTF-7 modificado de IMAP, y los alfabetos a medida de bcrypt y crypt. R puede leer un adjunto de Macintosh de los 80 y un hash de contraseña moderno con el mismo motor.
  • Las cadenas R se paran en 2^31 - 1 bytes, por eso base64enc aprendió a devolver un vector de líneas para entradas largas. El formato chocó con el muro del lenguaje, y el paquete le creció una escalera.
  • La palabra "base64" se codifica como YmFzZTY0. Un formato que puede describirse a sí mismo es el equivalente técnico de un espejo que habla en Morse.
  • Un solo byte NUL sigue costando cuatro caracteres: "AA==". En Base64, la nada siempre es algo.
  • Tu build de R tiene un apodo. R 4.5.0 se llama "How About a Twenty-Six", porque las versiones de R se llaman como cómics y películas de Peanuts, y las tiras de las que toman el nombre son décadas más antiguas que las releases que nombran. Hasta la versión con la que estás decodificando tiene sentido del humor.

Antes de irte

Elige tu decodificador según la compañía que lleves: base64enc para el trabajo de cada día, con strict = TRUE en los bordes; openssl si ya está en tu proyecto, siempre que compruebes tus resultados; b64 cuando quieras velocidad, vectores y los motores URL-safe; y los especialistas pequeños base64 y base64url para menudos de archivos y cadenas URL-safe. Cuida tus bytes con checkUTF8() antes de llamarlos texto, usa iconv() cuando se sepa que el charset es otra cosa, y recuerda que el vector raw del medio es una característica: te obliga a decidir qué significan los bytes en vez de dejar que una biblioteca adivine. Decodifica todo, confía solo en lo que verifica. Y cuando necesites ir en la otra dirección, empaquetando tus propios bytes en una cadena para el camino en vez de desembalar uno, el artículo hermano cubre la codificación Base64 en R en detalle.

Última actualización: 2026-09-08

Artículo relacionado: Codificación Base64 en R: una guía completa