文字列のエンコードとデコード
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 で die します。é のような 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、データベースのフィールドなど、それ以外のほぼすべての用途では誤りです。1 行で出力するなら '' を、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セーフな 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セーフな出力が必要なら、標準形を 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 のファイルに戻りました。1 行で出力するなら、3 で割り切れる任意のチャンクサイズで encode_base64($buf, '') を使ってください。このモジュールは、エンコードせずにサイズを教えてくれます。MIME::Base64::encoded_base64_length('hello') は 9(改行を含む)を、decoded_base64_length('aGVsbG8=') は 5 を返します。計算式は サイズ増加の解説 にあります。
コマンドラインでは、このモジュールを使った次の 2 つのワンライナーを覚えておくと便利です。
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セーフな入力を標準のデコーダーに渡すと、何も言わずに誤ったバイト列を生成します。トークンには 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 が改行を付けるのはなぜですか?
2 番目の引数が行末の文字列で、デフォルトが "\n" だからです。出力は 76 文字で折り返され、必ずその文字列で終わります。1 行で出力したいなら '' を渡してください。
「Wide character in subroutine entry」とはどういう意味ですか?
バイト列しか受け付けない encode_base64 に、コードポイントが 255 を超える文字列を渡したということです。先にテキストをエンコードしてください:encode_base64(encode('UTF-8', $text), '')。
decode_base64 は入力を検証しますか?
しません。アルファベット外の文字を無視し、デコードできたものを返すので、decode_base64('aGk=!!') は hi になります。信頼できない入力なら、デコードの前に正規表現で文字列をチェックしてください。