Bash での Base64 デコード:完全ガイド
ときどき、英字や数字、まれに + や /、= がまじった文字列がターミナルに落ちてくることがあります。そして、本当の中身がどうしても必要になります。チケットに貼り付けられた JWT、サポートメールに添付された .b64 ファイル、アルファベットの雑煮にしか見えない Kubernetes シークレット、HTML の data: タグにかくれている PNG。この記事は、Bash とシェルでそのバイトを取り戻すための現場ガイドです。使うのは、たぶんすでにインストール済みで間違いない道具たちです。
形式をひと呼吸で説明します:Base64 は、生のデータ 3 バイトごとに、64 文字のアルファベット(A-Z, a-z, 0-9、それに + と /)から 4 文字を取って書き換えます。バイト数が 3 の倍数でなかったときは、末尾に = を 1 つか 2 つ足します。デコードはその取引の縮む方向です:4 文字が入って、3 バイトが出てくる。このサイトのホームページでは形式をステップバイステップで解説しているので、この記事は自分にふさわしい場所、つまり仕事のシェル側で時間を過ごします。
ここからがどんでん返しです:base64 というコマンドは 1 つだけではありません。同じ名前を、GNU の C プログラム、Rust 書き直し版、BusyBox アプレット、BSD からの残り火、そしてフラッグの重なりが本気で危険な OpenSSL ユーティリティがみんな使っています。アルファベットについてはみんな一致していますが、壊れた入力がどういうものかをどう見るかでは、必ずしも一致しません。その差こそが、スクリプトの命取りになる場所です。だから第一歩は、base64 とタイプしたとき、誰が答えてくれるのかを確かめることです。
会話相手になっているデコーダーを知ろう
ほとんどを物語ってくれるのが、この 1 コマンドです:
base64 --version
機械次第で、あなたは次のどれと付き合うことになります:
| 見えるもの | 手元にあるもの | デコード用フラッグ |
|---|---|---|
base64 (GNU coreutils) 9.x |
古典的な C 実装。多くの Linux ディストリビューションで今も既定 | -d または --decode |
base64 (uutils coreutils) 0.8.x |
coreutils の Rust 書き直し版。現行 Ubuntu リリースの既定ユーザランド | -d(珍しく、-D も動きます) |
| BSD スタイルの usage テキストで、バージョンフラッグなし | macOS と BSD 各派の BSD base64。古い bintrans ツールの子孫 |
-D(この系譜では小文字の -d はデバッグのことで、デコードではありません) |
BusyBox v1.x |
Alpine Linux と組み込みシステムのオールインワンバイナリ | -d |
表から欠けているものに気づいてください:OpenSSL です。openssl base64 は別物の生き物で、その -d フラッグは復号という意味であり、デコードではありません。この記事に出てくるどの癖よりも、こっそり空になる出力ファイルを量産しているのがこの 1 つのフラッグです。なので、フォールバックの章でちゃんと出会っておきましょう。
ディストリビューションが複数の系譜を並行して同梱している場合(現行の Ubuntu はそうです)、あと 2 つのコマンドで全体像が見えます:
command -v base64
base64 --version 2>&1 | head -1
4 つの 1 行コマンドで、ほとんどの日は乗り切れる
標準入力から文字列をデコードします。1000 回使うことになる動きで、printf がシェルにペイロードを「装飾」させません:
printf '%s' "SGVsbG8sIFdvcmxkIQ==" | base64 -d
ファイルをデコードします。まともな実装はすべて FILE 引数を受け、データをシェルの引用機構から遠ざける最もクリーンな方法です:
base64 -d payload.b64 > payload.bin
here-string からデコードします。here-string は末尾に改行を足しますが、どのデコーダーも改行は無視できる空白として扱うので、小さなブロブならまったく安全です:
base64 -d <<< "SGVsbG8sIFdvcmxkIQ=="
heredoc で折り返された複数行のブロブをデコードします。区切り文字をシングルクォートで囲めば、中身は何も解釈されません:
base64 -d <<'EOF'
SGVs
bG8s
IFdv
cmxk
IQ==
EOF
4 つとも Hello, World! を表示します。macOS と BSD では、上記のすべての例で -d を -D に置き換えてください。構文の残りはすべて同じです。
乱れた入力の方が普通だ
現場で出会う Base64 が、まっすぐな 1 行であることはめずらしい。76 文字で折り返されて来るもの(MIME の慣習)もあれば、64 文字で折り返されたもの(PEM の慣習)もある。Windows から CRLF 改行付きで書き出されたり、チャットウィンドウから真ん中に余分なスペースごとコピーされたりするのも普通です。朗報は:改行が本物の改行である限り、デコーダーは改行の位置なんて気にしません。
どんな折り返し方にも効く万能処方は、デコード前に改行を洗い流すことです:
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!、exit 0 |
Hello, World!、exit 0 |
Hello, World!、exit 0 |
SGVsbG8s、8 文字、パディングなし |
Hello,、exit 0 |
Hello,、exit 0 |
Hello,、exit 0 |
Zg、2 文字、パディングなし |
f、exit 0 |
f、exit 0 |
エラー、「truncated input」 |
SGV、3 文字、パディングなし |
エラー、出力なし | He を出力してからエラー |
エラー、「truncated input」 |
| CRLF で折り返された行 | 正常にデコード、exit 0 | 部分出力してからエラー | 正常にデコード、exit 0 |
SGVs!bG8、浮いた句読点 |
エラー(-i なら Hello) |
部分出力(-i なら Hello) |
部分出力(-i フラッグ自体が存在しない) |
SGV=、正規でない余剰ビット |
エラー、出力なし | He を出力してからエラー |
He、exit 0 |
TQ==junk、パディングのあとのゴミ |
exit 0、junk をデコードし続ける |
exit 0、junk をデコードし続ける |
M を出力してから「truncated input」(exit 1) |
この表から学ぶべき点は 3 つです。1 つ目は、パディングのない末尾には共通のルールがないこと:BusyBox は長さが 4 の倍数であることを求め、GNU と uutils は合法な残りを認めますが、残りビットがすべて 0 のときだけです(だから Zg は通って SGV は通らない)。2 つ目は、GNU と BusyBox は失敗する前に、その時点でデコード済みのバイトを書き出してしまうことです。ファイルをリダイレクトして、あとから終了コードを確認するスクリプトは、半端にデコードされたファイルを見舞われても喜々として受け入れてしまいます。終了ステータスは必ず確認し、失敗したデコードが残したファイルはすべて怪しいものとみなしましょう。3 つ目は、最後の行です:uutils と GNU は junk を == のあともデコードし続けて exit 0 になります。ストリームがそこで終わるはずだったと教えてくれるものが何もないからです。例外が BusyBox で、パディングの前の 1 バイトを出力して、truncated-input エラーで失敗します。末尾のゴミが問題になりうるなら、出力を信頼する前に入力の形を検証しましょう。
base64url:トークンと URL のアルファベット
RFC 4648 の 5 節は第 2 の方言を定義しています: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 にはネイティブな経路もあります:basenc、base64 のより大きな兄弟が、この方言をそのまま理解します:
printf '%s' "Zg==" | basenc --base64url -d
f と表示されます。2 文字に隠れていた 1 バイトです。パイプラインを組み始める前に 1 つ警告:この時代の GNU basenc は、パディングのない base64url 入力(素の Zg)でも文句を言わずにデコードしてくれます。一方、uutils の basenc は、上の例のように、まずパディングの補完を求めます。上の小さな関数はどこでも動くので、それがポータブルな選択です。
JWT を開く
JSON Web Token(JWT)は、RFC 7515 に従い、ドットで結ばれた base64url の 3 セグメントです。最初の 2 つはそのままの JSON(ヘッダーとペイロードのクレーム)なので、読めるテキストにそのままデコードされます。3 つ目は署名、つまり生のバイナリダイジェストなので、触りません。デコードしても得られるのは署名のバイトで、メッセージではないからです。
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
2 つのハッシュが一致すれば、デコードが正確であることが証明できます。画像ビューアーや hex ダンプでいくら目で追っても勝てません。
シェル変数は別物で、壁です。理由は 2 つ。コマンド置換 $(...) は出力の末尾の改行を全部剥いでしまううえ、NUL バイトは一切保持できません:bash は警告を表示しつつこっそり捨てます。41 00 42 の 3 バイトのペイロードなら、両方の問題を 1 回のデモで示せます:
printf 'QQBC' | base64 -d > out.bin
xxd out.bin
ファイルには 3 バイト(41 00 42)全部がいます。変数ルートにはいません:
v=$(printf 'QQBC' | base64 -d)
printf '%s' "$v" | wc -c
2 が返り、標準エラーに無視された null バイトについての警告が出ます。教訓は短いです:データがバイナリになりうるなら、ファイルにデコードして、xxd で調べ、絶対に変数に通さないこと。
実作業のどこに Base64 が隠れているか
デコードに慣れてくると、どこを見てもこの形式が見え始めます。実世界のシェル作業で姿を見せる場所を、それぞれ正確な動きつきで紹介します。
Kubernetes シークレット。シークレットの .data 配下の全フィールドは Base64 で、公式ドキュメントはこれがエンコーディングであって暗号化ではないと強調しています。読み戻すのは古典的な 1 行コマンド:
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 を包みます。アーマー付きブロックを選び、2 行のフレームを落として、生の 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
まずバイト:文字セットと Unicode
Base64 作業で最も繰り返される間違いは、形式が知るのもバイトだけなのに、文字で考えてしまうことです。base64 -d が手渡すのは生のバイトで、それが読めるテキストになるかどうかは、次にそれを読む側が決めます。その決定が文字セットで、それはデコードのあとで起きて、決してその内側では起きません。
UTF-8 は既定の前提で、たいてい正解です。café という単語は UTF-8 では 5 バイトで、デコードは快調です:
printf 'Y2Fmw6k=' | base64 -d | xxd -p
これは 636166c3a9 です:caf に、é のための UTF-8 2 バイト c3 a9 が加わっています。ただ、古いシステムからは Latin-1(ISO-8859-1)バイトが手渡されることがあります。同じ文字が 1 バイトの e9 になっているものです。そんなブロブをデコードして UTF-8 ターミナルにそのまま表示すると、文字化けが返ってきます。対処は、他の何が見る前に iconv でバイトを再解釈することです:
base64 -d latin1.b64 | iconv -f ISO-8859-1 -t UTF-8 > utf8.txt
実地のデバッグ時間を救う、いくつかのバイトレベルの事実:
- UTF-8 BOM は 3 バイトの
ef bb bfで、77u/にエンコードされます。/に注意してください。URL にとって敵対的な文字で、base64url が生まれて直すのはまさにこういうものです。デコードしたファイルに「前の方に目に見えないゴミがある」と感じたなら、xxdで先頭 3 バイトを確認しましょう。 - 😀 のような絵文字は 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 つ覚えておいてください:エンコードされた形は必ず元より約 1/3 大きく、3 バイトが 4 文字です。だから誰かが「.b64 ファイルは中身と同じサイズのはずだ」と言っても、それは間違いで、あなたはもう「どれくらい」間違っているかも伝えられます:300 MB のペイロードは、おおよそ 400 MB のテキストとして届きます。
速度は実際には心配ではありません。これらは素のメモリ上を回る、テーブル駆動のループにすぎず、モダンなマシンでは GNU で 200 MB のブロブがおよそ 0.1 秒でデコードされ、共通実装で最も遅い BusyBox だとしても、その数倍程度。それでも 2 秒を大きく下回ります。何もバッファリングしないので、入力がどれだけ大きくなってもメモリ使用は一定のままです。
マシンに base64 がないとき
ほとんどのシステムは上記の道具を少なくとも 1 つ持ち、たいていは複数持っています。プラットフォームに coreutils 自体が全くない場合(削られたコンテナ、特殊なアプライアンス)、インストールの経路は次の通りです:
| プラットフォーム | 入手方法 | 備考 |
|---|---|---|
| Debian / Ubuntu | プリインストール済み。削られていたら apt install coreutils |
現行の Ubuntu リリースは uutils 系を既定にしています(25.10 以降)。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 |
何もしないまま黙って exit 0。そこの -d は復号だから |
openssl base64 -d -A -a にし、0 バイトの結果はエラーとして扱う |
macOS で -d を使ってデコード |
BSD base64 はそのフラッグを拒否(またはデバッグと解釈) | -D、または coreutils を入れて gbase64 を使う |
| 入力に CRLF 改行 | GNU は部分結果を表示してから失敗。uutils と BusyBox は受け入れる | まず tr -d '\r\n'。他人の入力には必ず |
| パディングのない末尾 | BusyBox はどんな残りも拒否。GNU と uutils は正規の末尾のみ受け入れ | デコード前に足りない = パディングを補う |
パディングのあとのゴミ、例:TQ==junk |
uutils と GNU はデコードを続けて exit 0。BusyBox はエラー | 出力を信頼する前に入力の形を検証する |
正規でない余剰ビット、例:SGV= |
uutils と GNU は拒否(GNU は部分出力のあと)。BusyBox はそれでもデコード | 入力が壊れている。上流でエンコードを作り直す |
| デコード結果を変数に読み込む | $(...) は末尾改行を剥ぎ、NUL バイトは一切保持できない |
ファイルにデコードして xxd で調べる |
信頼できない入力に -i |
破損がこっそり成功に変わる。無視された文字が隠しデータを運ぶことがある | 厳格にデコードし、エラーを読み、元を直す |
| 2 つのアルファベットを混同 | base64url を標準としてデコード(またはその逆)すると、誤ったバイトかエラー | デコード前に形式を知っておく。交換は tr '_-' '/+' |
| デコードしたシークレットをシークレットとして扱う | 1 コマンドで元通り。RFC には認証情報が漏洩した実際の事故も記録されている | エンコーディングではなく、本物の暗号化 |
OpenSSL の行は、失敗があまりにも静かなので、段落 1 つ分の価値があります。OpenSSL 3.x では単体アプリの -d は一般的な「復号」オプションで、Base64 処理は -a で選ぶ別モードです。だから openssl base64 -d は入力を読んで、何もせず、何も表示せず、exit 0 になります。動くデコードは openssl base64 -d -A -a で、-A が「入力は 1 つの連続した行だ」と伝えます。OpenSSL でデコードせざるを得ないなら、0 バイトの出力は、その度ごとに、エラーとして扱ってください。
きっちりしたルーティン
Base64 に決してやられないための癖:
- 大げさに失敗しろ。スクリプトは
set -euo pipefailで実行し、終了コードを確認する。共通のデコーダーはすべて不正な入力に exit 1。OpenSSL はうるさかったはずなのに静かになったので、これだけは出力が空でないことも確認する。 - 往復を証明しろ。期待バイトと復元バイトの
cmpかsha256sumが、唯一の確かな証拠だ。バイナリを目で見るのはやめる。 - バイナリはファイルへ、絶対に変数にはしない。末尾の改行も NUL バイトも、どちらもコマンド置換の犠牲者だ。
- 他人の入力は洗え。プラットフォームの境界を越えて来たものは、デコード前に
tr -d ' \r\n'。 - 既定は厳格に。何が具体的に間違っていたかを見るまで
-iは入れない。厳格な失敗は場所を教えてくれるが、寛大な失敗は何も教えてくれない。 - アルファベットに名前を付けろ。RFC 4648 に従い、標準 Base64 と base64url は別々のエンコーディングだ。JWT は base64url として、メール添付は標準としてデコードし、なぜか分からないまま文字を入れ替えない。
- ついデコードしたものを決して出力しない。シークレットの世界でこの形式の存在意義は「見えないこと」、ログの存在意義は「見えること」。この 2 つの目的は混ざらない。
「このマシンがどのフラッグを求めているのか」への小さなポータブルな shim:
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 各系を捕まえます - 3 つとも小文字フラッグを受け取ります - それ以外は BSD フラッグにフォールバックします。まさに実世界の 4 分かれ:GNU、uutils、BusyBox、BSD。
シェルはデコードをどう学んだか
形式は古い。でも、あなたが打ち込んでいるコマンドはそうではありません。シェルの側の物語の短いタイムライン:
- 1980 年、バークレー。Mary Ann Horton がカリフォルニア大学バークレー校で
uuencodeとuudecodeを書き、バイナリファイル(たいてい圧縮されたもの)をメールで運ぶようにしました。名前の意味は「Unix to Unix エンコーディング」、つまり文字セットを共有しないかもしれない Unix システム間でファイルを動かすための安全なエンコーディング。何十年間も、シェルユーザーが手に取るのはこれであり、Base64 ではありませんでした。 - ダイヤルアップ時代。最も古い base エンコーディングは同じ問題を背景に持っていました:UNIX には uuencode、TRS-80 と Apple II には BinHex(Macintosh はあと一歩)、それぞれが自らのターミナルが出せる文字だけ前提にしていました。
- 1993 年。MIME がメール用の Base64 を標準化(RFC 1521、のちに RFC 2045)。76 文字での行折り返しもその一部で、今日まで
base64の既定を定義し続けています。 - 2003 年と 2006 年 10 月。RFC 3548 がこの一族を整理し、RFC 4648 がそれに代わって登場。アルファベットと、この記事が何度も引用してきた厳格デコードのルール(base64url も含む)をもたらしました。
- 2006 年 8 月 15 日。coreutils 6.0 が
base64コマンド自体を追加。NEWS ファイルには率直に「base64 エンコードとデコード(RFC 3548)機能」とクレジットされました。それ以前、Linux のシェルユーザーが手に取っていたのはopenssl base64、uuencode -m、Perl、Python。だからこそ、OpenSSL が街の唯一の選択肢だと思っている古いスクリプトがこれほどあるのです。 - OS X 10.7。macOS が自前の
base64を同梱。BSD フレーバーで-Dフラッグを持ち、既定の行折り返しなし。-dと-Dの分かれ目はここから来ています。 - 2024 年 3 月。coreutils 9.5 がデコーダーを緩めました:デコード時にパディングが不要になり、0 でない余剰ビットを持つエンコーディングは、黙って受け入れるのではなく破損と診断されるようになりました。
- 2025 年。coreutils の Rust 書き直し版(uutils)が現行 Ubuntu リリースの既定に。コマンド名もフラッグも同じで、
-Dをエイリアスとして受け入れるといった、エッジケースについて独自の見解を持つ新しいエンジンです。
とっておきのトリビア
- コマンドは形式より若い。Base64 は 1993 年からメールにいますが、
base64コマンドが現れたのは 2006 年。13 年間、シェルスクリプトはこの仕事を他の道具でこなしてきました。その指紋は今も至る所に発見できます。 - ヘルプテキストの中の化石。uutils の
base64は今もヘルプでアルファベットを「RFC 3548」と説明しています。RFC 4648 の引退した前身です。GNU の兄弟はすでに現行標準を引用しています。小さな化石で、ヘルプを読まなければ見えないものです。 - 道具箱で最も高い「静かな無操作」。
openssl base64 -dは何もせず exit 0。デバッグの 1 時間がまるごと消え、終了コードの確認まで合格しています。 - BusyBox は余剰ビットを許容する。残りビットが 0 でなく、つまり正規でない
SGV=も平気でデコードしてくれます。同じ入力で GNU の従兄弟は暴れるのに。同じ RFC、違う神経。 - 形式の名前はどのマシンでも本当。
printf 'base64' | base64は GNU、uutils、BusyBox、OpenSSL どれもでYmFzZTY0を返します。2006 年から本当で、これからもずっと本当です。 - Base85 は Base64 ではない。Git の「binary patch」ブロックは素人の目には Base64 に見えますが、長さ付き接頭辞の行は base85 スタイルの方言です。grep してデコードする寄り道を払わされるのがこの偽物。
- 11 文字、64 ビット。YouTube の動画 ID は 11 文字の base64url 文字列。URL の服を着た 64 ビットの数値で、だからこそ 1 つのパーセントサインも使わずに URL に現れられます。
- デコーダーは 1 つの文字列で割れる。
SGV=のようなたった 1 つの文字列で、この分野は上の動作表が示すように 3 つの陣営に分かれます。「明らかに正当な」入力でデコードが失敗したなら、たぶんあなたは余剰ビットの線上に立っています。あのエンコーディングは最初から正規ではなかったのです。
さて、次に英字と数字、プラスとスラッシュの文字列がターミナルに落ちてきたら、あなたは全部の物語を知っています:どのデコーダーが見ているか、どのアルファベットが話しているか、改行はどこにかくれているか、そしてどうやってバイトを無傷に、バイト単位で証明されて、キャリッジリターンに 1 時間を失うことなく取り戻すか。そして仕事が反対の方向 - 自分のバイナリを移動のためにテキストの封筒に包むとき - を向いたら、下記にリンクする関連記事の Base64 エンコードが、同じ深さでその儀式を扱います。
最終更新: 2026-10-09