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ón | Al codificar | Al decodificar |
|---|---|---|
PRESENT (por defecto) | escribe = | exige = |
ABSENT | sin = | rechaza = |
PRESENT_OPTIONAL | escribe = | acepta ambos |
ABSENT_OPTIONAL | sin = | 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.decode | Mensaje |
|---|---|
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 |
a | Input 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
=. ConfiguraPRESENT_OPTIONALoABSENT_OPTIONALen lugar de rellenar las cadenas a mano. - La opción
DEFAULTde 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. UsaNO_WRAP, oNO_WRAP or URL_SAFE or NO_PADDINGpara tokens. - El juego de caracteres de
String.toByteArray(). En Kotlin es UTF-8 por defecto, a diferencia degetBytes()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 porU+FFFDy corrompe los datos. Quédate con elByteArray.
Notas de versión
- Kotlin 1.8.20: se añade
kotlin.io.encoding.Base64como experimental. - Kotlin 2.0.20:
withPaddingyPaddingOption. - 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.