Decodificação Base64 em Kotlin: um guia completo
Você está encarando um valor que se recusa a ser lido: SGVsbG8sIFdvcmxkIQ==. Uma sequência de letras e dígitos, o ocasional + ou / no meio, e geralmente um ou dois caracteres = pendurados no final. Esse é o Base64, e este guia é sobre transformar ele de volta no que ele costumava ser - uma frase, uma imagem, um certificado, um blob binário - do jeito Kotlin. Um resumo em uma linha antes de mergulharmos: o Base64 empacota cada três bytes em quatro caracteres tirados de um alfabeto de 64 símbolos, e uma cauda curta de padding = marca onde os dados de verdade pararam. O passeio completo pelo formato fica na página inicial, então gastamos aqui apenas uma frase, e mais uma: como quatro caracteres carregam o que três bytes carregavam, a forma em texto é mais ou menos um terço maior que os dados originais.
A boa notícia sobre o Kotlin: você não precisa de nenhum pacote. A biblioteca padrão traz a própria implementação de Base64 há anos; ela está totalmente estável desde o Kotlin 2.2 e roda em toda plataforma onde o Kotlin roda, do JVM do seu notebook a um celular Android, do Node.js a uma função edge WASI. Tudo abaixo funciona com o Kotlin que já vem com o seu projeto.
A boa notícia primeiro: o que você realmente precisa
Não existe um artefato base64 para adicionar ao Gradle, não existe pacote estilo NuGet, não existe módulo npm. A classe que você quer é kotlin.io.encoding.Base64, parte da própria biblioteca padrão do Kotlin. Se você sabe escrever println, você sabe decodificar Base64. Três APIs conseguem fazer trabalho de Base64 num projeto Kotlin, e escolher a certa é a primeira decisão de verdade:
| API | Onde roda | Quando recorrer a ela |
|---|---|---|
kotlin.io.encoding.Base64 |
Toda plataforma Kotlin: JVM, Android, JS, Native, Wasm | Escolha padrão. Estável desde o Kotlin 2.2, multiplataforma, API moderna |
java.util.Base64 |
Somente JVM (Java 8+; no Android, API 26+) | Codebases somente JVM que já vivem no terreno da interop com Java |
android.util.Base64 |
Somente Android (API 8+) | Código Android legado, ou quando você precisa especificamente das constantes de flag dele |
Duas notas de versão que valem a pena saber. Primeira: a classe da biblioteca padrão apareceu pela primeira vez no Kotlin 1.8.20 (abril de 2023) atrás de um portão @ExperimentalEncodingApi; o Kotlin 2.0.20 trouxe o controle withPadding e a regra estrita de padding, e o Kotlin 2.2.0 (junho de 2025) deixou a API estável e adicionou a instância PEM. Então no Kotlin 2.2 ou mais novo - incluindo a linha estável atual, 2.4.x - você pode usar tudo deste guia com zero anotações. Segunda: se o seu projeto trava uma versão do Kotlin entre 1.8 e 2.1, a mesma classe existe, mas está marcada como experimental, e o compilador não vai deixar você usá-la sem uma anotação @OptIn na função.
Uma armadilha de instalação que já custou mais de uma tarde: o pacote kotlin dos repositórios Debian e Ubuntu é a versão 1.3.31, que é anterior à API de Base64 da biblioteca padrão por completo, então ele não compila um único exemplo deste artigo. Pegue o compilador dos releases do Kotlin no GitHub ou do SDKMAN, e em projetos Gradle, fixe o plugin explicitamente:
plugins {
kotlin("jvm") version "2.4.10"
}
Sua primeira decodificação: duas linhas e um resultado em bytes
O ritual inteiro cabe em duas declarações, e a string clássica TWFu é um bom lugar para começar:
import kotlin.io.encoding.Base64
fun main() {
val packed = "TWFu"
val bytes = Base64.decode(packed)
println(bytes.decodeToString()) // Man
}
Leia isso devagar, porque três decisões de design estão escondidas ali. Primeira: Base64.decode(...) sem nenhum .Default não é um erro de digitação: Default é o objeto companion da classe, então chamar a função na própria classe é atalho para chamá-la em Base64.Default. Você também vai ver Base64.Default.decode(...) em tutoriais mais antigos, e significa exatamente a mesma coisa. Segunda, e isso importa mais do que parece: decode te entrega um ByteArray, nunca uma String. O payload pode ser um JPEG, um certificado X.509 ou uma frase, e a API se recusa a chutar qual é, então o salto de bytes para texto é um passo separado, deliberado. Terceira: é nesse passo que mora a decisão do charset, e é onde nasce a maioria dos bugs do tipo "meu Base64 voltou como lixo". Chegamos lá num instante; antes, uma ida e volta para provar que a decodificação é fiel:
import kotlin.io.encoding.Base64
fun main() {
val original = "Hello, World!".encodeToByteArray()
val packed = Base64.encode(original)
val back = Base64.decode(packed)
println(packed) // SGVsbG8sIFdvcmxkIQ==
println(back.contentEquals(original)) // true
}
Quatro esquemas, quatro personalidades
A classe nunca é instanciada; você escolhe uma das quatro instâncias prontas, e cada uma decodifica com um temperamento diferente:
import kotlin.io.encoding.Base64
fun main() {
val data = "Hello?".encodeToByteArray()
println(Base64.Default.encode(data)) // SGVsbG8/
println(Base64.UrlSafe.encode(data)) // SGVsbG8_
println(Base64.Mime.encode(data)) // SGVsbG8/
println(Base64.Pem.encode(data)) // SGVsbG8/
}
| Instância | Alfabeto | Como decodifica |
|---|---|---|
Base64.Default |
A-Z a-z 0-9 + / |
Estrita: qualquer caractere fora do alfabeto lança exceção; padding é obrigatório |
Base64.UrlSafe |
A-Z a-z 0-9 - _ |
Estrita, mas contra o alfabeto de URL; um + ou / na entrada lança exceção |
Base64.Mime |
A-Z a-z 0-9 + / |
Tolerante: ignora separadores de linha e outros caracteres fora do alfabeto, mas nada pode vir depois do padding =; padding é obrigatório |
Base64.Pem |
A-Z a-z 0-9 + / |
Tolerante, mesmas regras do Mime; esse é o sabor PEM/PKI do mesmo alfabeto |
A divisão tolerante/estrita é a coisa mais útil para internalizar. Default e UrlSafe tratam qualquer caractere estrangeiro como cena de crime e lançam exceção na hora. Mime e Pem se encolhem de ombros diante de quebras de linha, espaços e pontuação solta - porque é exatamente isso que arquivos reais de e-mail e certificado contêm - mas não são ilimitadas: no instante em que um caractere de dados aparece depois do padding, até elas lançam exceção. Você vai ver as mensagens de erro exatas no guia de falhas mais adiante neste artigo.
Uma consequência a mais das personalidades: um esquema não lê a saída de outro esquema. Mande um token base64url para o Base64.Default e o caractere - não está no alfabeto dele, então você recebe IllegalArgumentException: Invalid symbol '-'(55) at index .... Em dúvida sobre de onde uma string veio, escolha o esquema que bate com o produtor, não o que bate com o seu humor.
Base64 URL-safe e JWTs
Dois caracteres do alfabeto padrão causam problema assim que os dados precisam viajar por uma URL. Um + numa query string é reinterpretado como espaço com toda regularidade até alguém ler, e / é o separador de caminho, então ele nem pode aparecer num segmento de URL. O RFC 4648, seção 5, resolve isso trocando os dois últimos símbolos do alfabeto: + vira - e / vira _. O nome que você mais vai ouvir é base64url, e no Kotlin é Base64.UrlSafe.
O maior consumidor de base64url é o JSON Web Token. Um JWT na forma compacta é três partes base64url unidas por pontos: header.payload.signature. O RFC 7515 especifica essas partes como base64url sem padding, que é uma segunda diferença em relação ao alfabeto simples, e não só os caracteres. Aqui está um token sendo desembrulhado para inspeção:
import kotlin.io.encoding.Base64
fun main() {
val token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
val (header, payload, signature) = token.split(".")
val lenient = Base64.UrlSafe.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL)
println(lenient.decode(header).decodeToString())
// {"alg":"HS256"}
println(lenient.decode(payload).decodeToString())
// {"sub":"1234567890","name":"John Doe"}
println(signature.length) // 43
}
Dois pontos para notar. As partes do token não trazem padding, mas o Base64.UrlSafe de fábrica exige padding, então a linha withPadding(PRESENT_OPTIONAL) está fazendo trabalho de verdade: ela aceita entrada com padding e sem padding. E o split(".") com destructuring é Kotlin puro fazendo o que o formato pede. Um sério aviso: desembrulhar um JWT é para olhar um token, não para confiar nele. O header e o payload são dados comuns depois de decodificados; só uma assinatura verificada diz que o token é genuíno, e para isso você quer uma biblioteca de JWT de verdade, não divisão de string feita à mão.
Modos de padding e o controle de estritabilidade
Padding não é um fato fixo sobre o Base64 no Kotlin; é um ajuste. Toda instância carrega um PaddingOption, as quatro instâncias prontas começam em PRESENT, e withPadding te entrega uma instância nova com um ajuste diferente, deixando a original intocada. Aqui está o controle, opção por opção:
| Opção | Entrada sem padding | Entrada com padding correto |
|---|---|---|
PRESENT (padrão em todo lugar) |
Lança exceção | Decodifica |
ABSENT |
Decodifica | Lança exceção |
PRESENT_OPTIONAL |
Decodifica | Decodifica |
ABSENT_OPTIONAL |
Decodifica | Decodifica |
import kotlin.io.encoding.Base64
fun main() {
val data = "Hello".encodeToByteArray()
println(Base64.Default.withPadding(Base64.PaddingOption.ABSENT).encode(data))
// SGVsbG8
println(Base64.Default.encode(data))
// SGVsbG8=
val eitherWay = Base64.Default.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL)
println(eitherWay.decode("SGVsbG8").decodeToString()) // Hello
println(eitherWay.decode("SGVsbG8=").decodeToString()) // Hello
}
No lado da decodificação, PRESENT_OPTIONAL é a sua rede de segurança: é a opção que diz "eu não sei se o remetente colocou padding, e eu pretendo continuar funcionando". As mensagens de erro das outras combinações são incomumente úteis, então você vai reconhecê-las na hora quando um decodificador estrito encontrar a entrada errada: padding ausente sob PRESENT produz The padding option is set to PRESENT, but the input is not properly padded, e padding sob ABSENT produz The padding option is set to ABSENT, but the input has a pad character at index 7. Um comportamento merece destaque porque pega as pessoas de surpresa: um padding dobrado como SGVsbG8== não é "extra, mas tudo bem". O primeiro = encerra os dados, e o segundo é um caractere num lugar onde se esperava dados, então até os decodificadores mais tolerantes recusam.
Há também uma história de versões escondida aqui. Se você herdou código escrito contra a API experimental 1.8.x, lembre que o decode antigo aceitava entrada com ou sem padding. No Kotlin 2.0.20, o Default mudou para a regra estrita PRESENT, então uma entrada sem padding que funcionava antes agora lança exceção ao atualizar além daquele release. O conserto é uma linha: withPadding(Base64.PaddingOption.PRESENT_OPTIONAL), ou normalize suas entradas antes de decodificar.
De bytes a texto: charsets e Unicode
Quando você tem o seu ByteArray, a pergunta é o que ele significa. Se o payload é texto, a resposta padrão é decodeToString(), que interpreta os bytes como UTF-8 e funciona em toda plataforma. Para o caso comum de APIs modernas, e-mails e dados web, é tudo o que você vai precisar, e com emoji:
import kotlin.io.encoding.Base64
fun main() {
val original = "héllo 😀"
val packed = Base64.encode(original.encodeToByteArray())
println(packed) // aMOpbGxvIPCfmIA=
println(Base64.decode(packed).decodeToString()) // héllo 😀
}
No instante em que o remetente usou qualquer coisa além de UTF-8, porém, a decisão do charset é sua. As conversões de texto embutidas no Kotlin são só UTF-8 de propósito: decodeToString() não tem parâmetro de charset, e não existe função de string para bytes que tenha um. No JVM você desce para a API de charset da plataforma, que é honesta e explícita:
import kotlin.io.encoding.Base64
import java.nio.charset.Charset
fun main() {
val latinOne = "héllo".toByteArray(Charsets.ISO_8859_1)
val packed = Base64.encode(latinOne)
println(packed) // aOlsbG8=
val asUtf8 = Base64.decode(packed).decodeToString()
val asLatin = String(Base64.decode(packed), Charsets.ISO_8859_1)
println(asUtf8) // h?llo (o byte do é não é UTF-8 válido)
println(asLatin) // héllo
val byName = String(Base64.decode(packed), Charset.forName("ISO-8859-1"))
println(byName) // héllo
}
Esse ? na linha do meio não é problema de fonte; é o U+FFFD, o caractere de substituição do Unicode, ocupando o lugar de um byte que não forma UTF-8 válido. Se você vir uma string deles depois de decodificar, o seu payload está bem - a sua suposição de charset é que não está. Note também a assimetria que morde as pessoas: no lado da codificação a extensão do JVM toByteArray(charset) existe; no lado da decodificação o construtor correspondente é String(bytes, charset). Nenhum dos dois aceita um nome de charset; para isso você precisa de Charset.forName("..."), que lança UnsupportedCharsetException para um nome inventado, então um erro de digitação num valor de configuração falha rápido, em vez de escolher silenciosamente outra codificação.
Enquanto estamos na terra dos bytes, uma armadilha específica do Kotlin: um Char é um valor de 16 bits, e toByte() nele guarda em silêncio só os oito bits baixos. Se você montar bytes à mão a partir de caracteres, "中".first().code.toByte() te dá 45, um número que não tem nada a ver com o caractere. O caminho correto é sempre encodeToByteArray(), que faz o trabalho de codificação de verdade - o mesmo caractere são três bytes UTF-8, e a forma Base64 dele é 5Lit. Deixe a biblioteca padrão codificar; nunca empacote caracteres em bytes à mão.
Arquivos, substrings e entradas grandes
Dados Base64 não são sempre uma string arrumadinha em memória. Às vezes é um arquivo, um pedaço de uma resposta maior, ou grande demais para segurar tudo de uma vez. O Kotlin te dá as três portas.
Arquivos são o caso chato no melhor sentido: lê os bytes, decodifica, pronto. As duas APIs padrão de arquivo funcionam, seja qual for a que o seu projeto já usa:
import java.io.File
import kotlin.io.encoding.Base64
import kotlin.io.path.Path
import kotlin.io.path.readBytes
fun main() {
val fromFile = File("payload.b64").readBytes()
println(Base64.decode(fromFile.decodeToString()).size) // contagem de bytes decodificados
val fromPath = Path("payload.b64").readBytes()
println(Base64.decode(fromPath.decodeToString()).size) // mesmo número
}
Substrings é onde os overloads de CharSequence pagam a conta. decode aceita qualquer sequência de caracteres com índice de início e fim, então você pode entregar a ela um pedaço de um corpo de resposta longo sem fazer uma cópia do pedaço antes:
import kotlin.io.encoding.Base64
fun main() {
val body = "prefix junk SGVsbG8= trailing junk"
val bytes = Base64.decode(body, 12, 20)
println(bytes.decodeToString()) // Hello
}
Se você já sabe o tamanho da saída e quer reutilizar um buffer, decodeIntoByteArray escreve num array de destino à sua escolha e te conta quantos bytes escreveu. Dê a ela um buffer pequeno demais e ela lança IndexOutOfBoundsException com a capacidade necessária na mensagem, então o erro serve também de dica de dimensionamento.
Para streams realmente grandes no JVM, existe uma terceira porta: os decodificadores em streaming. Eles ainda estão marcados como experimentais - daí a anotação de opt-in - e existem só para o JVM, mas decodificam na hora, em vez de segurar tudo em memória:
import java.io.ByteArrayInputStream
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.io.encoding.decodingWith
@OptIn(ExperimentalEncodingApi::class)
fun main() {
val stream = ByteArrayInputStream("SGVsbG8gV29ybGQh".toByteArray())
stream.decodingWith(Base64.Default).use {
println(it.readBytes().decodeToString()) // Hello World!
}
}
Dois detalhes práticos. As funções extensão moram no nível superior do pacote, então você as importa pelo nome (um star import também funciona, mas nomes são mais amigáveis). E o decodificador trata o padding como parada dura: se o stream de origem continua depois da seção Base64, a leitura do stream decodificado termina no = e os bytes restantes continuam disponíveis no stream original. Isso torna a coisa arrumadinha para formatos que grudam Base64 na frente de outra coisa.
Nas trincheiras: APIs HTTP e corpos JSON
JSON não carrega bytes crus - é um protocolo de texto - então APIs que precisam mover binário (imagens, certificados, blobs qualquer) quase sempre o embrulham em Base64 dentro de um campo de string. O padrão é: fazer o parse do JSON, pegar o campo, decodificar. Com a biblioteca oficial de serialização, a parte do JSON está a duas anotações de distância:
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlin.io.encoding.Base64
@Serializable
data class ImageResponse(val name: String, val data: String)
fun main() {
val body = """{"name":"icon.png","data":"iVBORw0KGgo="}"""
val response = Json.decodeFromString<ImageResponse>(body)
val bytes = Base64.decode(response.data)
println("${response.name}: ${bytes.size} bytes") // icon.png: 8 bytes
}
Este exemplo precisa do plugin e da biblioteca de serialização, adicionados uma vez no build:
plugins {
kotlin("jvm") version "2.4.10"
kotlin("plugin.serialization") version "2.4.10"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
}
Sem a biblioteca a mesma ideia funciona na string crua, o que é útil para scripts rápidos: puxe o campo com substringBetween e decodifique. As armadilhas são as conhecidas de API: o campo pode na verdade ser um data URL completo (com o prefixo data:image/png;base64,, tratado mais adiante neste artigo), o payload pode estar embrulhado em MIME com quebras de linha, e o payload codificado pode ser mais ou menos um terço maior que o binário original, então fique de olho no orçamento de memória em respostas grandes.
Nas trincheiras: e-mail e entradas embrulhadas em MIME
E-mail é um mundo de texto de 7 bits, e a resposta do RFC 2045 para anexos binários é Base64 com um truque: a saída codificada deve ser quebrada para que nenhuma linha passe de 76 caracteres. Se você já recebeu um anexo como texto, é por isso que ele parece uma coluna de Base64 indentada. Para exatamente essa entrada, Base64.Mime é o decodificador certo, porque ele ignora separadores de linha e outros caracteres fora do alfabeto conforme vai:
import kotlin.io.encoding.Base64
fun main() {
val wrapped = "SGVs\nbG8=\r\n"
println(Base64.Mime.decode(wrapped).decodeToString()) // Hello
val withJunk = "Y@{mFz!Z!TY}0"
println(Base64.Mime.decode(withJunk).decodeToString()) // base64
}
A tolerância é real, mas com limites. Embrulhe a entrada, polvilhe um ou dois espaços, sem problema. Acrescente um caractere de dados depois do = final, porém, e até o Mime lança exceção: Symbol 'e'(145) at index 7 is prohibited after the pad character. E lembre que o Mime ainda exige que o padding esteja presente e correto; um decodificador MIME que também engolisse padding faltante seria pedir problema. A receita prática para payloads de e-mail de entrada bagunçados é decodificar com Mime primeiro, e se lançar exceção, olhar a mensagem - ela te conta exatamente qual símbolo, em qual índice, quebrou as regras.
Nas trincheiras: imagens e data URLs
Um data URL é o jeito da web de embutir um arquivo direto num documento: um tipo de mídia, um marcador base64, e o payload, tudo numa string só. Navegadores, CSS e interfaces embutidas adoram para assets pequenos - ícones, avatares, gráficos de placeholder - porque não há um segundo request a fazer. O formato parece assim:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==
Decodificar um no Kotlin é uma operação de string seguida de um decode Base64. O prefixo não guarda segredo; tudo depois da vírgula é o payload:
import kotlin.io.encoding.Base64
fun main() {
val dataUrl = "data:image/png;base64,iVBORw0KGgo="
val mediaType = dataUrl.substringBefore(";")
val packed = dataUrl.substringAfter("base64,")
val bytes = Base64.decode(packed)
println(mediaType) // data:image/png
println(bytes.size) // 8
println(bytes.contentToString()) // [-119, 80, 78, 71, ...]
}
Esse primeiro byte, -119 (que é 0x89), seguido das letras PNG, é o número mágico que identifica um arquivo PNG. Checar os primeiros quatro ou oito bytes depois de decodificar é um jeito barato de confirmar que um data URL de verdade contém o que o prefixo dele afirma. Dois avisos honestos: o Base64 adiciona mais ou menos um terço ao tamanho, então um data URL é uma troca de tamanho que você faz contra uma ida e volta de rede, e para qualquer coisa grande quase sempre é melhor servir o arquivo de uma URL de verdade e deixar o cache fazer o trabalho.
Nas trincheiras: configuração, variáveis de ambiente e bancos de dados
Base64 aparece em arquivos de configuração e variáveis de ambiente sempre que um valor binário precisa viajar por um canal só de texto: um ícone pequeno embutido num arquivo de properties, um token guardado numa variável de ambiente num container, um blob de bytes estacionado numa coluna de texto porque o schema é anterior a um tipo binário de verdade. O lado da decodificação são os mesmos dois passos em todo lugar - lê o texto, decodifica:
import kotlin.io.encoding.Base64
fun main() {
val line = "icon: UE5HREFUQQ=="
val packed = line.substringAfter("icon: ").trim()
val bytes = Base64.decode(packed)
println(bytes.decodeToString()) // PNGDATA
val fromEnv: String? = System.getenv("MY_ICON_B64")
if (fromEnv != null) {
println(Base64.decode(fromEnv).size)
}
}
A armadilha de todo esse reduto cabe em uma linha: Base64 não é criptografia. É um truque de transporte, não uma fechadura. Ninguém deve ler um valor Base64 e achar que os dados de dentro estão escondidos; está a uma chamada de função de ficar visível, e fica visível em toda linha de log que você escreve. Se um valor é sensível, mantenha ele sensível do início ao fim - um cofre de segredos, uma coluna cifrada, o que a sua stack fornecer - e use Base64 só para fazer os bytes viajarem por texto, não para protegê-los.
Nas trincheiras: a linha de comando
O caso de uso mais antigo de todos: transformar um blob Base64 na linha de comando num arquivo. Uma ferramenta completa são oito linhas de Kotlin, porque a biblioteca padrão faz o trabalho pesado. Compile uma vez com o compilador do Kotlin e é sua para sempre:
import java.io.File
import kotlin.io.encoding.Base64
fun main(args: Array<String>) {
val packed = if (args.isNotEmpty()) args[0] else readlnOrNull().orEmpty()
val bytes = Base64.decode(packed.trim())
File("decoded.bin").writeBytes(bytes)
println("Wrote ${bytes.size} bytes to decoded.bin")
}
Execute com um argumento para um valor pontual, ou mande um arquivo por pipe para trabalho em lote: o programa lê o primeiro argumento se existir e cai para a entrada padrão caso contrário. O trim() está fazendo serviço silencioso aqui, porque argumentos de shell e valores colados adoram chegar com espaço em branco solto que o decodificador estrito rejeitaria. E se os seus payloads são base64url, troque Base64.decode por Base64.UrlSafe.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL).decode e a ferramenta fica pronta para tokens também.
Um guia de campo para falhas de decodificação
Cada decodificador deste guia falha com um de dois tipos de exceção, e toda mensagem é específica o bastante para te contar exatamente o que deu errado. Aqui está o mapa completo, com as mensagens exatas que a biblioteca padrão produz:
| Situação | Exceção | Mensagem (como produzida) |
|---|---|---|
| Caractere fora do alfabeto (espaço, quebra de linha, símbolo do esquema errado) | IllegalArgumentException |
Invalid symbol ' '(40) at index 5 |
| Caractere de dados depois do padding | IllegalArgumentException |
Symbol 'e'(145) at index 7 is prohibited after the pad character |
Padding ausente enquanto a opção é PRESENT |
IllegalArgumentException |
The padding option is set to PRESENT, but the input is not properly padded |
Padding presente enquanto a opção é ABSENT |
IllegalArgumentException |
The padding option is set to ABSENT, but the input has a pad character at index 7 |
| Índice fora dos limites da origem | IndexOutOfBoundsException |
startIndex: 0, endIndex: 100, size: 8 |
startIndex maior que endIndex |
IllegalArgumentException |
startIndex: 3 > endIndex: 2 |
Buffer de destino pequeno demais para decodeIntoByteArray |
IndexOutOfBoundsException |
The destination array does not have enough capacity, destination offset: 0, destination size: 2, capacity needed: 8 |
Note o padrão nas duas primeiras linhas: a mensagem nomeia o símbolo culpado, o código numérico dele entre parênteses e o índice. Isso é um presente de depuração. Quando um decode lança exceção em produção, registre os primeiros dezenas de caracteres da entrada e o índice da mensagem, e quase sempre você encontra o culpado em segundos, seja uma quebra de linha colada, um payload truncado ou uma string base64url que entrou perdida num decodificador padrão.
Armadilhas que pegam especificamente desenvolvedores Kotlin
- Pressupor que o payload é texto.
decoderetorna umByteArrayde propósito. ChamardecodeToString()num JPEG porque "provavelmente é texto" te dá uma parede de caracteres de substituição. Decida o que os bytes são antes de convertê-los. - Espaço em branco de copiar e colar. O decodificador padrão é estrito, e um valor puxado de uma mensagem de chat ou de um log quase sempre chega com uma quebra de linha no final ou um espaço no início. Faça trim antes de decodificar, ou decodifique pelo
Mime, ou aceite oIllegalArgumentExceptione trate ele. - Atualizando da era experimental. Código escrito para a API experimental 1.8.x carregava anotações
@OptIn(ExperimentalEncodingApi::class)e contava com o padding ser opcional. A partir do 2.0.20, a mesma entrada pode lançar exceção. O conserto éPRESENT_OPTIONAL, ou limpar as entradas antes de chegarem ao decodificador. - Casando o esquema com o produtor errado. Uma parte de JWT decodificada com
Base64.Defaultfalha nos caracteres-e_; um payload do alfabeto padrão decodificado comUrlSafefalha em+e/. A exceção nomeia o símbolo exato, mas o conserto é saber de onde a string veio. - A lacuna do charset.
decodeToString()é só UTF-8, sem overload para outras codificações. Se o remetente usou Latin-1 ou Windows-1252, planejeString(bytes, charset)no JVM, e espere caracteres de substituição U+FFFD como sintoma quando esquecer. - O compilador do pacote de distribuição.
apt install kotlinno Debian e no Ubuntu serve a 1.3.31, de antes da existência desta API. Se os seus exemplos de repente se recusam a compilar com "unresolved reference", confira qual compilador está de fato no PATH.
Boas práticas para decodificar
- Decodifique para bytes primeiro, interprete depois. Mantenha
Base64.decodee a conversão para texto como passos separados. Isso torna o charset explícito, mantém payloads binários binários, e torna os testes triviais: compare arrays de bytes, não strings. - Escolha a instância que bate com o produtor. JWT e dados que vão para URL significam
UrlSafe; e-mail e arquivos PEM significamMimeouPem; tudo o mais começa noDefault. Os decodificadores tolerantes são para entrada conhecida por ser bagunçada, não uma rede de segurança geral. - Normalize entrada não confiável uma vez, barato. Um
trim()e, onde o formato é conhecido por ser limpo, uma remoção de espaços em branco antes de um decode estrito pega mais falhas do mundo real do que qualquer quantidade de try-catch pegaria. Um helper pequeno com fallbackPRESENT_OPTIONALé um bom padrão para valores de fontes desconhecidas. - Orce o tamanho antes de alocar. A saída decodificada tem no máximo três quartos do comprimento da entrada (quatro símbolos carregam três bytes), então um checadura rápida de comprimento te dá o tamanho do destino antes de decodificar, que é exatamente o que você quer antes de encher um buffer pré-alocado ou aceitar uma string de vários megabytes.
- Confie na mensagem de erro. A biblioteca padrão reporta o símbolo, o código dele e o índice. Registre a vizinhança daquele índice para entrada não confiável e pare de chutar.
- Não decodifique para esconder coisas, e não decodifique para provar coisas. Base64 é uma codificação de transporte. Não adiciona sigilo nem integridade; se você precisa de um dos dois, isso é trabalho de criptografia, não do decodificador.
Como o Base64 chegou ao Kotlin
Base64 é mais antigo que o Kotlin por algumas décadas - a especificação MIME que deu a regra da linha de 76 caracteres data de 1993, e o alfabeto em si de RFCs do meio dos anos 1990 - mas a história específica do Kotlin é curta e recente. O pacote kotlin.io.encoding chegou no Kotlin 1.8.20, em abril de 2023, trazendo o Base64 com três instâncias - Default, UrlSafe e Mime - atrás da anotação @ExperimentalEncodingApi, junto com as extensões de streaming somente para JVM que ainda são experimentais hoje. Por dois anos, usar significava uma linha de opt-in em toda função e uma pequena chance de a API se mover.
O Kotlin 2.2.0, lançado em junho de 2025, mudou o contrato. A API inteira ficou estável num release só, e a instância Pem entrou na família (a variante de linhas de 64 caracteres do RFC 1421, usada ao redor do PKI). A estritabilidade que faz código com padding opcional da era 1.8 precisar de atenção depois de uma atualização chegou um passo antes: no 2.0.20, quando o withPadding com seus quatro valores de PaddingOption substituiu o comportamento fixo antigo e o decodificador passou a exigir padding. O release 2.2 também estabilizou a classe vizinha HexFormat em kotlin.text, a API de formatação hexadecimal que era experimental desde o Kotlin 1.9, então as codificações textuais em nível de byte agora têm um endereço fixo na biblioteca padrão. E numa nota de manutenção: desde o Kotlin 2.4.0 a biblioteca padrão do JVM é lançada com uma janela de suporte de 18 meses por linha de release, que é mais um motivo para um projeto na linha 2.4.x atual tratar esta API como um ponto fixo, não um ponto móvel.
Fatos curiosos
- O objeto companion faz um serviço. Como
Defaulté o companion doBase64, o nome da classe faz as vezes da instância padrão:Base64.decode(x)eBase64.Default.decode(x)são a mesma chamada. É por isso que o exemplo de duas linhas no topo deste artigo continua com duas linhas. - Decodificar string ganha um atalho de velocidade no JVM. O loop comum de decodificação trabalha em bytes, mas strings do Kotlin são sequências de caracteres. A implementação do JVM dá a volta na conversão reinterpretando os caracteres de uma
Stringcomo valores de byte único ISO-8859-1 antes do loop compartilhado rodar - um truque que os comentários do código-fonte dizem ser até dez vezes mais rápido que o caminho comum, e é por isso quedecode(String)parece instantâneo mesmo em payloads longos. - O nome do pacote é uma dica. Você vai encontrar esta API em
kotlin.io.encoding, não emkotlin.text, porque o ponto inteiro é que os dados são bytes - entrada e saída têm formato de E/S, e o texto é só o que acontece com o resultado depois. - As mensagens de erro incluem o código do caractere.
Symbol 'e'(145)reporta o valor do símbolo culpado em octal, não só o glifo. Útil quando o culpado é espaço em branco:' '(40)te conta que era um espaço bem antes de você desconfiar de um. - PEM chegou tarde.
Base64.Pemnão fazia parte da API original do 1.8.20; apareceu com a estabilização do 2.2. Se um post de blog de 2023 ou 2024 lista só três instâncias, ele não está errado - está só dois releases defasado. - É escrito por plataforma, não delegado. A biblioteca padrão implementa o codec separadamente para cada alvo com funções expect/actual. No JVM existe até uma otimização comentada que entregaria o trabalho para o
java.util.Base64, desativada atrás de uma issue aberta do compilador, que é por isso que o comportamento da implementação do Kotlin é o comportamento de referência em toda plataforma.
Para fechar
Decodificar Base64 no Kotlin se reduz a uma lista curta de escolhas deliberadas: a instância que bate com o lugar de onde os dados vieram, o modo de padding que bate com o jeito como foram enviados, o buffer ou stream que bate com o tamanho que têm, e o charset que bate com o que significam. A biblioteca padrão te entrega as quatro como funções simples, sem dependências, e as mensagens de erro são específicas o bastante para uma falha ser um diagnóstico, não um mistério. A direção oposta - escolher o esquema, o padding e a quebra de linha certos quando você é quem produz o Base64 - tem as próprias decisões e as próprias armadilhas, e o artigo relacionado no site irmão cobre a codificação Base64 no Kotlin em profundidade.
Última atualização: 2026-09-08
Artigo relacionado: Codificação Base64 em Kotlin: um guia completo