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

C#(CSharp)での Base64 デコード:完全ガイド

それは一瞬で分かる:文字と数字の川、ときどき現れる + や /、そして最後尾にぶら下がっている = が1つか2つ。APIレスポンス、メール添付、設定ファイル、JWTのどこかで、誰かがバイナリデータをテキストに詰め込み、今、それを開くのはあなたの仕事です。ここはC#におけるBase64のデコード側で、最初の朗報は、フレームワーク以外に何一つ必要ないということだ。このデコーダーは20年以上前から System 名前空間に暮らしていて、どのモダンな.NETランタイムでもなお同梱されており、オリジナルよりもオプションが多く、パフォーマンスも向上しています。

簡単な復習をしましょう。このサイトのホームページでフォーマットが詳しく説明されているので:64文字の文字表から4文字がデータ3バイトを運び、末尾の = 1つか2つが残りバイトの印です。デコードはその取引を逆方向に回すので、結果は入力の約4分の3の大きさになります。問題の形を頭に描いて、いくつかパッケージを開けてみましょう。

デコーダーファミリー:オプションを知っておこう

最初の例に入る前に、手に取れるデコードAPIのファミリー全体と、それぞれがどんな場面のために作られているかを示しておきます。ここに挙げたものはすべて.NETランタイムそのものの一部ですが、古いフレームワークでのURLセーフクラスだけは小さなNuGetパッケージに載ってやって来ます:

API 利用可能になった時期 用途
Convert.FromBase64String(string) .NET Framework 1.1(2003) 定番。文字列1つがはいり、新しい byte[] がでる。通常の空白はスキップし、それ以外は例外を投げる。
Convert.FromBase64CharArray(char[], int, int) .NET Framework 1.1(2003) 同じデコードだが、すでに自分が持っている文字バッファの一片から読む。
Convert.TryFromBase64String, Convert.TryFromBase64Chars .NET Core 2.1(2018) 例外ではなくブール値で答え、あなたが提供するスパンに書き込む。信頼できない入力へのフレンドリーなガード。
System.Buffers.Text.Base64 .NET Core 2.1(2018) 厳格なスパンAPI:例外ではなく状態コード、インプレース・デコード、IsValid による事前チェック。
System.Buffers.Text.Base64Url .NET 9(2024) URLセーフの文字表(+ と / の代わりに - と _)、パディングの有る無しを問わず扱える。.NET Framework 4.6.2以降および.NET Standard 2.0では:Microsoft.Bcl.Memory NuGetパッケージ。
FromBase64Transform + CryptoStream .NET Framework 1.1(2003) ストリーミング・デコード:ファイルからファイルへ、ネットワークからディスクへ、チャンク単位で、ペイロード全体を読み込まずに。

プロジェクトが2018年以降の.NETバージョンをターゲットにしているなら、最初の4行は箱に入っています。Base64Url には.NET 9以降が必要で、それより古いものでは Microsoft.Bcl.Memory パッケージが必要です。今後の注記も1つ:執筆時点ではプレビュー中の.NET 11のライブラリは2026年後半の一般リリースが予定されており、既存の型にさらなるBase64の便利APIとオーバーロードを追加します。ファミリーはこれからも大きくなります。この記事ではそれ以外にパッケージを必要とすることはありません。

主力:Convert.FromBase64String

C#でのデコード生活の9割は、たった1つの呼び出しです。文字列を渡せば、中に詰め込まれていた正確なバイトを返してくれます:

using System;
using System.Text;
string packed = "TWFu";
byte[] bytes = Convert.FromBase64String(packed);
string text = Encoding.UTF8.GetString(bytes);
Console.WriteLine(text);
// Man

覚えておく価値がある詳細が3つあります。第一に、戻り値はバイトであり、テキストではありません:byte[]で、デコーダーは最初から最後までバイト指向です。しかもそれはまさにあなたが望むことで、ペイロードは文章かもしれないし、PNG、証明書、ハッシュかもしれないが、どれも特別扱いされるべきではないからです。バイトから読みやすいテキストへ戻る飛びは、Encoding 経由の別個の、意図的なステップであり、そのステップこそが文字セットの決断が生じる場所です(後ほど詳しく)。第二に、デコーダーは毎回、デコードされた長さに合わせた新しい配列を割り当てるので、余分な容量を持つバッファを渡してくることはありません。第三に、契約は小さくて正直です:空の文字列は空の配列にデコードされ、null 参照は ArgumentNullException を投げ、有効なBase64で何でもないものは FormatException を投げます。それ以外は、この3つのルールの細部です。

何を受け入れ、何を拒むか

ここがC#のデコーダーに個性があるところです。それもかなり個性的なもので。ちょうど1つのこと - 空白 - には寛大で、それ以外のすべてには容赦がありません。デコーダーは、文字列のどこに現れても、ちょうど4文字だけをスキップします:スペース(U+0020)、タブ(U+0009)、改行(U+000A)、キャリッジリターン(U+000D)。その方針は、Base64ペイロードが76文字の行に折り返されて届くメールへの意図的な配慮で、MIMEで折り返された添付ファイルは前処理ゼロでデコードできます。64文字の文字表の外のもの、長さルールを破るもの、パディングが間違った場所にあるもの - いずれも例外になります。いくつかの入力で、同じデコーダーの動きを見てみましょう:

入力 結果
"TWFu" Man(3バイト)にデコードされる。
"TWF\nu"(途中に改行) Man にデコードされる。空白はデコーダーにとって不可視です。
"TWFu\u00A0"(末尾に非ブレイクススペース) FormatException。スキップされるのは上記の4つの空白文字だけで、NBSPは含まれていません。
"TWE"(長さ3、4の倍数ではない) FormatException。空白を除いたペイロードの長さは、4の倍数でなければなりません。
"TWFu="(データの後の余分なパディング) FormatException。パディングは最大2文字で、しかも最末尾のみ。
"-_88"(URLセーフの文字表) FormatException。標準のデコーダーが知っているのは、標準文字表の64文字だけです。
null ArgumentNullException:値はnullにできません。(パラメータ 's')

暗記する価値のあるもう1つのクセ:どんなフォーマット違反でも、同じ1つのエラーメッセージを返します:入力は有効なBase-64文字列ではありません。base 64でない文字、2つを超えるパディング文字、あるいはパディング文字に含まれる不正な文字を含むためです。メッセージは考えられる3つの原因すべてを列挙し、あなたがどれに該当したかは言わず、場所も教えてくれません。失敗したペイロードをデバッグするなら、文字数を数える、文字表を確認する、パディングを確認する、という順で調べるのが答えです。

例外なしでデコードする:Try API

例外駆動の制御フローは正当なパターンですが、大量の入力や信頼できない入力には Try ファミリーの方が良き市民です。.NET Core 2.1で追加され、2つの風味があります:文字列から読むものと、文字スパンから読むものです。どちらもあなたが提供するバッファに書き込み、どれほど埋まったかを報告します:

using System;
using System.Text;
string payload = "TWFu"; // 任意のペイロード、有効でも無効でも
Span<byte> buffer = stackalloc byte[4096];
if (Convert.TryFromBase64String(payload, buffer, out int written))
{
  string text = Encoding.UTF8.GetString(buffer[..written]);
  Console.WriteLine(text);
}
else
{
  Console.WriteLine("Not a valid Base64 payload.");
}

2つの動作が、Try 版を別の種のように感じさせます。無効な入力は例外を投げる代わりに false を返すので、壊れたペイロードの連続がもたらすコストは例外ではなく分岐1つです。ただし注意:null 入力は契約の一部ではなく - ArgumentNullException を投げます - ため、Try のガードがカバーするのは壊れたペイロードで、存在しない可能性のある値には、先に独自のnullチェックが必要です。兄弟メソッドの Convert.TryFromBase64Chars は ReadOnlySpan<char> から同じ仕事を行い、ペイロードがより大きな文字バッファの中にあって、先に部分文字列を切り出したくないときに便利です。出力バッファは余裕を持って大きめに:デコード後の長さは(空白を除いた)入力の長さの4分の3が上限で、written のoutパラメータが正確にどれだけ出てきたかを教えてくれます。

System.Buffers.Text.Base64 によるスパンベース・デコード

アロケーションを数えているとき、あるいはデコーダーに失敗を投げるのではなく記述してほしいとき、System.Buffers.Text.Base64 クラスがその道具です。.NET Core 2.1から標準ライブラリに暮らす静的クラスで、マネージド配列ではなくスパンで動きます。そのデコードメソッドは4つのムードを持つ OperationStatus 値を返します:Done(成功)、DestinationTooSmall(バッファが小さすぎた)、NeedMoreData(入力がまだ4の倍数ではない、読み続けよ)、InvalidData(これはBase64ではない)。最後のブール値パラメータ isFinalBlock が、この2つを区別するものです:これが入力の続きがあるかをデコーダーに伝えます。ワンショット形式がこちらで、サイズはクラス自身のヘルパーで計算しています:

using System.Buffers;
using System.Buffers.Text;
using System.Text;
string payload = "TWFu";
byte[] input = Encoding.ASCII.GetBytes(payload);
byte[] output = new byte[Base64.GetMaxDecodedFromUtf8Length(input.Length)];
OperationStatus status = Base64.DecodeFromUtf8(input, output,
  out int consumed, out int written, isFinalBlock: true);
if (status == OperationStatus.Done)
{
  Console.WriteLine(Encoding.UTF8.GetString(output.AsSpan(0, written)));
  // Man
}

このクラスの2つのメンバーに1段落ずつを。最初は IsValid で、ペイロードをデコードせずに検証します。バイトスパンと文字スパンの2つの風味があり、1つのオーバーロードは判定とともにデコード後の長さを報告するので、チェック1つでバッファのサイズが決まります:

using System.Buffers.Text;
string payload = "TWFu";
if (Base64.IsValid(payload, out int decodedLength))
{
  Console.WriteLine("Valid, decodes to " + decodedLength + " bytes.");
  // Valid, decodes to 3 bytes.
}
else
{
  Console.WriteLine("Rejecting payload before allocating anything.");
}

二つ目は DecodeFromUtf8InPlace で、Base64テキストがすでに自分の持つバッファの中にあって、上書きされても構わないという状況用です。デコードはデータを縮めるので、結果は同じバッファの先頭に書き込まれ、メソッドはそれがどれほど長いかを報告します:

using System.Buffers;
using System.Buffers.Text;
using System.Text;
byte[] data = Encoding.ASCII.GetBytes("TWFu");
OperationStatus status = Base64.DecodeFromUtf8InPlace(data, out int written);
if (status == OperationStatus.Done)
{
  Console.WriteLine(Encoding.ASCII.GetString(data, 0, written));
  // Man、同じバッファの先頭3バイトに引っ越した
}

ポケットにしまっておくべき動作が1つ:このクラスも、4つの通常の空白文字(スペース、タブ、改行、キャリッジリターン)をスキップするので、行折返しされたペイロードもそのままきれいにデコードできます。大事な点では厳格です:空白を除いた長さが4の倍数ではないペイロードは、最終ブロックである場合 InvalidData となり、標準文字表外の文字は即座に拒否されます。このクラスにはどこにも、黙って片付ける仕組みはありません。

URLセーフBase64: Base64Url クラス

同じ64個の値のために第二の文字表があり、C#のウェブ作業では常にそれと出会うことになります。標準文字表では、値62と63が + と / で、URLではトラブルを招く2文字です:クエリ文字列の + は普通にスペースとしてデコードされ、/ と = はそれぞれパーセントエンコードを必要とします。RFC 4648セクション5は、URLのどんな文脈でも特別な意味を持たない - と _ に差し替えることでこれを直し、末尾の = パディングを任意にしました。その結果がbase64urlと呼ばれ、JWT、APIトークン、ファイルアップロードID、そして数多くのURLの文字表です(YouTubeの11文字の動画識別子はパディングなしのbase64url)。

.NET 9から、標準ライブラリにはそれに専用のクラスが同梱されています:System.Buffers.Text.Base64Url。これは Base64 クラスのURLセーフな双子で、独自のデコード、検証、長さヘルパーを持ちます:

using System.Buffers.Text;
using System.Text;
string token = "-__8";
byte[] bytes = Base64Url.DecodeFromChars(token);
Console.WriteLine(BitConverter.ToString(bytes));
// FB-FF-FC

あの例でクラシックなAPIが何もできなかったことに注目してください。同じ3バイトは標準文字表では +//8 とエンコードされ、Convert.FromBase64String("+//8") は動作しますが、Convert.FromBase64String("-__8") は例外を投げます。URLセーフ文字はあの文字表の外だからです。さらにbase64urlのペイロードはパディングなしで届くことが多く、クラシックなデコーダーはそれを拒否します。4文字の完全なグループを要求するからです。Base64Url クラスはこの問題の両方のバリアントをネイティブで扱います:TWE(3文字、パディングなし)を2バイトの Ma にデコードし、TWE= も同じくきれいにデコードします。

プロジェクトが古いランタイムで動いているなら、現実的な道は2つあります。.NET Framework 4.6.2以降では、Microsoft.Bcl.Memory NuGetパッケージを追加します。Microsoftが Base64Url(および他のいくつかのモダンな型)をバックポートするために公開しているパッケージです:

dotnet add package Microsoft.Bcl.Memory

あるいは、パッケージをまったく使わずに、クラシックなデコーダーに渡す前にペイロードを正規化します:URLセーフ文字を標準の双子に差し戻し、不足しているパディングを補います。この小さなヘルパーはC#コードで最も一般的な手づくりのbase64urlデコーダーで、.NET Framework 1.1以降のすべてのランタイムで動くので、知っておく価値があります:

using System;
using System.Text;
string segment = "TWE";
segment = segment.Replace('-', '+').Replace('_', '/');
segment += new string('=', (4 - segment.Length % 4) % 4);
byte[] bytes = Convert.FromBase64String(segment);
Console.WriteLine(Encoding.ASCII.GetString(bytes));
// Ma

(4 - length % 4) % 4 の式がパディング演算のすべてです:0、1、2の = 文字を足して長さを4の倍数に落とし、外側の剰余が、パディング済みの入力が余分なものを増やさないようにします。

バイトから言葉へ:テキスト、Unicode、文字セット

デコードが得てくれるのはバイトで、バイトは完全に中立なものです。それが「テキスト」になるのは、それをどの文字セットとして読むかを選んだときだけで、その選択はあなた次第です。Base64は、元の著者がどの文字セットを使っていたかという情報を一切持たないからです。実務ではこうなります:特に理由がなければUTF-8と仮定し、コードではそれを明示にしましょう。明示的な Encoding.UTF8 の呼び出しは、「たまたま正しいプログラム」と「設計によって正しいプログラム」の違いだからです:

using System;
using System.Text;
string original = "h\u00e9llo \u4e16\u754c";
byte[] utf8 = Encoding.UTF8.GetBytes(original);
string packed = Convert.ToBase64String(utf8);
byte[] decoded = Convert.FromBase64String(packed);
string restored = Encoding.UTF8.GetString(decoded);
Console.WriteLine(restored == original);
// True: h\u00e9llo \u4e16\u754c が完璧に往復する

微妙な罠は、ペイロードが実はLatin-1だったり、バイナリだったり、単に壊れていたりして、バイトが有効ではないUTF-8だったときに何が起きるかです。デフォルトでは、.NETのUTF-8デコーダーは不正なシーケンスをすべてUnicodeの置換文字(U+FFFD)に置き換えて先へ進みます。例外も警告もありません:データはただ消え、あなたのデータベースの中でクエスチョンマークに変わっています。それがいつ起きたかを知りたいなら、厳密なフォールバックでエンコーディングを構築します。黙っての置換が、うるさく鳴る DecoderFallbackException に変わるのです:

using System.Text;
byte[] bytes = Convert.FromBase64String("//4="); // バイト FF FE、有効なUTF-8ではない
Encoding strictUtf8 = Encoding.GetEncoding(
  "utf-8",
  new EncoderExceptionFallback(),
  new DecoderExceptionFallback());
string text = strictUtf8.GetString(bytes);
// FF FEはUTF-8のシーケンスではないため、DecoderFallbackExceptionを投げる

失敗するよりも生き残りたいペイロードには、置換フォールバックが穏やかな選択肢で、置換テキストを自分で選べます:

using System.Text;
byte[] bytes = Convert.FromBase64String("//4="); // バイト FF FE、有効なUTF-8ではない
Encoding forgivingUtf8 = Encoding.GetEncoding(
  "utf-8",
  EncoderFallback.ReplacementFallback,
  new DecoderReplacementFallback("[bad]"));
string text = forgivingUtf8.GetString(bytes);
Console.WriteLine(text);
// 黙ってのU+FFFD置換の代わりに [bad][bad]

C#特有の歴史教訓をもう1つ:Encoding.Default はランタイムによって意味が違います。Windows上の.NET FrameworkではシステムのANSIコードページ(多くの場合Windows-1252)ですが、.NET(Core)ではBOMなしのUTF-8です。そのため、Encoding.Default 経由でペイロードを往復させるコードは、2010年のマシンと2025年のマシンで異なるバイトを生むかもしれず、Base64は渡された方を喜んでエンコードしてくれます。アクセント付きの文字化けで埋まったデコード済み文字列をいつか見つけたら、Encoding.Default がまず見るべき場所です。

ファイルとバイナリペイロード

ファイルは最も素直なデコードの対象です。文字セットの問題がそもそもないからです:デコードしたバイトがそのままファイルで、ゼロもすべて、バイト単位でそっくりそのままです。パターンは呼び出し2つとファイル1つで、画像アップロードからバックアップツールまで、どこでもあります:

using System.IO;
string b64 = File.ReadAllText("payload.b64");
byte[] original = Convert.FromBase64String(b64);
File.WriteAllBytes("restored.bin", original);
Console.WriteLine("Restored " + original.Length + " bytes.");

実務的な注が2つあります。ファイルが空白や改行を含みうるなら(テキストファイルなので、ほぼ確実に含んでいます)、クラシックなデコーダーは先ほど見たように無料でそれを扱います。そしてペイロードが大きいなら、文字列を経由するのはやめて:ファイルから文字列への変換ステップを飛ばし、ストリームから直接デコードします。それが次のセクションです。テキストで、しかも文字セットがわかっているデコード済みペイロードには、ファイルの例が解決策全体で、文字セットセクションの Encoding.UTF8.GetString ステップが、デコードと利用のちょうど間に差し込まれます。

ストリームからのデコード:FromBase64Transform

Convert のメソッドは文字列に収まるペイロード向けに設計されており、公式ドキュメントもまさにそのことばで言います:ストリーミングデータにはtransformクラスを使え、と。FromBase64Transform は.NET Framework 1.1(2003)から System.Security.Cryptography の一部で、データが流れゆく途中で変換するためのフレームワークの汎用パイプである CryptoStream に接続されます。ファイルからファイルへのデコード全体は、4行のセットアップです:

using System.IO;
using System.Security.Cryptography;
using FileStream source = File.OpenRead("payload.b64");
using FromBase64Transform transform =
  new FromBase64Transform(FromBase64TransformMode.IgnoreWhiteSpaces);
using CryptoStream reader = new CryptoStream(source, transform, CryptoStreamMode.Read);
using FileStream target = File.Create("payload.bin");
reader.CopyTo(target);
Console.WriteLine("Done, " + target.Length + " bytes written.");

コンストラクタはモードを取り、2つのモードは名前で覚える価値があります。IgnoreWhiteSpaces(デフォルトで、クラシックなデコーダーの空白方針に一致)はストリームが流れる間に4つの通常の空白文字をスキップし、メールで折り返されたペイロードや改行だらけのペイロードにはこれが望ましいものです。DoNotIgnoreWhiteSpaces は厳格です:出会った最初の文字表外の文字で FormatException を投げます。ペイロードの中にある余分なスペースは肩をすくめられるべきものではなくバグであるべき、というときに望ましいのがこれです。内部ではtransformは入力を4文字のグループ単位で処理し、各グループが生み出す3バイトを返し、TransformFinalBlock が末尾を処理します。これらのメソッドを自分で呼ぶことはほとんどありません。CryptoStream が代わりにやってくれるからです。ただし4文字グループという事実が重要です:transformを手動で給餌するなら4の倍数で給餌してください。そうしないと最後の不完全なグループが最終ブロックの中に座り込みます。

JWT: 3つのセグメント、1つのドット

JSON Web TokenはC#のウェブ開発で最もトラフィックの高いbase64urlペイロードで、その形は驚くほどシンプルです:ドットで区切られた3つのセグメント。1つ目はエンコードされたヘッダ、2つ目はエンコードされたペイロード(別名claims)、3つ目は署名です。最初の2つはそれぞれ、JWS仕様に従い、パディングなしのUTF-8 JSONドキュメントのbase64urlです。分割してデコードするのはC#2行で済みます:

using System;
using System.Buffers.Text;
using System.Text;
using System.Text.Json;
string jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBZGEifQ.c2lnbmF0dXJl";
string[] parts = jwt.Split('.');
string headerJson = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[0]));
string payloadJson = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[1]));
using JsonDocument doc = JsonDocument.Parse(payloadJson);
Console.WriteLine(doc.RootElement.GetProperty("name").GetString());
// Ada

.NET 9より前のランタイムでは、同じ仕事がURLセーフセクションの正規化ヘルパーを通過します:- と _ を + と / に差し戻し、セグメントを4の倍数にパディングし、Convert.FromBase64String でデコードします。どちらのアプローチでも同じJSONが得られます;ターゲットフレームワークに合う方を選びましょう。

鋭く保っておくべき境界が1つ:JWTをデコードすることは、JWTを検証することではありません。上記のデコードは、ゴミのような署名のトークンのclaimsを喜んで読んでしまいます。署名は最初の2セグメントに対する別の暗号チェックだからです。本番のトークン作業では、手動でパースするのはやめてください:System.IdentityModel.Tokens.Jwt パッケージ(Microsoft.IdentityModelファミリーから)が、パース、検証、有効期限を1つで扱い、そのbase64urlの扱いこそがこのセクションが説明する文字表そのものです。デバッグや小さなユーティリティでは手動デコードを、ユーザーが到達しうるものにはライブラリによる検証を。

data URIと埋め込み画像

C#のコードの中には、data: URIを受け取ることが仕事というクラス丸ごとがあります。HTML、CSS、そして数多くのウェブAPIが、バイナリ内容をインラインで埋め込むのにこれを使うからです。RFC 2397で標準化されたこのスキームは data:[mediatype][;base64],payload:最初のコンマより前がすべてメタデータ(MIMEタイプと ;base64 フラグ)で、その後ろがすべてペイロードです。;base64 フラグがあるとき、ペイロードはBase64文字列で、コンマで分割することがパースのすべてです:

using System;
using System.Text;
string dataUri = "data:image/png;base64,iVBORw0KGgo=";
int comma = dataUri.IndexOf(',');
string mediaType = dataUri[..comma];        // data:image/png;base64
string b64 = dataUri[(comma + 1)..];        // iVBORw0KGgo=
byte[] imageBytes = Convert.FromBase64String(b64);
Console.WriteLine(imageBytes.Length);
// 8: PNGシグネチャのバイト 89 50 4E 47 0D 0A 1A 0A

例の iVBORw0KGgo= プレフィックスは8バイトのPNGマジックナンバーのBase64形式で、便利な指紋です:本物のPNGのdata URIはすべてこの形で始まるので、信頼できないHTMLをパースしているときの素早い健全性チェックになります。C#開発者への実務的な注が2つ。第一に、Uri クラスは.NET上でdata URIをネイティブに理解します:new Uri("data:text/plain;base64,TWFu") はきれいにパースされ、Scheme == "data" と報告するので、コードがURIでルーティングするなら、data URIはパイプラインに現れ、それをどう扱うかを決める必要があります。第二に、data URIが本当になのかを思い出しましょう:ファイルの完全なコピーが、3分の1膨らんで、あなたのドキュメントの中に座っているのです。4 KBのfaviconなら問題ありませんが、4 MBのロゴなら痛いです。あなたがそれらを生成する側なら(その側はエンコードの記事で扱う)、エンコードする前に画像のサイズを決めてください。

HTTP: Basic認証とAPIのやり取り

Base64は、どんなAPI作業でも触れるHTTPの少なくとも1つの場所に織り込まれています:Basic認証スキームです。クライアントは Authorization: Basic を送り、その後コロンでくっつけた username:password のBase64エンコードが続きます。サーバ側では、届いたヘッダのデコードは次の通りです:Basic プレフィックスを剥がし、デコードし、最初のコロンで分割します:

using System;
using System.Text;
string header = "Basic YWRhOnMzY3JldA==";
string encoded = header["Basic ".Length..].Trim();
string credentials = Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
int colon = credentials.IndexOf(':');
string user = credentials[..colon];
string password = credentials[(colon + 1)..];
Console.WriteLine(user);      // ada
Console.WriteLine(password);  // s3cret

UTF-8のステップは、見た目より重要です:RFC 7617は実際には文字セットを固定しておらず、下位互換のためにデフォルトを未定義にし、推奨としてのUTF-8ヒントを許可するだけですが、そのヒントこそがすべてのモダンなサーバが期待するものです。そのため、アクセント付き文字を含むユーザ名は、同じユーザ名をLatin-1として読むのと違い(そして正しい)バイト文字列を生みます。Basic認証のデコード側はこのパターンの単純な端です。ASP.NET Coreでは生のヘッダではなく認証ハンドラを通してそれに出会うことが普通ですが、その下で動いているのは同じデコードロジックで、APIサーバをフェイクする統合テストを書くときに必要になるのはまさにこうしたコードです。鏡像の操作であるクライアント側でのヘッダ構築は、エンコード側ではワンライナで、エンコードの記事では完全な例が得られます。

メール:MIMEと行折返しされたペイロード

メールこそがBase64にその名声を稼がせた場所で、今なおC#サービスが届くペイロードの多くの源です。SMTPはもともと7ビットのプロトコルだったため、バイナリ添付はそのままでは旅行できません:MIME仕様(RFC 2045)はそれらを Content-Transfer-Encoding: base64 ヘッダ付きのBase64としてエンコードし、出力を76文字で折り返し、行をキャリッジリターン-改行ペアで区切ります。実際の添付本文はしたがって76文字の行の1列に見え、C#への朗報は、クラシックなデコーダーがすでにその読み方を知らしていることです:文字列のどこでも空白をスキップするので、折り返された本文全体を、改行ごとそのまま渡せば、改行がなかったかのようにデコードしてくれます:

using System;
using System.Text;
string attachmentBody = "TWFu\r\nTWFu\r\nTWFu";
byte[] bytes = Convert.FromBase64String(attachmentBody);
Console.WriteLine(Encoding.ASCII.GetString(bytes));
// ManManMan

文字列ではなくストリームで届くペイロードには、空白無視モードの FromBase64Transform が、ストリーミング衣装作りの同じ話です。そして本文のデコード以上が必要になったとき、MIME構造を歩き、ヘッダをパースし、入れ子になったmultipartセクションを扱い、本物の .eml ファイルからすべての添付を抽出しなければならないとき、C#のエコシステムでの答えは MimeKit パッケージです:.NET向けの標準MIMEライブラリで、Base64とquoted-printableのコンテンツ転送エンコーディングを内部で扱い、「本文だけデコードすればいい」がもう問題を説明しなくなった瞬間に手を伸ばすべき道具です。フレームワーク自身の MailMessage クラスは単純な添付なら代わりにデコードしてくれますが、そのMIMEサポートはモダンな基準では意図的に控えめです。

PEM証明書

PEMはTLS世界の装甲フォーマットです:-----BEGIN CERTIFICATE----- と -----END CERTIFICATE----- マーカーの間のBase64本文が、RFC 7468で指定された通り64文字で折り返されます。C#開発者はHTTPSエンドポイントの裏にある証明書ファイルとしてそれに出会い、ここでのデコードの話は想像より良いものです。.NET 6から、フレームワークがBase64本文ごとPEMをあなたの代わりにパースしてくれるからです:

using System.IO;
using System.Security.Cryptography.X509Certificates;
string pem = File.ReadAllText("server.pem");
X509Certificate2 certificate = X509Certificate2.CreateFromPem(pem);
Console.WriteLine(certificate.Subject);
// CN=server.example.com

そこに手動Base64はどこにもありません:CreateFromPem がマーカーを見つけ、本文を解き、デコードし、生きた証明書を返してくれます。(ファミリーには秘密鍵と、証明書と鍵の組み合わせ形式の兄弟もいます。インフラがそうしたものを手渡すなら。)古いランタイムにいるか、装甲の内部にある生のDERバイトが必要な場合、手動版は2ステップの剥がしとデコードで、同じパターンがあらゆるPEM装甲のものに効くので、知る価値があります:

using System;
using System.Text;
string pem = File.ReadAllText("server.pem");
string body = pem
  .Replace("-----BEGIN CERTIFICATE-----", "")
  .Replace("-----END CERTIFICATE-----", "")
  .Replace("\r", "")
  .Replace("\n", "");
byte[] der = Convert.FromBase64String(body);
Console.WriteLine(der.Length);
// 装甲内のDER証明書の長さ

この片隅の落とし穴はすべて空白です:PEMファイルは多くの証明書ツールからCRLFの行末を持つので、デコード前に \r と \n の両方を剥がしてください。改行だけでは足りません。そして証明書本文と秘密鍵本文を混同しないでください。マーカーも中身も違います。デコーダーはその1つからあなたを救えません。

設定ファイル、環境変数、データベース

C#アプリケーションにおけるBase64の第三の住処はストレージです:設定ファイル、環境変数、データベースの列。パターンはどこも同じです。バイナリまたはシークレットの値は入るときに文字列としてエンコードされ、出るときにバイトとしてデコードされます。環境変数は最も見える例です。テキストしか持てないからです:

using System;
using System.Text;
string? encoded = Environment.GetEnvironmentVariable("API_KEY_B64");
if (encoded == null)
{
  throw new InvalidOperationException("Set the API_KEY_B64 environment variable first.");
}
byte[] keyBytes = Convert.FromBase64String(encoded);
string apiKey = Encoding.UTF8.GetString(keyBytes);
Console.WriteLine(apiKey.Length + " characters of API key, ready to use.");

データベースでは同じ考え方は、可搬性のためにテキスト列に保存したい byte[] プロパティとして現れることが多く、Entity Framework Coreにはまさにこれのための組み込み機構があります:すべての読み書きで、あなたのエンコード関数とデコード関数を透明に実行する値コンバータです:

using Microsoft.EntityFrameworkCore;
modelBuilder.Entity<Avatar>()
  .Property(a => a.ImageData)
  .HasConversion(
    v => Convert.ToBase64String(v),
    v => Convert.FromBase64String(v));

その1つのコンバータがデータベース統合の全体です:ImageData はC#コードでは byte[] のままで、データベースが見るのはBase64文字列です。このセクションに属する注意が2つあります。第一に、3バイト4文字の税金のため、与えられた幅の列は、生のバイナリよりエンコード済みテキストでは約3分の1少ないデータしか持ちません。固定幅の列なら、エンコード後の長さに合わせてサイズを決めてください。第二に、これがセキュリティの話です:設定ファイルの中のBase64は、値を1行に収めておくための便利さであって、値の保護ではありません。設定ファイルを読める誰であれ1つのコマンドで鍵をデコードできるので、本当のシークレットはシークレットストアに属し、そこでのBase64はただの伝送形式です。

ペイロードが大きいとき

Base64デコードには、エンコードにない快い性質があります:出力は常に、入力の約4分の3と、それより小さいのです。10メガバイトのテキストペイロードは約7.5メガバイトのバイトにデコードされるので、デコードがエンコードのようにメモリを膨張させることは決してありません。バッファを事前にサイズ決めする必要がある場合、その計算は2つの呼び出しのいずれかに帰着します:厳格なスパンクラスには Base64.GetMaxDecodedFromUtf8Length、クラシックなAPIには単純な割り算 length / 4 * 3、入力が折り返されているなら空白のための余裕を足して。(ヘルパーはデコード長の最大可能値を返します:実長がそれに等しいのは最後のグループにパディングがないときだけで、1つまたは2つのパッド文字で終わるときは1バイトまたは2バイト小さいのです。)

ただしペイロードが本当に大きい場合、正しい手はより大きいバッファではありません - いっそのことバッファなしです:文字列を完全にスキップし、ストリームセクションで示したように、FromBase64Transform にソースからターゲットへデコードをストリームさせるのです。守るべき唯一のルールは4文字グループの整合性です:Base64ストリームは、4文字の倍数の地点でしか切り取れません(空白を考慮した後)。そのためtransformを手動で給餌するなら、4の倍数のチャンクで読み、TransformFinalBlock に余りを排水させます。何百メガバイトに届かないものについては、ワンショット・デコードは十分速いため、これは必要性ではなく最適化です。ただしストリーミング形式は、メモリ制限下でもよく振る舞う形式でもあります。しかもそれはまさに、大きいペイロードが好きそうな環境です。

あなたのターミナルに入るデコーダー

どんな言語にも、15行のコンソールプログラムがコマンドラインツールになる満足感のある瞬間があり、C#のBase64デコーダーはそれをやるのに良い相手です。標準入力から読むため、シェルパイプへのそのままの差し替えになるからです。これがツールの全体です:パイプから(または引数から)Base64ペイロードを読み、デコードし、生のバイトをファイルに書き出します:

using System;
using System.IO;
using System.Text;
string input = args.Length > 0 ? File.ReadAllText(args[0]) : Console.In.ReadToEnd();
byte[] bytes = Convert.FromBase64String(input.Trim());
File.WriteAllBytes("output.bin", bytes);
Console.Error.WriteLine("Wrote " + bytes.Length + " bytes to output.bin.");

一度ビルドすれば、.NETランタイムのデコーダーを特に望む日に、シェルのbase64ユーティリティの隣に座っています:ファイルをパイプで通し、他のツールとチェーンし、厳格なC#検証ルール(空白は寛容、文字表は厳格、パディングは厳格)があなたのパイプラインの一部になります。Trim() はそこで静かな仕事をしています。テキストエディタが好きで足す末尾の改行をキャッチしているのです。ただし公平を期すなら、デコーダーはそれをどうせ無視していたでしょう。APIログにどんどん現れるようになったURLセーフなペイロードには、URLセーフセクションの Base64Url デコードを入れた同じ骨格が変更のすべてです。

速度:何を期待すべきか

モダンな.NETでのBase64は速く、ますます速くなっています。Convert メソッドと System.Buffers.Text クラスの両方のランタイム実装は、ハードウェアがサポートする場所でSIMDベクトル命令で最適化されており、1サイクルで多数の文字を処理します。実務的には、それは何メガバイトものペイロードが普通のデスクトップマシンで1桁から2桁下のミリ秒でデコードされることを意味し、あなたが書くどんなアプリケーションでもBase64デコードは実質的に無料なほど速いのです。だから実務的なパフォーマンスの助言は、デコーダーそのものではなく、あなたのコードの形についてです。壊れた入力が可能で、例外が値を失うホットパスでは、Try メソッドや状態を返すスパンメソッドを優先してください。ループで数千の小さなペイロードをデコードするなら、呼び出しごとに新しい配列を割り当てるのではなく、インプレースとスパンAPIでバッファを再利用してください。そして同じペイロードを二度とデコードしないでください:1回がコストであり、すでにデコードしたフィールドの2回目のデコードは純粋な無駄で、プロファイルには謎の2番目のBase64スパイクとして現れます。

セキュリティ:Base64がやらないこと

Base64に関する最も重要なセキュリティの事実は、初心者が最も見落としやすいものです:これはエンコーディングであり、暗号化ではありません。Base64文字列は誰にでも、どんなツールででも、一瞬で読め、この記事全体が示してきた通り、C#はその読み方をワンライナにします。Base64には鍵がなく、アルゴリズムパラメータもなく、突かれる脆弱さもありません。それは何も隠そうとしてこなかったからです:これは伝送形式で、バイナリをテキストだけのチャネルで生き延びさせる方法です。それらしく扱いなさい。Base64で「保護」された設定ファイルに、パスワード、トークン、シークレットを入れるのはやめてください。保護はちょうど Convert.FromBase64String の呼び出し1つ分の深さしかないからです。値がシークレットでなければならないなら、本当の保護が必要です(シークレットマネージャ、暗号化されたストア、最低限オペレーティングシステムのアクセス制御)。そしてそこでのBase64は、旅の間に着る形だけです。

2つ目のセキュリティ注は、あなたのデコード経路についてです。デコードするすべてのペイロードは、証明されるまで信頼できない入力で、設計すべき2つの失敗モードは、うるさい方(無効な入力。クラシックなAPIは FormatException で答えます。あなたはそれをキャッチして500ではなく400に変換すべきです)と、静かな方(有効なBase64なのに、期待通りのバイトにデコードされない:UTF-8でなく、要求したファイルタイプでなく、予算より長い)です。信じる前に検証してください:割り当てる前に IsValid か Try ファミリーで長さをチェックし、画像や証明書のパサーに渡す前に、デコードしたバイトを期待されるシグネチャ(PNGマジック、PKCSヘッダ)と突き合わせ、デコードの後ではなく前に、エンコードされた長さからバッファをサイズ決めします。Base64は整った形のものなら何でもデコードします。整った形があなたのアプリケーションで何を意味するかを決めるのは、あなたの仕事です。

噛みつく前に知っておくべき落とし穴

これらは実コードに繰り返し現れるC#特有の罠で、どれもフレームワークの動きに具体的な原因があります:

  • 文字列を通過するバイナリ。 C#の string はUTF-16コードユニットの列であり、デコードされたBase64はそうではありません。デコードしたバイトを文字列変数に押し込んだ瞬間(デコードしたPNGの Console.WriteLine、バイナリとの文字列結合、「テキスト」をシリアライズするJSONライブラリ)、下流の何かがそれを壊します。デコードしたバイナリは、本当にバイトを欲する場所に届くまで byte[] のままで保ってください。
  • Encoding.Defaultの分岐。 Encoding.Default でデコードしたバイトを読むコードは、.NET Framework(WindowsのANSIコードページ)と.NET(UTF-8)で異なるテキストを生みます。同じペイロード、異なる2つの出力、例外はなし。エンコーディングを明示的に固定してください。
  • JWTセグメントとクラシックなデコーダー。 生のJWTセグメントを Convert.FromBase64String に渡すと、同時に2つの面で失敗します:-/_ 文字は標準文字表の外であり、欠けたパディングが長さルールを破るからです。先に正規化するか、Base64Url を使います。
  • 見える空白と見えない空白。 デコーダーがスキップするのはスペース、タブ、改行、キャリッジリターンだけで、それ以外はスキップしません。ペイロードの中の非ブレイクススペース、Unicodeの行セパレーター、縦タブ(どれもいくつかのウェブページからのコピー&ペーストで生き残る)は FormatException であり、肩をすくむものではないのです。
  • すべての犯罪に1つのエラーメッセージ。 クラシックなデコーダーの FormatException は、どのルールがどこで破れたとは言いません。長さを確認し、次に文字表、最後にパディング、という順で調べるか、ブール値の答えのために TryFromBase64String と IsValid に切り替えてください。
  • 黙ってのUTF-8置換。 Encoding.UTF8.GetString は、文句も言わずに不正なバイトシーケンスをU+FFFDに変えます。ペイロードが有効なUTF-8でない可能性があるなら、文字セットセクションの厳密なフォールバックを使い、そうでなければ、起きた何週間後かにデータの欠けを調査することになります。
  • 間違った場所でのストリーム切断。 Base64ストリームは4文字の倍数の地点でしか切れない。他の境界でストリーミング・デコードをチャンク化すると、最後の不完全なグループは TransformFinalBlock に着地し、そこで属するものならそれでよく、属しないものはあなたの整合性の計算を壊します。
  • PEMの行末。 証明書ファイルはCRLFを持つ。アーマーを手動で解くときは \n とともに \r も剥がし、そうしなければ「デコードした」DERの最初の行は、データバイトの服を着たキャリッジリターンになります。
  • 二重エンコード。 ペイロードが届いた時点ですでにBase64だった場合(Base64文字列をBase64化した設定、別のエンコーダーの出力をエンコードしたAPI)、デコード1回ではあなたのデータではなく、より多くのBase64が得られます。往復が閉じるのは、エンコードの数だけデコードしたあとだけです。そしてそのバグのエンコーダー側が、エンコードの記事の主題です。

C#におけるBase64の短い歴史

C#でのBase64の物語は、.NETプラットフォームが成長した物語でもあり、多くの人が想像するよりずっと長く続きます:

  • .NET Framework 1.1、2003年4月。 Convert.FromBase64String とその兄弟が到着し、今もAPIを定義している設計を持ってきました:文字表には厳格、4つの空白文字には寛大、エラーにはぶっきらぼう。以降20年の大半で、この1つのメソッドがC#の「それ」Base64デコーダーです。
  • .NET 2.0、2005年。 Base64FormattingOptions 列挙型が Convert に加わり、MIME風の改行をエンコード側にもたらしました(対応する空白の許容はデコード側にももたらされ、そこではすでに静かに働いています)。
  • .NET Core 2.1、2018年。 スパンの時代。Convert が Try メソッドとスパンベースのエンコードを手に入れ、新しい System.Buffers.Text.Base64 クラスが OperationStatus 契約、インプレース・デコード、IsValid を携えて到着。メモリ重視の書き換えのゼロアロケーション世界のために作られました。
  • .NET 5、2020年。 16進の兄弟(Convert.ToHexString たち)が出荷。Base64と同じ設計パターンが16文字の文字表に適用され、変換クラスのパターンがハウスタイルになったことを示す印です。
  • .NET 6、2021年。 X509Certificate2.CreateFromPem がPEMを第一級の入力にし、手動アーマー剥離コードのクラス丸ごとがモダンなランタイムでは任意になりました。
  • .NET 9、2024年11月。 System.Buffers.Text.Base64Url がコミュニティからの何年もの要求を経てようやく箱に入り、Microsoft.Bcl.Memory パッケージがまだすべてを動かしているレガシーコードベースのために.NET Framework 4.6.2以降にバックポートしました。
  • .NET 11、執筆時点ではプレビュー中。 2026年後半のリリースが予定されている次のバージョンは、既存の型にさらなるBase64の便利APIとオーバーロードを追加し、より人間らしい手触りの表面へのゆっくりした行進を続けます。

心にとめておく価値があるのは、エンコード自体がこれらすべてよりずっと古いことです。現在MIME Base64と呼んでいるものの最初の標準化された利用は、1987年のPrivacy-Enhanced Mailプロトコル(RFC 989)で、MIMEが1993年に76文字の行折返し形式を標準化し、2006年のRFC 4648がこのフォーマットに現代的な、文字表を意識した仕様を与えました。URLセーフなバリアントも含めてです。C#はそれをすべて受け継ぎました:30年前のメールフォーマットで出会うすべての行折返しとパディングのクセは、C#のデコーダーが吸収するために設計されたクセなのです。

気になったC#の事実

  • 最小のスモークテスト。 "TWFu" は Man にデコードされる。3バイト、パディングなし、言い訳なし。これはC#でのBase64デバッグのhello worldで、4文字でハッピーパス全体を試し切りします。
  • 郵便の歴史を持つデコーダー。 空白の許容は実装の偶然ではなく - MIMEから受け継いだ設計判断です:76文字で折り返されたメール本文全体、そのCRLFペアすべてを込めて、Convert.FromBase64String に与えられる有効な単一の引数になりえます。このデコーダーは、メールが30年間使ってきたフォーマットを食べるために作られました。
  • 1つのエラー、3つの原因。 クラシックな FormatException メッセージは、報告しうる3つの失敗モード(不正な文字、パディング過多、場所のずれたパディング)をすべて列挙し、どれが発火したとは言いません。API表面で、四択問題のように働くエラーメッセージはこれだけです。
  • 少し嘘をつく名前空間。 System.Buffers.Text はテキスト処理についてのように聞こえますが、実際にはバイナリからテキストへの変換一般の住処です:数値や日付をUTF-8に直接パースする Utf8Parser と Utf8Formatter は、Base64クラスのすぐ隣に住んでいます。
  • ファミリーの片側ではパディングは任意。 Base64Url クラスは AQIDBA(6文字、パディングなし)と AQIDBA==(パディング付きの同じバイト)を同じ4バイトにデコードし、クラシックなデコーダーはパディング付きの形だけを受け入れます。2つのデコーダー、2つの契約、1つのランタイム。
  • 存在すべきでない文字列。 C#の文字列は合法的にNULバイトを含めうるため、デコードしたバイナリの Encoding.UTF8.GetString は、コントロール文字で埋まった「文字列」を生みうる。それをコンソール、あなたのCSVライター、そして地球上のJSONライブラリの半分が、それぞれ違った方法で扱います。型システムはそれを許可し、エコシステムはほとんどが許可しません。
  • 良好な状態の1.1の遺産。 Convert.FromBase64CharArray は2003年4月以来、同じ3パラメータのシグネチャを持っており、ジェネリクス革命、スパン革命、URLセーフ革命を、オーバーロードを1つも追加せずに生き延びています。C#のchar配列時代は消えていません;単に休んでいるだけです。
  • 11文字、8バイト。 YouTubeの動画識別子はパディングなしのbase64url: 8バイトにデコードされる11文字。Base64Url.GetMaxDecodedLength(11) が8を告げ、デコードはワンライナで、そうしたものを書くタイプの人間なら、それで一日を終えるのは良い方法です。

もう一方の方向へ

これがデコーダー側です。そして、痛みのほとんどが棲む場所でもあります。デコードとは、他の人々のデータに出会う場所だからです:彼らのパディングの選択、改行、文字表、トークン。逆方向、自分のバイトを取ってBase64に詰め込むことは、より穏やかな問題で、自分なりの決断のセットと、自分なりの罠のセットを持ちます。76文字の質問からURLセーフなトークンまで、C#でのBase64エンコードは、下のリンクのコンパニオン記事で詳しく扱われており、何を求めるかがわかれば、短くて満足感のある読み物になります。

最終更新: 2026-10-09

関連記事: C#(CSharp)での Base64 エンコード:完全ガイド