一覧CS-02-4 · 文字・バイト・データ量

Base64の仕組みとパディング

3バイトが4文字になる仕組みと、末尾の「=」の役割を学びます。

説明

画像などのデータを、文字として扱える形式で送ったり保存したりしたいことがあります。Base64は、そのために使われる符号化方式の一つです。バイト列を英大文字・英小文字・数字・記号の計64種類の文字で表します。暗号化ではないので、復号すれば元のデータを取り出せます。

このモジュールでは、Base64がどのように文字を選ぶのかと、末尾に付く「=」の意味を学びます。1バイトは8ビットですが、Base64では入力を6ビットずつに区切ります。6ビットは0〜63の64通りなので、各値を一つの文字に対応させられます。

まず「Man」という3バイトを使い、24ビットが4文字に変わる過程を説明します。続いて入力を「Ma」「M」と短くして、6ビットに足りない部分と「=」の関係を調べます。最後に日本語と、URLで使う文字表の例を扱います。全64値の対応表は説明欄の末尾から参照できます。

13バイトから4文字への変換

例として「Man」の3バイトをBase64へ変換します。

import base64Base64の変換機能を読み込む; raw = b'Man'入力の3バイト; encoded = base64.b64encode(raw)Base64へ符号化; encoded変換結果を表示

b'TWFu'と表示されました。先頭のbはPythonでバイト列を表す印で、Base64の文字には含まれません。入力のManは3バイト、出力のTWFuは4バイトです。

2入力のバイトと、出力文字の番号

入力と出力について、バイト列の内容と長さを並べます。16進数は0〜9とA〜Fを使い、1バイトを2桁で表します。

raw.hex(' '), encoded.hex(' ')入力と出力の16進表記, len(raw), len(encoded)入力と出力のバイト数

Manは4D 61 6E、TWFuは54 57 46 75です。ASCIIは英字や数字などに0〜127の番号を割り当てた文字コードで、出力文字はその番号を1バイトずつ使って保存しています。たとえばTのASCIIの値は16進数54、10進数84ですが、Base64の対応表では19に当たります。この19がどこから出てくるかを、次の操作で調べます。

3入力3バイトの24ビット

Base64がどのように出力文字を決めるかを調べるため、入力3バイトを24桁の2進数で表します。

bits = ''.join(format(b, '08b') for b in raw)各バイトを8桁の2進数にし連結; bits24ビットを表示

01001101 01100001 01101110という3組の8ビットです。Base64では、この24ビットを6ビットずつの4組として扱います。

46ビットの値から出力文字が決まる

24ビットを6ビットずつに区切り、それぞれの値を求めます。

groups = [bits[i:i+6] for i in range(0, len(bits), 6)]6ビットずつ取り出す; groups各組のビット列, [int(g, 2) for g in groups]各組を2進数として数値にする

010011・010110・000101・101110は、10進数で19・22・5・46です。Base64の対応表では、それぞれT・W・F・uに当たります。これがTWFuという出力になる理由です。

5復号すると元の3バイトに戻る

Base64の出力を復号し、元のバイト列に戻るかを確認します。

base64.b64decode(encoded)Base64を復号, base64.b64decode(encoded) == raw元のバイト列との一致を判定

b'Man'とTrueになりました。Base64の文字を対応表の番号へ戻し、6ビットずつつなげて8ビットごとに取り出すと、元のバイト列が得られます。

6入力が2バイトの場合の「=」

入力をMaにして符号化します。3バイトだったときの結果は履歴に残るので、出力の末尾がどう変わるかに注目してください。

raw = b'Ma'入力を2バイトに変更; encoded = base64.b64encode(raw)Base64へ符号化; raw.hex(' '), encoded入力と変換結果を表示

4D 61という2バイトからTWE=になりました。この「=」をパディングと呼びます。末尾の4文字組に対して、入力のバイトが不足していたことを示します。

7最後の6ビットに補う0

2バイトは16ビットなので、6ビットずつに区切ると6+6+4となります。最後の組には4ビットしかありません。

bits = ''.join(format(b, '08b') for b in raw)入力を16ビットの列にする; [bits[i:i+6] for i in range(0, len(bits), 6)]6ビットずつ取り出す

最後の0001に0を二つ補うと000100、10進数では4となり、対応する文字はEです。入力から得られた文字はTWEの3文字なので、4文字組の残りを「=」で埋めます。

8「=」と、値0の文字「A」の違い

Base64の対応表で0に当たる文字はAです。「=」の代わりにAを書いたTWEAと、元のTWE=を復号し、それぞれのバイト列を表示します。

base64.b64decode('TWE=').hex(' ')「=」付きの結果を復号, base64.b64decode('TWEA').hex(' ')「A」に置き換えた結果を復号

TWE=は4D 61、TWEAは4D 61 00です。Aに置き換えると、末尾に値0のバイトが増えました。「=」は入力に含まれない部分を示しますが、Aは実際の6ビットの値を表すため、同じ意味にはなりません。

9入力が1バイトなら「==」になる

入力をMだけにすると8ビットです。先頭の6ビットを取り出した後は2ビット残るので、末尾に0を四つ補います。

raw = b'M'入力を1バイトに変更; base64.b64encode(raw)Base64へ符号化, format(raw[0], '08b')入力の8ビット

出力はTQ==です。先頭の010011はT、残りの01を補った010000はQになります。入力から得られた文字が2文字なので、4文字組の残りに「==」が付きます。

10出力が4文字単位で増える理由

Aを1〜6個並べた入力を用意し、Base64に変換します。これまでの例と同様に、入力のバイト数と出力の長さに注目してください。

[(n, base64.b64encode(b'A' * n).decode('ascii')Aをn個並べて符号化し文字列表示) for n in range(1, 7)1〜6バイトを順に処理]

1〜3バイトは4文字、4〜6バイトは8文字になりました。通常のパディング付きBase64では、入力を3バイトずつの組に分け、端数も一組として数えます。組の数を4倍したものが出力の文字数です。式では4×ceil(入力バイト数÷3)となり、ceilは端数の切り上げを表します。

11日本語は文字コードでバイト列にしてから変換

Base64へ渡すのは文字そのものではなくバイト列です。「あ」をUTF-8という文字コードでバイト列に変換し、続けてBase64にします。\u3042は「あ」のコードポイントを指定する表記です。

raw = '\u3042'.encode('utf-8')「あ」をUTF-8へ符号化; encoded = base64.b64encode(raw)そのバイト列をBase64へ符号化; raw.hex(' '), encoded入力と変換結果を表示

「あ」のUTF-8はE3 81 82の3バイトです。そのためBase64では44GCという4文字になり、「=」は付きません。

12Base64とUTF-8を順に復号する

まずBase64を復号して、元のバイト列を得ます。その後、UTF-8で復号すると文字列になります。

base64.b64decode(encoded)元のバイト列に戻す.decode('utf-8')UTF-8で文字列に戻す

「あ」に戻りました。Base64の復号だけではE3 81 82というバイト列が得られるだけです。それを文字として表示するには、UTF-8など、元の文字コードで復号する処理も必要です。

13URL用Base64の文字表

標準のBase64には+と/が含まれます。URL用のBase64では、この二つの代わりに-と_を使います。FB FFという2バイトを両方の形式で変換します。

raw = bytes.fromhex('fb ff')FB FFの2バイト; base64.b64encode(raw)標準の文字表で符号化, base64.urlsafe_b64encode(raw)URL用の文字表で符号化

標準形式は+/8=、URL用は-_8=です。今回のPythonのurlsafe_b64encodeでは、URL用でもパディングの「=」が付きます。

14URL用の文字表による復号

urlsafe_b64decodeで-_8=を復号し、直前に用意したrawと比較します。

base64.urlsafe_b64decode('-_8=')URL用の文字表で復号 == raw元のバイト列との一致を判定

Trueになり、元のFB FFと一致しました。復号には、符号化した形式に合った文字表を使います。

まとめ

Base64では、3バイトの24ビットを6ビットずつに分け、四つの文字に変換します。出力の文字は、各6ビットの値と対応表から決まります。

入力が3バイトの倍数でない場合は、最後の組に0を補い、「=」で入力の不足を示します。「=」を値0の文字Aに置き換えると、復号後のデータも変わります。

操作履歴と解説を振り返れます。

Python 3

1
import base64; raw = b'Man'; encoded = base64.b64encode(raw); encoded
b'TWFu'
2
raw.hex(' '), encoded.hex(' '), len(raw), len(encoded)
('4d 61 6e', '54 57 46 75', 3, 4)
3
bits = ''.join(format(b, '08b') for b in raw); bits
'010011010110000101101110'
4
groups = [bits[i:i+6] for i in range(0, len(bits), 6)]; groups, [int(g, 2) for g in groups]
(['010011', '010110', '000101', '101110'], [19, 22, 5, 46])
Manの変換
6ビット10進数の値文字
01001119T
01011022W
0001015F
10111046u
5
base64.b64decode(encoded), base64.b64decode(encoded) == raw
(b'Man', True)
6
raw = b'Ma'; encoded = base64.b64encode(raw); raw.hex(' '), encoded
('4d 61', b'TWE=')
7
bits = ''.join(format(b, '08b') for b in raw); [bits[i:i+6] for i in range(0, len(bits), 6)]
['010011', '010110', '0001']
Maの変換
入力のビット0を補った6ビット10進数の値・出力文字
01001101001119 → T
01011001011022 → W
0001000100(末尾2ビットを補う)4 → E
入力なし—=
8
base64.b64decode('TWE=').hex(' '), base64.b64decode('TWEA').hex(' ')
('4d 61', '4d 61 00')
9
raw = b'M'; base64.b64encode(raw), format(raw[0], '08b')
(b'TQ==', '01001101')
10
[(n, base64.b64encode(b'A' * n).decode('ascii')) for n in range(1, 7)]
[(1, 'QQ=='), (2, 'QUE='), (3, 'QUFB'), (4, 'QUFBQQ=='), (5, 'QUFBQUE='), (6, 'QUFBQUFB')]
11
raw = '\u3042'.encode('utf-8'); encoded = base64.b64encode(raw); raw.hex(' '), encoded
('e3 81 82', b'44GC')
12
base64.b64decode(encoded).decode('utf-8')
'あ'
13
raw = bytes.fromhex('fb ff'); base64.b64encode(raw), base64.urlsafe_b64encode(raw)
(b'+/8=', b'-_8=')
14
base64.urlsafe_b64decode('-_8=') == raw
True

演習完了