Perl에서의 Base64 디코딩: 완전한 가이드
글자, 숫자, 가끔 보이는 +과 /로 이루어진 문자열 하나가 당신에게 넘어 왔습니다. 직감적으로는 그것이 겉보기 그대로가 아니라는 것을 알고 계실 텐데요. Authorization 헤더를 타고 다니는 토큰일 수도, 서포트 티켓에서 굴려 낸 .b64 파일일 수도, -----BEGIN 갑옷을 두른 인증서일 수도, 설정 파일에서 조용히 자리를 지키고 있는 덩어리일 수도 있습니다. 터미널을 열고 perl을 치면, 하나의 질문이 모든 것을 삼켜 버립니다. 진짜 데이터를 어떻게 되찾아야 할까?
답은 작고 안심됩니다. Perl은 2002년부터 언어 자체에 Base64 모듈을 싣고 다녔고, 함수 호출 하나, decode_base64만으로 일이 통째로 끝납니다. 설치할 것도, 설정할 것도 없으니까요. 커피가 우려지는 동안 빠른 복습을 하나. Base64는 데이터 3바이트를 64문자 알파벳에서 고른 4문자로 다시 쓰고, 꼬리에 = 기호 하나 또는 둘로 패딩해 결과가 언제나 4의 배수에 닿게 합니다. 그래서 인코딩된 형태는 원래보다 보통 33퍼센트쯤 더 크게 자랍니다. 이 사이트의 홈 페이지는 포맷을 끝까지 상세히 설명해 주므로, 이 가이드는 Perl 쪽 울타리 안에 있는 일에만 시간을 쓰겠습니다. 디코더의 규칙, 알파벳 변형들, 그리고 현실에서 실제로 만나는 포맷들이요.
도구함: 함수 다섯 개, 설치 제로
필요한 호출은 전부 MIME::Base64 안에 모여 있습니다. 5.8부터 Perl 코어 배포판의 일부였기에, 라우터 펌웨어에 심겨 있는 것부터 데이터베이스 서버의 것까지, 진지한 설치 환경이라면 어디든 존재합니다. 확인은 한 줄이면 됩니다:
perl -MMIME::Base64 -e 'print $MIME::Base64::VERSION, "\n"'
# 3.16_01
이것이 모듈의 디코딩 쪽, 그 전체입니다:
| 함수 | 하는 일 | 참고 |
|---|---|---|
decode_base64($str) |
이 글의 주인공: Base64 덩어리를 원시 바이트로 되돌린다 | 알파벳 밖의 모든 문자를 영원히 조용히 무시한다 |
MIME::Base64::decode($str) |
같은 디코더를 임포트 없이 부르는 형태 | 오래된 스크립트에서 제법 자주 만나는 모습 |
decode_base64url($str) |
-와 _를 쓰는 URL-safe 변형을 디코딩, 패딩 유무 관계없음 |
2010년 3.11에서 추가, JWT를 읽는 쪽 |
MIME::Base64::decoded_base64_length($str) |
디코딩하지 않고도 디코딩된 데이터의 크기를 알려 준다 | 기본적으로는 내보내지 않음, 버퍼 미리 크기 잡기에 유용 |
unpack("u", $data) |
Base64 이전 포맷인 uuencoded 데이터를 디코딩 | Perl 자체에 내장되어 있어 모듈이 필요 없다 |
오래된 기계들의 함대를 돌보고 계신 분을 위해, 해당 함수들의 버전 지도를 정리합니다:
| 기능 | 첫 등장 |
|---|---|
C 속도 경로를 갖춘 decode_base64() |
2002년 Perl 5.8, 모듈이 코어에 합류한 해 |
decoded_base64_length() |
2010년 모듈 3.10 |
decode_base64url() |
2010년 모듈 3.11 |
| 조용한 디코딩, 의심스러운 입력에도 경고 없음 | 2010년 모듈 3.11 |
| 현행 3.16 라인 | 2020년, Perl 5.6 이상 요구 |
어떤 이유로 시스템 Perl에 이 모듈이 없다면 (있어야 마땅하지만), 수선은 두 가지 한 줄 중 하나면 됩니다. Debian과 Ubuntu의 디스트로 패키지 libmime-base64-perl, 또는 CPAN에서 현행 릴리스를 가져 오는 cpanm MIME::Base64. 이 모듈은 코어 시절부터 이중 생활 패키지로 살아 왔거든요. C 컴파일러가 없는 드문 기계에는, CPAN의 순수 Perl 쌍둥이 MIME::Base64::Perl이 같은 기본 인터페이스를 제공합니다. 몇 배는 느리지만 벌크 작업을 제외하면 충분하죠. 의존성의 이야기는 여기가 전부입니다. 그 외에는 아무것도 없으니까요.
거절하는 법이 없는 디코더
계약은 한 줄짜리입니다. 문자열을 건네면, 원시 옥텟을 담은 범상한 Perl 문자열로 디코딩된 바이트를 돌려 줍니다. 객체도, 예외도, 플래그도 없습니다. 공식 문서는 이 디코더의 성격을 규정하는 두 가지 규칙을 단 하나의 문장에 담았습니다. 65문자 Base64 부분집합에 속하지 않는 문자는 조용히 무시되고, = 패딩 문자 뒤에 나오는 문자는 결코 디코딩되지 않는다. 이 예의 바름은 이 글에서 가장 중요한 것이므로, 한 번만이라도 당신을 위해 일하게 두세요:
use MIME::Base64 qw(decode_base64);
print decode_base64("TWFu!"), "\n"; # Man - 느낌표는 흔적도 없이 사라진다
print decode_base64("TWFu=XX"), "\n"; # Man - = 이후의 모든 것은 건너뜀
print decode_base64("TQ"), "\n"; # M - 경고도 없고, 코멘트도 없음
print decode_base64("T"), "\n"; # 빈 문자열, 그래도 불평은 없음
마지막 두 줄이 관대함의 극단입니다. TQ는 온전한 바이트 하나와 여분 비트 4개를 싣고 있는데, 디코더는 그저 바이트만 남기고 나머지는 버립니다. T는 온전한 바이트 하나조차 담지 못해, 결과가 비게 됩니다. 옛날식 까다로움을 되살려 줄 엄격 모드도, 검증기도 이 모듈에는 없습니다. 2010년 3.11부터 decode_base64는 잘린 입력에 대해 경고조차 하지 않으며, 더 오래된 버전은 Base64 데이터의 조기 종료라는 경고를 -w 아래에서 꺼내놓곤 했죠. 덩어리가 틀렸든 말든 디코딩해 버리니, 품질 관문은 결국 당신인 겁니다.
관대함 정책을 한자리에 모았으니, 한눈에 전체를 보실 수 있습니다:
| 입력 | 결과 | 이유 |
|---|---|---|
"TWFu" |
Man |
깨끗한 입력, 정상 경로 |
"TWFu!" |
Man |
느낌표는 알파벳에 없으니 건너뜀 |
"TWFu=XX" |
Man |
패딩 이후의 것은 결코 디코딩되지 않음 |
"TWFuIFdvcmxkIQ==" |
Man World! |
어디에 공백이 있어도 무료 |
"TQ" |
M |
온전한 바이트 하나는 들어가고, 여분 비트는 조용히 버려짐 |
"T" |
빈 문자열 | 온전한 바이트 하나조차 못 담고, 경고도 없음 |
"ab-cd_efgh" |
조용히 틀린 바이트 | URL-safe 글자들이 잡음으로 버려지는, 고전적인 함정 |
기억해 두어야 할 것은 바로 마지막 줄입니다. base64url 세그먼트를 표준 디코더에 건넨다고 해서 실패하지 않습니다. 그럴듯해 보이는 쓰레기로 디코딩되죠. -와 _ 문자는 이질적인 잡음으로 취급되고, 남은 글자들은 여전히 유효한 그룹을 이루기 때문입니다. 디코더는 증인이지 문지기가 아닙니다. 그러니 입력을 신뢰할 수 없다면, 검증은 본인이 하셔야 합니다. 작은 엄격 검사 하나면 충분합니다:
sub strict_base64 {
my ($blob) = @_;
$blob =~ s/[\r\n]//g; # 디코더가 무시하듯 우리도 무시
return 0 unless length($blob) % 4 == 0;
return $blob =~ /\A[0-9A-Za-z+\/]+(?:={1,2})?\z/ ? 1 : 0;
}
print strict_base64("TWFu"), "\n"; # 1
print strict_base64("TQ="), "\n"; # 0 - 패딩 개수 불일치
print strict_base64("ab-cd"), "\n"; # 0 - URL-safe 알파벳
그 정규식에 대해, 뼈저린 경험에서 얻은 한마디를 덧붙입니다. 서브가 맨끝에 나란히 놓인 return $x =~ /.../로 끝나고, 실패한 매칭 결과가 그대로 printf에 들어갈 때, Perl은 깨끗한 0 대신 오도하는 printf의 인자 누락 경고를 띄웁니다. 위의 함수처럼 반환 전에 ? 1 : 0로 매칭을 강제 전환하면, 그 함정은 완전히 사라집니다.
바이트 먼저, 문자 나중에
decode_base64가 돌려 주는 것이 무엇인지 기억해 두세요. 원시 바이트, UTF-8 플래그가 세워지지 않은 범상한 문자열입니다. 그 바이트들이 무슨 의미인지는 오직 당신이 내릴 수 있는 결정이며, Unicode가 사람들을 붙잡아 놓는 바로 그 단계입니다. Perl은 문자열이 문자를 담고 있는지 바이트를 담고 있는지 추적하고, length(), substr(), 그리고 대부분의 정규식도 그에 따라 다르게 동작합니다. 수선은 의도적으로 인코딩을 이름 붙이는 것인데, 모든 Perl 설치에 들어 있는 Encode 모듈이 그 일입니다:
use MIME::Base64 qw(decode_base64);
use Encode qw(decode);
my $raw = decode_base64("SMOrbGxvIFdvcmxkIQ==");
my $text = decode("UTF-8", $raw);
print $text, "\n"; # Hëllo World!
print length($text), " chars\n"; # 12
print length($raw), " bytes\n"; # 13
숫자 이 쌍이 바로 전체 교훈입니다. 그 덩어리는 바이트로 13바이트지만 문자로는 12문자에 불과합니다. 발음 기호가 붙은 글자가 UTF-8에서는 2바이트를 차지하기 때문이죠. 문자셋 단계를 건너뛰면 그 바이트는 UTF-8 터미널에 여전히 깨끗하게 인쇄되는데, 바로 그 때문에 실수는 문자열 함수가 그것들을 세거나, 바이트가 문자를 기대하는 파이프라인을 지나갈 때까지 숨어 있게 됩니다. 의심스러우면 엄격한 문자셋으로 디코딩해, 예외가 바이트에 대한 진실을 말하게 하세요. Encode::FB_CROAK를 넘기면 decode()는 기본값의 조용한 U+FFFD 대체 대신 잘못된 시퀀스에서 죽어 버리는데, 그것은 기능입니다.
실제로 손이 갈 문자셋의 짧은 목록입니다:
| 문자셋 | 언제 쓸 것인가 | 주의할 점 |
|---|---|---|
UTF-8 |
기본 가정: API, JSON, 웹 콘텐츠, 현대 텍스트 | 잘못된 시퀀스는 기본적으로 U+FFFD가 된다. Encode::FB_CROAK를 쓰면 깨끗이 죽으므로, 바로 당신이 원하는 모습 |
Latin-1 |
레거시 서양 텍스트, 문자당 1바이트, 절대 실패하지 않는다 | UTF-8을 기꺼이 이중 인코딩된 문자열로 망가뜨린다 |
ASCII |
단순 7비트 텍스트임을 확신하는 데이터 | 127를 초과하는 바이트는 기본적으로 U+FFFD가 된다 (Encode::FB_CROAK 아래에서는 죽음) |
UTF-16 |
바이트 순서 마크가 엔디안을 결정하는 Windows 텍스트 | BOM이 유일한 엔디안 힌트이므로, 바이트 안에 유지해 두자 |
그리고 이것이 바로 문자셋 단계가 당신을 지켜 주는 함정입니다. 디코딩한 바이트가 이미 UTF-8인데, 나가는 길에 encode("UTF-8", ...)를 한 번 더 통과시켜 보면 복본을 얻는 것이 아니라 이중 인코딩을 얻게 됩니다. 발음 기호가 붙은 글자마다 제법 두 개 글자로 부풀어 오르는 형태죠. 고전적인 증상은, Hëllo로 읽히던 텍스트가 이제 Hëllo로 읽히는 것인데, 선의 반대편 수신 측이 그것을 충실히 디코딩해 줍니다. 바이트가 들어가면 바이트가 나오고, 그 사이에는 이름 붙은 변환이 한 번만.
base64url: URL과 토큰을 위한 알파벳
현대 인터넷을 건너다니는 Base64의 절반은 애초에 표준 알파벳이 아닙니다. + 문자는 브라우저가 쿼리 스트링에서 공백을 인코딩하는 방식이고, /는 경로 구분자이므로, 표준 글자들은 URL 속에서 재앙입니다. RFC 4648의 5절이 그 수단을 정의합니다. +와 /를 -와 _로 바꾸는 두 번째 알파벳으로, 관례상 = 패딩과 줄바꿈도 함께 빠뜨립니다. RFC는 이 인코딩을 base64 인코딩과 같은 것으로 간주해서는 안 된다고 명시하고 있으며, Perl에도 2010년 3.11 버전부터 이를 위한 전용 쌍이 있습니다:
use MIME::Base64 qw(decode_base64url);
my $raw = decode_base64url("c3Vuc2V0LTQy");
print $raw, "\n"; # sunset-42
아는 것이 두 가지입니다. 첫째, decode_base64url은 패딩 없는 입력을 기꺼이 받아들입니다. 실제로 야생에서 만나는 모습이 그 형태라서, 다른 언어가 요구하는 패딩을 먼저 회복하는 의례는 여기에 적용되지 않습니다. 패딩 있는 입력도 동작하죠. 둘째, 표준 디코더는 또 다른 존재입니다. base64url 세그먼트를 먹여 주면 조용히 틀린 바이트가 나오는데, -와 _ 문자는 잡음으로 버려지고 나머지는 여전히 디코딩되기 때문이죠. 올바른 디코더를 쓰시거나, 레거시 코드 경로에 묶여 있으면 수동으로 정규화하세요:
my $seg = "ab-cd_efgh";
$seg =~ tr{-_}{+/}; # URL-safe 글자들, 고향으로 번역해 돌아감
$seg .= "=" x (-length($seg) % 4); # 표준 디코더를 위해 패딩 회복
my $raw = decode_base64($seg);
print unpack("H*", $raw), "\n"; # 69bf9c77f79f82 - 7바이트, 표준 알파벳으로 돌아감
base64url은 바로 JWT, 모든 현대 API가 나눠 주는 토큰에서, 그리고 URL 속에 사는 어떤 불투명한 ID에서도 곧 만나게 됩니다. 11자리의 비디오 ID, URL-safe 알파벳으로 저장된 UUID(CPAN에는 정확히 이를 위한 Data::UUID::Base64URLSafe가 있습니다), 그리고 주소 창을 견뎌야 하는 데이터베이스 키가 그 예죠. 코어 함수가 나오기 전의 오래된 Perl을 쓰고 있다면, 2006년의 독립 모듈 MIME::Base64::URLSafe가 Python의 urlsafe 코덱을 이식한 것으로, urlsafe_b64encode와 urlsafe_b64decode를 제공합니다. 3.11 이상이라면 내장 함수가 더 나은 선택이죠.
JWT: 헤더와 페이로드를 읽는 법
구조적으로 보자면, JSON Web Token은 위장을 한 JSON 두 조각에 암호학적 영수증 하나가 붙은 것입니다. RFC 7515의 컴팩트 형태는 점으로 이어진 base64url 세그먼트 3개입니다. 보호된 헤더, 페이로드, 서명. 하나를 잘라서 읽는 일은 3줄이면 됩니다:
use MIME::Base64 qw(decode_base64url);
use JSON::PP;
my $jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJob21lciJ9.uzM6l0c4...";
my ($head_b64, $claims_b64, $sig_b64) = split /\./, $jwt, 3;
my $head = decode_json(decode_base64url($head_b64));
my $claims = decode_json(decode_base64url($claims_b64));
print $claims->{sub}, " (", $head->{alg}, ")\n"; # homer (HS256)
역할 분담에 주목하세요. decode_base64url이 각 세그먼트를 바이트로 바꾸고, Perl 5.14부터 존재하는 코어 JSON::PP 모듈의 decode_json이 헤더와 페이로드의 바이트를 Perl 데이터 구조로 만들어 줍니다. 서명 세그먼트도 base64url이지만, 그것은 암호학적 다이제스트입니다. 그래서 디코딩하는 것은 언제나 처음 두 세그먼트만이고, 세 번째는 제대로 된 라이브러리에 맡기세요.
함정은 모두의 기억에서 잘 잊히는 그겁니다. 읽을 수 있다는 것은 유효하다는 뜻이 아닙니다. 헤더와 페이로드는 설계상 읽을 수 있게 되어 있는데, 이것은 곧 누구나 그것들을 다시 쓸 수 있다는 뜻이기도 하죠. 서명이 유일한 증명입니다. 진지한 용도라면 검증하세요, 디코딩만 해서는 안 됩니다. CryptX를 바탕으로 하는 CPAN 모듈 Crypt::JWT가 이 일을 통째로 해 줍니다:
use Crypt::JWT qw(decode_jwt);
my $claims = decode_jwt(
token => $jwt,
key => $secret,
accepted_alg => "HS256",
);
잘못된 서명에는 죽음을 선언하며, accepted_alg를 고정하면 공격자가 토큰을 더 약한 변형으로 뒤바꾸는 알고리즘 혼동 구멍을 막아 줍니다. 서포트 통화 도중 토큰을 들여다보는 용도로는 디코딩해서 출력하는 워크플로가 충분합니다. 하지만 그것은 인증이 아닙니다.
파일: .b64에서 원본으로
파일은 Perl의 원라이너 문화가 진정으로 빛나는 곳입니다. 그리고 이 일 전체가 하나의 명령어에 들어맞죠. -0777 플래그가 바로 그 비장의 재료인데, 디코더에 한 줄씩 먹여 주는 대신 파일 전체를 하나의 문자열로 통째로 읽어들이기 때문입니다:
perl -MMIME::Base64 -0777 -ne 'print decode_base64($_)' < in.b64 > out
줄마다 디코딩하는 형태는 한 가지 특정 범주의 파일에 안전합니다. 모든 줄이 4의 배수인 Base64 문자를 담고 있는 파일. 76은 4의 배수이므로 제대로 MIME 래핑된 본문은 전부 여기에 속하죠. 래핑 지점이 고르지 않게 되는 순간 - 손으로 래핑한 파일에서는 대개 그렇습니다 - 줄마다 디코딩은 데이터 한가운데에서 패딩을 만들어내기 시작합니다. 통째로 읽는 모드에는 그런 조건이 없기에, 그것이 기본 선택지가 되는 이유가 됩니다:
perl -MMIME::Base64 -ne 'print decode_base64($_)' < in.b64 > out
스크립트 안에서 패턴은 표준 Perl 파일 춤이며, 조용하지만 중요한 디테일이 하나 있습니다. 두 핸들 모두에 :raw 레이어를 두는 것. 그러면 Perl이 들어오는 길이나 나가는 길에서 바이트를 플랫폼 텍스트로 해석하려 하지 않습니다:
use MIME::Base64 qw(decode_base64);
use Digest::SHA qw(sha256_hex);
open my $in, "<:raw", $ARGV[0] or die $!;
local $/;
my $blob = <$in>;
close $in;
my $decoded = decode_base64($blob);
print sha256_hex($decoded), "\n"; # 보낸 사람이 공개한 체크섬과 비교
open my $out, ">:raw", $ARGV[1] or die $!;
print {$out} $decoded;
close $out;
해시 행은 과시하려고 있는 것이 아닙니다. 디코더는 거의 무엇을 받아들이든 받으므로, 보낸 사람이 공개한 값과 체크섬이 일치하는 것이 이번 이동이 바이트 단위 정확했음을 보여주는 유일한 증명입니다. 정말로 거대한 파일의 경우, 래핑이 4자 경계에 걸쳐 있는 것을 전제로 하면 줄마다 반복하는 루프가 저메모리 대안이며, MIME::Base64::decoded_base64_length는 버퍼를 확정하기 전에 출력이 얼마나 커질지 알려 줍니다.
PEM 아머: 코트를 벗기고 DER는 지키라
모든 보안 스택의 .pem 파일은 갑옷을 입은 똑같은 Base64입니다. 헤더 줄, 푸터 줄, 그리고 옛 Privacy Enhanced Mail 관례에 따라 64자마다 래핑된 본문. 래퍼가 유일한 흥미로운 부분인데, 이 모듈의 디코더는 줄 길이에 아랑곳하지 않기 때문입니다:
use MIME::Base64 qw(decode_base64);
open my $fh, "<:raw", "cert.pem" or die $!;
local $/;
my $blob = <$fh>;
close $fh;
my @body = grep { !/^-----/ && /\S/ } split /\n/, $blob;
my $der = decode_base64(join "", @body);
print length($der), " bytes of DER\n";
BEGIN 줄과 END 줄은 벗겨 내고, 나머지는 하나의 문자열로 이어 붙이며, 지나가는 길의 모든 줄바꿈은 무시됩니다. 일상적인 인증서 작업에서는 OpenSSL 도구들이 이미 이 일을 대신해 주죠. 위 8줄은 해시, 지문, 또는 비교를 위해 원시 DER 바이트를 스스로 필요로 할 때 기억해 두어야 할 패턴입니다.
Data URI: 스스로 주소를 짊어지는 이미지
RFC 2397의 data: 스킴은 페이로드를 URL 안에 그대로 인라인으로 넣습니다. data:, 선택적인 미디어 타입, 선택적인 ;base64 플래그, 쉼표, 그리고 데이터. 이미지 같은 바이너리 미디어는 이 플래그를 쓰므로, 페이로드는 패딩을 단 표준 알파벳이며, 범상한 디코더가 작은 한 줄 잘라내기로 그것을 처리합니다:
use MIME::Base64 qw(decode_base64);
my $uri = "data:image/png;base64,iVBORw0KGgo...";
$uri =~ s/^data:[^,]+,// or die "not a data URI";
my $raw = decode_base64($uri);
print unpack("H8", $raw), "\n"; # 89504e47: PNG 매직 바이트
매직 바이트를 확인하는 것이 바로 그 수입니다. 앞의 16진수 8자자가 89504e47가 아니라면, 미디어 타입이 아무리 다른 말을 해도 그 이미지는 PNG가 아닙니다. 그리고 절대 불평하지 않는 디코더는 바로 그런 조용한 거짓말을 가능하게 하니까요.
사촌과 화석: uuencode와 그 밖의 알파벳들
Base64가 승리하기 전, 바이너리를 메일로 보내는 고전적인 UNIX 방식은 uuencode였으며, 오래된 메일링 리스트와 오래된 도구에서 여전히 만나게 됩니다. 좋은 소식: Perl에는 이미 내장 디코더가 있어서 모듈이 필요 없습니다. pack과 unpack의 u 템플릿 덕분에 가능하죠:
my $uu = pack("u", "Hello, World!");
print $uu, "\n"; # -2&5L;&\L(%=O<FQD(0`` 이고 줄바꿈 하나 추가
my $back = unpack("u", $uu);
print $back, "\n"; # Hello, World!
두 호출은 정확히 서로의 역수이고, 당신이 알아야 할 이야기는 전부입니다. 그리고 UNIX 툴체인에서 온 고전적인 uuencode 명령은 맨 줄을 begin 헤더와 end 푸터로 싸 주는 것뿐이므로, 디코딩하는 페이로드는 그 사이의 부분입니다.
Base64에도 변형 사촌들이 있으며, 어떤 디코더가 어떤 것을 먹는지 아는 것이 디버깅 세션을 하나 아껴 줍니다:
| 변형 | 래핑 | 어디서 만나는가 | decode_base64가 하는 일 |
|---|---|---|---|
| MIME (RFC 2045) | 76자 | 이메일 본문 | 그대로 디코딩: 줄바꿈과 CRLF는 무시 |
| PEM (RFC 1421) | 64자 | 인증서와 키 | 그대로 디코딩 |
| PKIX (RFC 7468) | 64자 | X.509 텍스트 구조 | 그대로 디코딩 |
| OpenPGP 아머 (RFC 9580) | 76자에 CRC24 줄 추가 | PGP 키와 서명 | 그대로 디코딩, 체크섬 줄은 무시됨 |
| IMAP (RFC 3501) | 없음 | 메일박스 이름 | 이 알파벳이 아님: 슬래시가 쉼표가 되므로, 먼저 글자를 번역 |
핵심은 이것입니다. 래핑 방식만 다른 표준 알파벳 변형이라면, 관대한 디코더 하나면 전부 커버합니다. 알파벳 자체가 바뀔 때에만 글자를 먼저 번역해야 합니다.
설정, 데이터베이스와 환경 변수
컨테이너 플랫폼, 클라우드 콘솔, 그리고 의외로 많은 수의 설정 파일이 자격 증명과 작은 문서를 불투명한 Base64 문자열로 저장합니다. 글자와 숫자의 덩어리는 그것이 사실인 비밀번호보다 덜 위험해 보이니까요. 디코딩은 언제나 같은 두 단계입니다. decode_base64에 문자셋 결정 하나를 더하는 것:
use MIME::Base64 qw(decode_base64);
use Encode qw(decode);
my $secret = decode("UTF-8", decode_base64($config->{api_key}));
이 포맷이 이 자리에 이렇게 인기 있는 이유는, 정확히 RFC 4648이 경고하는 그겁니다. 사람들은 데이터가 읽을 수 있다는 사실을 더 이상 눈치채지 않게 됩니다. 그래서 디코딩된 출력을 돌려받는 순간부터 기밀로 취급하고, 덩어리도 그 결과도 로그 파일, 알림, 디버그 덤프에서 멀리 두세요.
같은 모양은 데이터베이스에도 나타납니다. 컬럼이 임의의 바이트를 건드리지 않고 통과시키겠다는 보장을 할 수 없기 때문에, 바이너리 데이터가 Base64를 타고 TEXT 컬럼에 실려 다니는 일이 많죠:
use MIME::Base64 qw(decode_base64);
my $icon = decode_base64($row->{icon_data});
open my $fh, ">:raw", "icon.png" or die $!;
print {$fh} $icon;
close $fh;
이메일: MIME 파트와 첨부 파일
이메일은 Base64가 이름을 얻은 곳이며, 이 모듈의 관대함은 정확히 이 트래픽을 위해 설계되었습니다. Content-Transfer-Encoding: base64를 가진 MIME 파트는 CRLF로 끝나는 76자 줄의 텍스트로 도착하고, 디코더는 줄바꿈을 포함해 봉투 전체를 그대로 삼킵니다:
use MIME::Base64 qw(decode_base64);
my $part_body = "SGVsbG8sIHF1ZXJ5IQpUaGlzIE1JTUUgcGFydCB0cmF2ZWxsZWQgYXMgYmFzZTY0LCB3cmFwcGVk";
$part_body .= "\r\nIGF0IDc2IGNoYXJhY3RlcnMsIENSTEYgYmV0d2VlbiBsaW5lcy4=";
my $text = decode_base64($part_body);
print $text; # 원래의 2줄 메시지 본문
프레임워크로 메일을 만들거나 파싱한다면, 이 일들을 하나도 손으로 하지 않습니다. attach에 Encoding => "base64"를 넘기면 MIME::Lite가 첨부 파일을 Base64 인코딩해 주며, Email::MIME도 자동으로 같은 일을 합니다. 위에서 손으로 짠 버전은, 로그, 티켓, 또는 전달된 메시지 속에서 원시 텍스트로 도착하는 메일을 위한 것입니다. 실무에서는 그런 메일이 아주 많죠.
함정, 모아서 순서대로
이 모듈은 외울 정도로 작으므로, 함정 목록 전체를 한자리에 정리합니다. 대략 물리는 빈도 순으로요:
| 함정 | 무슨 일이 일어나나 | 해결책 |
|---|---|---|
| base64url 세그먼트를 표준 디코더에 먹이는 것 | -와 _는 잡음으로 버려지고, 나머지는 조용히 틀린 바이트로 디코딩된다 |
decode_base64url을 쓰거나, 먼저 알파벳을 번역하고 패딩을 회복 |
| 손상된 입력에 대한 침묵을 믿는 것 | 이질적인 문자, 잘림, 틀린 알파벳도 경고 한 번 없이 전부 디코딩된다 | 먼저 엄격 검사를 실행하고, 원본이 있다면 해시로 검증 |
| 덩어리 안의 비(Non-)ASCII 문자 | 실수로 들어온 발음 기호 글자나 붙여 넣은 Unicode 공백은 조용히 무시되고, 아무 설명 없이 결과가 줄어든다 | 같은 엄격 검사가 7비트 알파벳 밖의 모든 것을 거부한다 |
| 결과를 텍스트로 취급하는 것 | 바이트는 UTF-8 플래그를 품고 있지 않으므로, length()는 바이트를 세고 문자열 함수들은 틀린 그림을 얻는다 |
모든 텍스트 처리 전에 decode("UTF-8", $raw)나 당신이 고른 문자셋을 이어 붙이기 |
| 나가는 길의 이중 인코딩 | 이미 UTF-8인 바이트를 encode("UTF-8", ...)에 먹이면 Hëllo가 Hëllo가 된다 |
바이트가 아니라 문자를 인코딩하고, 의심스러우면 utf8::is_utf8()로 플래그를 확인 |
| 고르지 않은 래핑으로 파일을 줄마다 디코딩하는 것 | 4자 경계에서 끝나지 않는 줄은 출력 한가운데에 패딩을 만든다 | -0777로 통째로 읽거나, 4자 래핑 지점을 보장 |
| 실패한 정규식 매칭을 수치 맥락으로 반환하는 것 | return $x =~ /.../로 끝나 그 결과를 printf에 먹이는 서브는 오도하는 printf의 인자 누락 경고를 일으킨다 |
매칭을 강제 전환: return $x =~ /.../ ? 1 : 0 |
| 옛 경고를 기대하는 오래된 코드 | 3.11 이전, Base64 데이터의 조기 종료 카프를 -w 아래에서 의지하던 스크립트는 이제 아무것도 보지 못한다 |
스스로 엄격 검사를 추가. 그 경고는 영영 사라졌다 |
| 디코딩이 검증이라고 가정하는 것 | 디코더는 거의 무엇을 받아들이든 받고, 아무 말도 하지 않는다 | 일치하는 체크섬이나 검증된 서명만이 중요한 증명이다 |
| 디코딩한 것을 로그에 남기는 것 | 이 포맷은 아무것도 숨기지 않으며, 로그 파일은 바로 다음 사람이 그것을 발견하는 곳이다 | 디코딩된 기밀을 로그, 알림, 디버그 덤프에서 멀리 두어라 |
좋은 습관
Base64가 당신의 스크립트에 승리를 거두지 못하게 해 주는 습관들:
- 바이트를 기대하라, 언제나.
decode_base64가 원시 옥텟을 돌려준다는 사실을 아는 코드를 쓰세요. 터미널이 제대로 해 주기를 바라기보다, 문자셋decode를 명시적으로 이어 붙이세요. - 문자셋에 이름을 붙여라. 기본값은
UTF-8이고, 데이터가 다르게 말하기 전에는 바꾸지 마세요.decode("UTF-8", ..., Encode::FB_CROAK)의 엄격한 실패는 기능입니다. 바이트가 당신이 가정하던 것이 아님을 알려 주니까요. - 알파벳을 소스에 맞추어라. URL, 토큰, ID에는
decode_base64url, 나머지는 전부decode_base64. 두 알파벳은 교환이 불가능하며, 당신이 잘못 고를 때 디코더는 알려 주지 않습니다. - 디코딩 전에 검증해라. 이 모듈에는 엄격 모드 플래그가 없으므로, 작은 검사가 바로 문지기입니다.
- 파일을 기본적으로 통째로 읽어라.
-0777또는local $/ = undef는 래핑 지점 버그의 한 카테고리를 통째로 없애 주며, 실제로 디코딩하는 파일에겐 메모리 비용은 문제가 아닙니다. - 모든 파일 핸들에
:raw를 쓰라. 바이너리가 들어가면 바이너리가 나온다. 텍스트 레이어는 사람을 위한 것이지, 바이트를 위한 것이 아닙니다. - 해시로 검증해라. 원본이 있다면, 일치하는 체크섬이 바이트 단위 정확한 디코딩의 유일한 증명입니다.
- 디코딩한 것을 절대 로그에 남기지 마라. 이 포맷은 아무것도 숨기지 않습니다.
변경 기록이 말해주는 짧은 역사
포맷은 오래되었고, Perl과 그 포맷의 인연은 겉보기보다 더 오래되었습니다. 몇 가지 확인된 날짜를 순서대로:
- C 코드는 Perl 5보다 앞선다. 모듈 안의 빠른 디코더는 Bellcore의 메일 프로그램 metamail의 코드로 거슬러 올라가며, 저작권은 1991년, 최초의 Perl 5 릴리스의 3년 전입니다. 오늘 당신이
decode_base64를 부를 때, 90년대의 한 조각이 일을 하고 있는 셈이죠. - 웹 도구에서 태어났다. 모듈은 90년대 중반 libwww-perl 안에서
LWP::Base64로 시작했습니다. Martijn Koster와 Joerg Reichelt가 썼고, 1997년 4월에는 자신의 CPAN 배포판MIME::Base64로 독립했습니다. 버전 2.00, 변경 기록 항목은 libwww-perl-5.08 기반이라고 적혀 있습니다. - 경고의 시대. 1997년 2.03부터, 잘린 입력은 croak 대신 Base64 데이터의 조기 종료 경고를
-w아래에서 냈고, 1999년 2.11에는 멀쩡한 데이터에도 경고를 냈던 빌드들을 고쳤습니다. 디코더들에게 좀 더 신경질적이었던 십년이었죠. - 2002년부터 코어에. Perl 5.8이 이 모듈을 코어 배포판에 들여왔고, 같은 12월의 코어와 동기화한 2.13은 EBCDIC 지원을 함께 가져왔습니다. 이것이 바로 인코더와 디코더가 아직까지 메인프레임에서 동작하는 이유입니다.
- URL-safe 변형은 2010년에 Perl 코어에 도착했다. 버전 3.11이
decode_base64url과 그 형제를 추가했는데, 독립 모듈MIME::Base64::URLSafe가 2006년 CPAN에 내려앉은 지 4년 후의 일입니다. 그 해는 RFC 4648이 이 변형을 법전으로 삼은 해이기도 하죠. - 조용해짐. 그 3.11 릴리스는, 의심스러운 입력이 의도적인 것일지도 모른다는 가능성 때문에, 오래된 잘림 경고마저 제거했습니다. 그 이후 모든 릴리스, 2020년의 현행 3.16 라인을 포함해, 디코더는 예의 바르고 조용한 것을 유지해 왔습니다.
재미 있는 사실, 특히 Perl
투어를 마무리하며, 이 이야기를 좋은 이야기로 만들어 주는 흥미로운 사실들:
- 포맷 자체의 이름을 디코딩해 보라.
decode_base64("YmFzZTY0")는base64를 돌려 줍니다. 1997년부터 참이었고, 영원히 참일 겁니다. - 디코더는 예의 바른 유령이다. 3.x 역사에서 그것은 잘못된 입력에 대해 한 번도 예외를 던진 적이 없습니다. 손상, 잘림, 틀린 알파벳: 모든 것을 디코딩하고 아무것도 불평하지 않으며, 2010년에 변경 기록이 의도적으로 굳힌 행위입니다.
- MIME 래핑은 의도적으로 4의 배수다. 76자 한계는 3바이트 그룹 열아홉 개, 총 57바이트, 곱하기 문자 4개. 이것이 바로 줄마다 디코딩이 제대로 래핑된 MIME 본문이면 안전하고, 그 밖에는 안전하지 않은 이유입니다.
- uuencode는 소문자를 절대 출력하지 않는다. 그 알파벳의 끝은 밑줄입니다. 그래서 오래된 uuencoded 파일은 전부 대문자 기계로 입력한 것처럼 보이고, 그래서 Perl은 인터넷보다 오래된 포맷의 내장 디코더를 아직까지 품고 있습니다.
- Perl은 한때 자신의 decode-base64 명령을 싣고 다녔다. 2003년 2.14부터 2004년 3.05까지의 릴리스는
encode-base64,decode-base64, 그리고 그들의 quoted-printable 쌍둥이를 스크립트로 번들했습니다. 2005년 3.06은 그것들을 별도의 MIME-Base64-Scripts 배포판으로 옮겼습니다. PATH에 그 명령이 있는 오래된 설치를 발견하면, 이제 어디서 왔는지 아시겠죠. - YouTube 비디오 ID는 위장한 base64url이다. 주소 창에 있는 11자 ID는 패딩을 뺀 URL-safe 알파벳의 64비트 숫자입니다. 당신이 본 모든 비디오의 URL에는 Base64 문자열이 들어 있고,
decode_base64url는 그것을 읽을 수 있습니다. - 관대함은 버그가 아니라 표준이다. 받아들일 때는 관대하라, 그 MIME의 원칙이 바로 이 디코더가 어지러운 데이터의 30년을 살아남은 이유이며, RFC 4648이 신뢰할 수 없는 입력을 믿으면 같은 관대함이 은밀한 채널로 바뀔 수 있다고 경고하는 이유이기도 합니다.
그러니 다음에 글자, 숫자, 더하기, 슬래시의 문자열이 터미널에 떨어져도, 당신은 전체 이야기를 알고 있습니다. 함수 호출 하나가 일을 하고, 디코더는 당신을 결코 거절하지 않는 예의 바른 유령이고, base64url에는 전용 디코더가 있으며, 문자셋은 의도적으로 내리는 결정이고, 파일은 원시로 들어와 원시로 나가고, 해시가 유일한 중요한 증명입니다. 그리고 어느 날 반대 방향으로 여행을 해야 한다면, 자신의 원시 데이터를 텍스트 봉투에 싸서 세상으로 보내야 한다면, 아래에서 링크된 Perl에서의 Base64 인코딩 관련 글이 그 의례를 같은 깊이로 다룹니다.
마지막 업데이트: 2026-09-08
관련 문서: Perl에서의 Base64 인코딩: 완전한 가이드