Base64 Data Encoding#

RFC 4648で標準化されたデータのエンコーディング方法

RFC 4648 - The Base16, Base32, and Base64 Data Encodings

Base64 Encoding#

概要#

任意のバイト列(8bit単位)を、6bit単位にして、64種類の文字を使った文字列に再構成するエンコーディング。

主な用途#

バイナリをテキストにできるのが便利な点なので、テキストだけで表現したい環境で使われる。

  1. メール

    • バイナリファイルの添付ができる

  2. HTML

    • 例えば画像を埋め込める

      <img src="data:image/png;base64,..." />
      
  3. API通信

    • JSON内にバイナリを埋め込むことができる

  4. 認証

    • 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'