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 61 | A-Z a-z 0-9 | A-Z a-z 0-9 |
| Valor 62 | + | - |
| Valor 63 | / | _ |
Preenchimento com = | Obrigatório, a menos que o formato que o usa diga o contrário | Normalmente omitido |
| Seguro em URLs e nomes de arquivo sem alteração | Não | Sim |
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 oURLSearchParams, o$_GETdo PHP e a maioria dos frameworks de servidor. Um token comoab+cchega comoab c./separa segmentos de caminho, então um valor Base64 em um caminho cria segmentos extras, e alguns servidores web recusam a forma codificada%2Fem 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 grupo | Base64 | Base64URL 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
| Contexto | Variante |
|---|---|
| Cabeçalho, payload e assinatura de um JWT | Base64URL sem preenchimento |
Valores de JSON Web Key, como n e e | Base64URL sem preenchimento |
code_challenge do OAuth PKCE | Base64URL 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 PEM | Base64 padrão, em linhas de 64 caracteres (RFC 7468) |
| Cabeçalho de autenticação HTTP Basic | Base64 padrão (RFC 7617) |
| Tokens em URLs, nomes de arquivo, chaves de cache | Base64URL é 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. Ob64decodedo 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,%2Fe%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.