문자열 인코딩과 디코딩
use strict;
use warnings;
use utf8;
use MIME::Base64 qw(encode_base64 decode_base64);
use Encode qw(encode decode);
my $enc = encode_base64(encode('UTF-8', 'héllo wörld'), '');
print "$enc\n"; # aMOpbGxvIHfDtnJsZA==
print decode('UTF-8', decode_base64($enc)), "\n"; # héllo wörld
Perl은 문자 문자열과 바이트 문자열을 구분하며 encode_base64는 바이트를 원하므로, 텍스트는 먼저 Encode::encode('UTF-8', ...)를 거쳐야 합니다. U+00FF보다 큰 문자가 있는 문자열은 Wide character in subroutine entry로 죽고, é처럼 Latin-1 문자만 있는 문자열은 UTF-8이 아니라 Latin-1로 조용히 인코딩됩니다.
'' 인수는 다음 절에서 설명합니다.
줄 끝 인수
encode_base64($bytes, $eol)은 출력을 76자에서 줄 바꿈하고 마지막 줄을 포함한 모든 줄에 $eol을 붙입니다. 기본값은 "\n"입니다.
print encode_base64('hi'); # "aGk=\n"
print encode_base64('x' x 60);
# eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4
# eHh4
MIME 이메일 본문에는 이것이 맞지만, HTTP 헤더, JSON, URL, 데이터베이스 필드 등 거의 모든 다른 곳에는 맞지 않습니다. 한 줄 출력에는 ''를, 엄격한 RFC 2045 줄 끝에는 "\r\n"을 넘기세요.
HTTP 헤더라면 다음과 같습니다.
print "Authorization: Basic ", encode_base64('user:pass', ''), "\n";
# Authorization: Basic dXNlcjpwYXNz
디코딩은 줄 끝을 신경 쓰지 않습니다. decode_base64("aGVs\r\nbG8K")는 "hello\n"을 반환하므로 \n이든 \r\n이든 MIME 본문을 그대로 디코딩할 수 있습니다.
URL-safe Base64
MIME::Base64는 요청하면 encode_base64url과 decode_base64url을 내보냅니다. 인코더는 패딩을 제거하고 디코더는 패딩 없는 입력을 받습니다.
use MIME::Base64 qw(encode_base64url decode_base64url);
print encode_base64url('<<???>>'), "\n"; # PDw_Pz8-Pg
print decode_base64url('PDw_Pz8-Pg'), "\n"; # <<???>>
패딩을 유지하는 URL-safe 출력이 필요하면 표준 형식을 tr로 변환하세요.
my $padded = encode_base64('<<???>>', '') =~ tr{+/}{-_}r; # PDw_Pz8-Pg==
/r 플래그(Perl 5.14 이상)는 수정된 사본을 반환합니다. 어떤 시스템이 어떤 변형을 기대하는지는 Base64와 Base64URL 비교에서 설명합니다.
파일 인코딩과 디코딩
어떤 계층도 줄 바꿈이나 인코딩을 변환하지 않도록 파일을 :raw 모드로 여세요.
open my $in, '<:raw', 'hello.txt' or die $!;
my $data = do { local $/; <$in> };
close $in;
print encode_base64($data, ''), "\n"; # aGVsbG8K
open my $out, '>:raw', 'copy.txt' or die $!;
print {$out} decode_base64('aGVsbG8K');
close $out;
큰 파일을 조각으로 처리하기
57바이트의 배수로 읽으세요. 57바이트는 정확히 76자로 인코딩되므로 조각마다 온전한 MIME 줄이 나오고, 이어 붙인 출력은 파일 전체를 인코딩한 결과와 같습니다.
open my $in, '<:raw', 'big.bin' or die $!;
open my $out, '>:raw', 'big.b64' or die $!;
while (read($in, my $buf, 57 * 1024)) {
print {$out} encode_base64($buf);
}
close $in;
close $out;
결과를 base64 -d로 디코딩하면 원래의 300 KB 파일로 돌아왔습니다. 한 줄 출력에는 3으로 나누어떨어지는 아무 조각 크기와 함께 encode_base64($buf, '')를 쓰세요. 모듈은 인코딩하지 않고 크기만 알려 줄 수도 있습니다. MIME::Base64::encoded_base64_length('hello')는 9(줄 바꿈 포함)를, decoded_base64_length('aGVsbG8=')는 5를 반환합니다. 공식은 크기 증가 가이드에 있습니다.
명령줄에서는 알아 둘 만한 한 줄짜리 명령 두 가지가 모듈과 함께 제공됩니다.
perl -MMIME::Base64 -0777 -ne 'print encode_base64($_, "")' < file.bin
perl -MMIME::Base64 -0777 -ne 'print decode_base64($_)' < file.b64 > file.bin
검증: decode_base64는 절대 불평하지 않습니다
decode_base64는 설계상 관대합니다. 알파벳 밖의 모든 것을 버리고, 패딩에서 멈추고, 가진 것을 반환합니다.
decode_base64('aGk=!!'); # "hi"
decode_base64('aGk'); # "hi" (missing padding is fine)
decode_base64('aGl='); # "hi" (non-zero padding bits ignored)
decode_base64('a'); # "" (too short, no warning)
decode_base64('PDw_Pz8-Pg=='); # "<<\x0f\xcf\xc3\xe0", not "<<???>>"
마지막 경우가 위험합니다. URL-safe 입력을 표준 디코더에 넣으면 조용히 잘못된 바이트가 나옵니다. 토큰에는 decode_base64url을 쓰세요. 신뢰할 수 없는 입력은 알파벳과 길이를 확인한 다음 왕복 결과를 확인하세요.
sub is_base64 {
my ($s) = @_;
return 0 unless $s =~ m{\A[A-Za-z0-9+/]*={0,2}\z} && length($s) % 4 == 0;
return encode_base64(decode_base64($s), '') eq $s ? 1 : 0;
}
# is_base64('aGk=') = 1, 'aGk' = 0, 'aGl=' = 0, 'aGk=!!' = 0
Base64 검사기는 브라우저에서 같은 검사를 하고 어떤 문자가 잘못되었는지 알려 줍니다. 로그에서 가져온 깨진 문자열은 Base64 복구를 사용해 보세요. 흔한 원인은 흔한 Base64 오류에서 설명합니다.
함정
- 헤더 끝의 줄 바꿈.
"Authorization: Basic " . encode_base64("$user:$pass")는\n으로 끝나서 요청이 깨집니다.''를 넘기세요. - 이중 인코딩.
use utf8과:encoding(UTF-8)출력 계층을 쓰는 상태에서decode('UTF-8', ...)없이 디코딩한 바이트를 출력하면héllo처럼 글자가 깨집니다. - Base64를 비밀로 취급하기. 누구나 되돌릴 수 있습니다. 인코딩과 암호화의 차이를 참고하세요.
자주 묻는 질문
encode_base64가 줄 바꿈을 붙이는 이유는 무엇인가요?
두 번째 인수가 줄 끝 문자열이고 기본값이 "\n"이기 때문입니다. 출력은 76자에서 줄 바꿈되고 항상 그 문자열로 끝납니다. 한 줄로 받으려면 ''를 넘기세요.
'Wide character in subroutine entry'는 무슨 뜻인가요?
바이트만 받는 encode_base64에 코드 포인트가 255보다 큰 문자열을 넘겼다는 뜻입니다. 먼저 텍스트를 인코딩하세요. encode_base64(encode('UTF-8', $text), '').
decode_base64는 입력을 검증하나요?
아니요. 알파벳 밖의 문자를 무시하고 디코딩할 수 있는 만큼 반환하므로 decode_base64('aGk=!!')는 hi를 돌려줍니다. 신뢰할 수 없는 입력이라면 디코딩 전에 정규식으로 문자열을 확인하세요.