C での Base64 エンコード:完全ガイド
あなたはバイトを持っています。ディスクから読んだJPEGかもしれません、トークンかもしれません、クライアントが送信しようとしているパスワードかもしれません、あるパイプラインがJSONフィールドの中にあると期待しているファイルの生バイトかもしれません。そして道の先に、テキストだけしか話さないチャネルがあります。JSON文字列、URL、メール本文、設定ファイル、テキストを装ったデータベースカラム。Base64エンコーディングの出番がここです。生のデータ3バイトごとに、64文字の文字表からの4文字に書き直すので、結果は地球上のあらゆるテキストパイプラインを生き抜く素のASCIIになります。このサイトのホームページはフォーマット全体を説明しています。この記事は、Cでその仕事をしっかりやる方法についての話です。Cでは - いつものように - 言語が代わりにやってくれることはないので。
頭に叩き込んでおくべき数字は1つ:エンコードはデータを大きくします。3バイトが4文字になるので、すべてのペイロードはプログラムをあとにするころに約3分の1大きくなり、改行が加わるたびにさらに少し大きくなります。これが税金で、逃れる道はありません - ただしCでは、この税金に明細があります。出力バッファはあなたが割り当てるもので、ちょうど大きさが合う必要があるからです。計算を1度正しくやれば、この記事のすべてのエンコーダーが予測可能になります。オーバーフローもアンダーフローもなく、次のバイトがどこに落ちるか心配することもなくなります。それから選ぶべき工具箱 - OpenSSL、Mbed TLS、APR-Util、GLib - があります。選択は重要です。それぞれが異なる方法で折り返し、異なる方法で終端し、異なる方法でエラーを出すからです。
4つのエンコーダー、4つの個性
4つのライブラリはどれも、標準文字表を正しく、そして同一にエンコードします。同じバイトが入れば、同じ文字が出る、常に。違いはパッケージングにあり、パッケージングこそが相互運用バグが隠れる場所です。全体の眺めはこうです:
| ライブラリ | ヘッダー | 出力スタイル | 失敗モード |
|---|---|---|---|
| OpenSSL(libcrypto) | <openssl/evp.h> |
改行なし。NUL終端子を書く | 実質なし(割り当てのみ) |
| Mbed TLS | <mbedtls/base64.h> |
改行なし。NUL終端 | 必要サイズを伴う「バッファが小さすぎる」コード |
| APR-Util | <apr-1.0/apr_base64.h> |
改行なし。NULを追加 | なし - バッファサイズを信じて |
| GLib | <glib.h> |
改行なし。NUL終端、ヒープ割り当て | NULL を返す(割り当てのみ) |
テーブルに何が入っていないか注意してください。どのライブラリもデフォルトでは行を折り返しません。それは意図的なことです。RFC 4648は、周囲の仕様が明確に要求しない限り実装は改行を追加してはならないと定めていて - それは安心材料です。JSON文字列やURLの中に迷い込んだ改行は、機能ではなくエラーだからです。折り返しはメールとPEMのために存在し、必要ならOpenSSLのストリーミング経路から得るか、5行で自分で折り返せばよいです(メールのセクションでは両方を見せます)。ライブラリの選び方:すでにOpenSSLをリンクしているならOpenSSLを、1キロバイトごとに議論される組み込みビルドならMbed TLSを、Apacheエコシステムの中ならAPR-Utilを、プログラムの残りがすでにGLibならGLibを。インストール:libssl-dev(Debian/Ubuntu)または openssl-devel(Fedora/RHEL)または brew install openssl(macOS)、Mbed TLSなら libmbedtls-dev、APR-Utilなら libaprutil1-dev と libapr1-dev、GLibなら glib2.0-dev。
割り当てる前に計算をする
コードの前に、計算からです。Cは小さすぎるバッファからあなたを救ってくれないので。入力の3バイトごとに、ちょうど出力文字4つが生まれます。入力の長さが3の倍数でなければ、最後のグループも4文字を産み、使われないスロットは = のパッドで印づけられます。入力1バイトはパッド2つ付きの4文字に、入力2バイトはパッド1つ付きの4文字になります。つまり n バイトの正確なエンコード長は:
size_t encoded_chars(size_t n) {
return ((n + 2) / 3) * 4;
}
1000バイトなら1336文字、1バイトなら4、0なら0です。そこから2つの調整が続きます。第一に、OpenSSLとMbed TLSはどちらもデータの後にNUL終端子を付けます(Mbed TLSはサイズを問い合わせたとき、その分の空間も予約する)。だからバッファは余分な1バイトを欲しがります:encoded_chars(n) + 1。第二に、行折り返しされた出力が欲しいなら、行ごとに改行を1つ足します。OpenSSLのストリーミングエンコーダーは入力48バイトごとに64文字行を出力するので、折り返し後の長さは encoded_chars(n) + (n + 47) / 48 です。1000バイトで検証します。1336文字に改行21個を足して1357 - エンコーダーが生むのはちょうどこれです。数式を1度関数として書いてどこでも使いましょう。「ちょうど収まる」と、午前3時のヒープ破損との差は、まさにそこにあるからです。
size_t b64_buffer_size(size_t in_len) {
return ((in_len + 2) / 3) * 4 + 1; /* 文字数 + NUL */
}
size_t b64_buffer_size_wrapped(size_t in_len) {
return ((in_len + 2) / 3) * 4 + (in_len + 47) / 48 + 1;
}
OpenSSL:ワンショットのブロックか、流れ続ける水栓か
OpenSSLのワンショット関数は作業馬で、その中で最も親切です:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
const char *text = "Mane";
unsigned char out[32];
int n = EVP_EncodeBlock(out, (const unsigned char *)text,
(int)strlen(text));
printf("len=%d str=%s\n", n, (char *)out);
return 0;
}
エンコードされた文字を out に書き、その後にNULを付け、NULを含まない長さを返します - だから %s で印刷するのは安全で、必要なら長さも手に入ります。出力バッファは encoded_chars(n) + 1 バイトを保持できる必要があります。処理すべきエラー経路はありません。エンコードは失敗しません。どんなバイトも合法な入力なので、この関数につまずく入力検証という概念もないからです。うまくいかない唯一の方法は、あなたが小さすぎるバッファを渡すことです。数学のセクションがその解毒剤です。
ストリーミング組は、データが大きく、あるいは断片として届くときのためのものです。EVP_EncodeUpdate は入力を48バイトブロックで処理し、完成したブロックごとに64文字と改行(65バイト)を書き、余りをコンテキストに保持します。次のデータか最終呼び出しが来るまで:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
if (ctx == NULL) {
return 1;
}
EVP_EncodeInit(ctx);
unsigned char in[1000];
for (int i = 0; i < 1000; i++) {
in[i] = (unsigned char)(i % 251);
}
unsigned char out[1400]; /* 1336文字 + 改行21 + 余裕 */
int outl = 0;
int total = 0;
EVP_EncodeUpdate(ctx, out + total, &outl, in, 1000);
total += outl;
EVP_EncodeFinal(ctx, out + total, &outl);
total += outl;
int nl = 0;
for (int i = 0; i < total; i++) {
if (out[i] == '\n') nl++;
}
printf("encoded 1000 bytes into %d chars, %d newlines\n",
total, nl);
EVP_ENCODE_CTX_free(ctx);
return 0;
}
そのプログラムの出力は、改行21個を含む1357バイトです - 数学のセクションの数式が、現実にしたもの。実用的な注記が2つあります。バージョンに関する注記:OpenSSL 1.1.0(2016年)からコンテキスト型が不透明になったので、EVP_ENCODE_CTX_new() で割り当て、EVP_ENCODE_CTX_free() で解放してください。1.0.2以前のターゲットのチュートリアルに見かける古いスタックパターン EVP_ENCODE_CTX ctx; は、OpenSSL 3.xを含む現行のヘッダーに対してはコンパイルできません。そして設計に関する注記:EVP_EncodeUpdate が出力するのは完成した48バイトブロックのみなので、最もきれいなチャンクパイプラインは48の倍数を渡します - そうすれば関数が書く行はすべて完成した行になり、尾部の折り返し方を決めるのは EVP_EncodeFinal だけになります。入力がどんなサイズで届いても(ネットワーク読み込みなど)、コンテキストは整列を代わりに処理してくれます。48の倍数という習慣は、単に出力を予測可能にするためのものなのです。
Mbed TLS:聞く、エンコードする、文字列を手に入れる
Mbed TLSのエンコードは、このグループの中で最もクリーンな契約を持っていて、NULLの宛先で呼び出せるサイズクエリを中心に作られています:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <mbedtls/base64.h>
int main(void) {
const char *text = "Mane";
size_t slen = strlen(text);
size_t needed = 0;
int rc = mbedtls_base64_encode(NULL, 0, &needed,
(const unsigned char *)text, slen);
if (rc != MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) {
printf("size query failed: %d\n", rc);
return 1;
}
printf("needs %zu bytes\n", needed);
unsigned char *out = malloc(needed);
size_t olen = 0;
rc = mbedtls_base64_encode(out, needed, &olen,
(const unsigned char *)text, slen);
if (rc != 0) {
printf("encode failed: %d\n", rc);
free(out);
return 1;
}
printf("olen=%zu str=%s\n", olen, out);
free(out);
return 0;
}
詳細をよく読みましょう。友好的なAPIのマスタークラスだからです。サイズクエリは、needed を「エンコードされた文字数にNULのための1」で報告します - 「Mane」なら8に1を足して9 - そして、「バッファが小さすぎる」コード(MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL、すなわち -0x002A)でそれを示します。NULLの宛先は定義上、小さすぎるからです。本番の呼び出しが文字とNULを書くと、*olen は8で戻ってきます - 終端子を含めない長さ - だからバッファはもう印刷可能なC文字列です。1バイト足りないバッファを渡せば、同じ「小さすぎる」コードとともに、必要サイズが *olen に入っていて戻ってきます。つまり失敗は、あなたがどれだけ足りなかったかを正確に教えてくれるのです。もう1つ注記:このライブラリはテーブル参照を一定時間ヘルパー経由で行いますが、これはほとんどのエンコーダーでは見られない小さな気遣いです。
APR-UtilとGLib:もう2つ
APR-Utilのエンコーダーは、int 長さを持つ素の関数の組です:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <apr-1.0/apr_base64.h>
int main(void) {
const char *text = "Mane";
int needed = apr_base64_encode_len((int)strlen(text));
char *out = malloc((size_t)needed);
int n = apr_base64_encode(out, text, (int)strlen(text));
printf("n=%d (includes NUL) str=%s\n", n, out);
free(out);
return 0;
}
ここで apr_base64_encode_len() も返り値もNULを数えるので、n は文字数より1大きくなります - OpenSSLやMbed TLSとの管理の違いで、1つずれるバグを1つではないコードベースに生んだものです。同じ32ビットの長さ制限も適用されます。2 GBに近い値やそれを超える値には、これは道具になりません。apr_base64_encode_binary() もあります。EBCDICマシンでは入力のEBCDIC-ASCII変換をスキップします - その変換が起きるはずのメインフレームで、それ以外のどこでも何もしない違いです。その組、バイナリアリアント、対応するデコード関数、実はapr-utilが晒すbase64の顔の全部です:プール割り当て版もストリーミング版もないので、上記のmallocパターンが唯一のパターンです。
GLibのエンコーダーはヒープ割り当てスタイルです - NUL終端の文字列と、1つの義務がもらえます:
#include <stdio.h>
#include <glib.h>
int main(void) {
const char *text = "Mane";
gchar *enc = g_base64_encode((const guchar *)text, strlen(text));
printf("%s\n", enc);
g_free(enc);
return 0;
}
折り返しなし、NUL終端、g_free で解放してください - 静的アナライザーにそれを教えているのは、プロトタイプの G_GNUC_MALLOC 注釈です。改行が本当に欲しいときには、増分組が道具になります:g_base64_encode_step() は状態のintと break_lines フラグを受け取り、何バイトの出力を書いたかを教えてくれ、g_base64_encode_close() が最後の部分的なグループを仕上げます。これはOpenSSLのストリーミング組と同じ状態機械の形で、GLibのパラメータスタイルのものです。
手作りのURL安全Base64
上記の4つのライブラリはすべて標準文字表を話します。A-Z、a-z、0-9、プラス、スラッシュ。しかしウェブは、RFC 4648のセクション5の第2の方言、base64url と呼ばれるものをますます話しています。同じエンコーディングを、+ を - に、/ を _ に交換し、長さが分かるときは末尾の = パッドを落としたもの。JSON Web Token、OAuthの状態パラメータ、無数のAPI IDがこれを使います。+ と / はURLではどちらも危険物ですが、- と _ は予約なし文字で、すんなり通り抜けられるからです。Cライブラリのどれがこの方言をネイティブで出力することもないので、自分で作ります - しかも2つの小さな変更だけです。標準文字表のエンコーダーはもう持っているからです:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
static void to_base64url(const char *std_b64, char *out, size_t out_cap) {
size_t i = 0;
for (const char *p = std_b64; *p && *p != '='; p++) {
char c = *p;
if (c == '+') c = '-';
if (c == '/') c = '_';
out[i++] = c;
}
out[i] = '\0'; /* パディングは意図的に落としてある */
}
使い方は2ステップです - 標準でエンコードし、それから変換します:
unsigned char enc[32];
EVP_EncodeBlock(enc, (const unsigned char *)"hi>there", 8);
char url_safe[32];
to_base64url((const char *)enc, url_safe, sizeof(url_safe));
printf("%s\n", url_safe); /* aGk-dGhlcmU */
2つの注意。ループは最初の = で止まります。パッドを落とすのはそれです - 「修正」しないでください。パッドを落とすのがポイントです(必要なら、受信側は長さから再追加できます)。out のサイズは、より少なくでなく、完全なエンコード長で取ってください:変換はパッドまで文字通り1文字ずつなので、標準形式のためにすでに割り当てた容量がちょうど正しい大きさです。相互運用のための正直な注記を1つ。データがたまたま + や / に対応するバイトを含まないなら、標準形式とURL安全形式は同一になり、取り違えに誰も文句を言いません - バグが顔を出すのは、データがついに1つでも含むときです。方言はデータのプロパティとしてではなく、チャネル(URL、トークン)のプロパティとして扱ってください。
テキストと文字セット:UTF-8はただのバイト
Cの初心者を驚かせる質問:アクセント付きのテキスト、絵文字、CJKの文字はどうなるのか?答えは、この記事の中で最も解放的な事実です - 何も起きる必要はないのです。Base64はバイト上で動きます。Cはバイトの言語です。テキストがUTF-8なら(2026年なら、たぶんそうです)、「café」のUTF-8エンコーディングは5バイト - 63 61 66 c3 a9 - で、Base64はそれらの5バイトを、他の5バイトとまったく同じようにエンコードし、Y2Fmw6k= を生みます。文字セットパラメータも、BOMも、変換ステップも、ライブラリ呼び出しも、不要です。コーデックは、バイトの意味を知りもせず、気にも止めません。設計はまさにそれそのものです。
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
const char *utf8 = "caf\303\251"; /* UTF-8のcafé */
unsigned char enc[32];
int n = EVP_EncodeBlock(enc, (const unsigned char *)utf8,
(int)strlen(utf8));
printf("%.*s\n", n, (char *)enc); /* Y2Fmw6k= */
return 0;
}
このセクションの端には2つの罠がいます。第一は wchar_t です。データがワイド文字で届いたなら、エンコードの前にまずバイト列へ変換しなければなりません(Linuxでは、wcstombs() やロケールの仕組みを通じてUTF-8へ)。wchar_t 配列のBase64は、テキストのエンコーディングではなく内部表現のエンコーディングで、プラットフォームごとに違ってしまいます。第二はソースエンコーディングです。Cファイルの中の文字列リテラルは、ソースファイルのエンコーディングでエンコードされています(現代的なプロジェクトではUTF-8)。だから "café" を直接書いても、ファイルが本当にUTF-8で、コンパイラにそれが伝えられていれば(現代的なツールチェーンではデフォルトでそうなります)動きます。送りたいバイトをエンコードし、バイトの意味は受信側に任せましょう。
画像:バッファから文字列へ
ウェブCで最も一般的な「実戦の」エンコード仕事:バイナリファイル - JPEG、PNG、アイコン - がテキストチャネルを通って旅しないといけないので、Base64文字列になります。レシピは、ファイルをバッファに読み、数式で出力のサイズを決め、エンコードして、次に進む、それだけです。ファイルを読む半分は、注意を払う価値があります。Cプログラムが実際に壊れるのがそこだからです:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/evp.h>
int main(void) {
FILE *f = fopen("photo.png", "rb");
if (f == NULL) {
return 1;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
unsigned char *data = malloc((size_t)size);
size_t got = fread(data, 1, (size_t)size, f);
fclose(f);
size_t out_cap = ((got + 2) / 3) * 4 + 1;
unsigned char *enc = malloc(out_cap);
int n = EVP_EncodeBlock(enc, data, (int)got);
printf("png of %zu bytes becomes %d base64 chars\n", got, n);
free(data);
free(enc);
return 0;
}
注記:バイナリの読み取りには rb - あらゆるプラットフォームで譲れない。テキストモードはバイトを変換して got を変えてしまうことがあるからです。サイズ出しには fseek/ftell の組(シークできないパイプやソケットでは、代わりに伸びるバッファに読み取って)。エンコードには size ではなく got を。短読は現実に起こり得るからです。1 MBの画像は約1.33 MBのテキストになります - 税金は前払いで請求され、JSONに入ったbase64画像ペイロードを見たとき、ほんとうのファイルアップロードの方が安価ではなかったかを立ち止まって問うべき理由が、ここにあります。
ファイルと.b64の習慣
画像仕事の裏の顔:ファイルのBase64形式をディスクに書き出さなければなりません - .b64 のサイドカー、テキスト安全なストレージへのバイナリのバックアップ、メーラー用の添付ファイル。同じ計算、書き手が違うだけ。採り入れる価値がある習慣は、受信側が期待する長さで明示的な改行つきテキスト出力を書くことです。メールなら76、PEM型の消費者なら64、受信が自分自身のコードなら改行なし:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
FILE *f = fopen("data.bin", "rb");
if (f == NULL) {
return 1;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
unsigned char *data = malloc((size_t)size);
size_t got = fread(data, 1, (size_t)size, f);
fclose(f);
unsigned char *enc = malloc(((got + 2) / 3) * 4 + 1);
int n = EVP_EncodeBlock(enc, data, (int)got);
FILE *out = fopen("data.b64", "w");
for (int i = 0; i < n; i += 76) {
int chunk = i + 76 < n ? i + 76 : n;
fwrite(enc + i, 1, (size_t)(chunk - i), out);
fputc('\n', out);
}
fclose(out);
free(data);
free(enc);
return 0;
}
ループは76文字行と、最後の短い行を書きます。空白をスキップするデコーダー(まともなものはすべてそうします)は行の長さにまったく気にしません。だから相談相手は受信側で、あなたの好みではないのです。プラットフォームの改行が欲しいなら書き込み側で出力ファイルをテキストモード(w)にし、受信が文字を厳密に数えるなら wb にします - そして数えるなら、それはあなたが約束したものとぴったりのものが欲しがるのです:76文字と改行1つ、それ以外は何も。.b64 ファイルをフォーマットにしているのは、バイトではなく、その約束です。
data URI:本格的な埋め込み
data URI(RFC 2397)は、デコード記事のお馴染みの訪問者の裏側です。data:image/png;base64,... を受けるのではなく、あなたがそれを作ります。形は data:、メディアタイプ、;base64、カンマ、ペイロード - Cで作るのは、エンコードのあとに snprintf を1回するだけです:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
/* 8バイトのPNGシグネチャ */
const unsigned char png_sig[8] =
{ 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' };
unsigned char enc[32];
int n = EVP_EncodeBlock(enc, png_sig, 8);
char uri[96];
snprintf(uri, sizeof(uri), "data:image/png;base64,%.*s",
n, (char *)enc);
printf("%s\n", uri);
return 0;
}
設計の注記が3つあります。URIに入れたメディアタイプは、あなたが責任を持つ主張です。実ファイルのマジックバイトを先にスニフしてください。さもないとJPEGを載せた data:image/png が、消費者をそれぞれ別の方法で混乱させます。;base64 フラグは、ペイロードがBase64のとき必須です。それを省略すると、ペイロードはパーセントエンコードされたテキストでなければならず、それはまったく別のフォーマットです。そしてRFC自身のガイドラインは、data URIは短い値用だということ。HTMLページに5 MBのロゴをインラインで埋め込むのは動きますが、それは本当の資産URLで直せる設計の悪臭です。クライアントがフォームデータと同じリクエストにアバターを載せたいJSON APIで、同じ構成はしょっちゅう登場します - エンコード、先頭に付け、送る。
HTTPとJSON:生き抜くペイロード
Cでエンコードする現代的な最大の理由はJSONです。JSON文字列はエスケープルールを持つ文字の並びで、生のバイトはそこに収まりません:文字列リテラルの真ん中のNULはCの問題で、JSON文字列の中の実際の改行は不正なJSONで、任意のバイトには定義されたエスケープの物語が必要です。Base64は、JSONが決してエスケープする必要のない文字だけを生み出すことで、この問題全体を横切ります - 文字表の64文字に、標準方言なら = が加わり、クォートでもバックスラッシュでもありません。バイナリは文字列として入っていって、向こう側では入ったのまま出てくる:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
unsigned char enc[64];
int n = EVP_EncodeBlock(enc, (const unsigned char *)"hello", 5);
char json[160];
snprintf(json, sizeof(json),
"{\"avatar\": \"%.*s\"}", n, (char *)enc);
printf("%s\n", json);
return 0;
}
これには {"avatar": "aGVsbG8="} と印刷されます - 完成した、有効なJSONオブジェクトで、エスケープの機構は何も関わらず、%.*s の精度が、NUL終端しないエンコーダーに切り替えたときでも長さを正確に保ちます。正直なコストはサイズです。JSON文字列として送る1バイトは、3分の4倍の空間を消費し、さらにフィールド名とクォートまで請求されます。だから10 KBのバイナリは、JSONの中で13.3 KBの文字列になります。ときどき現れる小さなブロブ(アイコン、サムネイル、署名、トークン)なら、それは妥当な価格です。500 MBのアップロードなら、それはあなたが後悔するアーキテクチャで、本当のファイルアップロードがその仕事の道具です。書いておく価値があるのは1行だけ:標準文字表の + と / はJSON文字列の中なら安全ですが、同じ文字列が後でURLクエリを旅するなら、そうではありません - それはURL安全セクションの仕事です。
JWT: 3つの部分、1つの文字表
Cにおけるbase64urlの看板消費者はJSON Web Tokenです。RFC 7519のコンパクトJWTは、ドットで結ばれたbase64urlエンコードの3つの部分 - ヘッダー、ペイロード、署名 - から成り、1つを作るのは楽しい練習です。すべての部品がすでに持っている関数だからです。標準でエンコード、URL安全に変換、署名、繰り返す。OpenSSLのHMACで作るHS256トークンがこれです:
#include <stdio.h>
#include <string.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>
static void to_base64url(const char *std_b64, char *out, size_t out_cap) {
size_t i = 0;
for (const char *p = std_b64; *p && *p != '='; p++) {
char c = *p;
if (c == '+') c = '-';
if (c == '/') c = '_';
out[i++] = c;
}
out[i] = '\0';
}
int main(void) {
const char *secret = "my-hmac-secret-key";
const char *header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
const char *payload = "{\"sub\":\"114365\",\"name\":\"Alice\"}";
unsigned char hb[64], pb[64];
EVP_EncodeBlock(hb, (const unsigned char *)header,
(int)strlen(header));
EVP_EncodeBlock(pb, (const unsigned char *)payload,
(int)strlen(payload));
char hu[64], pu[64];
to_base64url((const char *)hb, hu, sizeof(hu));
to_base64url((const char *)pb, pu, sizeof(pu));
char signing_input[256];
snprintf(signing_input, sizeof(signing_input), "%s.%s", hu, pu);
unsigned char mac[EVP_MAX_MD_SIZE];
unsigned int mac_len = 0;
HMAC(EVP_sha256(), secret, (int)strlen(secret),
(const unsigned char *)signing_input,
(size_t)strlen(signing_input), mac, &mac_len);
unsigned char mb[64];
EVP_EncodeBlock(mb, mac, (int)mac_len);
char mu[128];
to_base64url((const char *)mb, mu, sizeof(mu));
printf("%s.%s.%s\n", hu, pu, mu);
return 0;
}
構造が教えてくれることが2つあります。第一に、署名の入りはドットで結ばれた2つのURL安全な部分で、受信者が見るバイトとまったく同じです。だからbase64urlへの変換は、署名の後にではなく前に起きたいのです。標準文字表の形式に署名すると、受信者の検証が失敗します。これはコンパイルも実行もして、鍵の不一致に見えるバグです。第二に、ヘッダーとペイロードは、Base64のラッパーの中の素のJSONです。誰にでも読め、それが設計です。トークンは署名されたメモで、封印された封筒ではありません - だから、傍受するユーザーが読んでも構わないと心に決められないものは入れず、パスワードをJWTペイロードに入れるのは、絶対にやめましょう。「エンコードされているから」という理由で。この仕事のBase64部分は小さくて退屈ですが、JWT実装に贈れる最高の賛辞なのです。
HTTP Basic認証:トークンを組む
最古の認証ヘッダーは、最もシンプルなBase64の仕事でもあります:username:password を標準文字表でエンコードし、言葉 Basic の後に続ける。Cで組むのは2行で、唯一の微妙な点は、パスワードにコロンが含まれる可能性があることです(そして受信側の分割は最初の1つで行うべきです):
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
const char *user = "alice";
const char *pass = "s3cr3t";
char creds[128];
snprintf(creds, sizeof(creds), "%s:%s", user, pass);
unsigned char enc[160];
int n = EVP_EncodeBlock(enc, (const unsigned char *)creds,
(int)strlen(creds));
printf("Authorization: Basic %.*s\n", n, (char *)enc);
return 0;
}
これには Authorization: Basic YWxpY2U6czNjcjN0 と印刷されます。RFCの警告は送信側にも適用されます:これは保護ではなくエンコーディングです。素のHTTP接続では、資格情報はワイヤ上の誰にでも1回の base64 -d で届き、だからBasic認証はHTTPS専用の習慣です。(現代的な代替 - ベアラートークン、mTLS - はどれも同じ機械を再利用します:文字列を組み、エンコードし、ヘッダーに入れる。Base64は、プロトコルにヘッダーがあったときから、HTTPがテキストヘッダーを通じて構造化データを密かに運ぶ方法だったのです。)
メールとPEM:ラッピングが生きている場所
メールこそが、行折り返しが存在する理由そのものです。SMTPは行長を制限するので、MIMEはエンコードされた行を76文字までに制限し(祖先であるPEMは64文字)、すべてのメールシステムが30年にわたりその上限を遵守してきました。あなたのCプログラムがメール本文や添付ファイルのためにBase64を生むなら、折り返しは任意の化粧ではありません - 折り返されない200 KBの行は、メールインフラの一部分に拒否され、あるいは壊されます。OpenSSLのストリーミングエンコーダーは、折り返された出力を無料で与えてくれます(64文字という遺産の長さで)。ちょうど76が要るとき、ワンショットの結果を折り返すのは5行のループです:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
unsigned char enc[64];
int n = EVP_EncodeBlock(enc, (const unsigned char *)"hello world", 11);
for (int i = 0; i < n; i += 76) {
int chunk = i + 76 < n ? i + 76 : n;
printf("%.*s\r\n", chunk, (char *)enc + i);
}
return 0;
}
\r\n に注意してください:メールはCRLFの行終わりを欲しがいます。そしてエンコードされたテキストが複数のMIMEパートの1つなら、base64ブロックの周囲のすべてが同じルールに従います - 行長、CRLFの終わりで、例外なし。PEMファイル(鍵と証明書の多くのフォーマット)は、-----BEGIN と -----END マーカーのあいだの64文字行で同じアイデアを使い、OpenSSLのツールは鍵を再保存するとき、そのアーマーが見られることを期待します - だからあなたのプログラムがPEMに触れるなら、64で折り返し、ラベルを残してください。他のどこでも - JSON、URL、API、データベース - RFCのルールが適用され、折り返しはしません。
設定やカラムに値を忍ばせる
静かなユースケース:テキストフォーマットを壊してしまう値は、Base64に詰めることで壊さなくなります。セミコロン入りのデータベースDSN、クォート入りのパスワード、改行付きのトークン - オペレーション担当者が1回エンコードすれば、設定ファイルは厄介な文字をもう見ません:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
const char *dsn = "pg:host=db;password=qu\"ote";
unsigned char enc[128];
int n = EVP_EncodeBlock(enc, (const unsigned char *)dsn,
(int)strlen(dsn));
printf("DB_DSN_B64=%.*s\n", n, (char *)enc);
return 0;
}
プログラムは、.env ファイルに貼り付けるための正確な行を印刷し、後でそれを読むCコードは getenv とデコードです。正直な注意が3つ、すべては「これが何ではないか」についてです。暗号化ではありません:設定ファイルを読める誰にでも、1回の呼び出しで値をデコードできるので、シークレットをBase64に詰めて保護されていると呼ぶのはやめましょう。エスケープでもありません:フォーマットが構造の保持を必要とするなら、本当のエンコーディング(URLにはパーセントエンコーディング、JSONにはJSONエスケープ)が正しい道具で、Base64は、そのフォーマットが表現できない値 - つまりバイナリの値 - 用のものです。そしてサイズのコストがかかります:データベースのTEXTカラムにBase64として保存された値は、元の約33パーセント大きな空間を占めます。トークンなら問題のない数字で、ファイルカラムなら現実的な数字です(BLOBカラムがまさにそのためにあります)。
シェルからエンコードする
cc の呼び出しに手を伸ばす前に、標準の2つのツールはどちらもエンコードし、しかも速いということを思い出してください。coreutilsが汎用の楽器です:base64 はデフォルトで76文字の折り返しでエンコードし、-w が列を変え、-w 0 が折り返しを完全に無効にします:
base64 photo.png > photo.b64
base64 -w 0 photo.png > photo-oneline.b64
cat note.txt | base64 -w 0
OpenSSLのツールは、同じ仕事をTLS系譜のパッケージングでやります:openssl base64(openssl enc -base64 の友好的な別名)は64文字で折り返し、-A が1行に切り替えます:
openssl base64 < photo.png > photo.b64
openssl base64 -A < photo.png > photo-oneline.b64
なぜ折り返しの違いを気にするのか?下流のパーサーが文字を数える場合、2つのツールのデフォルト出力は交換可能ではないからです。1行76と1行64はファイルの中で見える違いで、空白を剥ぐパーサーは気にしませんが、行長を検証するパーサーは絶対に気にします。あなたのCプログラムが生産者でシェルが消費者なら(あるいはその逆)、まず折り返しで合意してください。BSD風味のシステム向けの方言の注記を1つ:そこのデコードフラグは歴史的に -D でした。古いmacOSリリースはそれをまだ覚えています。エンコード側はどこでも base64 で、これは本来このセクションが扱う唯一の方向です。
大きなものをストリーミングで
数ギガバイトのファイルを1回の malloc でエンコードするのは、あなたが必要としたことのないメモリの問題です。ストリーミング経路はちょうどこれのために存在し、OpenSSLのブロック規律がコードをほとんど自明にします:ファイルが与えてくれるだけ EVP_EncodeUpdate に渡し、各部分の48バイトブロックの余りをコンテキストに保持させ、各ブロックの出力65バイトをそのまま目的ファイルに書き出す。ピークメモリはあなたの2つのバッファ - 数十キロバイト - で、ファイルがどんなに大きくても:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
if (ctx == NULL) {
return 1;
}
EVP_EncodeInit(ctx);
FILE *in = fopen("video.mp4", "rb");
FILE *out = fopen("video.b64", "w");
if (in == NULL || out == NULL) {
return 1;
}
char inbuf[48 * 1024]; /* 48の倍数:きれいな行 */
unsigned char outbuf[1024 * 65 + 65]; /* 48バイトブロックごとに出力65バイト、余裕付き */
size_t got;
while ((got = fread(inbuf, 1, sizeof(inbuf), in)) > 0) {
int outl = 0;
EVP_EncodeUpdate(ctx, outbuf, &outl,
(const unsigned char *)inbuf, (int)got);
fwrite(outbuf, 1, (size_t)outl, out);
}
unsigned char tail[66];
int outl = 0;
EVP_EncodeFinal(ctx, tail, &outl);
fwrite(tail, 1, (size_t)outl, out);
EVP_ENCODE_CTX_free(ctx);
fclose(in);
fclose(out);
return 0;
}
そのループの中で仕事をしている設計詳細が2つあります。入力バッファは48バイトの倍数なので、すべての呼び出しがエンコーダーに完成ブロックを渡し、書く行はすべて完成した64文字行になります。最終呼び出しが本当の尾部を折り返します。入力が任意のサイズで来ても(ソケット、遅いディスク)、コンテキストは不整列を正しく吸収してくれます - 48の倍数の選択は出力の予測可能性についてのもので、正しさについてはありません。2つ目の詳細は出力バッファのサイズです:入力48バイトごとに65バイトと、最終ブロックの余裕(66)。これは数学セクションの折り返し数式をチャンクごとに適用したものです。進捗表示は1行 - ファイルサイズから計算した総量に対して out に書き出したバイトを数える - そしてエンコードはデータを大きくするので、出力ファイルは入力より約33パーセント大きく着地します:ディスクには前もって請求させてください。
エンコードの鋭い刃
罠をすべて集めてみました。全部がC型のものです:
- 両方向に1つずれる。 OpenSSLはNULを含まない長さを返し、Mbed TLSのサイズクエリはNULの空間を含み、APRは長さにNULを数えます。3つのライブラリ、3つの管理規則。バッファサイズの関数を1度書いて(数学のセクション)、呼び出し場所で頭の中で計算するのはやめましょう。
- NULは払わなければならない1バイト。この記事のすべてのエンコーダーは、終端のために出力に余分な1バイトを欲しがります。あなたが手書きするバッファ追加も同じです。
encoded_chars(n)にちょうど合わせたバッファは、printf("%s")が動いてほしいと誰かが思った瞬間に1バイト足りなくなります。 - エンコードは失敗しないから、オーバーフローを許してはならない。小さすぎるバッファを拾うエラーコードはありません - エンコーダーは嬉々として末端の先へ書き込みます。CにおけるBase64エンコーディングの失敗モデルは、まったくあなたのものです:サイズを正しく決めなければ、診断なしにメモリを壊します。
- 二重エンコードは定番の沈黙バグ。すでにBase64の値をエンコーダーにもう一度通すと、完璧に有効なBase64文字列が生まれますが、それはデータではなくBase64文字列にデコードされます。症状 - 「デコードはされる、でも違うものに」 - は半天かかります。値が「事前エンコード」で届いたら、生データと仮定する前に、長さが4の倍数で、文字表の文字しか含まれないかを検証してください。エンコード済みなら、エンコードをスキップします。
- URLの中のプラス記号。標準文字表の出力をクエリ文字列に入れたら、あなたのサーバーがフォームをパースするころには、
+はスペースに変わって届きます - パーセント/フォームエンコーディングでは+はスペースです。URLを旅するトークンとIDは、URL安全な方言を望みます。以上です。 - パイプの反対側にあるテキストモード。バイナリファイルをテキストモードで読むと、バイトを変換して(一部のプラットフォームで)長さをずらす可能性があります。折り返されたBase64を誤った行終わりの慣例で書くと、文字を数える受信者が壊れます。バイナリ入力には
rb、仕様が要求するところでは明示的な\r\nまたは\n、そしてCランタイムにあなたの行終わりを黙って決めるのを決して許さないでください。 - サイズ計算のintオーバーフロー。
((n + 2) / 3) * 4のint演算は、おおよそ1.5 GBを超える入力でオーバーフローし、小さな正の「必要サイズ」とヒープの粉砕を生みます。計算はsize_t(またはuint64_t)でやってください。APRのintベースAPIが工学的に払拭できない2 GBの上限を持つのも、そのためです。 - 受信者が期待していない場所での折り返し。RFC 4648はこう言います:周囲の仕様が要求しない限り、改行は追加しない。JSON文字列値の中の改行は不正で、URLの中では別のリクエストです。メールのために折り返し、PEMのために折り返し、そこ以外ではどこでも折り返しません。
短いチェックリスト
バッファサイズは推測ではなく数式で計算し、コードベース全体でサイズの関数は1つにしてください。バッファがNUL終端であっても、(ポインタ、長さ) は一緒に持ちましょう。長さが契約で、NULは便宜だからです。方言はチャネルで選びます:JSONと本文には標準を、URLとトークンにはURL安全を、メールとアーマーには折り返しを、他のどこでもなしを。data URIでMIMEタイプを主張する前に、マジックバイトを検証してください。Base64を暗号化として、パーセントエンコーディングの代わりとして、シークレットを隠す場所として使うのは、絶対にやめましょう - それは箱で、錠前ではないからです。そしてデータが大きいときは、ストリーミングで:ブロックベースのエンコーダーはまさにそれのために設計され、一定のメモリこそがすべてなのです。
歴史:パッキングが標準化された道
エンコーダーの歴史は、行長の物語です。最初のBase64は、1990年代初頭のCプログラムでした。Privacy-Enhanced Mail(RFC 1421、1993年)は7ビットメールを通じてバイナリを運ぶ必要があり、その著者たちは1文字あたり6ビット、64文字行を選びました - 64はSMTPの行長許容の残骸で、Cコードはテーブル参照を1つまた1つと繰り返してパッキングをやっていました。MIMEが同じ文字表をウェブのために標準化したとき(RFC 1521、1993年、RFC 2045、1996年)、行を76文字に緩め、世界は2つの習慣 - 64と76 - を持ちました。どちらも「それこそが」Base64の行長だと主張していました。エンコーダーは合わせました:OpenSSLのストリーミング経路は64を保ち(そのPEMの遺産)、coreutilsのツールは76を選び(そのMIMEの遺産)、同じマシンの2つのツールは、改行がどこに行くべきかについて今も意見が割れています。標準は2006年にようやく立場を決めました:RFC 4648は、参照している仕様が明確に指示しない限り、実装は改行をまったく追加してはならないと言いました。この記事のすべてのライブラリがデフォルトで折り返しなしの出力にするのはそのためで、折り返しが今やメールとアーマーのためのオプトイン機能なのはそのためです。文字表そのもの、パディングルール、「パッドビットはゼロでなければならない」という正規性ルールは、それらの古いPEMとMIMEのRFCに遡り、4648はそれらをファミリーの正規ルールとして再宣言しています。そしてRFCのセクション11はリファレンス実装を指します - ISO C99のプログラムで、コード自身は「手続き上の理由からこのRFCに含めることはできなかった」ために外部にホストされています。このフォーマットにおいて、Cは二級市民ではない、というもう一つの提醒です。Cの標準ライブラリは、その部分を、決して追いつきませんでした:C89はこれらすべての前に、1990年に凍結し、2024年のC23もまだBase64関数なしで出荷されています。つまり、あなたがリンクするライブラリが標準であり、それらの選択は小さくとも実在する設計判断です - この記事が扱ってきたのはまさにそれでした。
ちょっと変わった豆知識
ただ楽しいだけ、という事実をいくつか。全部がCのパッキング側についてのものです:
- 「64」は基数です:出力文字はそれぞれ6ビットで、2の6乗は64。このフォーマットは、Cが整数を名付けるのと同じ方法で文字表を名付けています - 数字が実際に何であるかによって。
- OpenSSLのストリーミングエンコーダーの48バイトブロックは、恣意的な管理ではありません:入力48バイトはちょうど3のグループ16個で、出力64文字はちょうど4のグループ16個です。両方の数字は16の倍数で、これはハードウェアとキャッシュラインを喜ばせる種類の丸さです - どうせならコードを読む人間を喜ばせるために。
- 入力1バイトは4文字にエンコードされ、その2つが
=です。考えられる最小の空でないペイロードは、パディング50パーセントです - このフォーマットの中で最も無駄なエンコーディングで、テストスイートがすべて使うのは、それだけ書き間違えやすいからです。 - Mbed TLSは、この記事の中で参照を一定時間で行う唯一のエンコーダーです。組み込みの暗号を書く人々は、暗号ででもないコードであっても、可変時間のテーブル参照を信用しないからです。パラノイアは移るのです。
- 正規エンコーディングのルール - 使われないパッドビットはゼロでなければならない - は、それを破ると2つの異なる文字列が同じバイトにデコードでき、すべての「この文字列はあのファイルのエンコーディングか?」というチェックを壊すことを知るまでは、自明に聞こえます。あなたのエンコーダーはすべて遵守しています。だからbase64はハッシュ安定な表現で、コンテンツストアのファイル名の代わりになれるのです。
- OpenSSLの
EVP_EncodeBlockは、返り値、自分の出力、NUL終端がすべて一致する、珍しいC関数の1つです:n文字とNULを書き、nを返します。オフバイワンで有名な言語において、それは平和の瞬間です。 - APR-Utilは、ここにいてEBCDICの意味を問う唯一のエンコーダーです。Apacheは、文字の順番がASCIIと違うマシンでまだ動いているからです。そんなマシンでは、文字列を「エンコード」することは、まず黙ってその文字表を並べ替えることを含みます。
- 空の入力は、すべてのライブラリで、パッドも改行もなく、空の文字列にエンコードされます。このフォーマットの単位元で、4つすべてに揃って正しく現れ、あなたが書くことになる最も安いユニットテストになります。
デコーダー側へ裏返して
さて、これがパッキング側のすべてです。計算、4つのエンコーダー、方言たち、そしてバイトが行く場所。これが仕事の静かな半分です。エンコードには不正な入力がなく、あなたに反論するデコーダーもいないからです。もう一方の方向 - 外の世界のBase64と出会い、バイトを取り戻す - が、痛みが集中する場所です:ゼロ埋めの尾部、沈黙する切断、厳格と寛容の文字表、そして末尾の改行を食べるコマンドライン。CでのBase64デコードは、このページからリンクされている関連記事で詳しく扱われ、この記事の自然な相棒です:エンコーダーが箱を書き、デコーダーがそれを開き、2つのあいだには、Cプログラムが会うことになるすべてのBase64の仕事があります。
最終更新: 2026-10-10
関連記事: C での Base64 デコード:完全ガイド