RFC 4648で標準化されたデータのエンコーディング方法
Base64 Encoding¶
概要¶
任意のバイト列(8bit単位)を、6bit単位にして、64種類の文字を使った文字列に再構成するエンコーディング。
主な用途¶
バイナリをテキストにできるのが便利な点なので、テキストだけで表現したい環境で使われる。
メール
バイナリファイルの添付ができる
HTML
例えば画像を埋め込める
<img src="data:image/png;base64,..." />
API通信
JSON内にバイナリを埋め込むことができる
認証
BASIC認証では
username:passwordをbase64でエンコードして送信する。盗聴されやすいが幅広いブラウザで対応している
特徴¶
可逆圧縮:情報を捨てない。完全に復元できる
データ量は増える:本来8bitで送れる情報を6bitごとに小分けにするので、英数字3文字(24bit = 8bit x 3文字)が4文字(6bit x 4文字)にエンコーディングされ、データ量は約133%増加する
暗号化ではない:誰でもデコードできる
text = "cats"
for char in text:
h = char.encode("utf-8").hex()
b = bin(int(h, 16))
print(f"{char}: {b}")c: 0b1100011
a: 0b1100001
t: 0b1110100
s: 0b1110011
Step1. 3バイト(24bit)ごとに分割¶
英数字はUTF-8エンコードだと1文字1バイトなので、“cats” なら “cat” と “s” に分割する。
"s"は24bitではないので右側に0を埋めて24bitに整える
data = "cats".encode("utf-8")
for i in range(0, len(data), 3):
chunk: bytes = data[i:i+3]
print(f"{chunk=}")
# 24bitにまとめる
buffer: int = 0
for b in chunk:
buffer = (buffer << 8) | b # 8bitずらして追加
print(f"{bin(buffer)=}")
# 24bitに満たない場合は右側に0ビットを追加
# このpaddingは最終的に=で置き換えられる
padding: int = 3 - len(chunk)
print(f"{padding=}")
buffer <<= (padding * 8)
print(f"{bin(buffer)=}")
print()chunk=b'cat'
bin(buffer)='0b11000110110000101110100'
padding=0
bin(buffer)='0b11000110110000101110100'
chunk=b's'
bin(buffer)='0b1110011'
padding=2
bin(buffer)='0b11100110000000000000000'
Step2. 6bitごとに分割¶
# 6bitずつ取り出す(4回)
buffer = 0b11000110110000101110100 # "cat"
indices = []
for j in range(18, -1, -6):
index = (buffer >> j) & 0b111111 # j bit右にずらして右側6bitを取り出す
print(f"{index=}, {bin(index)=}")
indices.append(index)index=24, bin(index)='0b11000'
index=54, bin(index)='0b110110'
index=5, bin(index)='0b101'
index=52, bin(index)='0b110100'
Step3. 64文字の集合から対応する文字を取得¶
Base64では
A-Zの26文字a-zの26文字0-9の10文字+/の2文字(URLで使えるように-と_に置き換えた変種もある)
で64文字、さらにパディングに = の合計65文字が使われる
各6bit(通り)を、64文字の集合から対応する文字を取得する(写像する)形で変換していく
BASE64_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
for index in indices:
encoded = BASE64_TABLE[index]
print(f"{index=} {encoded=}")index=28 encoded='c'
index=48 encoded='w'
index=0 encoded='A'
index=0 encoded='A'
実装イメージ¶
これまでの流れを1つの関数にまとめると次のようになる
def base64_encode(data: bytes) -> str:
BASE64_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
result = []
# 3バイトずつ処理
for i in range(0, len(data), 3):
chunk = data[i:i+3]
# 24bitにまとめる
buffer = 0
for b in chunk:
buffer = (buffer << 8) | b # 8bit左にずらして次の8bitを右に追加
# バイト数に応じてシフト調整
padding = 3 - len(chunk)
buffer <<= (padding * 8)
# 6bitずつ取り出す(4回)
for j in range(18, -1, -6):
index = (buffer >> j) & 0b111111 # j bit右にずらして右側6bitを取り出す
result.append(BASE64_TABLE[index])
# パディング '='
if padding:
result[-padding:] = "=" * padding
return "".join(result)# 実行例
base64_encode("cats".encode("utf-8"))'Y2F0cw=='base64パッケージ¶
Pythonには標準パッケージでbase64がある
base64 --- Base16, Base32, Base64, Base85 データのエンコード — Python 3.14.3 ドキュメント
import base64
base64.b64encode(b"cats")b'Y2F0cw=='Base32 Encoding¶
あまり使われないがBase32もRFC 4648で標準化されている。
こちらは5bitごとに区切り、32文字の文字セットでマッピングする。
文字セット: A-Z の26文字 + 2-7 の6文字(0, 1はO, Iと誤読しやすいため除外)
import base64
base64.b32encode(b"cats")b'MNQXI4Y='info - Unknown Directive
info - Unknown Directive[Crockford Base32 Encoding](https://www.crockford.com/base32.html)という変種が提案されており、ULIDというID生成アルゴリズムで採用されている。 RFCのBase32よりCrockfordのほうが多く使われているかもしれない。
Base16 Encoding¶
4bit単位に区切り、16進数表現するもの。
import base64
base64.b16encode(b"cats")b'63617473'