Base64 com acentos: como codificar e decodificar sem erro

O Base64 codifica bytes, não texto, então o que define o resultado é a codificação do texto antes da conversão. Com UTF-8, acentos e emojis voltam iguais. Este guia explica por que o btoa erra com acentos e emojis e mostra como codificar e decodificar Base64 e Base64URL em JavaScript, Python e PHP.

Por Leonardo Gaertner · Publicado em

O que o Base64 faz

O Base64 representa uma sequência de bytes usando só 64 caracteres seguros (letras, números, + e /), com = no fim quando falta completar. Ele aumenta o tamanho em cerca de um terço e não protege nada: qualquer pessoa decodifica. Não é criptografia.

Como ele trabalha com bytes, o texto precisa virar bytes antes. Letras com acento ocupam mais de um byte em UTF-8, e é aí que os erros aparecem. Para testar um valor agora, use o codificador de Base64 ou o decodificador de Base64.

Por que o btoa erra com acentos e emojis

No navegador, o btoa trata cada caractere como um byte (até o código 255, o Latin-1). Com btoa("ação") não há erro, mas o resultado é diferente do Base64 em UTF-8 que o resto do mundo espera, e a outra ponta decodifica letras trocadas. Com um caractere fora do Latin-1, como btoa("✓") ou um emoji, ele falha com InvalidCharacterError. O atob tem o mesmo limite na volta.

A solução é converter o texto em bytes UTF-8 com TextEncoder, codificar esses bytes e, na volta, usar TextDecoder:

JavaScript
function codificarBase64(texto) {
  const bytes = new TextEncoder().encode(texto);
  let binario = '';
  bytes.forEach((b) => (binario += String.fromCharCode(b)));
  return btoa(binario);
}

function decodificarBase64(base64) {
  const binario = atob(base64);
  const bytes = Uint8Array.from(binario, (c) => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

Funciona no navegador e também no Node.js.

Em Node.js

No Node.js o Buffer já cuida da codificação do texto, e o UTF-8 é o padrão:

JavaScript
function codificarBase64(texto) {
  return Buffer.from(texto, 'utf8').toString('base64');
}

function decodificarBase64(base64) {
  return Buffer.from(base64, 'base64').toString('utf8');
}

Em Python e PHP

Em Python, base64.b64encode recebe bytes: chame encode("utf-8") antes e decode("utf-8") depois. Em PHP, as strings já são sequências de bytes, então base64_encode funciona direto, e vale usar o modo estrito no base64_decode.

Python
import base64


def codificar_base64(texto: str) -> str:
    return base64.b64encode(texto.encode("utf-8")).decode("ascii")


def decodificar_base64(b64: str) -> str:
    return base64.b64decode(b64).decode("utf-8")
PHP
<?php
function codificarBase64(string $texto): string
{
    return base64_encode($texto);
}

function decodificarBase64(string $b64): string
{
    $saida = base64_decode($b64, true); // true: recusa caracteres que não são Base64
    if ($saida === false) {
        throw new InvalidArgumentException('Base64 inválido');
    }
    return $saida;
}

Base64URL: a variante para endereços e tokens

O + e o / têm significado em endereços, então a variante Base64URL troca + por - e / por _ e costuma omitir o = do fim. É o formato usado nas partes de um JWT (veja o guia de JWT). No Node.js, Buffer.toString("base64url") já faz isso. Nas outras linguagens, a troca é simples:

JavaScript
function paraBase64Url(base64) {
  return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function deBase64Url(url) {
  const base64 = url.replace(/-/g, '+').replace(/_/g, '/');
  return base64 + '='.repeat((4 - (base64.length % 4)) % 4);
}

Converte de e para o Base64 comum. O preenchimento com = é refeito na volta.

Python
import base64


def codificar_base64url(dados: bytes) -> str:
    return base64.urlsafe_b64encode(dados).decode("ascii").rstrip("=")


def decodificar_base64url(texto: str) -> bytes:
    return base64.urlsafe_b64decode(texto + "=" * (-len(texto) % 4))
PHP
<?php
function codificarBase64Url(string $dados): string
{
    return rtrim(strtr(base64_encode($dados), '+/', '-_'), '=');
}

function decodificarBase64Url(string $texto): string
{
    $b64 = strtr($texto, '-_', '+/');
    return base64_decode($b64 . str_repeat('=', (4 - strlen($b64) % 4) % 4), true);
}

Cuidados

  • Não use Base64 para esconder senhas ou dados sensíveis: ele só muda a representação.
  • Ao decodificar um texto de origem desconhecida, trate o erro: valores com caracteres inválidos ou tamanho errado não decodificam.
  • Quebras de linha no meio do Base64 (comum em e-mail e certificados) precisam ser removidas antes de decodificar em algumas bibliotecas.
  • Se o resultado decodificado não for texto, como uma imagem, trate-o como bytes e não como string.

Achou um erro neste guia? Avise por e-mail.

Perguntas frequentes

Base64 é criptografia?

Não. É só uma forma de representar bytes com caracteres comuns, e qualquer pessoa decodifica sem chave. Para proteger dados, use criptografia de verdade.

Por que o Base64 termina com um ou dois sinais de igual?

O Base64 trabalha em blocos de 3 bytes. Quando o último bloco tem menos que isso, o sinal de igual completa o tamanho: um quando faltou um byte e dois quando faltaram dois. Na variante URL, ele costuma ser omitido.

Qual a diferença entre Base64 e Base64URL?

O Base64URL troca o mais e a barra por hífen e sublinhado, para não atrapalhar endereços e nomes de arquivo, e costuma omitir o preenchimento com igual. O conteúdo codificado é o mesmo.

O Base64 aumenta o tamanho dos dados?

Sim, em cerca de 33%: a cada 3 bytes originais, ele gera 4 caracteres.