Base64 vs Base64URL: o que muda e quando usar cada um

Base64URL é o Base64 padrão com dois caracteres trocados, - no lugar de + e _ no lugar de /, e normalmente sem o preenchimento com =.

As duas codificações são definidas na RFC 4648: o Base64 padrão na seção 4 e a variante "URL and filename safe", a base64url, na seção 5. Elas dividem os dados em grupos de 6 bits exatamente da mesma forma e compartilham 62 dos seus 64 caracteres. Só mudam os caracteres dos valores 62 e 63, além da convenção sobre o preenchimento (padding). Por isso converter de uma para a outra é só questão de trocar caracteres, sem recodificar os dados, e por isso misturar as duas é um bug fácil de cometer e que muitas vezes passa despercebido.

Os dois alfabetos lado a lado

Base64 (RFC 4648, seção 4)Base64URL (RFC 4648, seção 5)
Valores de 0 a 61A-Z a-z 0-9A-Z a-z 0-9
Valor 62+-
Valor 63/_
Preenchimento com =Obrigatório, a menos que o formato que o usa diga o contrárioNormalmente omitido
Seguro em URLs e nomes de arquivo sem alteraçãoNãoSim

Os mesmos bytes nas duas formas:

Bytes       fb ff bf
Base64      +/+/
Base64URL   -_-_

Text        <<???>>
Base64      PDw/Pz8+Pg==
Base64URL   PDw_Pz8-Pg

Dados binários codificados, como hashes, chaves e tokens aleatórios, muito frequentemente contêm um + ou uma / em algum ponto, então a diferença está longe de ser um caso raro. A própria RFC 4648 diz que base64url não deve ser considerada a mesma codificação que base64, e que o código deve nomear a variante que espera.

Por que o Base64 padrão quebra em URLs e nomes de arquivo

Cada um dos três caracteres extras do Base64 padrão tem um significado em outro contexto:

  • + é lido como espaço quando uma query string é decodificada como dados de formulário, que é o que fazem o URLSearchParams, o $_GET do PHP e a maioria dos frameworks de servidor. Um token como ab+c chega como ab c.
  • / separa segmentos de caminho, então um valor Base64 em um caminho cria segmentos extras, e alguns servidores web recusam a forma codificada %2F em caminhos. Esse caractere também não é permitido em nomes de arquivo no Windows, no macOS nem no Linux.
  • = separa o nome de um parâmetro do seu valor.

Codificar o valor em percentual (%2B, %2F, %3D) funciona quando todas as partes do código envolvidas fazem isso exatamente uma vez. Na prática, uma camada esquece ou codifica duas vezes, e é por isso que formatos pensados para URLs usam Base64URL e dispensam o escape por completo.

Regras de preenchimento

O Base64 codifica 3 bytes em 4 caracteres. Quando o tamanho dos dados não é múltiplo de 3, o último grupo fica mais curto, e o Base64 padrão o completa até 4 caracteres com =:

Bytes no último grupoBase64Base64URL sem preenchimento
1, por exemplo ff/w==_w
2, por exemplo fb ff+/8=-_8
3, por exemplo fb ff bf+/+/-_-_

A seção 3.2 da RFC 4648 exige preenchimento, a menos que a especificação que a referencia diga explicitamente o contrário. A seção 5 acrescenta que o = normalmente precisaria ser codificado em percentual em uma URL e que o preenchimento pode ser omitido quando o tamanho dos dados já é conhecido. Os formatos baseados em Base64URL aproveitam essa opção: as JSON Web Signatures (RFC 7515), e portanto os JWTs, removem todos os = finais, e os code challenges do PKCE (RFC 7636) fazem o mesmo.

Sem preenchimento, n bytes produzem ⌈4n/3⌉ caracteres: 16 bytes viram 22 caracteres em vez de 24, e 32 bytes viram 43 em vez de 44. Um comprimento que deixa resto 1 na divisão por 4 nunca é válido em nenhuma das duas variantes.

Onde cada um é usado

ContextoVariante
Cabeçalho, payload e assinatura de um JWTBase64URL sem preenchimento
Valores de JSON Web Key, como n e eBase64URL sem preenchimento
code_challenge do OAuth PKCEBase64URL sem preenchimento
Data URIs, data:image/png;base64,...Base64 padrão (RFC 2397)
Anexos de e-mail (MIME)Base64 padrão, em linhas de no máximo 76 caracteres (RFC 2045)
Certificados e chaves PEMBase64 padrão, em linhas de 64 caracteres (RFC 7468)
Cabeçalho de autenticação HTTP BasicBase64 padrão (RFC 7617)
Tokens em URLs, nomes de arquivo, chaves de cacheBase64URL é a escolha segura

O comportamento padrão das bibliotecas segue o Base64 padrão: btoa, o b64encode do Python, o base64_encode do PHP e o Base64.getEncoder() do Java produzem +, / e preenchimento. O Base64URL precisa ser pedido explicitamente.

Converter de um para o outro

atob e btoa só conhecem o Base64 padrão, então com eles é você quem converte os caracteres e ajusta o preenchimento:

// Base64 -> Base64URL
const url = b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

// Base64URL -> Base64
let std = url.replace(/-/g, '+').replace(/_/g, '/');
std += '='.repeat((4 - std.length % 4) % 4);

Navegadores recentes também têm Uint8Array.prototype.toBase64({ alphabet: 'base64url', omitPadding: true }) e Uint8Array.fromBase64(s, { alphabet: 'base64url' }), que trabalham diretamente com bytes; confira o suporte nos navegadores que você precisa atender antes de depender deles. Outras plataformas têm suporte direto ou uma expressão curta:

// Node.js
Buffer.from(data).toString('base64url');     // no padding
Buffer.from(s, 'base64url');

# Python
base64.urlsafe_b64encode(data).rstrip(b'=')
base64.urlsafe_b64decode(s + '=' * (-len(s) % 4))

// PHP
rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
base64_decode(strtr($s, '-_', '+/'), true);

// Java
Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
Base64.getUrlDecoder().decode(s);

O urlsafe_b64decode do Python rejeita entradas sem preenchimento, daí os sinais = acrescentados. O base64_decode do PHP aceita a falta de preenchimento, mesmo no modo estrito.

Armadilhas

  • Bugs que passam nos testes. Um decodificador padrão só falha com entradas Base64URL que contêm - ou _. Com dados aleatórios, isso acontece em cerca de metade dos valores de 16 bytes e em cerca de três quartos dos valores de 32 bytes, como os digests SHA-256 (1 − (62/64) elevado a 42 ≈ 74%). Um teste com um ou dois valores de exemplo pode passar facilmente. Por que os decodificadores rejeitam essas strings, e como cada linguagem reage de um jeito diferente, é o assunto do guia String Base64 inválida.
  • Decodificadores tolerantes escondem a confusão. O Buffer.from(s, 'base64') do Node.js aceita os dois alfabetos, o que é conveniente, mas faz o bug aparecer só em outro serviço. O b64decode do Python, no modo padrão, descarta - e _ sem avisar e depois devolve bytes errados ou um erro enganoso sobre o número de caracteres.
  • Base64URL não é codificação de URL. Codificar em percentual uma string Base64 padrão gera %2B, %2F e %3D; isso não a transforma em Base64URL, e um decodificador que espera uma das formas não aceita a outra.
  • Nenhuma das variantes esconde nada. O payload de um JWT é só JSON codificado em Base64URL, legível por qualquer pessoa que tenha o token. Cole um token no decodificador JWT para ver o cabeçalho e o payload.

O codificador Base64 deste site produz Base64 padrão com preenchimento; aplique a substituição mostrada acima se precisar de Base64URL. Para decodificar, o decodificador Base64 aceita os dois alfabetos e acrescenta sozinho o preenchimento que falta, então você pode colar qualquer uma das formas.

Perguntas Frequentes

Qual é a diferença entre Base64 e Base64URL?

O Base64URL troca + por - e / por _, para que o resultado seja seguro em URLs e nomes de arquivo, e normalmente omite o preenchimento com =. Os outros 62 caracteres e a forma de dividir os dados em grupos de 6 bits são idênticos.

Um decodificador Base64 comum consegue ler Base64URL?

Só depois de uma conversão: troque - por + e _ por /, e acrescente = até o comprimento ser múltiplo de 4. Alguns decodificadores, como o Buffer do Node.js, aceitam os dois alfabetos diretamente.

Por que os JWTs usam Base64URL sem preenchimento?

Tokens circulam em URLs, cabeçalhos HTTP e cookies, onde +, / e = causam problemas. A RFC 7515 define a codificação das JSON Web Signatures como base64url com todos os caracteres = finais removidos.

O Base64URL é mais longo que o Base64?

Não. Os dois produzem os mesmos caracteres por byte; sem preenchimento, o Base64URL fica até dois caracteres mais curto.