Subir un archivo
  1. base64hub
  2. Base64 en código
  3. Kotlin
Código

Base64 en Kotlin

Kotlin incluye un objeto multiplataforma kotlin.io.encoding.Base64, estable desde Kotlin 2.2. En la JVM también puedes usar java.util.Base64. Las salidas de esta página son de Kotlin 2.4.21 sobre Java 21.

Codificar y decodificar una cadena

Base64.encode recibe un ByteArray y devuelve un String. encodeToByteArray() y decodeToString() se encargan del UTF-8 en ambos sentidos:

import kotlin.io.encoding.Base64

val enc = Base64.encode("héllo wörld".encodeToByteArray())
println(enc)                                // aMOpbGxvIHfDtnJsZA==
println(Base64.decode(enc).decodeToString()) // héllo wörld

Base64 es en sí mismo la instancia estándar (Base64.Default). También existen Base64.UrlSafe y Base64.Mime.

En Kotlin anterior a 2.2, la API necesita @OptIn(ExperimentalEncodingApi::class). Si no puedes salir de una versión antigua, java.util.Base64 funciona igual que en Base64 en Java:

java.util.Base64.getEncoder().encodeToString("hi".toByteArray())  // aGk=

Los usos típicos son cabeceras HTTP y data URI:

val token = Base64.encode("user:pass".encodeToByteArray())
println("Authorization: Basic $token")       // Authorization: Basic dXNlcjpwYXNz

val png = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47)
println("data:image/png;base64," + Base64.encode(png))  // data:image/png;base64,iVBORw==

Base64 URL-safe y opciones de relleno

Base64.UrlSafe cambia +/ por -_. El relleno se controla con withPadding, que devuelve una nueva instancia configurada:

val d = "<<???>>".encodeToByteArray()

println(Base64.UrlSafe.encode(d))                  // PDw_Pz8-Pg==

val urlNoPad = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT)
println(urlNoPad.encode(d))                        // PDw_Pz8-Pg
println(urlNoPad.decode("PDw_Pz8-Pg").decodeToString())  // <<???>>

Las cuatro opciones:

OpciónAl codificarAl decodificar
PRESENT (por defecto)escribe =exige =
ABSENTsin =rechaza =
PRESENT_OPTIONALescribe =acepta ambos
ABSENT_OPTIONALsin =acepta ambos

Las instancias predeterminadas son estrictas con esto:

Base64.UrlSafe.decode("PDw_Pz8-Pg")
// IllegalArgumentException: The padding option is set to PRESENT,
// but the input is not properly padded

Para las partes de un JWT y otros tokens, ABSENT_OPTIONAL o PRESENT_OPTIONAL suele ser la opción correcta, porque los productores no se ponen de acuerdo con el relleno. Contexto: Base64 frente a Base64URL y el relleno explicado.

Por ejemplo, para leer el payload de un JWT:

val jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.sig"
val url = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL)
println(url.decode(jwt.split(".")[1]).decodeToString())   // {"sub":"1234"}

Cortes de línea MIME

Base64.Mime escribe líneas de 76 caracteres separadas por \r\n e ignora los separadores de línea al decodificar:

Base64.Mime.encode("x".repeat(60).encodeToByteArray())
// "eHh4eHh4...eHh4" (76 chars) + "\r\n" + "eHh4"

Base64.Mime.decode("aGVs\r\nbG8K").decodeToString()  // "hello\n"

Para comparar, pega la misma entrada en el codificador Base64 MIME.

Codificar y decodificar un archivo

Los archivos pequeños caben en memoria:

import java.io.File

println(Base64.encode(File("hello.txt").readBytes()))   // aGVsbG8K
File("copy.txt").writeBytes(Base64.decode("aGVsbG8K"))

Archivos grandes en flujo

En la JVM, encodingWith y decodingWith envuelven streams. Siguen siendo experimentales en Kotlin 2.4, así que necesitan un opt-in y sus propios imports:

import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.io.encoding.decodingWith
import kotlin.io.encoding.encodingWith
import java.io.File

@OptIn(ExperimentalEncodingApi::class)
fun encodeFile(src: File, dst: File) {
    dst.outputStream().encodingWith(Base64).use { out ->
        src.inputStream().use { it.copyTo(out) }
    }
}

@OptIn(ExperimentalEncodingApi::class)
fun decodeFile(src: File, dst: File) {
    src.inputStream().decodingWith(Base64).use { inp ->
        dst.outputStream().use { inp.copyTo(it) }
    }
}

Al cerrar el stream de codificación, algo que el bloque use hace por ti, se escriben el último grupo y el relleno. Con un archivo de prueba de 300 KB, la salida en flujo fue idéntica a Base64.encode(readBytes()), y la ida y vuelta devolvió los mismos bytes. Si prefieres una vía no experimental, java.util.Base64.getEncoder().wrap(outputStream) hace el mismo trabajo.

Validación y mensajes de excepción

El decodificador de Kotlin es más estricto que el de Java. Rechaza los bits de relleno distintos de cero, que Java y la mayoría de las bibliotecas aceptan. Todos los fallos son IllegalArgumentException:

Entrada a Base64.decodeMensaje
PDw_Pz8-Pg==Invalid symbol '_'(137) at index 3
"aGVs\nbG8K"Invalid symbol '\n'(12) at index 4
aGk=!!Symbol '!'(41) at index 3 is prohibited after the pad character
aGk==Symbol '='(75) at index 3 is prohibited after the pad character
aGl=The pad bits must be zeros
aInput should have at least 2 symbols for Base64 decoding, startIndex: 0, endIndex: 1

El número entre paréntesis es el código del carácter en octal (137 es _, 12 es un salto de línea). Una función de validación cabe en una línea:

fun isBase64(s: String) = runCatching { Base64.decode(s) }.isSuccess

Para ver dónde se rompe una cadena concreta, usa el validador Base64; la guía de errores de Base64 enumera las causas habituales.

Trampas

  • Entradas sin relleno con la instancia predeterminada. Los segmentos de un JWT y muchos tokens no llevan =. Configura PRESENT_OPTIONAL o ABSENT_OPTIONAL en lugar de rellenar las cadenas a mano.
  • La opción DEFAULT de Android. android.util.Base64.encodeToString(bytes, Base64.DEFAULT) inserta un salto de línea cada 76 caracteres y otro al final. En una cabecera HTTP, eso rompe la petición. Usa NO_WRAP, o NO_WRAP or URL_SAFE or NO_PADDING para tokens.
  • El juego de caracteres de String.toByteArray(). En Kotlin es UTF-8 por defecto, a diferencia de getBytes() de Java en los JDK antiguos, así que "héllo".toByteArray() es seguro.
  • Decodificar binario a cadena. decodeToString() sobre los bytes de una imagen sustituye las secuencias no válidas por U+FFFD y corrompe los datos. Quédate con el ByteArray.

Notas de versión

  • Kotlin 1.8.20: se añade kotlin.io.encoding.Base64 como experimental.
  • Kotlin 2.0.20: withPadding y PaddingOption.
  • Kotlin 2.2: la API principal pasa a ser estable; las funciones de flujo de la JVM siguen siendo experimentales.

Preguntas frecuentes

¿Sigo necesitando @OptIn(ExperimentalEncodingApi::class)?

No para Base64.encode y Base64.decode en Kotlin 2.2 o posterior. Las funciones de flujo encodingWith y decodingWith todavía requieren el opt-in en Kotlin 2.4.

¿Cómo decodifico Base64 sin relleno en Kotlin?

Usa una instancia configurada: Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT) para entradas sin relleno, o PRESENT_OPTIONAL para aceptar ambas. Las instancias predeterminadas exigen el relleno.

¿Uso android.util.Base64 o kotlin.io.encoding.Base64?

En código nuevo, la API de Kotlin funciona en todas las versiones de Android porque forma parte de la biblioteca estándar. android.util.Base64 añade saltos de línea salvo que pases Base64.NO_WRAP, una causa frecuente de cabeceras rotas.

Sigue leyendo