Codificação Base64 em Bash: um guia completo
Você tem bytes e precisa de uma string. Um arquivo de texto que precisa morar dentro de um corpo JSON. Uma imagem que precisa caber numa linha de configuração. Um token que vai viajar através de uma URL, de uma variável de ambiente ou de um cabeçalho HTTP. Uma chave privada que pertence a um repositório de certificados. Este é o trabalho de todos os dias da codificação Base64 no shell, e a resposta do shell é um único comando, pequeno e notavelmente portável.
A troca numa só respiração: o Base64 reescreve cada três bytes de dados brutos como quatro caracteres de um alfabeto de 64 letras (A-Z, a-z, 0-9, mais + e /), enchendo a cauda com um ou dois sinais de = quando a contagem de bytes não é um múltiplo de três. A página inicial deste site explica o formato por completo; aqui gastamos nosso tempo produzindo o texto, escolhendo o dialeto certo para o destino e pagando a conta do tamanho de olhos abertos. Um número para guardar no bolso: a forma codificada é normalmente cerca de um terço maior que o original, quatro caracteres por três bytes, e ele fica voltando.
O elenco é pequeno. O comando base64 do coreutils (GNU ou a família mais nova Rust uutils), o basenc da mesma família para o dialeto URL-safe, o openssl base64 para máquinas sem coreutils, o applet do BusyBox para sistemas embutidos e o sabor BSD no macOS. Cinco ferramentas, um trabalho, algumas flags que valem a pena conhecer.
Escolha seu codificador
Cada uma dessas lê bytes do standard input ou de um arquivo e escreve texto no standard output, então todas entram nos mesmos pipelines. As diferenças são o embrulho de linha padrão e os dialetos disponíveis:
| Ferramenta | Onde mora | Embrulho de linha padrão | Recorra quando |
|---|---|---|---|
base64 (coreutils) |
Linux, e macOS via Homebrew | 76 caracteres | a escolha padrão; adicione -w 0 para uma linha |
basenc (GNU coreutils) |
Linux com coreutils | 76 caracteres | você precisa de --base64url, base32, base16 ou companhia |
openssl base64 |
em todo lugar onde o OpenSSL está instalado | 64 caracteres | coreutils ausente; -A para uma linha |
busybox base64 |
Alpine, Linux embutido | 76 caracteres | sistemas mínimos; as mesmas flags num corpo menor |
base64 (BSD/macOS) |
macOS, os BSDs | nenhum (uma linha longa só) | trabalho nativo no macOS; -b define a largura |
Leia aquela coluna de embrulho duas vezes, porque é a diferença silenciosa entre as famílias. O Coreutils e o BusyBox embrulham a 76 por padrão, o OpenSSL embrulha a 64, e a ferramenta BSD não embrulha de jeito nenhum. Nenhuma está errada; elas só herdaram convenções diferentes (o MIME diz 76, o PEM diz 64, e a ferramenta BSD é simplesmente mais antiga que o hábito de embrulhar). Quando o seu consumidor se importa, defina a largura explicitamente e nunca confie no padrão.
Texto primeiro: o newline invisível
Codificar texto num shell começa com uma armadilha: o echo adiciona um newline. Aquelas cinco letras "hello" viram seis bytes no momento em que passam pelo echo, e o sexto byte vai junto para a saída, invisível e permanente:
echo "hello" | base64
Isso imprime aGVsbG8K, e o caractere final codifica o newline. A correção é a que você deveria usar para texto onde a contagem de bytes importa: printf com um formato, sem decoração:
printf '%s' "hello" | base64
Agora a saída é aGVsbG8=, exatamente cinco bytes de conteúdo, e o último caractere é um sinal de padding em vez de um byte vivo. A mesma regra se aplica aos here-strings, que anexam um newline final como o echo: base64 <<< "hello" te dá a versão aGVsbG8K de novo. Na dúvida, pergunte a si mesmo o que é o último byte antes de codificá-lo.
Para qualquer coisa que você queira numa linha só, adicione -w 0 (ou os primos do -w 0 abaixo), o que também remove o newline final que o comando de outro modo emitiria:
printf '%s' "hello world and more" | base64 -w 0
Isso é uma linha limpa e ininterrupta sem newline final, pronta para cair numa URL, num valor JSON ou num arquivo de configuração sem qualquer outra cerimônia.
Arquivos e a largura do embrulho
Arquivos são o caso comum, e toda implementação aceita um argumento FILE, o que mantém os bytes completamente longe da mecânica de aspas do shell:
base64 -w 0 report.pdf > report.b64
Sem -w 0, a saída chega embrulhada a cada 76 caracteres, que é exatamente o que um consumidor MIME quer:
base64 report.pdf > report.mime.b64
A largura é um dial que você controla por consumidor. Setenta e seis é a convenção MIME do RFC 2045, sessenta e quatro é a convenção PEM usada por certificados e chaves, e zero significa uma linha ininterrupta para URLs e APIs:
base64 -w 64 key.bin | head -2
Se o consumidor mora no Windows e espera quebras de linha CRLF, converta depois do embrulho, não antes:
base64 -w 76 attachment.bin | sed 's/$/\r/' > attachment.crlf.b64
No caminho do OpenSSL, o equivalente do modo de linha única é a flag -A, que também suprime o newline final:
openssl base64 -A < report.pdf
Antes de embarcar um arquivo embrulhado, um check de sanidade de tamanho não custa nada e pega uma quantidade surpreendente de erros (arquivo codificado duas vezes, arquivo codificado com a entrada errada):
wc -c report.pdf
base64 report.pdf | wc -c
O segundo número deveria ser cerca de quatro terços do primeiro, mais um byte por linha embrulhada para o newline. Se estiver muito diferente, pare e veja o que você realmente passou ao codificador.
Base64 URL-safe: trocando os dois caracteres difíceis
Dois caracteres do alfabeto padrão, + e /, são os filhos problema: um + numa query string de URL significa espaço, um / pode parecer separador de caminho, e os dois forçam percent-encoding no momento em que a string entra numa URL, num cookie ou num nome de arquivo. A seção 5 do RFC 4648 resolve isso com um dialeto que substitui exatamente aqueles dois caracteres por - e _, e descarta o padding, porque uma URL raramente precisa anunciar o comprimento exato em bytes.
A receita do shell é uma troca mais um corte: uma passada no tr para o alfabeto e uma para remover o padding:
printf '\376\117\202' | base64 -w 0 | tr '+/' '-_' | tr -d '='
Aqueles três bytes normalmente codificam para /k+C, o que ficaria feio numa URL. O pipeline transforma em _k-C, quatro caracteres que podem viajar a qualquer lugar. A troca é posicional, então a direção é fácil de misturar: para codificar vai tr '+/' '-_' (mais vira traço, barra vira underline), e a reversa, que pertence à decodificação, vai tr '_-' '/+'. Uma direção trocada não dá erro, ela só produz bytes diferentes, que é o pior tipo de bug para embarcar.
O dialeto importa sempre que a string sai do controle do shell: segmentos de JWT, tokens em query strings, valores em cookies ou nomes de arquivo, e qualquer identificador que outro sistema vai ler como parte de uma URL. O basenc do GNU produz o dialeto nativamente, com o padding ainda no lugar:
printf '%s' "hello" | basenc --base64url
Remova o padding com tr -d '=' se o consumidor quiser a forma sem padding, como a maioria quer.
Cunhando um JWT no shell
JSON Web Tokens são o consumidor mais visível do Base64 URL-safe no mundo das APIs. Um JWT compacto é três segmentos base64url unidos por pontos: o cabeçalho, o payload e a assinatura, segundo o RFC 7515. Os dois primeiros são JSON puro, e a assinatura é um resumo binário dos dois primeiros segmentos unidos por um ponto, que é exatamente o tipo de coisa em que o openssl é bom.
key="supersecretkey"
h=$(printf '%s' '{"alg":"HS256","typ":"JWT"}' | base64 -w 0 | tr '+/' '-_' | tr -d '=')
p=$(printf '%s' '{"sub":"42","name":"homer"}' | base64 -w 0 | tr '+/' '-_' | tr -d '=')
s=$(printf '%s' "$h.$p" | openssl dgst -sha256 -hmac "$key" -binary | base64 -w 0 | tr '+/' '-_' | tr -d '=')
printf '%s.%s.%s\n' "$h" "$p" "$s"
Isso imprime um JWT HS256 compacto que qualquer biblioteca padrão em qualquer plataforma vai aceitar. Repare na divisão do trabalho: a parte Base64 é o alfabeto, a parte openssl dgst -sha256 -hmac é a criptografia, e a união por pontos é o formato. Mantenha os três papéis separados na cabeça e o pipeline fica óbvio.
Três avisos para o campo. Primeiro, o MAC é computado sobre o texto ASCII dos dois primeiros segmentos mais o ponto, então os segmentos já devem estar na forma final base64url quando você assina. Re-embrulhar ou re-padding depois de assinar quebra o token. Segundo, a chave fica fora do token: a assinatura prova quem assinou, a chave mantém o segredo. Terceiro, cunhar num script de shell é uma ferramenta de teste e automação, não um substituto para o servidor que vai de verdade emitir e verificar esses tokens, e um token cunhado com alg: none não prova nada.
Data URIs: arquivos viajando dentro de strings
O RFC 2397 define o esquema de URL data:, e sua forma Base64 deixa um arquivo morar dentro de uma URL: data:, então um media type opcional, então ;base64 quando o payload é codificado em Base64, então uma vírgula, então os dados. Omitir o media type faz o padrão virar text/plain;charset=US-ASCII, que é uma pegadinha que vale conhecer, porque a maioria das pessoas quer dizer uma imagem ou um documento JSON, não texto ASCII.
printf 'data:text/plain;base64,%s\n' "$(printf '%s' "hi there" | base64 -w 0)"
Isso imprime data:text/plain;base64,aGkgdGhlcmU=, uma URL completa e autocontida que um navegador vai exibir com prazer. Para uma imagem, a mesma forma com um media type de verdade:
printf 'data:image/png;base64,%s\n' "$(base64 -w 0 icon.png)" > icon.uri
Cole o resultado numa tag img do HTML, no src, ou num background de CSS, e a imagem viaja junto com o documento, sem segundo pedido HTTP. As armadilhas são todas sobre tamanho: o próprio RFC diz que o esquema só é útil para valores curtos, navegadores impõem seus próprios limites de comprimento de URL, cada byte inline custa a sobretaxa de 33 por cento por cima do tamanho próprio da imagem, e uma página cheia de data URIs é uma página sem história de cache para aquelas imagens. Para ícones pequenos e gráficos embutidos de uma vez só, é um prazer. Para uma biblioteca de fotos, é um imposto.
Segredos, configuração e variáveis de ambiente
O Base64 aparece no mundo de configuração e segredos por um motivo específico: ele transforma bytes arbitrários, incluindo espaços, aspas e newlines, numa string que sobrevive a um export, a uma linha de configuração ou a um campo JSON sem qualquer acrobacia de aspas. O Kubernetes é o exemplo mais visível: todo campo sob o .data de um secret é Base64, então criar um secret no shell é simplesmente codificar:
kubectl create secret generic app --from-literal=password='s3cret'
O API server guarda a senha como czNjcmV0 sob .data, e qualquer nó com acesso ao secret pode lê-la de volta com uma decodificação. O mesmo movimento funciona para os seus próprios arquivos de configuração:
export API_TOKEN_B64=$(printf '%s' "$API_TOKEN" | base64 -w 0)
Ou, para um arquivo que o aplicativo lê na inicialização:
printf 'token=%s\n' "$(printf '%s' "$API_TOKEN" | base64 -w 0)" >> app.conf
Aqui vem o aviso que pertence numa parede: Base64 é codificação, não criptografia. A seção de segurança do RFC 4648 é direta sobre isso, observando que a codificação "oculta visualmente informação de outro modo facilmente reconhecível, como senhas, mas não fornece nenhuma confidencialidade computacional", e que esse exato mal-entendido já causou incidentes de segurança reais quando alguém colou uma troca de protocolo "protegida" num relatório de bug e revelou as credenciais por acidente. Se o valor precisa ser segredo, criptografe-o (e depois Base64 o cifrário para armazenamento). Se Base64 é tudo o que você tem, trate o valor codificado como texto puro no momento em que ele sai da tela.
Unicode, charsets e os bytes por baixo
O codificador lê bytes, não caracteres, e o shell entrega a ele os bytes que a locale e o comando produziram. Para texto UTF-8, isso é normalmente exatamente o que você quer: o é do héllo já representa dois bytes, c3 a9, e a codificação só os carrega junto:
printf 'h\xc3\xa9llo' | base64
Isso imprime aMOpbGxv, e um consumidor UTF-8 do outro lado recebe o héllo de volta, byte por byte. O problema começa quando a fonte não é UTF-8. Um arquivo Latin-1 com a mesma palavra guarda um byte único e9 para o é, e codificar aqueles bytes sem conversão produz texto que só um consumidor Latin-1 consegue ler de volta. Converta primeiro, codifique depois:
iconv -f ISO-8859-1 -t UTF-8 note.txt | base64 -w 0
Mais duas verdades de nível de byte. Um BOM UTF-8, três bytes no começo de um arquivo, codifica para 77u/ e vai ficar no começo da sua saída decodificada para sempre, a menos que você o remova primeiro:
sed '1s/^\xef\xbb\xbf//' file.txt | base64 -w 0
E a locale nunca muda a codificação em si, porque o codificador é uma máquina de bytes. Ela só muda o que você digitou. Quando a saída parece errada, cheque os bytes que você alimentou, não a codificação que você rodou.
E-mail, APIs e uploads
O e-mail é onde o Base64 aprendeu boas maneiras, e as maneiras são ainda a convenção. O SMTP historicamente só carregava ASCII de 7 bits, então anexos viajam como Base64 embrulhado a cada 76 caracteres com quebras de linha CRLF, segundo o RFC 2045. Produzir aquela forma exata para uma parte MIME é o embrulho mais a conversão de quebra de linha:
base64 -w 76 attachment.bin | sed 's/$/\r/' > attachment.mime
O guarda antigo ainda está de serviço nos sistemas embutidos: o uuencode do BusyBox com a flag -m produz Base64 MIME embrulhado na moldura familiar begin-base64, e seu irmão uudecode lê de volta:
busybox uuencode -m photo.jpg < photo.jpg > photo.uu
APIs e uploads usam a mesma ideia vestindo JSON: o binário vira uma string Base64 dentro de um campo JSON, e o curl carrega. Montar o corpo numa variável de shell mantém as aspas honestas:
body="{\"file\":\"$(base64 -w 0 upload.bin)\"}"
curl -fsS -X POST https://httpbin.org/post -H "Content-Type: application/json" -d "$body"
Duas armadilhas de interoperabilidade moram aqui. Primeira: cheque qual alfabeto a API quer; algumas esperam Base64 padrão, algumas esperam o dialeto URL-safe, e uma string com caracteres + enviada a um endpoint URL-safe (ou o contrário) falha a validação ou, pior, decodifica para os bytes errados. Segunda: fique de olho na codificação dupla, o bug clássico em que um script codifica um valor que o servidor codifica de novo, e a ida e volta precisa de duas decodificações para desatar.
Quando o payload fica grande
O codificador, como o decodificador, é uma máquina de streaming: ele lê em chunks e escreve em chunks, então um tarball de 10 GB não precisa de 13 GB de RAM, e o comando roda tranquilamente por minutos em entradas grandes com uso de memória plano. A conta de tamanho é a única ferramenta de planejamento que você precisa: a saída é quatro caracteres por três bytes de entrada, mais um byte por linha embrulhada, então um arquivo de 300 MB vira uns 400 MB de texto. Para um check de realidade rápido em qualquer arquivo:
base64 -w 0 big.bin | wc -c
Quando o texto em si precisa ser movido por um canal com limite de tamanho (limite de anexo de e-mail, sistema de chamados, mensagem de IM), divida a forma codificada, nunca o binário bruto, para que cada chunk continue sendo texto comum que você pode colar, comprimir ou encaminhar:
base64 -w 0 big.bin | split -b 4000 - part_
Isso produz uma série de partes de 4000 caracteres. O destinatário faz cat delas de volta na ordem e decodifica uma vez. E quando o payload é comprimível, comprima antes de codificar, porque o Base64 adiciona redundância por cima do que os dados já contêm: um tarball de um diretório de projeto tipicamente encolhe várias vezes sob gzip antes da sobretaxa de 33 por cento do Base64 ser aplicada:
tar czf - project/ | base64 -w 0 > project.b64
Velocidade não será a sua restrição. Esses codificadores empurram gigabytes em bem menos de um segundo numa máquina moderna. Um arquivo de 200 MB leva mais ou menos um décimo de segundo nas implementações coreutils e OpenSSL, e até o BusyBox, o mais lento dos comuns, termina numa fração de segundo (medido em mais ou menos um quarto de segundo para 200 MB numa máquina moderna, algumas vezes mais lento que o coreutils mas nada perto de um gargalo). O gargalo em pipelines reais é quase sempre a rede, não a codificação.
Os pequenos personagens que mordem
As armadilhas do lado da codificação são menores que as do lado da decodificação. E isso é só justo:
| Armadilha | O que acontece | A correção |
|---|---|---|
Alimentar o codificador com echo |
um newline final vai junto para a saída, e o último caractere o codifica | printf '%s' para texto onde a contagem de bytes importa |
| Confiar no embrulho padrão | 76, 64 ou zero, dependendo da ferramenta; um consumidor de uma linha engasga com entrada embrulhada | definir -w 0 (ou a largura que o consumidor quer) explicitamente |
| Newline final na saída | os modos com embrulho de linha terminam com um newline que polui URLs e JSON quando capturado | -w 0 para uma linha, ou capturar via $(...) que o remove |
+ ou / numa URL |
o mais é lido como espaço numa query string; os dois forçam percent-encoding | usar o dialeto URL-safe para qualquer coisa que entre numa URL |
Direção do tr trocada |
a troca produz bytes válidos mas errados, sem erro em lugar nenhum | codificar é tr '+/' '-_'; decodificar é tr '_-' '/+' |
| Codificar um valor já codificado | codificação dupla que precisa de duas decodificações para desatar | checar se a fonte já é Base64 antes de codificar |
| BOM UTF-8 na entrada | três bytes extras no começo de toda saída decodificada | remover o BOM primeiro: sed '1s/^\xef\xbb\xbf//' |
| Guardar um segredo de verdade como Base64 | um comando desfaz; o RFC registra incidentes reais de credenciais vazadas | criptografia para sigilo, Base64 apenas para a forma de transporte |
| Assumir o alfabeto do consumidor | o descasamento entre padrão e URL-safe falha a validação ou decodifica errado | ler a documentação da API; codificar no dialeto que o consumidor pede |
Hábitos que te mantêm a salvo
- Nomear a contagem de bytes.
printf '%s'para texto, o argumentoFILEpara arquivos, e um check de sanidade comwc -cantes de embarcar qualquer coisa onde tamanho importa. - Definir o embrulho explicitamente.
-w 0para URLs e JSON,-w 76para MIME,-w 64para PEM. Nunca deixe a largura por conta do padrão da ferramenta. - Escolher o alfabeto para o destino. Padrão para e-mail e arquivos, URL-safe para tokens e URLs, e cheque a documentação do consumidor antes de codificar.
- Comprimir antes de codificar. Para qualquer payload comprimível,
gzipoutar czfprimeiro; a sobretaxa de 33 por cento se aplica ao que você entregar ao codificador. - Dividir o texto, não o binário. Quando um limite de tamanho está no caminho,
splitda forma codificada para que cada chunk continue seguro para colagem, e remonte na ordem antes da decodificação única. - Manter os três trabalhos do JWT separados. Alfabeto, criptografia, formato: codifique os segmentos, assine o texto ASCII dos segmentos unidos, e então emita. Reordene e o token quebra.
- Nunca deixar o Base64 fazer o papel de criptografia. Se o valor é segredo, criptografe e então codifique o cifrário. Se não é segredo, diga isso e pare de se preocupar.
Uma breve história da codificação no shell
- 1980, Berkeley. Mary Ann Horton escreve
uuencodeeuudecodena Universidade da Califórnia, Berkeley, para carregar arquivos binários via e-mail entre sistemas Unix. O nome, "codificação Unix-para-Unix", é o certificado de nascimento do formato, e pela década seguinte é com isso que os usuários de shell codificam. - A era do dial-up. O uuencode no UNIX e o BinHex no TRS-80 e no Apple II com o Macintosh um passo atrás resolvem o mesmo problema com alfabetos diferentes, cada um confiando só nos caracteres que seu próprio terminal consegue imprimir.
- 1993. O MIME padroniza o Base64 para e-mail no RFC 1521, depois RFC 2045, com o embrulho de linha a cada 76 caracteres que o padrão do coreutils ainda carrega hoje.
- Antes de 2006 no Linux. Não existe comando
base64. Scripts de shell recorrem aopenssl base64,uuencode -m, Perl ou Python, e o hábito do OpenSSL é tão profundo que metade dos velhos one-liners ainda na vida real começa com ele. - 15 de agosto de 2006. O coreutils 6.0 empacota o comando
base64, seu arquivo NEWS o creditando como "funcionalidade de codificação e decodificação base64 (RFC 3548)", e a era do comando único começa. Alguns meses depois, em outubro de 2006, o RFC 4648 formaliza a família de alfabetos, incluindo o dialeto URL-safe que este artigo continua recorrendo. - OS X 10.7. O macOS empacota seu próprio
base64, o sabor BSD sem embrulho padrão, que é por que "só rode base64" precisa de um check de plataforma em scripts portáveis. - 2024. O coreutils 9.5 muda como os decodificadores tratam entrada sem padding e não canônica, o que na prática significa que os codificadores ganham passe livre: saída que versões mais antigas do GNU teriam rejeitado agora decodifica limpo. O lado da codificação do formato é o estável; os decodificadores são os que se moveram.
- 2025. A reescrita do coreutils em Rust (uutils) vira o padrão nas versões atuais do Ubuntu. Mesmo comando, mesmas flags, um motor novo e o mesmo padrão de 76 caracteres herdado da versão C.
Pequenas maravilhas
- O nome do formato é verdadeiro em toda máquina. O
printf 'base64' | base64dáYmFzZTY0no GNU, uutils, BusyBox, OpenSSL e macOS, todos iguais. É verdade desde 2006 e sempre será. - Um arquivo de nada codifica para uma muralha de As. Alimente-o com três bytes NUL e a saída é
AAAA, porque três bytes de valor zero mapeiam para quatro índices zero no alfabeto, cada um representado por A. Um arquivo.b64que começa com uma longa sequência de As é normalmente zero-padding no arquivo original (bytes NUL), não um mistério. - O imposto de 33 por cento não tem descontos. Quatro caracteres por três bytes, sem compressão, sem segunda chance. A única saída é comprimir os dados primeiro, que é por que o
tar czfé o herói de verdade dos pipelines de payload grande. - Dois caracteres causaram todo o problema de URL. O
+e o/são os únicos membros do alfabeto que jamais precisaram de substituto, e um dialeto inteiro do formato existe para aposentá-los. Sessenta e dois e sessenta e três, os dois últimos lugares do alfabeto. - Onze caracteres, sessenta e quatro bits. Um ID de vídeo do YouTube é uma string base64url de 11 caracteres, um número de 64 bits em roupa de URL, que é por que ele viaja por URLs sem um único sinal de percent.
- O impostor mais famoso do Git. Os blocos binários do
git diff --binaryparecem Base64, mas as linhas com prefixozsão um dialeto estilo base85 em si mesmas. Um olhar e você sabe que não é o seu alfabeto; um desvio de grep-e-decodificar e você perde vinte minutos. - Toda ferramenta embrulha de um jeito diferente, de propósito. 76 para MIME, 64 para PEM, zero na ferramenta BSD: três padrões, três convenções herdadas, um formato. A largura era sempre sua para escolher; as ferramentas só lembraram de padrões diferentes.
- O codificador nunca falha nos seus dados. Diferente de seu primo decodificador, o codificador não tem entrada inválida, não tem corrupção, não tem modo estrito. Ele recebe bytes e devolve letras, toda vez. Os bugs deste artigo estão todos nos bytes que você entrega a ele e no destino para onde você os manda.
E quando a viagem aponta para o outro lado, quando uma longa string de letras, dígitos e o ocasional traço ou underline cai no seu terminal e você precisa dos bytes de volta, o artigo relacionado de decodificação Base64 vinculado abaixo cobre esse ritual na mesma profundidade, de segmentos de JWT sem padding a todas as armadilhas de newline que os decodificadores escondem.
Última atualização: 2026-09-08
Artigo relacionado: Decodificação Base64 em Bash: um guia completo