Base64 形式を扱う必要がありますか?それならこのサイトが最適です!データをエンコードまたはデコードするために便利なオンラインツールをご利用ください。

JavaScript/Node.js での Base64 デコード:完全ガイド

あなたのアプリケーションが Base64 文字列を受け取ったとしましょう。それは届いてきたリクエストの Authorization ヘッダーかもしれないし、JSON ペイロード内のフィールドかもしれない、data URL にしのんでいる画像かもしれない、設定ファイルに貼り付けられた証明書かもしれません。これらはすべて同じものです:ASCII の仮面を被った生のバイト。この記事は、JavaScript と Node.js でその仮面を外す方法、そしてその過程で 1 バイトたりとも失わない方法についてです。

形式そのものについて一言だけ:Base64 は、入力 3 バイトを 4 個の表示可能文字に写すテキストエンコーディングです。このサイトのホームページではアルファベット、計算、パディングを詳しく説明しているので、ここでは 1 文に留めます。覚えておくべき帰結が 1 つあります:エンコードされたデータは、中身のバイトより約 33% 膨らむということです。つまりデコードは縮む方向の作業であり、この記事のどこにも秘密の追加も剥奪もありません。開いているのは包みであり、封ではありません。

朗報です:インストールするのは何もありません。ブラウザは 20 年もの間 atob() を搭載し続けており、Node.js には組み込みの base64 モードを持つ Buffer クラスがあります。さらにモダンなランタイムには、ES2026 仕様の厳格で設定可能な新顔 Uint8Array.fromBase64() まで備わりました。腕の見せ所は、仕事に合った道具を選び、各道具が何を許すのかを正確に知ることです。サーバー上では見知らぬ人からのデータをデコードしますから、寛大さは災いの入り口になります。

デコーダーの選び方

3 つの API が、デコード作業の大部分をカバーします。性格はそれぞれ違っており、その違いがすべてです:

デコーダー 利用可能な環境 性格
Buffer.from(string, 'base64') Node.js(実用上すべてのバージョン) 寛大:未知の文字はスキップ、最初の = で停止、例外を投げない
atob(string) すべてのブラウザ、Node.js 16 以降 厳格:不正な入力に InvalidCharacterError を投げ、ASCII 空白はスキップ、パディング不足も許す
Uint8Array.fromBase64(string) Chrome 140 以降、Firefox 133 以降、Safari 18.2 以降、Node.js 25 以降 設定可能:アルファベットと、最後のチャンクをどのくらい厳格に扱うかは自分で決める

3 つとも、同じクラシックなペイロードを同じ開き方で開きます:

// Node.js の主力
const { Buffer } = require('node:buffer');
console.log(Buffer.from('aGVsbG8gd29ybGQ=', 'base64').toString('utf8')); // "hello world"
// レガシーなペア(すべてのブラウザ、Node.js 16 以降)
console.log(atob('aGVsbG8gd29ybGQ=')); // "hello world"、ただしバイナリ文字列として
// モダンな ES2026 メソッド(Chrome 140 以降、Node.js 25 以降)
console.log(new TextDecoder().decode(Uint8Array.fromBase64('aGVsbG8gd29ybGQ='))); // "hello world"

atob() に頼る前に 1 つ注意:返されるのは文字列ですが、バイナリ文字列です。各文字が 0 から 255 のコードポイントとして 1 バイトの生バイトを運んでいる文字列。表示するだけなら問題ありません。しかし JSON、データベース、クッキーに格納すると、その生バイトの値がそのまま同行してしまうので、デコード直後に本物のバイト、または本物のテキストへ変換してください。

寛容なデコーダーと、それが飲み込むもの

Node の Buffer は寛容な読み手であり、それは双刃の剣です。荒れた道を旅したデータには最高です:改行だらけの MIME メール、手でコピーされた文字列、余分な空白を挟んだログ出力。しかし自分が生成しなかったデータには危険です。Buffer は決して文句を言わないから。実際に何が起きるのか見てみましょう:

入力 Buffer.from(input, 'base64') がすること
'!!!' 空の Buffer を返す。ゴミはすべてスキップされ、何もデコードされず、エラーもない。
'aGVsbG8== garbage' 「hello」を返す。最初の = でデコードは終了し、その後は無視される。
'aG!VsbG8' 「hello」を返す。バングはスキップされるだけで、エラーにはならない。
'aGVs=bG8' 「hel」を返す。文字列の途中で現れる = が、デコードを早めに止めてしまう。
'aGVsbG8====' 「hello」を返す。末尾の余分なパディングは無視される。
'=aGVsbG8' 空の Buffer を返す。データより前にあるパディングには何の意味もない。

信頼できない入力への処方箋はバリデーションです。Base64 の文法は十分小さく、1 つの正規表現に収まります:

const STRICT = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/;
function decodeStrict (base64) {
  if (!STRICT.test(base64)) {
    throw new TypeError('Not a valid base64 string');
  }
  return Buffer.from(base64, 'base64');
}
console.log(decodeStrict('aGVsbG8gd29ybGQ=').toString('utf8')); // "hello world"
try {
  decodeStrict('aGVs!bG8');
} catch (error) {
  console.log(error.message); // "Not a valid base64 string"
}

正規表現がチェックするのは形です:正しいパディングを持つ 4 文字グループ。チェックできないルールが 1 つあります。RFC 4648 の正規エンコーディング規則で、最後のグループの未使用パッドビットはゼロでなければならないというものです。Uint8Array.fromBase64() の strict モードならこれをチェックしてくれます。したがって Node.js 25 やモダンなブラウザでは、正規表現を完全にスキップして、監査をプラットフォームに任せることもできます:

console.log(new TextDecoder().decode(Uint8Array.fromBase64('aGVsbG8'))); // "hello"、loose モードはパディング不足を許す
try {
  Uint8Array.fromBase64('QQB=', { lastChunkHandling: 'strict' });
} catch (error) {
  console.log(error.name); // "SyntaxError"、パッドビットがゼロでないため
}

lastChunkHandling オプションには、知っておくべき 3 つの設定があります。"loose"(デフォルト)は空白をスキップし、パディング不足を許容して、残ったパッドビットを無視します。"strict" は、全パッドビットがゼロに設定された完全なパディング付き最終グループを要求します。そして "stop-before-partial" は完全な 4 文字グループのみをデコードし、末尾の断片を引き継ぐよう残しておきます。これがストリームデコードを快適にする部分です。後ほどこの記事の中でその恩恵を見ます。

バイトからテキストへ:文字セットの決断

Base64 をデコードすると手に入るのはバイトです。バイトがテキストになるのは、あなたが文字セットを選んだときだけ。その選択はあなたに委ねられており、通常は送信者が約束したものに基づきます。Node のデフォルトは、ほとんどいつも欲しいもの:

const { Buffer } = require('node:buffer');
const bytes = Buffer.from('w6k=', 'base64'); // 2 バイトの C3 A9
console.log(bytes.toString('utf8'));   // "é"、2 バイトが 1 文字に結合
console.log(bytes.toString('latin1')); // "é"、同じバイトを 1 文字ずつ読んで解釈

UTF-8 には 1 つひっかけがあります。バイト列が有効な UTF-8 でない場合、Node は例外を投げません。Unicode 置き換え文字(U+FFFD、疑問符付きのダイヤ)に置き換えてそのまま進んでしまうため、破損したペイロードはパイプラインをくぐり抜け、そのままデータベースに流れ込む可能性があります。プラットフォーム本物のテキストデコーダー TextDecoder(Node.js とすべてのブラウザのグローバル)には、破損をキャッチできる TypeError に変えてくれる fatal オプションがあります:

const stray = new Uint8Array([0xe9]); // 孤立した 1 バイト、有効な UTF-8 でない
console.log(new TextDecoder().decode(stray)); // 置き換え文字が返り、エラーにはならない
try {
  new TextDecoder('utf-8', { fatal: true }).decode(stray);
} catch (error) {
  console.log(error.name); // "TypeError"
}

レガシーシステムは決して死にませんし、TextDecoder は今もそれらを読む方法を知っています。WHATWG Encoding Standard のラベル表をすべて受け入れるため、1990 年代の Windows アプリ、日本のメインフレーム、古い FTP ミラーからきた Base64 ペイロードも、'windows-1250'、'shift_jis'、'euc-kr'、'gb18030' のようなラベルでデコードできます。すべて大文字小文字を区別しません。1 つのラベルには警告が必要です。実際にデバッグ時間を奪ってきたからです。仕様は 'iso-8859-1'、'latin1'、さらには 'us-ascii' までを Windows-1252 デコーダーへの別名として定義しています。バイト 0x80 は本当の Latin-1 では制御文字ですが、ユーロ記号として出力されてしまいます:

console.log(new TextDecoder('iso-8859-1').decode(new Uint8Array([0x80]))); // "€"、求めた Latin-1 ではない
// 真の意味でバイト単位の一対一の Latin-1 読みなら、Buffer 側を使う:
console.log(Buffer.from('gA==', 'base64').toString('latin1')); // 生の 0x80 制御文字

本当にその生マッピングが必要なら、Buffer の 'latin1' エンコーディング(そのレガシー別名 'binary' は、Node ドキュメントの表現を借りれば「非常に誤解を招く名前」)が、Windows への寄り道なしでバイト N をコードポイント N に写します。モダンな用途のすべてには、UTF-8 と fatal: true の組み合わせが安全なペアです。

JWT をこじ開ける

JavaScript サービスがデコードする Base64 ペイロードで、ダントツで多いのは JSON Web Token です。ウェブの半分の Authorization ヘッダーにしのんでいる xxxxx.yyyyy.zzzzz という文字列です。RFC 7515 によれば、コンパクト JWS はドットで区切られた 3 つの部分からなり、最初の 2 つはパディングなしの base64url でエンコードされた JSON オブジェクトです。Node.js でそれらを読むのに儀礼は不要です。base64url モードは一級エンコーディングだから:

const { Buffer } = require('node:buffer');
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJuYW1lIjoiQWRhIn0.JMjpmDdNzQZpTuUO1H33GJsj7nWhBu-qxkPD0GL2uaA';
const [head, body, signature] = token.split('.');
console.log(JSON.parse(Buffer.from(head, 'base64url').toString('utf8'))); // { alg: 'HS256', typ: 'JWT' }
console.log(JSON.parse(Buffer.from(body, 'base64url').toString('utf8'))); // { sub: '123', name: 'Ada' }

何度言ってもいいですが、繰り返す価値があります:デコードは検証ではありません。ヘッダーもペイロードも、暗号化ではなく仮面を被っただけであり、トークンを持っている誰でも両方を読めます。必ずチェックすべきは第 3 の部分、署名です。クラシックな HMAC-SHA256 トークンなら、検証全体は組み込みの crypto モジュール数行で済み、注意が要する唯一の点は、攻撃者に 1 バイトずつの比較の時間を測られないよう timingSafeEqual で比較することです:

const crypto = require('node:crypto');
const expected = crypto.createHmac('sha256', 'topsecret').update(head + '.' + body).digest();
const actual = Buffer.from(signature, 'base64url');
console.log(crypto.timingSafeEqual(expected, actual)); // true
console.log(crypto.timingSafeEqual(crypto.createHmac('sha256', 'wrong-secret').update(head + '.' + body).digest(), actual)); // false

実サービスでは、通常これを手作業で組みません。jose パッケージ(依存ゼロ、Node.js、ブラウザ、エッジランタイムで動作)と、長年使われてきた jsonwebtoken パッケージ(Node.js)がこの踊りを包み込み、RSA と ECDSA のアルゴリズム系を扱い、exp、aud、iss クレームを強制します。どのライブラリを選んでも、その下の Base64 の配管は、先ほど見た同じ 2 つの呼び出しです。

HTTP:ヘッダー、クエリ文字列、クッキー

配線の 3 つのコーナーには Base64 が溢れています。最も古いのは RFC 7617 で定義された HTTP Basic 認証です。クライアントは Authorization: Basic と user-id:password の Base64 を送ります。サーバー側では 1 回のスライスと 1 回のデコードで済み、プロトコル上の小さな詳細が 1 つあります。ユーザ名とパスワードを区切るのは最初のコロンのみなので、パスワードだけがさらなるコロンを含むことを許され、ユーザ名は許されません:

const { Buffer } = require('node:buffer');
const header = 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==';
const credentials = Buffer.from(header.slice(6), 'base64').toString('utf8');
const [user, ...rest] = credentials.split(':');
console.log(user, rest.join(':')); // "Aladdin" "open sesame"

Basic 認証が何であるかを思い出しておきましょう:暗号化ではなく隠蔽(オブファスケーション)、つまり目隠しにすぎません。資格情報は仮面を被った状態で配線を渡り、だからこそこのスキームは HTTPS でのみ許容されます。2 つ目のコーナーはクエリ文字列です。Base64 世界の最もやっかいな地雷が隠されています:

const params = new URLSearchParams('token=aGVs+bG8=');
console.log(params.get('token')); // "aGVs bG8="、プラスが空白になってしまった

あなたの Base64 が勝手に壊れたのではありません。URL 層が、フォームエンコーディング規則に代わって丁寧に行ってくれました。+ を空白と扱う規則です。クエリ文字列に生きるトークンが URL 安全アルファベットを使うのは、まさにこのためで、それは下記のセクションで扱います。3 つ目のコーナーはクッキーです。クッキーは ASCII のみなので、そこに格納された非 ASCII の値はほぼ確実に Base64 であり、JSON の blob を Base64 化してクッキーに詰め込む古いパターンは、驚くほどの数の本番システムで生きているのです。デコードはもう知っている同じものです。まず形を検証してください。クッキーは、ユーザーやブラウザの拡張機能がゴミを差し出してくる場所だからです。

ファイル、画像、Data URL

Node のファイルシステムは Base64 を直接話します。だからファイル全体を 1 行で JSON の境界を越えさせることができる:

const fs = require('node:fs');
const base64 = fs.readFileSync('./photo.png', 'base64');
console.log(base64.length); // ファイル、約 33% 重くなっている
const bytes = Buffer.from(base64, 'base64');
fs.writeFileSync('./photo.copy.png', bytes);

もう 1 つのファイル形のペイロードが data URL です。フロントエンドがインライン画像のためにこよなく愛する data:image/png;base64,... という文字列。どんなランタイムでもレシピは同じです:最初のコンマで切り、その前にあるメタデータを解析し、残りをデコード。ここが本物の 1 ピクセル PNG が甦る瞬間です:

const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
const comma = dataUrl.indexOf(',');
const meta = dataUrl.slice(5, comma);
const bytes = Buffer.from(dataUrl.slice(comma + 1), 'base64');
console.log(meta); // "image/png;base64"
console.log(bytes.subarray(0, 8).toString('hex')); // "89504e470d0a1a0a"、PNG シグネチャ

シグネチャの確認は安価な習慣です。PNG の先頭 8 バイトは常に 89 50 4E 47 0D 0A 1A 0A であり、JPEG は FF D8 FF で始まります。クライアントから来た「Base64 画像」が約束したマジックバイトで始まっていなければ、何か高価なことをする前に、あなたはもうそれを知っていることになります。

URL 安全 Base64:トークンのためのアルファベット

クラシックな Base64 は + と / を 2 つの特殊文字として使います(RFC 4648、セクション 4)。そしてどちらも URL では厄介です。フォームデコードの途中で + は空白になり、/ はパスセパレータだからです。セクション 5 の URL とファイル名安全バリアント - 誰もが base64url と呼ぶもの - はこれらを - と _ に置き換え、長さが文脈からわかっている場合は末尾の = パディングを丸ごと落とすことができます。まさにその組み合わせが JWT、OAuth トークン、ディープリンクが必要とするもので、だからこそ base64url は実世界で最もよく目にするアルファベットです。

Node の Buffer はこの問題を完全に消してくれます。'base64' も 'base64url' も、デコードモードとしては 4 つの特殊文字のすべてを受け入れ、同じ値に写します。だから JWT の部分、OAuth トークン、クラシックな Base64 の blob は、すべて文字の交換という儀式なしにデコードできます:

const { Buffer } = require('node:buffer');
console.log(Buffer.from('aGVs-bG8', 'base64').toString('hex'));    // "68656cf9b1bc"
console.log(Buffer.from('aGVs+bG8', 'base64url').toString('hex')); // "68656cf9b1bc"、まさに同じ 6 バイト

ES2026 API は意図的にシビアで、同じ柔軟さを明示的なダイヤルで与えてくれます。alphabet オプションは "base64"(デフォルト、+ と /)と "base64url"(- と _)の間で選び、間違ったアルファベットの文字を投入すると SyntaxError になります。静かにアルファベットをまたいでデコードされることはありません:

console.log(Uint8Array.fromBase64('aGVs-bG8', { alphabet: 'base64url' }).length); // 6
try {
  Uint8Array.fromBase64('aGVs-bG8'); // デフォルトのアルファベットはクラシックな方
} catch (error) {
  console.log(error.name); // "SyntaxError"、ダッシュはクラシックの文字ではない
}

まだ新しいメソッドを持っていないブラウザでは、クラシックなアルファベットの知識しかない atob() に文字列を渡す前に、小さな交換という回り道をします。送信者がパディングを落とした場合(トークン風のペイロードではこれが普通)は、それを復元する必要もあります:

function decodeBase64Url (value) {
  const classic = value.replace(/-/g, '+').replace(/_/g, '/');
  const padded = classic + '='.repeat((4 - (classic.length % 4)) % 4);
  const binary = atob(padded);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes;
}
console.log(new TextDecoder().decode(decodeBase64Url('aGVsbG8gd29ybGQ'))); // "hello world"

実世界での Base64:ペイロードが潜む場所

JavaScript の世界で Base64 は、バイトのための郵便局です。どこに現れるかをツアー形式で巡り、各目的地のデコードレシピを紹介します:

  • JSON API フィールド:断トツで最も一般的な運搬手段です。アバター、サムネイル、生成されたドキュメント、アップロード物は、普通の JSON の中の Base64 文字列として届きます。JSON に「これはバイトです」という言い方がないから。他の何をする前に、そのフィールドをデコードしてください。
  • 環境変数と設定ファイル:いくつかのシークレットマネージャー、CI システム、そして npm CLI 自体が、Base64 の blob をあなたに手渡します(古い npm バージョンはレジストリの認証情報を user:password の Base64 として .npmrc に保存していました。最新の npm は _authToken に生のベアラートークンを書きます)。起動時に一度だけデコードし、平文をメモリに留めるのは必要な間だけにとどめてください。
  • Kubernetes とクラスタツール:k8s のシークレットは、API でも etcd でも Base64 エンコードされていることで有名です。公式ドキュメントは、それがエンコーディングであって暗号化ではないと繰り返し強調しています。あなたのデコードコードは、結果を安全の証明ではなくシークレットとして扱うべきです。
  • データベース:JSON 列(Postgres の jsonb、MongoDB のドキュメント、Redis)に保存されたバイナリは、しばしば Base64 文字列です。読み取りパスで Buffer や Uint8Array にデコードし、データベースはテキストのままでいてもらいましょう。
  • メール:76 文字の行折り返し付きの MIME Base64 が、添付ファイルやバイナリヘッダーが SMTP を渡る方法です。SMTP は元々 7 ビット専用のプロトコルでした。Node のデコーダーは改行を代わりにスキップしてくれます。だから本体全体を 1 回の呼び出しで、後処理なしにデコードできます。
  • CI/CD パイプライン:ビルドシステムやシークレットインジェクターは、トークンを Base64 の環境値として渡します。パイプラインスクリプト内でデコードし、デコードされた値をログに書き出すことは絶対にしないでください。
  • ディレクトリデータと SAML:LDIF ファイルは、バイナリ属性(証明書などを想像してください)を Base64 で保存します。SAML レスポンスは、HTTP の境界を渡る前に deflate 圧縮されてから Base64 エンコードされることが多いです。
  • ワーカースレッドとエッジランタイム:Base64 文字列は worker_threads の境界を、ごく普通の structured-clone 可能な文字列として渡ります。だから重いデコードはワーカー側に置き、メインスレッドのイベントループは空いていられるのです。

これらの目的地のうち 2 つは、面接でも本番でも顔を出すため、じっくり見る価値があります:

const { Buffer } = require('node:buffer');
// 環境変数:シークレットは Base64 エンコードされた状態で届く
const token = Buffer.from(process.env.REGISTRY_TOKEN_B64, 'base64').toString('utf8');
// JSON API フィールド:何をする前に先に開封する
const body = { attachment: 'iVBORw0KGgo...' };
const imageBytes = Buffer.from(body.attachment, 'base64');
console.log(imageBytes.subarray(0, 4).toString('hex')); // "89504e47"、また PNG シグネチャ
// MIME メール:改行はスキップされ、後処理は不要
const mimeBody = 'SGVsbG8sIHdyYXBw\nZWQgYmFzZTY0IQ==';
console.log(Buffer.from(mimeBody, 'base64').toString('utf8')); // "Hello, wrapped base64!"

このツアーで見つけたいアンチパターンは、どこでも同じです:生バイトがすでに許されていた場所にある Base64。WebSocket フレーム、ファイルストリーム、Postgres の bytea 列 - いずれもバイナリをネイティブで運べます。だからそこでの Base64 の往復は純粋なオーバーヘッドであり、恩恵の期待できない 33% のサイズ課税です。ネイティブのバイナリ経路があるなら、それをとりましょう。

断片ごとにデコード:ストリームとビッグデータ

Base64 は 4 文字のグループが 3 バイトをエンコードするため、チャンクのストリームはグループを真ん中で割ってしまえます。無邪気なアプローチ - すべてのチャンクをデコードして祈る - は、ランダムな境界で出力を壊します。ES2026 API はまさにこれのために設計されました:setFromBase64() は事前に確保した配列に書き込み、消費した入力文字数を報告します。また "stop-before-partial" モードなら、最後の完全なグループで止まり、断片を次のチャンクのために残してくれます。このパターンは TextDecoder のストリーム API を模したものです:

const { Buffer } = require('node:buffer');
const chunks = ['aGVsbG8', 'gd29ybGQ='];
let leftover = '';
const parts = [];
for (const chunk of chunks) {
  const pending = leftover + chunk;
  const space = new Uint8Array(Math.ceil(pending.length * 3 / 4));
  const { read, written } = space.setFromBase64(pending, { lastChunkHandling: 'stop-before-partial' });
  parts.push(Buffer.from(space.buffer, space.byteOffset, written));
  leftover = pending.slice(read);
}
parts.push(Buffer.from(Uint8Array.fromBase64(leftover)));
console.log(Buffer.concat(parts).toString('utf8')); // "hello world"

新しいメソッドのないランタイムでは(Node の LTS シリーズもしばらくそれがありませんでした)、同じループは部分グループを追跡する小さなユーザーランドのデコーダーでも動き、あるいは単にチャンクをバッファリングしてグループ境界で切れるまで待つだけです。大切な考えはキャリー(繰り越し)です:断片を単独でデコードしないこと。

大きなペイロードは、さらなる 2 つの制限をあなたの注意に引き寄せます。第一に、文字列そのもの:Node の buffer.constants.MAX_STRING_LENGTH は 536870888 文字で、テキストにして約 512 MiB、デコードすると約 400 MB のバイトになります。それより大きな「base64 ファイル」には、単一の readFileSync ではなくストリームアプローチが必要です。第二に、メモリ:エンコードされた文字列は JavaScript ヒープ上で UTF-16 として生き、1 文字 2 バイトです。デコードされた Buffer はデータの 2 番目のコピーです。大きなペイロードでは一時的に両方を保持することになるので、コードが許す限りエンコード済み形を短く保ち、ファイルサイズ級のものはストリームを優先してください。

ターミナルから

Node は立派なコマンドラインの Base64 デコーダーとしても使えます。リクエストのデバッグや設定値の調査時に便利:

# 引数として渡されたクラシックな Base64 文字列をデコード
node -e 'console.log(Buffer.from(process.argv[1], "base64").toString("utf8"))' "aGVsbG8gd29ybGQ="
# URL 安全バリアント、パディングは省略可
node -e 'console.log(Buffer.from(process.argv[1], "base64url").toString("utf8"))' "aGVsbG8gd29ybGQ"
# stdin からデコード、パイプのためのもの
echo -n "aGVsbG8gd29ybGQ=" | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>console.log(Buffer.from(d.trim(),"base64").toString("utf8")))'

3 つとも hello world を表示します。マシンに coreutils のクラシックな base64 コマンドもあるなら、base64 -d で同じことができます。ただし Node のバージョンは base64url を知っており、伝統的なツールは知りません。

JavaScript アクセントのついた落とし穴

これらすべてが、JavaScript か Node.js で誰かの失われた午後の原因になってきました:

  • 無言のデコーダー:Buffer.from('!!!', 'base64') はエラーではなく空の Buffer を返します。半分壊れた入力は、警告なしに半分壊れたデータへデコードされます。信頼できない入力は strict な正規表現(または strict な fromBase64 モード)で検証し、空でない文字列から空の Buffer が返ってきたら赤信号だと思ってください。
  • 足りないエンコーディング引数:第 2 引数なしの Buffer.from('aGVsbG8=') は何もデコードしません。それらの文字の UTF-8 バイトから Buffer を組み立てるだけなので、「デコードした」データは文字そのものがバイトに詰め直されただけです。'base64' 引数がすべてなのです。
  • バイナリ文字列の仮面:atob() の出力は、あなたがテキストだと宣言するまでテキストではありません。JSON レスポンス、クッキー、ログ行に詰め込んでも「動く」し、すべてのヌルバイトもそのまま保持されます。ログ収集基盤もシリアライズ基盤も同様に驚かされるでしょう。charCodeAt() で Uint8Array へ、あるいは UTF-8 テキストへ、すぐに変換してください。
  • クエリ文字列のプラス:フォームデコードされたクエリ値の中の + は、URLSearchParams があなたに手渡す時点ですでに空白になっています。URL に生きるものには base64url を優先し、クラシックな Base64 トークンをエスケープなしでクエリ文字列に貼り付けることはしないでください。
  • 置き換え文字:無効な UTF-8 は Buffer の UTF-8 モードではエラーにならず、静かにダイヤの疑問符になります。だから壊れたペイロードはパイプラインを通過してデータベースに辿り着いてしまいます。破損を大きな失敗にすべき場所では、TextDecoder で fatal: true を有効にしてください。
  • Windows への寄り道:TextDecoder に 'iso-8859-1' や 'latin1' を要求すると、バイト 0x80 がユーロ記号になる Windows-1252 デコーダーが返ります。真の意味でバイト単位の一対一の Latin-1 なら、代わりに Buffer を toString('latin1') で読みましょう。また 'binary' が、同じ Latin-1 マッピングに対する誤解を招くだけの別名なのも覚えておいてください。
  • サイズの上限:64 ビットシステムで buffer.constants.MAX_LENGTH は 9007199254740991 バイト(2 の 53 乗マイナス 1)ですが、Base64 を運ぶ文字列は MAX_STRING_LENGTH の 536870888 文字を超えることはできません。だから 1 つの文字列が運べるデコード済みデータは、わずか 400 MB を少し超える程度にとどまります。それ以上なら、ストリームにしましょう。
  • メモリの請求書:Base64 文字列は 1 文字につきヒープ 2 バイト(UTF-16)を消費し、デコードされた Buffer は完全な 2 番目のコピーです。100 MB のファイルは、あなたのプロセス内で一時的に、約 133 MB の文字列と 100 MB の Buffer になります。エンコード済み形が参照され続ける時間を縮めてください。
  • 遠隔は厳格、ローカルは寛容の不一致:あなたの Node デコーダーは、あちこちの strict なデコーダー(Python スクリプト、Go サービス、モバイルアプリ)が拒否するものを許してしまいます。システムの一方が strict で他方が寛容なら、バグは特定のペイロード長でのみ現れます。これが最悪のバグです。厳格さは頭の中でではなく、プロトコルレベルで取り決めてください。

JavaScript がデコーダーを育てた道

ブラウザ側の歴史は、長く、退屈で、頼もしいものです。atob() と btoa() は 2011 年初頭の HTML5 草案で仕様化されました(ブラウザは仕様より先にそれを持っていました)。以来、すべての主要ブラウザに据え置かれ、10 年以上にわたり動作は不変です。それらは言語標準(ES2015)での型付き配列より古く、だから彼らはバイトではなく「バイナリ文字列」で話をします。

Node.js は別のタイムラインでデコーダーを育てました。Buffer クラスは 2010 年夏、バージョン 0.1.103 でグローバルになり、Node 1.0 のほぼ 5 年ほど前でした。そして最初から 'base64' モードを持っています。Node の生涯のほとんどを、それだけが町の唯一のデコーダーでした。その後、ウェブ標準の波がやってきました:2021 年の Node 16 は、ブラウザ向けに書かれたコードがポリフィルなしでサーバーでも動くよう、atob() と btoa() をグローバルとして追加し、どちらも初日から Legacy とマークされました。2025 年 10 月 15 日にリリースされた Node 25 は V8 を 14.1 に引き上げ、ES2026 のメソッド Uint8Array.fromBase64()、setFromBase64() とその hex の兄弟をランタイムにもたらしました。その過程で、古い new Buffer() コンストラクタは非推奨になりました(Node 10 が 2018 年に警告を開始)。代わりに Buffer.from()、alloc()、allocUnsafe() が推されます。初期化されていない割当てが、そこに前にもあったメモリを漏洩し得るためというのも理由の一部です。

ブラウザでは同じ波がやや早く着きました:Firefox 133 と Safari 18.2 が 2024 年に新しいメソッドを搭載し、Chrome 140(2025 年 9 月 2 日に stable)がセットを完成させると、その機能はブラウザ各社の Baseline プログラムで Baseline Newly available と宣言されました。オールインワンの JavaScript ランタイム、Bun は 2024 年 8 月のバージョン 1.1.22 で搭載しています。もし新しいランタイムを要求できないなら、core-js と es-shims プロジェクトの es-arraybuffer-base64 パッケージがこれらすべてのポリフィルを配布しており、多くのフレームワークが内部でもこの道をとっています。

彼らが仕える形式は、それよりも古い系譜を持ちます。このアルファベットは最初に 1987 年に Privacy-Enhanced Mail のために標準化されました(RFC 989)。1993 年の改訂(RFC 1421)は同じアルファベットを維持し、MIME が 1996 年(RFC 2045)に、その改訂から約 3 年後に 76 文字の行折り返しとともに採用しました。2003 年の RFC 3548 は base16、base32、base64 を 1 つの文書に統合し、2006 年の RFC 4648 はそれを再発行しました。RFC 3548 が追加した URL 安全アルファベットも残されています - それは 10 年後、すべての JWT の中に入るのです。URL 安全バリアントは楽しいトリビアです:それがトークンと出会うずっと前に、2001 年のピアツーピア識別子についてのメーリングリストの投稿で提案されていました。

次のスタンドアップで披露する豆知識

  • WebSocket RFC の例のキー dGhlIHNhbXBsZSBub25jZQ== をデコードすると、「the sample nonce」という言葉になります。標準化委員会は、自分の例の中にウィンクをしのばせておき、Node の atob() は 1 回の呼び出しでそのジョークを割ります。
  • Buffer.from('!!!', 'base64') は長さゼロの Buffer を返します。中身のない本物の割当て。本当に何もない。これが Node が肩をすくめるのに最も近い瞬間です。
  • Node の Base64 デコーダーは、仕様が要求したこともないほど二言語に堪能です:+、-、/、_ はどれも 'base64' モードでも 'base64url' モードでも歓迎され、それぞれのペアが同じ値に写ります。
  • Node の atob() のドキュメントには、「代わりに Buffer.from(data, 'base64') を使ってください」という一文が含まれています。自分のグローバルの使用をやめろとユーザーに言うランタイムで、しかも移行を代わりにやってくれる公式の codemod(npx codemod@latest @nodejs/buffer-atob-btoa)まで備えています。
  • 小さな Buffer は共有スラブから切り出されます:Buffer.poolSize は 65536 バイトで、すべての小さな割当てはそのプールからチャンクを再利用します。Buffer の作成が速いのはこのためであり、「unsafe」な割当てという言葉の意味を知っておくべきなのもこのためです。
  • 小さな base64-js パッケージ - 関数は 3 つで依存はゼロ - は npm で毎週 1 億ダウンロード以上を記録しており、そのほとんどは他のパッケージの中の隠れた依存としてです。Base64 はエコシステムで最も密輸されるコードです。
  • Uint8Array.fromBase64() には "stop-before-partial" というモードがあり、それは 4 文字グループを一度も割らずにストリームをデコードできるように、ただそれだけに存在します。自分が拒否することを名前にしたモードは、めずらしい API の詩です。
  • Unix のパスワードの世界は、独自の Base64 フレーバーのアルファベットを使います。パディングはなく、混乱を招くことに、すべてが同じ順ではありません。クラシックな crypt(3) の「hash64」アルファベットは ./0-9A-Za-z ですが、bcrypt は同じ 64 文字を ./A-Za-z0-9 に並べ替えています。bcrypt バージョンに会うのは、多くの JavaScript プロジェクトがユーザーパスワードに保存する $2b$ ハッシュの中でです。セキュリティ文脈で「base64」が 2 つだけでなくいくつものアルファベットを指し得るのは、これが理由です。

あと 1 方向が残っている

JavaScript と Node.js での Base64 デコードは、3 つの正直な道具の積み重ねです:寛容な主力馬 Buffer.from(string, 'base64') は両方のアルファベットを受け入れ、余分な文字はすべてスキップし、strict な正規表現で守るのがベスト;TextDecoder は、古いウェブが発明したどんな文字セットの本物のテキストにも対応し、破損が痛みになるべき場所には fatal モード;そして新しい Uint8Array.fromBase64() は、strict なアルファベット、strict なパッドビット、アクロバティックでないストリームを求めるバイト優先のコードのために。文字セットを定め、見知らぬ人から来たものを検証し、署名は timingSafeEqual で比較すれば、ブラウザ/サーバーの境目の両側で、この形式は謎でなくなります。

そして包みを開き終えたら、誰かがそれらに封をしたのだと覚えておいてください。エンコード側には独自の罠があります:btoa() を文の途中で止めさせる Unicode の壁、MIME の行折り返し、base64url のパディング規則、そして omitPadding オプションを持つ新しい Uint8Array.toBase64()。その物語は、各ステップにコード例を添えて、姉妹サイトの関連する Base64 エンコード記事で詳しく扱われています。次はそれを読んでください。アルファベットのあの側では、罠が違って、もっと面白いからです。

最終更新: 2026-10-10

関連記事: JavaScript/Node.js での Base64 エンコード:完全ガイド