Perl での Base64 デコード:完全ガイド
あなたの手に渡されたのは、英字と数字の文字列で、たまに + や / が混じるものです。心の底では、それが見た目ほど素直なものではないことはわかっています。それは Authorization ヘッダーに乗ってきたトークンかもしれない。サポートチケットから掘り出した .b64 ファイルかもしれない。-----BEGIN の鎧をまとった証明書かもしれない。あるいは設定ファイルの中で静かに座っている文字の塊かもしれません。あなたはターミナルを開き、perl と入力する。するとある 1 つの問いが他のすべてを奪い取ります:本当のデータをどうすれば取り戻せるのか?
答えは小さく、安心させられます。Perl には 2002 年から Base64 モジュールが言語本体と一緒に同梱されており、decode_base64 という関数 1 回の呼び出しで仕事が全部終わります:インストールも不要、設定も不要。コーヒーが淹れられる間に、ささっと復習しましょう:Base64 はデータ 3 バイトごとを、64 文字のアルファベットから選んだ 4 文字に書き換え、末尾を 1 つか 2 つの = でパディングして、結果が必ず 4 の倍数に落ちるようにします。だからこそ、エンコード後の形は元よりおおむね 33 パーセント大きくなりがちです。このサイトのホームページではこの形式を丸ごと説明しているので、このガイドは Perl 側の話に時間を全部使います:デコーダーのルール、各バリエーション、そして実際に現場で出会うフォーマットたちです。
ツールボックス:5 つの関数、インストールはゼロ
必要な呼び出しはすべて MIME::Base64 に集まっています。これは 5.8 から Perl のコアの一部なので、ルーターのファームウェアに埋め込まれたものからデータベースサーバーの上のものまで、まともなインストールには必ず存在します。確認は 1 行で:
perl -MMIME::Base64 -e 'print $MIME::Base64::VERSION, "\n"'
# 3.16_01
モジュールのデコード側はこのとおり、全部でこれだけです:
| 関数 | 何をするか | 備考 |
|---|---|---|
decode_base64($str) |
この記事の主人公:Base64 の塊を生のバイト列に変換する | アルファベット外の文字はすべて、永遠に黙って無視する |
MIME::Base64::decode($str) |
同じデコーダーを、インポートなしで呼ぶ形式 | 古いスクリプトでたっぷり見かける形 |
decode_base64url($str) |
- と _ を使う URL 安全なバリエーションを、パディングの有無にかかわらずデコードする |
2010 年の 3.11 で追加。JWT を読むのはこれ |
MIME::Base64::decoded_base64_length($str) |
デコードせずに、デコード後のデータがどれくらいの大きさになるかを教えてくれる | デフォルトではエクスポートされない。バッファの事前サイズ決めには便利 |
unpack("u", $data) |
uuencode データ、つまり Base64 に先立つ形式をデコードする | 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 に、なぜかこのモジュールがない場合(あるべきなのに)、直す方法は 2 行のどちらかです:Debian と Ubuntu ならディストリパッケージの libmime-base64-perl、もしくは cpanm MIME::Base64 で CPAN から現在のリリースを引っ張ってくる。このモジュールはコアだった時代から、コアと CPAN の両方で暮らすパッケージとして CPAN に住み続けています。C コンパイラがない珍しいマシン向けには、CPAN の純 Perl ツイン MIME::Base64::Perl が同じ基本的なインターフェースを提供します。数倍遅くなりますが、大量処理を除けば十分な性能です。依存関係の話はこれで全部です:それ以外には何もありません。
「ノー」とは絶対に言わないデコーダー
契約書は 1 行です。文字列を渡すと、生のオクテットを保持する普通の Perl 文字列としてデコード済みバイトが返ってきます。オブジェクトも、例外も、フラグもありません。ドキュメントは、その個性を定義する 2 つのルールを 1 文で述べています: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"; # 空の文字列、それでも文句はなし
最後の 2 行は寛大さの極みです。TQ は 1 バイト分と余り 4 ビットを保持していて、デコーダーはバイトを残して残りだけを捨てます。T は 1 バイト分すら保持していないので、結果は空になります。このモジュールには、旧来の神経質さを取り戻すための厳格モードもバリデーターもありません:2010 年の 3.11 以降、decode_base64 は切り詰められた入力に対して警告すら出さず、古いバージョンは -w の下で Premature end of base64 data 警告をぶつぶつ言っていました。塊が間違っていようと、それはデコードしてしまうのです。つまり品質ゲートとなるのはあなたです。
寛大さのポリシーを 1 か所に集めておいたので、一目で全体が見えます:
| 入力 | 結果 | なぜか |
|---|---|---|
"TWFu" |
Man |
クリーンな入力、ハッピーパス |
"TWFu!" |
Man |
感嘆符はアルファベットに含まれないので、スキップされる |
"TWFu=XX" |
Man |
パディングの後は決してデコードされない |
"TWFuIFdvcmxkIQ==" |
Man World! |
空白はどこになっても無料 |
"TQ" |
M |
1 バイト分は収まる。余りビットは静かに捨てられる |
"T" |
空の文字列 | 1 バイト分すらなく、警告もなし |
"ab-cd_efgh" |
黙って間違ったバイト列 | URL 安全な文字がノイズとして捨てられる、古典的な罠 |
覚えるべきは最後の行です。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 安全なアルファベット
その正規表現について、血の代価で得た教訓を 1 つ:サブが素の return $x =~ /.../ で終わり、マッチ失敗の結果がそのまま printf に渡されると、Perl はきれいなゼロの代わりに誤解を招く Missing argument in 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
その 2 つの数字が、全部の教訓です。塊は 13 バイトですが文字としては 12 だけです。アクセント付きの文字が UTF-8 では 2 バイトを使うからです。文字コードのステップを飛ばしても、バイトは UTF-8 ターミナルにはそのままきれいに出力され、それがまさに、ある文字列関数がそれらを数えたり、バイトが文字を期待するパイプラインを通るまで、この間違いが隠れ続ける理由です。迷ったら厳格な文字コードでデコードして、例外にバイトの真実を告げてもらいましょう:Encode::FB_CROAK を渡せば、decode() はデフォルトの静かな U+FFFD 置換の代わりに、無効なシーケンスで死にます。それは機能です。
実際に手が伸びる文字コードの短いリスト:
| 文字コード | 使う場面 | 注意すべき点 |
|---|---|---|
UTF-8 |
デフォルトの前提:API、JSON、Web コンテンツ、現代的なテキスト | 無効なシーケンスはデフォルトで U+FFFD になる。Encode::FB_CROAK ならきれいに死ぬ。まさにそれが欲しい挙動 |
Latin-1 |
レガシーの西方テキスト。1 文字 1 バイトで、失敗することはない | UTF-8 を二重エンコードされたモジバケに変えてしまうことを平気で行う |
ASCII |
7 ビットのプレーンテキストだと確信できるデータ | 127 を超えるバイトはデフォルトで U+FFFD になる(Encode::FB_CROAK では死ぬ) |
UTF-16 |
Windows テキスト。バイトオーダーマークがエンディアンを決める | BOM が唯一のエンディアン手がかりなので、バイトの中に残しておく |
そしてここが、文字コードのステップがあなたを守ってくれる罠です。デコードしたバイトがすでに UTF-8 のまま、出口で encode("UTF-8", ...) を通すと、コピーは手に入りません:二重エンコーディングが手に入ります。すべてのアクセント付き文字が、それぞれ 2 文字に膨れ上がる、という二重エンコードです。典型的な症状は、かつて Hëllo と読めたテキストが今は Hëllo と読めることになり、ワイヤの向こう側の受信側はそれを忠実にデコードしてくれます。バイトが入り、バイトが出。その間に名づけられた変換を 1 つ。
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
知っておくべきは 2 つのことです。第一に、decode_base64url はパディングなしの入力でも喜んで動きます。実在の現場で実際に見かけるのはこの形ですし、他の言語が要求する「先にパディングを復元する」儀式はここでは必要ありません。パディング付きの入力でも動きます。第二に、標準デコーダーは別物です:base64url セグメントを渡すと、黙って間違ったバイト列が返ってきます。なぜなら - と _ がノイズとして捨てられ、残りはそのままデコードされてしまうからです。正しいデコーダーを使いましょう。レガシーなコードパスに縛られている場合は、手で正規化してください:
my $seg = "ab-cd_efgh";
$seg =~ tr{-_}{+/}; # URL 安全な文字を標準文字へ書き換え
$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 安全なアルファベットで保存された UUID(ちょうどこれのために CPAN に Data::UUID::Base64URLSafe があります)、そしてアドレスバーを生き延びる必要があるデータベースキー。さらに、コア関数よりも古い Perl を使っているなら、2006 年のスタンドアロンモジュール MIME::Base64::URLSafe が urlsafe_b64encode と urlsafe_b64decode を提供します。これは Python の urlsafe コーデックの移植です。3.11 以降なら、組み込みの方が良い選択肢です。
JWT:ヘッダーとペイロードを読む
構造的には、JSON Web Token は偽装をした 2 つの JSON に、暗号学的な領収書が 1 枚付いたものです。RFC 7515 のコンパクト形式は、ドットでつなげた 3 つの base64url セグメントです:保護済みヘッダー、ペイロード、署名。1 つを分解して読むのは 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 が各セグメントをバイト列に変え、コアの JSON::PP モジュール(Perl 5.14 から存在)の decode_json がヘッダーとペイロードのバイトを Perl のデータ構造に変えます。署名セグメントも base64url ですが、これは暗号学的ダイジェストなので、デコードするのは常に最初の 2 セグメントだけで、3 つ目はまともなライブラリに任せます。
罠は、誰もが一番忘れやすいものです:読めることは、有効なこととは限りません。ヘッダーもペイロードも、設計上そのまま読めるようになっており、つまり誰でも書き換えてしまうことができます。唯一の証拠は署名です。何か本当に大事なものの場合は、検証してください。デコードするだけではだめです。CryptX を土台とする CPAN モジュール Crypt::JWT は、この仕事を全部やってくれます:
use Crypt::JWT qw(decode_jwt);
my $claims = decode_jwt(
token => $jwt,
key => $secret,
accepted_alg => "HS256",
);
署名が不正なら croak しますし、accepted_alg をピン留めしておけば、攻撃者がトークンを弱いバリエーションに切り替えてしまうアルゴリズム混同の穴も塞げます。デコードして出力する工作流は、サポートコール中にトークンを調べるときなら十分です。ただし、それは認証ではありません。
ファイル:.b64 から元ファイルへ戻る
ファイルこそ、Perl のワンライナー文化が本当に輝く場所です。全部の仕事が 1 コマンドに収まります。-0777 フラグが秘密の材料です。デコーダーに 1 行ずつ食わせるのではなく、ファイル全体を 1 つの文字列に読み込むから:
perl -MMIME::Base64 -0777 -ne 'print decode_base64($_)' < in.b64 > out
1 行ずつの形式が安全なのは、特定の 1 種のファイルだけです:各行が Base64 文字の 4 の倍数分を保持しているファイル。正しく MIME ラップされた本文は必ずそうで、76 は 4 の倍数だからです。ラップ位置が不揃いになった瞬間 - 手でラップしたファイルでは普通そうなり - 1 行ずつのデコードはデータの中にパディングを生やし始めます。全体を一度に読むモードにはそのような条件がありません。それがデフォルトの選択肢になる理由です:
perl -MMIME::Base64 -ne 'print decode_base64($_)' < in.b64 > out
スクリプトの中では、このパターンは標準的な Perl のファイル操作です。静かだが重要な細部が 1 つあります:両方のハンドルに :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;
ハッシュの行は、ただの自慢大会ではありません。デコーダーはほぼ何でも受け入れるので、送信者が公開したものとの一致するチェックサムが、この旅がバイト単位で正確だった唯一の証拠です。本当に巨大なファイルでは、1 行ずつのループが低メモリな代替手段になります(ラップ位置が 4 文字の境界に座る場合)。また MIME::Base64::decoded_base64_length は、バッファに踏み切る前に出力がどれくらいの大きさになるかを教えてくれます。
PEM アーマー:鎧を剥いで、DER は残す
あらゆるセキュリティスタックにある .pem ファイルは、同じ Base64 が鎧をまとったものです:ヘッダー 1 行、フッター 1 行、そして古い 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 の行は剥がれ、残りは 1 つの文字列に結合され、その過程ですべての改行は無視されます。日々の証明書作業では、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!
2 つの呼び出しは正確な逆関数であり、それで必要な話は全部です。UNIX ツールチェーンの古典的 uuencode コマンドは、素の行を begin ヘッダーと end フッターで包むだけなので、デコードするペイロードはそれらの間にある部分です。
Base64 にはダイアレクトのいとこたちもいて、どのデコーダーがどれを食べるのかを知っておくと、デバッグのセッションが 1 つ節約できます:
| バリエーション | ラップ幅 | 出会う場所 | 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) | なし | メールボックス名 | このアルファベットではない:スラッシュがコンマに変わるため、先に文字を書き換える |
要点はこれです:ラップのしかただけが違う標準アルファベットのバリエーションは、1 つの寛大なデコーダーで全部カバーできます。アルファベットそのものが変わる場合だけは、先に文字を書き換える必要があります。
設定ファイル、データベース、環境変数
コンテナプラットフォーム、クラウドのコンソール、そして驚くほど多くの設定ファイルは、認証情報や小さな文書を不透明な Base64 文字列として保存しています。文字と数字の塊は、それが実際にパスワードであるということよりも、ずっと危険に見えないからです。デコードは常に同じ 2 ステップです:decode_base64 に文字コードの決定を加える:
use MIME::Base64 qw(decode_base64);
use Encode qw(decode);
my $secret = decode("UTF-8", decode_base64($config->{api_key}));
この形式がこの場所にこれほど人気がある理由は、まさに RFC 4648 が警告していることです:人間は、データが読めることに気づかなくなっていく。だから、デコードされた出力は返された瞬間から機密として扱い、塊そのものも結果も、ログファイル、アラート、デバッグダンプの外に出さないでください。
同じ形はデータベースにも現れます。バイナリデータは、カラムが任意のバイトをそのまま通すことを約束できないために、TEXT カラムの中で Base64 として乗って運ばれることが多いからです:
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 も自動的に同じことをします。上記の手作業版は、ログ、チケット、転送メッセージの中で素のテキストとして到着するメール用です。実際には、そういうメールの方がかなり多いのです。
罠のコレクション、頻度順に並べて
このモジュールは暗記できるほど小さいので、罠のリストを全部 1 か所に集めておきます。およそ噛み付く頻度順に並べました:
| 罠 | 何が起きるか | 直し方 |
|---|---|---|
| base64url セグメントを標準デコーダーに渡す | - と _ がノイズとして捨てられ、残りが黙って間違ったバイト列にデコードされる |
decode_base64url を使う、または先にアルファベットを書き換えてパディングを復元する |
| 壊れた入力に対する沈黙を信頼する | 外国の文字、切り詰め、間違ったアルファベット、どれも 1 個の警告もなくデコードされる | 先に厳密チェックを実行し、元があるならハッシュで検証する |
| 塊の中の非 ASCII 文字 | 紛れ込んだアクセント付きの文字や貼り付けられた Unicode スペースは黙って無視され、結果がコメントもなく縮む | 同じ厳密チェックが、7 ビットアルファベットの外すべてを拒否する |
| 結果をテキストとして扱う | バイトには UTF-8 フラグが立っていないので、length() はバイトを数え、文字列関数は間違った絵を掴む |
テキスト処理の前に、decode("UTF-8", $raw) か選んだ文字コードをチェーンする |
| 出口での二重エンコーディング | すでに UTF-8 のバイトを encode("UTF-8", ...) を通すと、Hëllo が Hëllo になる |
エンコードするのは文字で、生のバイトは絶対になし。迷ったら utf8::is_utf8() でフラグを確認する |
| ラップが不揃いのファイルを 1 行ずつデコードする | 4 文字の境界で終わらない行が、出力の真ん中にパディングを生む | -0777 で一度に全部読み込むか、4 文字のラップ位置を保証する |
| マッチ失敗の結果を数値コンテキストで返す | return $x =~ /.../ で終わる sub がその結果を printf に渡すと、誤解を招く Missing argument in printf 警告が上がる |
マッチを強制変換する:return $x =~ /.../ ? 1 : 0 |
| 古い警告を期待する古いコード | 3.11 以前のスクリプトは -w 下の Premature end of base64 data 警告に頼っていたが、今は何も見えない |
自分の厳密チェックを追加する。警告は完全に消えた |
| デコードは検証だと決めつける | デコーダーはほぼ何でも受け入れ、何も言わない | 一致するチェックサムか検証済み署名だけが、唯一の重要な証拠 |
| デコードしたものをログに出す | この形式は何も隠さないし、ログファイルこそ次に誰かがそれを見つける場所そのもの | デコードしたシークレットを、ログ、アラート、デバッグダンプの外に出さない |
良い習慣
Base64 がいつだってあなたのスクリプトに勝たせない、その習慣たち:
- 常にバイトを期待する。
decode_base64は生のオクテットを返すと知っているコードを書き、ターミナルが正しいことをしてくれることを願う代わりに、文字コードのdecodeを明示的にチェーンする。 - 文字コードに名前をつける。デフォルトは
UTF-8、データがそうだと示すときだけ切り替える。decode("UTF-8", ..., Encode::FB_CROAK)の厳格な失敗は機能です:それは、バイトがあなたが想定したものではないことを教えてくれる。 - アルファベットを出所に合わせて選ぶ。URL、トークン、ID には
decode_base64url、それ以外はdecode_base64。2 つのアルファベットは相互に置き換えられるものではなく、間違った方を選んでもデコーダーは言ってくれない。 - デコードする前に検証する。このモジュールには厳格モードのフラグがないので、小さなチェックがドアマンになる。
- ファイルはデフォルトで全部を一度に読む。
-0777かlocal $/ = undefがラップ位置のバグの 1 種を丸ごと消し、メモリのコストは実際にデコードするファイルでは問題にならない。 - すべてのファイルハンドルに
:rawを使う。バイナリが入り、バイナリが出る。テキストレイヤーは人間用で、バイト用ではない。 - ハッシュで検証する。元が手元にあるなら、一致するチェックサムこそがバイト単位で正確なデコードの唯一の証拠。
- デコードしたものは絶対にログに出さない。この形式は何も隠さない。
チェンジログが語る、短い歴史
この形式は古い。Perl との関係は、見えるよりもずっと古い。確認済みのいくつかの日付を、順番に:
- C のコードは Perl 5 より古い。モジュールの中の高速デコーダーは、Bellcore のメールプログラム metamail のコードに由来します。著作権表示は 1991 年で、最初の Perl 5 リリースの 3 年前のこと。今日あなたが
decode_base64を呼ぶとき、90 年代の一片がその仕事をしています。 - Web ツールの中で生まれた。モジュールは 90 年代半ばに libwww-perl の中身として
LWP::Base64で始まり、Martijn Koster と Joerg Reichelt が書いて、1997 年 4 月に独自の CPAN ディストリビューションMIME::Base64へ卒業しました。バージョンは 2.00 で、チェンジログの項目は based on libwww-perl-5.08 と書かれています。 - 警告の時代。1997 年の 2.03 から、切り詰められた入力は croak ではなく
-w下の Premature end of base64 data 警告を出すようになり、1999 年の 2.11 は、問題のないデータまで警告するビルドを直しました。デコーダーにとっては、神経質だった 10 年でした。 - 2002 年からコア。Perl 5.8 がモジュールをコアディストリビューションに取り込み、同年 12 月のコアとの 2.13 同期は EBCDIC サポートも一緒に連れてきました。それが、エンコーダーもデコーダーも今なおメインフレームで動く理由です。
- URL 安全なバリエーションが 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 の歴史の中で、悪い入力に対して例外を投げたことは 1 度もありません。破損、切り詰め、間違ったアルファベット:すべてをデコードし、何も文句を言わない。チェンジログが 2010 年にあえて固めた挙動です。
- MIME ラップは 4 の倍数に決まっている。76 文字の上限は、3 バイト組 19 個、合計 57 バイトに 4 文字を掛けたものです。だから 1 行ずつのデコードは、正しくラップされた MIME 本文なら安全で、それ以外は安全ではない。
- uuencode は小文字を一切出力しない。アルファベットの上限はアンダースコアなので、古い uuencode ファイルは大文字専用マシンで入力されたように見えます。そして、インターネットより古い形式の組み込みデコーダーを 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 安全なアルファベットの 64 ビット数なので、あなたがこれまで見てきたすべての動画の URL に Base64 文字列が入っており、
decode_base64urlはそれを読むことができます。 - 寛大さはバグではなく規格だ。「受け入れるものには寛容であれ」という MIME の掟が、このデコーダーが 30 年の汚いデータを生き延びてきた理由であり、同じ寛大さが信頼できない入力を信頼すれば隠蔽チャネルに転用されると RFC 4648 が警告する理由でもあります。
つまり次に、英字、数字、プラス、スラッシュの文字列があなたのターミナルに降ってきたら、あなたは全部の物語を知っています。関数 1 回の呼び出しが仕事をし、デコーダーは決してあなたを断らない丁寧な幽霊で、base64url には専用のデコーダーがあり、文字コードは意識的にあなたが下す判断で、ファイルは生で入って生で出て、唯一大事な証拠はハッシュ。そしていつか、この旅の逆方向をやる必要がある日 - 自分で生のデータをテキストの封筒に入れて、世界に送り出す - が来たら、下にリンクする Perl での Base64 エンコードの関連記事が、同じ深さでその儀式を扱っています。
最終更新: 2026-10-09