0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[RFC3548] データを文字で扱うBaseエンコーディングの仕組みを理解しよう!

Posted at

はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、Base16・Base32・Base64といったエンコーディングの仕組みを統一的に定義し、互換性やセキュリティを確保するための仕組みを提案しているRFC3548についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFCについてまとめています。

概要

Baseエンコーディング(Base64など)は、データを文字列として扱うための仕組みです。
特に、古いシステムや特定の環境では US-ASCII(英数字や記号のみを扱う文字コード)しか使えない場合があります。
そのような場面でバイナリデータを直接扱えないため、テキストとして表現できる形式に変換する方法が必要になります。
これが「Baseエンコーディング」です。

また、最新のシステムやアプリケーションでも、テキストエディタでデータを簡単に操作できるという利点から使われ続けています。

過去の問題点

これまで、アプリケーションやプロトコルごとに少しずつ異なるBaseエンコーディングの仕様が使われてきました。
その結果、同じ「Base64」と書いてあっても、以下の違いから互換性の問題が起きていました。

  • 改行(line-wrapping)の扱い
  • アルファベット以外の文字の取り扱い

目的

RFC3548の目的は、Base64などのBaseエンコーディングに関して共通のアルファベットやエンコードのルールを定めることです。
これにより、他の仕様や実装間での曖昧さを減らし、より高い相互運用性を実現することを目指しています。

実装

過去には、Baseエンコーディングの実装において、改行の入れ方やパディングの有無、不正な文字をどう扱うかなどが規格ごとに異なっていました。
そのため、同じBase64でも結果が一致しないことがありました。
RFC3548では、こうした違いに対して「改行は入れない」、「パディングは必ず付ける」、「不正な文字があればエラーにする」といった統一的なルールを定め、互換性を確保しています。

改行の扱い

MIME(電子メールで使われる規格)では、Base64エンコーディングを使う際に76文字ごとに改行を入れる決まりがあります。
これは、もともとPEMという別の規格から引き継がれたもので、SMTPという古いメール送信プロトコルの制限が背景にあります。
PEMでは64文字ごとに改行を入れていました。
しかし、Base64そのものには本来改行は不要です。
そのため、特別に指示がない限り、実装では改行を入れないことが推奨されています。

パディングの扱い

Base64では、元のデータの長さが3の倍数にならない場合に「=」を末尾に追加する「パディング」が使われます。
この「=」があることでデコード時に正しく元のデータを復元できます。
一部の用途ではパディングを省略しても問題ない場合もありますが、基本的には「=」を含めることが必須とされています。
仕様で明示的に不要とされている場合を除き、必ず「=」を追加するべきです。

アルファベット外の文字

Baseエンコーディングでは、限られた文字セット(アルファベット)を使ってデータを表現します。
しかし、何らかの理由で規定外の文字が混入することがあります。
例えば、データ破損、不正なデータの挿入、あるいは攻撃目的での利用などです。
このような文字を受け入れると、セキュリティ上のリスク(バッファオーバーフロー攻撃など)につながる可能性があります。
そのため、仕様で特別に許可されていない限り、アルファベット以外の文字を含むデータはエラーとして拒否する必要があります。
ただし、MIMEのように「余分な文字は無視する」という方針を採用する場合もあります。
その場合、改行コード(CRLF)や末尾の「=」の数が多すぎる場合も柔軟に解釈されることがあります。

アルファベットの選び方

Baseエンコーディングに使う文字は、用途によって異なる要件があります。
例えば、人間が目視する場合には「0」と「O」や「1」と「l(エル)」、「I(アイ)」が混同されやすいという問題があります。
そのため、Base32では「0」と「1」を使わない設計がされています。
また、ファイル名やURLに使う場合には「/」などの文字が問題を起こすことがありますし、検索ツールによっては「+」や「/」が単語の区切りと解釈されることもあります。
このように、すべての状況に完全に適したアルファベットは存在しないため、RFC3548では複数のアルファベットが定義され、それぞれの利用目的に応じて使い分けることが推奨されています。

仕様

Base64エンコーディングの仕組み

Base64は、任意のバイナリデータ(8ビットごとのデータ、オクテット)を文字列として表現する方法です。
人間が直接読めることを目的にはしていませんが、テキストとして扱える形式にすることで、メールやURLなどバイナリを扱えない環境でも安全にデータを転送できるようにしています。

US-ASCII文字セットのうち65種類の文字を使い、その中の64文字で6ビット分の情報を表現します。
残りの1文字「=」は、データの終端処理(パディング)に使われます。

エンコードの流れ

Base64の変換は、3バイト(24ビット)のデータを一度に処理するのが基本です。
流れを整理すると以下の通りです。

  1. 入力データを3バイト(=24ビット)ごとに区切る。
  2. その24ビットを6ビットずつに分割する(4つの6ビットのかたまりになる)。
  3. 各6ビットを整数値(0〜63)として解釈する。
  4. その値をBase64アルファベット表の文字に置き換える。

こうして、3バイトの入力は4文字の出力に変換されます。

Base64のアルファベット

以下の64文字が使われます。

A–Z → 0〜25
a–z → 26〜51
0–9 → 52〜61
+   → 62
/   → 63
=   → パディング用

つまり、0〜63の数値をこの文字表で置き換えることで、6ビット単位の情報を文字に変換できます。

パディングの処理

入力データの長さが3の倍数にならない場合は、そのままでは24ビットに満たない部分が出ます。
このとき、足りないビットをゼロで埋めた上で、末尾に「=」を追加して調整します。

具体的には以下のようになります。

  1. 入力が24ビット(3バイト)の倍数 → そのまま4文字に変換し、パディング不要
  2. 入力が8ビット(1バイト)だけ余る場合 → 2文字に変換し、末尾に「==」を追加
  3. 入力が16ビット(2バイト)余る場合 → 3文字に変換し、末尾に「=」を追加

これにより、出力は常に4文字単位になり、整合性が保たれます。

実際の例

文字列 "Man" をBase64に変換してみます。

  • ASCIIコード: M = 77, a = 97, n = 110

  • 2進数にすると:

    M   = 01001101
    a   = 01100001
    n   = 01101110
    
  • 3バイトをまとめて24ビットにすると以下のようになります。

    010011 010110 000101 101110
    
  • 6ビットごとに区切った値をアルファベット表に当てはめると、以下のように3バイトの入力が4文字のBase64文字列("TWFu")になります。

    19 = T, 22 = W, 5 = F, 46 = u
    

URLやファイル名で安全に使えるBase64エンコーディング

通常のBase64は「+」と「/」を文字として使います。
しかし、これらの文字はURLやファイル名の一部として使うと問題が起きることがあります。
例えば、「/」はパスの区切り記号として解釈されてしまい、「+」はURLのクエリ文字列でスペースを意味することがあります。
そのため、URLやファイル名に使うには不適切です。

そこで、同じ仕組みのBase64を使いつつ、問題のある文字を安全な文字に置き換えた方式が用意されています。

通常のBase64との違い

通常のBase64と仕組み自体は全く同じで、3バイトを4文字に変換する流れや「=」を使ったパディングのルールは変わりません。
違うのは、アルファベットの62番目と63番目に割り当てられる文字だけです。

  • 通常のBase64

    • 62番目
      • +
    • 63番目
      • /
  • URL/ファイル名安全版Base64

    • 62番目
      • -(マイナス記号)
    • 63番目
      • _(アンダースコア)

アルファベット表

A–Z → 0〜25
a–z → 26〜51
0–9 → 52〜61
-   → 62
_   → 63
=   → パディング用

利用のポイント

この形式は、URLやファイル名に含めても誤解されることがないため、WebアプリケーションやAPIでよく使われます。
ただし、通常のBase64と区別されるべきものであり、単に「base64」と呼ぶのではなく「URL-safe Base64」などと明示する必要があります。

文字列 "??" を通常のBase64とURL-safe Base64で表現して比較してみます。

import base64

data = b"??"

# 通常のBase64
print(base64.b64encode(data))  
# 出力: b"Pz8="

# URL-safe Base64
print(base64.urlsafe_b64encode(data))  
# 出力: b"Pz8="

この例では出力が同じですが、もし「+」と「/」が出てくるケースではURL-safe版では「-」、「\_」に置き換わります。

Base32エンコーディングの仕組み

Base32は、任意のバイナリデータを文字列に変換する仕組みです。
Base64と似ていますが、Base32では「大文字アルファベット」と「数字(2〜7)」の合計32文字を使います。
そのため、変換後の文字列は大文字・小文字の区別をしなくてもよい(大文字だけで構成される)という特徴があります。
人間が直接読むことは想定されていませんが、システム間でデータを扱いやすくする目的で使われます。

使われる文字

Base32で利用される文字は次の通りです。

A–Z → 0〜25
2–7 → 26〜31
=   → パディング用

つまり、AからZまでの26文字と、数字の2〜7の6文字で合計32文字を使います。
0(ゼロ)」や「1(イチ)」を使わないのは、文字の混同を避けるためです。

エンコードの流れ

Base32は40ビット(= 5バイト)ごとに処理するのが基本です。
流れを整理すると次のようになります。

  1. 入力データを5バイト(40ビット)ごとに区切る
  2. 40ビットを5ビットずつに分ける(8個のグループになる)
  3. 各5ビットを整数値(0〜31)として解釈する
  4. その値をアルファベット表の文字に置き換える

これによって、5バイトの入力が8文字の出力に変換されます。
ビットの並びは「上位ビットから順に処理する」と決まっており、1バイト目の最初のビットが最も重要なビットになります。

パディングの処理

入力データの長さが5の倍数にならない場合、残りの部分はゼロで埋めて処理し、末尾に「=」を追加して調整します。
これにより、出力は必ず8文字単位でそろいます。

具体的には以下のパターンがあります。

  1. 入力が40ビット(5バイト)の倍数 → そのまま8文字に変換、パディング不要
  2. 入力が8ビット(1バイト)だけ余る場合 → 2文字に変換し、末尾に「======」を追加
  3. 入力が16ビット(2バイト)余る場合 → 4文字に変換し、末尾に「====」を追加
  4. 入力が24ビット(3バイト)余る場合 → 5文字に変換し、末尾に「===」を追加
  5. 入力が32ビット(4バイト)余る場合 → 7文字に変換し、末尾に「=」を追加

実際の例

文字列 "f" をBase32に変換してみます。

  • "f" のASCIIコードは 102(2進数で 01100110

  • 8ビットしかないので、残りはゼロ埋めして40ビットにする

    01100110 00000000 00000000 00000000 00000000
    
  • 5ビットずつに分割すると以下のようになります。

    01100 11000 00000 00000 00000 00000 00000 00000
    
  • それぞれを整数に変換すると [12, 24, 0, 0, 0, 0, 0, 0]

  • Base32アルファベット表で置き換えると "MY======"

Base16エンコーディングの仕組み

Base16は、一般的に「16進数表記(Hex)」として知られている方法です。
任意のバイナリデータを、16種類の文字を使って表現します。
大文字・小文字の区別はなく、base16 または hex と呼ばれます。

1文字で4ビットを表現できるため、1バイト(8ビット)は常に2文字で表されます。
そのため、データの長さが変換後にちょうど2倍になるという特徴があります。

アルファベット

Base16で使う文字は以下の16種類です。

0 1 2 3 4 5 6 7 8 9 A B C D E F

数値で表すと 0 から 15 に対応します。
例えば、10はA、15はFとして表現されます。

エンコードの流れ

Base16の処理はとてもシンプルです。

  1. 入力データから1バイト(8ビット)を取り出す。
  2. その8ビットを上位4ビットと下位4ビットに分ける。
  3. それぞれを0〜15の数値として解釈し、対応する文字に変換する。
  4. 2つの文字を並べて1バイトを表現する。

この繰り返しによって、バイナリデータ全体を文字列に変換します。

パディングの必要がない理由

Base32やBase64では、入力の長さが決まった単位に合わないときに「=」を使って調整(パディング)を行います。
しかし、Base16では1バイトが必ず2文字に変換されるため、データが途中で途切れることはなく、特別な調整は不要です。

実際の例

文字列 "Hi" をBase16で表現してみます。

  • "H" = ASCIIコード 72 → 16進数で 48
  • "i" = ASCIIコード 105 → 16進数で 69

それぞれを文字列として並べると、Base16表記は次のようになります。

4869

このように、"Hi" という2バイトのデータは4文字のHex表記に変換されます。

Baseエンコーディングの具体例

Base64の変換イメージ

Base64では、入力データを3バイト(24ビット)単位で処理します。
24ビットを6ビットごとに4つに分け、それぞれを数値化して対応する文字に変換します。

図で表すと以下のようになります。

+--first octet--+-second octet--+--third octet--+
|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|
+-----------+---+-------+-------+---+-----------+
|5 4 3 2 1 0|5 4 3 2 1 0|5 4 3 2 1 0|5 4 3 2 1 0|
+--1.index--+--2.index--+--3.index--+--4.index--+

この図は、3バイトの入力を4つのインデックス(数値)に分け、それをBase64の文字テーブルに対応させる様子を表しています。

Base32の変換イメージ

Base32では、入力データを5バイト(40ビット)単位で処理します。
40ビットを5ビットごとに8つに分け、それぞれを文字に変換します。

図で表すと以下のようになります。

        1          2          3
01234567 89012345 67890123 45678901 23456789
+--------+--------+--------+--------+--------+
|< 1 >< 2| >< 3 ><|.4 >< 5.|>< 6 ><.|7 >< 8 >|
+--------+--------+--------+--------+--------+

ここでは、8文字の出力がどのように5バイトのデータから作られるかを示しています。1文字目は最初の5ビット、2文字目は次の5ビット…という順番で並んでいきます。

Base64の具体例

ここからは実際にBase64に変換する例を見てみます。

例1: 入力 0x14fb9c03d97e

入力データ (Hex):  14 fb 9c 03 d9 7e
2進数(8ビット単位): 00010100 11111011 10011100 00000011 11011001 11111110
6ビットごとに区切り: 000101 001111 101110 011100 000000 111101 100111 111110
10進数:               5      15     46     28     0      61     37     62
出力(Base64):         F      P      u      c      A      9      l      +

結果は "FPucA9l+" になります。

例2: 入力 0x14fb9c03d9

入力データ (Hex):  14 fb 9c 03 d9
2進数:             00010100 11111011 10011100 00000011 11011001
6ビット分割:       000101 001111 101110 011100 000000 111101 100100
10進数:             5      15     46     28     0      61     36
出力(Base64):       F      P      u      c      A      9      k
パディング:         =

結果は "FPucA9k=" になります。

例3: 入力 0x14fb9c03

入力データ (Hex):  14 fb 9c 03
2進数:             00010100 11111011 10011100 00000011
6ビット分割:       000101 001111 101110 011100 000000 110000
10進数:             5      15     46     28     0      48
出力(Base64):       F      P      u      c      A      w
パディング:         =      =

結果は "FPucAw==" になります。

セキュリティ

実装時のリスク

Base16、Base32、Base64といったエンコーディング方式を実装する際には、バッファオーバーフローのような脆弱性を生まないように注意する必要があります。
特にデコーダー側では、入力が不正な形式であっても処理が破綻しないように作らなければなりません。
例えば、入力データにヌル文字(ASCIIコード0)が含まれていたとしても、プログラムが予期せず停止したりエラーを引き起こしたりしてはいけません。

不正文字の扱い

Baseエンコーディングでは、定められたアルファベット以外の文字が入力に混入することがあります。
本来であれば、こうした入力は拒否するのが安全です。
ところが、無視して処理を続けるように実装すると「隠れた情報伝達の手段(covert channel)」として悪用される可能性が出てきます。
つまり、仕様外の文字を利用して秘密の情報を漏らす経路にされてしまうということです。

同じように、Base16やBase32では大文字・小文字の区別をしないケースがありますが、この「大文字か小文字か」という違いを利用して情報を隠して送ることもできてしまいます。
こうした仕組みが潜在的なリスクになることを理解しておく必要があります。

暗号的な安全性はない

Baseエンコーディングは「見た目を変える」だけで、暗号のようにデータを保護するわけではありません。
例えば、パスワードをBase64に変換しても、それは簡単に元に戻すことができます。
見かけ上はバイナリデータが読めない文字列になったとしても、本質的に「秘匿性」は持っていません。

実際に、ネットワークのトラブル報告などで、ユーザーが通信データをBase64のまま公開してしまい、その中にパスワードが含まれていたために漏洩事故につながったケースもあります。
エンコードはあくまで「転送や保存のためにデータを扱いやすくする手段」であって、「セキュリティの保護」ではないという点を強調する必要があります。

最後に

今回は「Base16・Base32・Base64といったエンコーディングの仕組みを統一的に定義し、互換性やセキュリティを確保するための仕組みを提案しているRFC3548」についてまとめてきました!
いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

Twitter @cardene777

他の媒体でも情報発信しているのでぜひ他も見ていってください!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?