Base64 Data Encoding#
RFC 4648で標準化されたデータのエンコーディング方法
RFC 4648 - The Base16, Base32, and Base64 Data Encodings
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%増加する
暗号化ではない:誰でもデコードできる
アルゴリズムの流れ#
Step0. 入力のバイト列#
例として”cats”はバイトにすると
“c”:
01100011“a”:
01100001“t”:
01110100“s”:
01110011
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(\(2^6=64\)通り)を、64文字の集合から対応する文字を取得する(写像する)形で変換していく
BASE64_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
for index in indices:
encoded = BASE64_TABLE[index]
print(f"{index=} {encoded=}")
index=24 encoded='Y'
index=54 encoded='2'
index=5 encoded='F'
index=52 encoded='0'
実装イメージ#
これまでの流れを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='
Base16 Encoding#
4bit単位に区切り、16進数表現するもの。
import base64
base64.b16encode(b"cats")
b'63617473'