Bash에서의 Base64 디코딩: 완전한 가이드
가끔은 문자와 숫자, 그리고 그 사이에 가끔 섞이는 +, /, =가 담긴 문자열 하나가 터미널에 도착하는데, 그때 진짜 원본이 필요할 때가 있습니다. 티켓에 붙여 넣어진 JWT, 지원 이메일에 첨부된 .b64 파일, 알파벳 수프를 읽는 것 같은 Kubernetes 시크릿, HTML data: 태그 안에 숨어 있는 PNG. 이 글은 Bash와 셸에서 그 바이트를 되찾아 오는 실전 가이드입니다. 거의 틀림없이 이미 설치해 두었을 도구를 쓰는 법이죠.
포맷을 한 번의 숨으로 요약하면: Base64는 원시 데이터의 바이트 3개씩을 64글자 알파벳(A-Z, a-z, 0-9, 그리고 +과 /)에서 고른 문자 4개로 다시 씁니다. 바이트 수가 3의 배수가 아니면 끝에 = 기호 하나 또는 두 개를 붙여 주죠. 디코딩은 그 교환의 줄어드는 방향입니다. 문자 4개가 들어가면 바이트 3개가 나옵니다. 이 사이트의 홈 페이지가 포맷을 단계별로 자세히 다루므로, 이 글은 그 일의 셸 쪽이라는 제자리에 시간을 씁니다.
여기 반전이 있습니다. base64 명령은 단 하나만 있는 것이 아닙니다. 그 이름은 GNU의 C 프로그램, Rust로 다시 쓴 버전, BusyBox 애플릿, BSD에서 넘어온 유산, 그리고 플래그가 정말 위험하게 겹치는 OpenSSL 유틸리티가 함께 쓰고 있습니다. 알파벳에 대해서는 전부 동의하지만, 깨진 입력이 어떤 모양인지에 대해서는 항상 같은 답을 주지 않습니다. 그리고 스크립트가 죽어 나가는 곳은 바로 그 차이에서입니다. 그래서 첫 번째 단계는 base64를 입력했을 때 누가 답장하는지 알아보는 일입니다.
상대하는 디코더를 알아라
명령 한 개가 이야기의 대부분을 알려 줍니다:
base64 --version
머신에 따라, 당신은 다음 중 하나를 상대하고 있습니다:
| 보이는 것 | 사용 중인 것 | 디코딩 플래그 |
|---|---|---|
base64 (GNU coreutils) 9.x |
클래식한 C 구현으로, 여전히 대부분의 Linux 배포판의 기본값 | -d 또는 --decode |
base64 (uutils coreutils) 0.8.x |
coreutils의 Rust 재작성 버전으로, 최신 Ubuntu 릴리스의 기본 사용자 공간 도구 | -d (그리고 특이하게도 -D도 동작합니다) |
| BSD 스타일의 사용법 문구, 버전 플래그 없음 | macOS와 BSD에 있는 BSD base64로, 오래된 bintrans 도구에서 내려온 계보 |
-D (이 계열에서는 소문자 -d가 디코딩이 아니라 디버그를 의미합니다) |
BusyBox v1.x |
Alpine Linux와 임베디드 시스템의 올인원 바이너리 | -d |
그 표에서 빠져 있는 것을 알아채셨을 겁니다: OpenSSL. openssl base64는 전혀 다른 동물이고, 거기서 -d 플래그는 복호화를 뜻하고 디코딩을 뜻하지 않습니다. 그 플래그 하나 때문에 조용히 빈 출력 파일이 생기는 일은 이 글에 나오는 다른 어떤 습관보다 많습니다. 그래서 폴백 섹션에서 제대로 만나볼 겁니다.
배포판에 여러 계열이 나란히 들어 있는 경우(현재의 Ubuntu가 그렇습니다), 명령 몇 개가 전체 그림을 보여 줍니다:
command -v base64
base64 --version 2>&1 | head -1
대부분의 날을 덮어 주는 원라이너 4개
표준 입력에서 문자열을 디코딩합니다. 천 번은 해 볼 동작이고, printf가 셸이 페이로드를 꾸미는 일을 막아 줍니다:
printf '%s' "SGVsbG8sIFdvcmxkIQ==" | base64 -d
파일을 디코딩합니다. 진지한 구현은 전부 FILE 인자를 받으며, 데이터를 셸의 따옴표 처리 장치에서 완전히 벗어나게 하는 가장 깔끔한 방법입니다:
base64 -d payload.b64 > payload.bin
여기스트링에서 디코딩합니다. 여기스트링은 끝에 줄바꿈을 붙여 주지만, 모든 디코더는 줄바꿈을 무시해도 되는 공백으로 대하므로, 작은 데이터라면 이 방식이 완전히 안전합니다:
base64 -d <<< "SGVsbG8sIFdvcmxkIQ=="
헤레독으로 줄이 여러 번 바뀐 블롭을 디코딩합니다. 구분자를 작은 따옴표로 감싸면 셸이 내부의 어떤 것도 해석하지 못합니다:
base64 -d <<'EOF'
SGVs
bG8s
IFdv
cmxk
IQ==
EOF
네 가지 모두 Hello, World!를 출력합니다. macOS와 BSD에서는 위의 모든 예에서 -d를 -D로 바꾸면 되고, 나머지 문법은 완전히 같습니다.
엉망진창인 입력이 정상입니다
현장에서 마주하는 Base64가 한 줄짜리 깔끔한 형태인 경우는 드뭅니다. 76자에서 줄이 바뀐 채(MIME 관례)로, 혹은 64자에서 줄이 바뀐 채(PEM 관례)로, CRLF 줄 끝을 가진 채 Windows에서 내보내져 온 것으로, 아니면 중간에 여분의 공백이 섞인 채 채팅 창에서 복사해 온 것으로 도착합니다. 좋은 소식은: 줄바꿈이 진짜 줄바꿈만이라면, 디코더는 줄바꿈이 어디에 있어도 신경 쓰지 않는다는 것입니다.
어떤 줄 바꿈 스타이든 통하는 치료법은, 디코딩 전에 줄바꿈을 씻어 내는 것입니다:
tr -d '\r\n' < blob.b64 | base64 -d
캐리지 리턴은 특수한 경우입니다. 줄바꿈은 허용되는 입력이지만, 엄격한 디코더에게는 \r가 줄바꿈이 아닙니다. Windows 시스템을 거친 블롭은 GNU 디코더를 넘어뜨립니다. 부분 결과만 출력하고 실패하죠. 치료법은 캐리지 리턴을 먼저 제거하는 것입니다:
printf 'SGVs\r\nbG8s\r\n' | tr -d '\r' | base64 -d
Hel 같은 조각 뒤에 에러가 보인다면, 당신은 CRLF 블롭 위에 서 있는 것입니다. 디코딩 전에 그와 같은 세척, tr -d '\r\n'를 하는 것이 본인이 만들지 않은 어떤 입력에 대해서든 이식 가능한 습관입니다.
정말 깨진 입력에는, GNU와 uutils가 알파벳 밖의 문자를 건너뛰고 가능한 만큼 디코딩하는 -i (--ignore-garbage) 플래그를 제공합니다:
printf 'SGVs!bG8' | base64 -di
이것은 Hello를 출력합니다. -i를 기본 습관으로 만들기 전에, 표준이 왜 경고를 하는지 알아두세요. RFC 4648 3.3절은, 무시된 문자가 디코딩된 출력에는 결코 드러나지 않는 데이터를 몰래 실어 나르는 은밀한 채널로 악용될 수 있기 때문에, 구현은 알파벳 밖의 문자를 포함하는 데이터는 반드시 거부해야 한다고 말합니다. -i는 문서에서 붙여 넣은 것이 구두점을 함께 끌고 왔을 때 쓰면 되고, 신뢰하는 데이터를 검증할 때는 쓰지 마세요.
주요 디코더 3가지가 모서리 상황에서는 실제로 어떻게 행동하는지, 2026년 도구를 기준으로(uutils 0.8.x, GNU coreutils 9.7, BusyBox 1.37) 정리해 보면 이렇습니다:
| 입력 | uutils 0.8.x | GNU 9.7 | BusyBox 1.37 |
|---|---|---|---|
SGVsbG8sIFdvcmxkIQ==, 깨끗한 입력 |
Hello, World!, 종료 0 |
Hello, World!, 종료 0 |
Hello, World!, 종료 0 |
SGVsbG8s, 여덟 문자, 패딩 없음 |
Hello,, 종료 0 |
Hello,, 종료 0 |
Hello,, 종료 0 |
Zg, 두 문자, 패딩 없음 |
f, 종료 0 |
f, 종료 0 |
오류, "truncated input" |
SGV, 세 문자, 패딩 없음 |
오류, 출력 없음 | He 출력 후 오류 |
오류, "truncated input" |
| CRLF 줄바꿈으로 감긴 줄 | 정상 디코딩, 종료 0 | 부분 출력 후 오류 | 정상 디코딩, 종료 0 |
SGVs!bG8, 엉뚱한 구두점 |
오류(-i 사용 시: Hello) |
부분 출력(-i 사용 시: Hello) |
부분 출력(-i 플래그가 아예 없음) |
SGV=, 비정형 여분 비트 |
오류, 출력 없음 | He 출력 후 오류 |
He, 종료 0 |
TQ==junk, 패딩 뒤의 쓰레기 |
종료 0, junk 디코딩을 계속 |
종료 0, junk 디코딩을 계속 |
M 출력 후 "truncated input" (종료 1) |
그 표에서 가져갈 점은 세 가지입니다. 첫째, 패딩 없는 꼬리에 대해 보편적인 규칙은 없습니다. BusyBox는 길이가 4의 배수여야 하지만, GNU와 uutils는 합법적인 나머지는 받되, 남는 비트가 전부 0일 때만 받습니다(그래서 Zg는 통과하고 SGV는 통과하지 못하는 것이죠). 둘째, GNU와 BusyBox는 실패하기 전에 이미 디코딩한 바이트를 먼저 쓰므로, 파일로 리다이렉트한 뒤 종료 코드를 확인하는 스크립트는 반쯤 디코딩된 파일을 기꺼이 그대로 남겨 두게 됩니다. 항상 종료 상태를 확인하고, 실패한 디코딩이 남긴 파일은 전부 의심스러운 것으로 대하세요. 셋째, 마지막 행입니다. uutils와 GNU는 == 뒤의 junk도 계속 디코딩하고 종료 0으로 나옵니다. 스트림이 여기서 끝나야 한다는 것을 알려 주는 것이 아무것도 없기 때문이죠. BusyBox가 예외로, 패딩 직전의 1바이트만 출력하고 나서 truncated input 오류로 실패합니다. 꼬리의 쓰레기가 중요하면, 출력을 신뢰하기 전에 입력의 모양을 검증하세요.
base64url: 토큰과 URL의 알파벳
RFC 4648 5절은 두 번째 방언을 정의합니다. 6비트 계산은 같지만, +와 / 대신 -와 _를 쓰고, 패딩은 없애 버립니다. URL이 정확한 바이트 수를 알릴 필요가 거의 없기 때문이죠. RFC는 이 부분에 대해 분명히 말합니다. 이 인코딩은 "base64 인코딩과 같은 것으로 여겨서는 안 된다"고요. JWT를 한 번이라도 살펴 본 적이 있다면 이미 이 방언을 만난 것입니다. 그 세그먼트는 패딩이 제거된 base64url이니까요.
셸 레시피는 2단계 교환입니다. URL 안전 문자를 표준 알파벳의 동류로 되돌리고, 나서 디코딩하는 것이지요:
printf '%s' "_k-C" | tr '_-' '/+' | base64 -d | xxd -p
이것은 fe4f82를 돌려 줍니다. URL옷을 입었을 뿐인 원시 바이트 3개지요. 교환은 자리에 근거하므로 방향이 중요합니다. 인코딩은 tr '+/' '-_', 디코딩은 tr '_-' '/+'입니다. 섞어 버려도 에러는 나지 않습니다. 그저 조용히 다른 바이트를 만들어 줄 뿐이고, 이는 최악의 종류의 버그입니다.
다음은 base64url을 그냥 Base64처럼 다룬 사람들을 잡는 함정입니다. 이 방언에서는 패딩이 선택 사항이고, 길이가 4로 나눈 나머지 3인 세그먼트는 바로 엄격한 디코더가 샅샅이 조사하는 모양입니다. 튼튼한 수법은 빠진 = 문자를 먼저 다시 채우는 것으로, 작은 함수 하나면 됩니다:
b64url_decode () {
local s=$1
local n
n=$(printf '%s' "$s" | wc -c | tr -d '[:space:]')
while [ $(( n % 4 )) -ne 0 ]; do
s="${s}="
n=$(( n + 1 ))
done
printf '%s' "$s" | tr '_-' '/+' | base64 -d
}
b64url_decode "eyJzdWIiOiJob21lciJ9"
마지막 줄은 {"sub":"homer"}를 출력합니다. 꾸밈없는 JSON 개체로, 웹 서버가 방금 전 서명하거나 봉인한 것이지요. 길이가 4로 나눈 나머지 1인 세그먼트는 애초에 잘못된 형태이고, 아무리 패딩을 채워도 살릴 수 없습니다. 그래서 함수가 그 경우를 거부하는 것은 버그가 아니라 기능입니다.
GNU coreutils에는 네이티브 경로도 있습니다. base64의 더 큰 형제인 basenc는 이 방언을 직접 이해합니다:
printf '%s' "Zg==" | basenc --base64url -d
이것은 f를 출력합니다. 두 문자 안에 숨어 있던 그 1바이트이지요. 파이프라인을 세우기 전에 경고 하나. 이 시대의 GNU basenc는 패딩 없는 base64url 입력(그냥 Zg)도 불평 없이 디코딩하지만, uutils의 basenc는 위의 예처럼 여전히 패딩을 먼저 복원해 주길 원합니다. 위 작은 함수는 어디서든 동작하므로, 그것이 바로 이식 가능한 선택인 이유입니다.
JWT 열기
RFC 7515에 따르면, JSON Web Token은 점으로 이어 붙인 base64url 세그먼트 3개입니다. 앞의 두 개는 순수한 JSON(헤더와 페이로드 클레임)이라 곧바로 읽을 수 있는 텍스트로 디코딩됩니다. 세 번째는 서명, 즉 원시 바이너리 다이제스트입니다. 건드리지 마세요. 디코딩해도 서명 바이트가 나오지, 메시지가 나오는 것이 아닙니다.
token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiJ9.abc123"
b64url_decode "$(printf '%s' "$token" | cut -d. -f2)"
이것은 {"sub":"42"}, 즉 subject 클레임을 서버 도움 없이 출력합니다. 디코딩이 무엇이고 무엇 아닌지 머리를 맑게 하고 클레임을 읽으세요. 디코딩은 드러내 줄 뿐, 검증해 주지는 않습니다. 키를 가진 서버가 다시 계산하기 전까지 서명은 아무것도 말해 주지 않습니다. 다시 계산하는 것은 openssl dgst의 일이지요(인코딩 아티클은 전체 발급 과정을 보여 줍니다), 이 글의 일은 아닙니다. 현장에서 흔히 하는 실수는 디코딩된 페이로드를 토큰이 유효하다는 증명으로 여기는 것입니다. 공격자는 서명 없는 토큰이나 약하게 서명된 토큰을 손으로 만들어 낼 수 있고, 디코더는 그것들을 모두 기분 좋게 읽어 줍니다.
파일, 바이트, 그리고 변수의 벽
디코딩은 원시 바이트를 당신 손에 넘겨 줍니다. 그것들은 영어 문장을 이을 수도 있고, PNG나 공유 라이브러리, zip 아카이브의 한가운데일 수도 있습니다. 파일은 셸 스크립트에서 바이트가 완전히 안전한 유일한 장소입니다. 그래서 기본 워크플로우는 파일로 디코딩한 뒤 비교하는 것입니다:
base64 -d photo.b64 > photo.png
원본이 바로 옆에 있다면, 바이트 단위의 비교가 유일한 증거입니다:
cmp photo.png photo.png.orig && echo "byte-for-byte identical"
원본이 멀리 있다면, 대신 체크섬을 비교하세요:
sha256sum expected.bin
base64 -d blob.b64 | sha256sum
두 해시가 일치하면, 당신의 디코딩은 증명 가능할 정도로 정확합니다. 이미지 뷰어나 헥스 덤프에서 아무리 눈대중으로 봐도 이길 수 없는 것이죠.
셸 변수는 또 다른 이야기이고, 두 가지 이유로 벽입니다. 명령 대체 $(...)는 출력의 맨 뒤에 붙은 줄바꿈을 전부 제거하고, NUL 바이트는 아예 담을 수 없습니다. bash는 경고를 출력하고 조용히 버리죠. 41 00 42라는 3바이트 페이로드는 두 문제를 한 번의 데모로 보여 줍니다:
printf 'QQBC' | base64 -d > out.bin
xxd out.bin
파일은 세 바이트(41 00 42)를 모두 담고 있습니다. 변수 경로는 아닙니다:
v=$(printf 'QQBC' | base64 -d)
printf '%s' "$v" | wc -c
얻는 것은 2이며, 무시된 널 바이트에 대한 경고가 표준 에러로 나옵니다. 교훈은 짧습니다. 데이터가 바이너리일 수 있다면, 파일로 디코딩하고, xxd로 검사하고, 절대 변수를 지나가게 하지 마세요.
실제 일에서 Base64가 숨어 있는 곳
디코딩이 편해지면, 어디서나 이 포맷을 보기 시작합니다. 실제 셸 작업에서 이 포맷이 나타나는 구석들과, 각각의 정확한 수법을 소개합니다.
Kubernetes 시크릿. 시크릿의 .data 아래 모든 필드는 Base64이며, 공식 문서들은 이것이 인코딩이지 암호화가 아니라고 강조합니다. 그중 하나를 다시 읽어 오는 일은 고전적인 원라이너입니다:
kubectl get secret db-creds -o jsonpath='{.data.password}' | base64 -d; echo
Git 바이너리 패치. diff가 바이너리 파일을 건드리면, git diff --binary는 GIT binary patch 블록을 출력합니다. 여기서 base64 -d를 꺼내지 마세요. 그 블록의 줄들은 git 자체의 base85 스타일 인코딩(각 줄은 길이 문자 A-Z 또는 a-z로 시작하고, 뒤를 base85 데이터가 따름)이지 Base64가 아니라, 평범한 디코더는 그 앞에서 막힙니다. 올바른 도구는 그 포맷의 주인입니다:
git diff --binary | grep -a -A2 'GIT binary patch'
diff를 git apply나 git am에 넘기고, 그들에게 압축을 풀게 하세요.
data URI. HTML이나 CSS에 매립된 이미지는 data:image/png;base64,iVBOR... 같은 모양입니다. 쉼표까지 포함해 전부 잘라 내고, 줄바꿈을 씻어 내고, 디코딩하면 파일이 손에 쥡니다:
cut -d, -f2- icon.uri | tr -d '\r\n' | base64 -d > icon.png
PEM 아머. 인증서와 개인 키는 Base64를 아예 Base64가 아닌 프레임 줄로 감싸고 있습니다. 아머 블록을 골라 두 프레임 줄을 떨어 트리고, 원시 DER 바이너리로 디코딩합니다:
awk '/BEGIN CERTIFICATE/{f=1;next} /END CERTIFICATE/{f=0} f' cert.pem | tr -d '\r\n' | base64 -d > cert.der
CERTIFICATE 대신 PRIVATE KEY, 혹은 당신의 파일이 가진 어떤 라벨로 바꾸세요. 모양은 같습니다.
MIME 이메일. Content-Transfer-Encoding: base64를 가진 이메일의 어떤 부분도, RFC 2045가 그러하다고 정했기 때문에, CRLF 줄 끝으로 76자마다 줄이 바뀝니다. 이식 가능한 조합은 세척과 디코딩입니다:
tr -d '\r\n' < attachment.b64 | base64 -d > attachment.bin
클립보드 붙여 넣기. 브라우저, 채팅 창, 문서에서 복사한 텍스트는 엉뚱한 공백과 끝에 붙은 줄바꿈을 함께 가져 옵니다. 공백은 알파벳 문자가 아니므로, 엄격한 디코더는 붙여 넣기를 거부합니다. 표준 구제법은 먼저 공백을 떨어 트리는 것입니다:
tr -d ' \r\n' < pasted.b64 | base64 -d
바이트가 우선: 캐릭터셋과 유니코드
Base64 작업에서 가장 반복되는 실수는, 이 포맷은 바이트만 알 텐데 문자로 생각하는 것입니다. base64 -d는 원시 바이트를 넘겨 주며, 그것이 읽을 수 있는 텍스트가 될지는 다음으로 그것을 읽는 쪽이 내리는 결정입니다. 그 결정은 캐릭터셋이며, 디코딩 이후에 일어나지 디코딩 안에서는 일어나지 않습니다.
UTF-8이 기본 가정이며, 보통은 맞는 가정입니다. café라는 단어는 UTF-8로 다섯 바이트이고, 디코딩은 즐거움 그 자체입니다:
printf 'Y2Fmw6k=' | base64 -d | xxd -p
그것은 636166c3a9입니다. caf에 é를 위한 UTF-8 두 바이트 c3 a9가 더해진 것이죠. 그러나 오래된 시스템은 Latin-1 (ISO-8859-1) 바이트를 넘겨 줄 때가 있고, 거기서 같은 글자는 단 하나의 바이트 e9입니다. 그런 블롭을 디코딩해 UTF-8 터미널에 바로 출력하면 깨진 글자가 납니다. 치료법은, 다른 무엇이 그 바이트를 보기 전에 iconv로 바이트를 다시 해석하는 것입니다:
base64 -d latin1.b64 | iconv -f ISO-8859-1 -t UTF-8 > utf8.txt
실제 디버깅 시간을 아껴 주는 바이트 수준 사실 몇 가지:
- UTF-8 BOM은 세 바이트
ef bb bf로,77u/로 인코딩됩니다. URL에 적대적인 문자인/를 알아채 보세요. base64url이 존재하는 이유가 바로 이런 것을 고치기 위해서입니다. 디코딩된 파일이 "앞에 보이지 않는 쓰레기가 있다"면,xxd로 처음 세 바이트를 확인하세요. - 😀 같은 이모지는 UTF-8 4바이트로, Base64 문자 8개
8J+YgA==가 됩니다. 그 안의+는 표준 알파벳이 설계대로 정확히 작동한 것이지요. base64url 옷을 입으면8J-YgA가 됩니다. - 올바르지 않은 UTF-8 시퀀스도 바이트로서는 완벽하게 디코딩되고, 나중에는 쓰레기나 대체 문자로 표시됩니다. 그것은 Base64의 실패가 아닙니다. 디코딩은 제 일을 한 것이죠.
xxd나hexdump -C가 그 바이트가 실제로 무엇인지를 보여 줍니다. - 터미널의 로케일이 그 바이트를 어떻게 렌더링할지 결정합니다. "터미널이 쓰레기를 보인다"는 데이터에 대한 이야기가 아니라 표시에 대한 이야기입니다. 바이트는 이동 도중에는 바뀌지 않았죠.
거대한 블롭: 스트림, 분할, 그리고 속도
Base64는 스트림 포맷이고, 디코더는 진정한 스트리밍 파이프로서 작동합니다. 10 GB 파일은 절대 메모리에 앉아 기다리지 않고, 그저 통과할 뿐이지요. 덕분에 디코딩 쪽은 규모가 커져도 거의 지루할 정도로 안정적입니다. 바로 그것이 원하는 모습이죠.
큰 블롭이 전송을 위해 청크로 쪼개져 있었다면(이메일 제한, 티켓 첨부파일 크기, IM 메시지), 다시 조립하는 일은 올바른 순서로 cat한 뒤 통상적인 세척을 하는 것뿐입니다:
cat part_* | tr -d '\r\n' | base64 -d > big.bin
크기에 대한 마음의 모델 하나를 남겨 두세요. 인코딩된 형태는 항상 원본보다 약 1/3 크고, 바이트 3개당 문자 4개입니다. 누군가 .b64 파일은 숨기는 대상과 같은 크기여야 한다고 말한다면, 그 사람은 틀린 것이고, 이제 얼마만큼 틀린지도 정확히 알려 줄 수 있습니다. 300 MB 페이로드는 대략 400 MB의 텍스트로 도착하니까요.
속도는 실제로는 걱정이 아닙니다. 이것은 순수한 메모리 위의 테이블 주도 루프이고, 최신 머신에서 GNU로는 200 MB 블롭이 약 0.1초 만에 디코딩됩니다. 흔한 구현 중 가장 느린 것(BusyBox)도 그 몇 배에 불과할 뿐, 여전히 2초를 훨씬 밑돕니다. 아무것도 버퍼링되지 않으므로, 입력이 아무리 커져도 메모리 사용량은 평탄하게 유지됩니다.
머신에 base64가 없을 때
대부분의 시스템은 위 도구 중 적어도 하나를 갖고 있으며, 대부분의 시스템은 여러 개를 갖고 있습니다. 플랫폼에 coreutils가 아예 없다면(깎아 내린 컨테이너, 이색적인 전용 장비), 설치 경로는 이렇습니다:
| 플랫폼 | 가져 오는 법 | 비고 |
|---|---|---|
| Debian / Ubuntu | 미리 설치됨. 깎여 있다면 apt install coreutils |
Ubuntu의 현재 릴리스는 25.10부터 uutils 계열이 기본이며, GNU 쌍둥이는 gnubase64로 사용할 수 있습니다. Debian 13은 여전히 기본적으로 GNU coreutils를 제공합니다 |
| RHEL / Fedora | dnf install coreutils |
사실상 모든 이미지에서 미리 설치됨 |
| Alpine | apk add busybox (보통 이미 존재) |
busybox base64 -d. -i 플래그 없음 |
| macOS | 내장됨. GNU 버전은 brew install coreutils |
BSD 플래그: 디코딩은 -D, 줄 너비는 -b. brew는 gbase64를 줍니다 |
패키지 매니저를 전혀 쓸 수 없다면, 아래 범용 폴백은 전부 표준 입력에서 읽고 바이트를 표준 출력에 쓰므로, 동일한 파이프라인에 그대로 끼어 듭니다:
openssl base64 -d -A -a < in.b64 > out.bin
perl -MMIME::Base64 -0777 -ne 'print decode_base64($_)' < in.b64 > out.bin
python3 -c 'import base64, sys; sys.stdout.buffer.write(base64.b64decode(sys.stdin.buffer.read()))' < in.b64 > out.bin
base64 애플릿이 컴파일에서 빠져 버린 BusyBox 시스템에서는, 오래된 수호자가 여전히 작동합니다. uuencode -m가 MIME Base64를 만들고, 그 형제가 다시 읽어 옵니다:
busybox uudecode -o out.bin in.uu
오후를 통째로 삼키는 함정들
아래 모든 것은 현장에서 만나게 될 동작이며, 한곳에 모은 것입니다:
| 함정 | 무슨 일이 일어나는가 | 치료법 |
|---|---|---|
openssl base64 -d만 단독으로 |
거기서 -d가 복호화를 뜻하기 때문에, 조용히 아무것도 하지 않고 종료 0 |
openssl base64 -d -A -a. 그리고 0바이트 결과는 에러로 대하기 |
macOS에서 -d로 디코딩 |
BSD base64는 그 플래그를 거부함(또는 디버그로 취급) | -D. 아니면 gbase64를 위해 coreutils 설치 |
| 입력 안의 CRLF 줄 끝 | GNU는 부분 결과를 출력한 뒤 실패. uutils와 BusyBox는 허용 | 먼저 tr -d '\r\n'. 타지에서 온 입력에는 항상 |
| 패딩 없는 꼬리 | BusyBox는 어떤 나머지든 거부. GNU와 uutils는 정형 꼬리만 허용 | 디코딩 전에 빠진 = 패딩을 복원 |
패딩 뒤의 쓰레기, 예: TQ==junk |
uutils와 GNU는 디코딩을 계속하고 종료 0. BusyBox는 에러 | 출력을 신뢰하기 전에 입력의 모양을 검증 |
비정형 여분 비트, 예: SGV= |
uutils와 GNU는 거부(GNU는 부분 출력 후). BusyBox는 그래도 디코딩 | 입력이 깨진 것. 원천에서 인코딩을 다시 생성 |
| 디코딩된 출력을 변수로 읽기 | $(...)는 뒤의 줄바꿈을 제거하고 NUL 바이트는 아예 담을 수 없음 |
파일로 디코딩, xxd로 검사 |
신뢰하지 않는 입력에 -i |
손상이 조용한 성공이 됨. 무시된 문자가 숨겨진 데이터를 실어 날릴 수 있음 | 엄격하게 디코딩, 에러를 읽고, 원천을 수정 |
| 두 알파벳을 헷갈리기 | base64url을 표준으로 디코딩(또는 그 반대)하면 잘못된 바이트 또는 에러가 나옴 | 디코딩 전에 포맷을 알아 두기. 교환은 tr '_-' '/+' |
| 디코딩된 시크릿을 시크릿으로 대하기 | 명령 하나로 되돌려 놓음. RFC에는 자격 증명 유출이라는 실제 사건들이 기록되어 있음 | 진짜 암호화이지, 인코딩이 아님 |
OpenSSL 행은 실패가 너무 조용하기 때문에, 자신의 단락 하나를 받을 자격이 있습니다. OpenSSL 3.x에서는 독립 애플리케이션의 -d가 일반적인 "복호화" 옵션이고, Base64 처리는 -a로 선택하는 별도의 모드입니다. 그래서 openssl base64 -d는 당신의 입력을 읽고, 아무것도 하지 않고, 아무것도 출력하지 않고, 종료 0입니다. 실제로 작동하는 디코딩은 openssl base64 -d -A -a이며, 거기서 -A는 입력이 한 줄짜리 연속된 선이라는 것을 알려 줍니다. 디코딩에 OpenSSL을 써야 한다면, 0바이트 출력을 매번, 예외 없이 에러로 대하세요.
촘촘한 루틴
Base64가 당신을 결코 이기지 못하게 해 주는 습관들:
- 크게 소리 치며 실패하게. 스크립트는
set -euo pipefail로 실행하고 종료 코드를 확인하세요. 흔한 디코더는 전부 잘못된 입력에 종료 1을 냅니다. OpenSSL은 소리를 지르던 쪽이 조용해진 것이므로, 그것을 위해서는 출력이 비어 있지 않은지도 확인하세요. - 왕복을 증명하라. 기대하는 바이트와 되살려 낸 바이트 사이의
cmp나sha256sum이 유일한 증거입니다. 바이너리에 눈대중으로 대하지 마라. - 바이너리는 파일로, 절대 변수로. 끝의 줄바꿈과 NUL 바이트는 둘 다 명령 대체의 희생자입니다.
- 타지의 입력은 씻어 내라. 플랫폼 경계를 건넌 것은 디코딩 전에
tr -d ' \r\n'. - 기본은 엄격하게. 정확히 무엇이 잘못됐는지 보기 전까지
-i는 꺼 두세요. 엄격한 실패는 위치를 알려 주지만, 관대한 실패는 아무것도 알려 주지 않습니다. - 알파벳을 명명하라. RFC 4648에 따르면 표준 Base64와 base64url은 별개의 인코딩입니다. JWT는 base64url로, 이메일 첨부파일은 표준으로 디코딩하고, 왜 바꾸는지 모른 채 문자를 바꾸지 마세요.
- 방금 디코딩한 것을 절대 출력하지 마라. 시크릿 세계에서 이 포맷의 존재 이유는 보이지 않는 것이고, 로그의 존재 이유는 보이는 것입니다. 그 두 목표는 섞일 수 없습니다.
"이 머신은 어떤 플래그를 원하는가"라는 질문에 대한 작은 이식 가능한 얇은 래퍼입니다:
case "$(base64 --version 2>&1 | head -2)" in
*uutils*|*GNU*|*BusyBox*) b64d () { base64 -d; } ;;
*) b64d () { base64 -D; } ;;
esac
b64d < in.b64 > out.bin
case 문은 버전 배너로 GNU, uutils, BusyBox 계열을 붙잡습니다. 세 계열 모두 소문자 플래그를 받으니까요. 나머지는 전부 BSD 플래그로 폴백하며, 이것은 바로 현실의 4분할, GNU, uutils, BusyBox, BSD입니다.
셸이 디코딩을 배운 방식
포맷은 오래됐지만, 당신이 입력하는 명령은 그렇지 않습니다. 이야기의 셸 쪽에 대한 짧은 타임라인입니다:
- 1980, 버클리. Mary Ann Horton가 캘리포니아 대학교 버클리에서 바이너리 파일(보통은 압축된 파일)을 이메일로 실어 나르기 위해
uuencode와uudecode를 작성합니다. 이름의 뜻은 "Unix에서 Unix로의 인코딩"으로, 캐릭터셋을 공유하지 않을지도 모를 Unix 시스템 사이에서 파일을 옮기기에 안전한 인코딩입니다. 수십 년간 셸 사용자가 손이 간 것은 Base64가 아니라 이것이었습니다. - 다이얼업 시대. 가장 이른 base 계열 인코딩은 모두 같은 문제를 타고 달립니다. UNIX에는 uuencode, TRS-80과 Apple II에는 BinHex, 그리고 Macintosh가 한 발 늦게 따라 오고, 각각 자기 터미널이 출력할 수 있는 문자만 전제로 합니다.
- 1993. MIME이 이메일을 위한 Base64를 표준화합니다(RFC 1521, 이후 RFC 2045). 오늘도 여전히
base64의 기본값을 정의하는 76자 줄 바꿈과 함께이지요. - 2003년과 2006년 10월. RFC 3548이 이 가족을 다듬고, RFC 4648이 그 자리를 이어받아 이 글이 계속 인용하는 알파벳들과 엄격한 디코딩 규칙, 그리고 base64url까지 넣습니다.
- 2006년 8월 15일. coreutils 6.0이
base64명령 그 자체를 추가합니다. NEWS 파일에는 그것을 솔직히 "base64 encoding and decoding (RFC 3548) functionality"로 올려 줍니다. 그 날짜 이전에는 Linux의 셸 사용자는openssl base64,uuencode -m, Perl, 또는 Python에 손을 뻗었고, 그래서 많은 오래된 스크립트가 OpenSSL이 동네에서 유일한 선택지라고 가정합니다. - OS X 10.7. macOS는 자기만의
base64를 내놓습니다.-D플래그를 가진 BSD 풍미로, 기본 줄 바꿈은 없으며,-d대-D분열의 출발점이 바로 여기입니다. - 2024년 3월. coreutils 9.5가 디코더를 완화합니다. 디코딩 시 패딩이 더 이상 필수적이지 않고, 0이 아닌 여분 비트를 가진 인코딩은 조용히 허용되는 대신 손상으로 진단됩니다.
- 2025. coreutils의 Rust 재작성 버전(uutils)이 최신 Ubuntu 릴리스의 기본이 됩니다. 같은 명령 이름, 같은 플래그, 그리고 자기만의 모서리 의견을 가진 새 엔진. 예를 들어
-D를 별칭으로 받아 주는 것이 그렇죠.
남겨 둘 만한 호기심거리들
- 명령은 포맷보다 젊다. Base64는 1993년부터 이메일에 있었지만,
base64명령은 2006년에서야 나타났습니다. 열세 해 동안 셸 스크립트들은 다른 도구로 이 일을 해 왔고, 그 지문은 지금도 어디서든 찾아 볼 수 있습니다. - 도움말 속에 든 화석. uutils의
base64는 아직 그 도움말에서 자기 알파벳을 "RFC 3548"으로 설명합니다. RFC 4648의 은퇴한 전임자이지요. 그에 비해 GNU 형제는 이미 최신 표준을 인용합니다. 작은 화석으로, 도움말을 읽어 봐야만 보입니다. - 도구함에서 가장 비싼 조용한 무조치.
openssl base64 -d는 아무것도 하지 않고 종료 0입니다. 디버깅 한 시간이 통째로 사라지고, 종료 코드 테스트는 통과했습니다. - BusyBox는 여분 비트를 그냥 받아 둔다. 남는 비트가 0이 아니므로 정형이 아닌
SGV=도 기꺼이 디코딩합니다. 같은 입력에 대해 GNU 사촌은 격렬히 화를 내는데도 말입니다. 같은 RFC, 다른 신경 시스템. - 포맷의 이름은 모든 머신에서 참이다.
printf 'base64' | base64는 GNU, uutils, BusyBox, OpenSSL 어디서나YmFzZTY0를 줍니다. 2006년부터 참이었고, 언제나 참일 것입니다. - Base85는 Base64가 아니다. Git의 "binary patch" 블록은 비전문가의 눈에는 Base64처럼 보이지만, 길이 접두가 있는 줄들은 base85 스타일의 방언 중 하나입니다. grep해서 디코딩하는 우회로에 값을 치르게 하는 사칭자입니다.
- 열한 문자, 예순네 비트. YouTube 동영상 ID는 11문자의 base64url 문자열, URL옷을 입은 64비트 숫자입니다. 그래서 퍼센트 기호 하나 없이 URL에 나타날 수 있는 것이죠.
- 디코더들은 한 문자에서 의견이 갈린다.
SGV=같은 문자열 하나만으로도, 위 동작 표가 보여 주듯, 이 분야는 세 진영으로 나뉩니다. "분명히 유효해 보이는" 입력에서 디코딩이 실패했다면, 당신은 아마 여분 비트 선 위에 서 있는 것이고, 그 인코딩은 애초에 정형이 아니었을 것입니다.
그래서 다음에 문자와 숫자, 더하기와 슬래시 문자열이 터미널에 도착하면, 당신은 온전한 이야기를 알 것입니다. 어느 디코더가 지켜 보고 있는지, 어느 알파벳이 말하고 있는지, 줄바꿈이 어디에 숨어 있는지, 그리고 캐리지 리턴 때문에 한 시간을 잃지 않고 온전한 채로, 바이트 단위로 증명된 채로, 그 바이트를 정확히 어떻게 되찾을지. 그리고 일이 반대 방향으로 향할 때, 즉 자기 바이너리를 이동용 텍스트 봉투에 싸 넣을 때는, 아래에 링크된 관련 Base64 인코딩 아티클이 그 의식을 같은 깊이로 다룹니다.
마지막 업데이트: 2026-09-08
관련 문서: Bash에서의 Base64 인코딩: 완전한 가이드