あのURLで見たBASE64の名前を僕達はまだ知らない。
あれは BASE64URL Encoding と言うらしい。
(どれだよ)
Base64#変形版 - Wikipedia
URLアプリケーションのための変形 Base64
('base64url' encoding)
概要
BASE64 でエンコードした文字列には、 URL 上で意味を持つ + 、 / 、 = が含まれている。そのため URL で利用するためには工夫が必要である。BASE64 には BASE64URL Encoding と言う変形版が公式仕様にあり、URL Encoding や Percent Encoding よりも、URL上で利用するには都合が良い様に感じた。
今回WEBアプリで BASE64URL Encoding を扱うにあたり、ついでに BASE64 の自体も実装をしてみようと思い、やってみた、そんな話。
BASE64の解説
① BASE64 (original)
読んでみると超シンプルな代物。
Base64 - Wikipedia
バイナリ全体をbitの配列 (bit stream) として捉え、6bit ずつ区切り、対応する文字列に置き換える。
6bit = 2^6 = 64 (⇒ 64文字種)
文字テーブル:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
(64文字種)
3btyte (24bit) ずつ処理し、4byteに伸長する。※ 24は8と6の最小公倍数。
不足するbitは0で埋める。
不足するbyteは = 文字で埋める。
必ず文字列長は4の倍数になる。
コーディングの前に視覚化して整理するのも良いと思う。
② BASE64 URL Encoding (URL安全なBASE64)
BASE64 の仕様 RFC 4648 内で Section 5 「Base 64 Encoding with URL and Filename Safe Alphabet」 にて定義されている。
・RFC 4648 - The Base16, Base32, and Base64 Data Encodings #Section-5 Base 64 Encoding with URL and Filename Safe Alphabet
+ 、 / 、 = は URL 上意味を持つ記号のため、それらの文字を回避したもの。 (URL上の役割は割愛。)
62番 + ⇒ - で表現する。
63番 / ⇒ _ で表現する。
= によるパディングは行わない。
結構シンプルで、ほぼ一緒。
#
bit
hex
BASE64
BASE64URL
#
bit
hex
BASE64
BASE64URL
0
00000000
0x00
A
A
32
00100000
0x20
g
g
1
00000001
0x01
B
B
33
00100001
0x21
h
h
2
00000010
0x02
C
C
34
00100010
0x22
i
i
3
00000011
0x03
D
D
35
00100011
0x23
j
j
4
00000100
0x04
E
E
36
00100100
0x24
k
k
5
00000101
0x05
F
F
37
00100101
0x25
l
l
6
00000110
0x06
G
G
38
00100110
0x26
m
m
7
00000111
0x07
H
H
39
00100111
0x27
n
n
8
00001000
0x08
I
I
40
00101000
0x28
o
o
9
00001001
0x09
J
J
41
00101001
0x29
p
p
10
00001010
0x0A
K
K
42
00101010
0x2A
q
q
11
00001011
0x0B
L
L
43
00101011
0x2B
r
r
12
00001100
0x0C
M
M
44
00101100
0x2C
s
s
13
00001101
0x0D
N
N
45
00101101
0x2D
t
t
14
00001110
0x0E
O
O
46
00101110
0x2E
u
u
15
00001111
0x0F
P
P
47
00101111
0x2F
v
v
16
00010000
0x10
Q
Q
48
00110000
0x30
w
w
17
00010001
0x11
R
R
49
00110001
0x31
x
x
18
00010010
0x12
S
S
50
00110010
0x32
y
y
19
00010011
0x13
T
T
51
00110011
0x33
z
z
20
00010100
0x14
U
U
52
00110100
0x34
0
0
21
00010101
0x15
V
V
53
00110101
0x35
1
1
22
00010110
0x16
W
W
54
00110110
0x36
2
2
23
00010111
0x17
X
X
55
00110111
0x37
3
3
24
00011000
0x18
Y
Y
56
00111000
0x38
4
4
25
00011001
0x19
Z
Z
57
00111001
0x39
5
5
26
00011010
0x1A
a
a
58
00111010
0x3A
6
6
27
00011011
0x1B
b
b
59
00111011
0x3B
7
7
28
00011100
0x1C
c
c
60
00111100
0x3C
8
8
29
00011101
0x1D
d
d
61
00111101
0x3D
9
9
30
00011110
0x1E
e
e
62
00111110
0x3E
+
-
31
00011111
0x1F
f
f
63
00111111
0x3F
/
_
pad
n/a
n/a
=
n/a
実装
① BASE64 ⇔ BASE64URLの相互変換
BASE64 Safe URL Convert.(Java) | Online editor and compiler
変換.java
/**
* URL安全なBASE64に変換.
*
* @param base64
* @return base64url
*/
private static String convertBase64SafeUrl(final String base64) {
if(base64 == null) {
return null;
} else {
String ret = base64;
ret = ret.replaceAll("\\+", "-");
ret = ret.replaceAll("\\/", "_");
ret = ret.replaceAll("=+$", "");
return ret;
}
}
/**
* 通常 (URL非安全) のBASE64に変換.
*
* @param base64url
* @return base64
*/
private static String convertBase64UnSafeUrl(final String base64url) {
if(base64url == null) {
return null;
} else {
String ret = base64url;
ret = ret.replaceAll("-", "+");
ret = ret.replaceAll("_", "/");
ret += "===";
ret = ret.substring(0, ((ret.length() / 4) * 4));
return ret;
}
}
変換結果
url safe :8KCut/CfpJTwn42G8J+RjQ== (24) >>>> 8KCut_CfpJTwn42G8J-RjQ (22)
url unsafe :8KCut_CfpJTwn42G8J-RjQ (22) >>>> 8KCut/CfpJTwn42G8J+RjQ== (24)
url safe :01234=== (8) >>>> 01234 (5)
url safe :012345== (8) >>>> 012345 (6)
url safe :0123456= (8) >>>> 0123456 (7)
url safe :01234567 (8) >>>> 01234567 (8)
url safe :a+/b==a+/b++a+/b//a+/b==a+/b==a+/b== (36) >>>> a-_b==a-_b--a-_b__a-_b==a-_b==a-_b (34)
url safe :a+/b==a+/b++a+/b//a+/b==a+/b==a+/b (34) >>>> a-_b==a-_b--a-_b__a-_b==a-_b==a-_b (34)
url unsafe :01234 (5) >>>> 01234=== (8)
url unsafe :012345 (6) >>>> 012345== (8)
url unsafe :0123456 (7) >>>> 0123456= (8)
url unsafe :01234567 (8) >>>> 01234567 (8)
url unsafe :012345678 (9) >>>> 012345678=== (12)
url unsafe :a-_b==a-_b--a-_b__a-_b==a-_b==a-_b (34) >>>> a+/b==a+/b++a+/b//a+/b==a+/b==a+/b== (36)
url unsafe :a-_b==a-_b--a-_b__a-_b==a-_b==a-_b (34) >>>> a+/b==a+/b++a+/b//a+/b==a+/b==a+/b== (36)
url unsafe :?*+/-_=== (9) >>>> ?*+/+/====== (12)
文字列途中にある = については、そもそものルール違反のため、関知しない。
BASE64の文字についても、そもそものルール違反のため、関知しない。
元々末尾に = がついていた場合も、関知せずパディングを行う。
② BASE64のEncode/Decode
STEP① とりあえずBYTE配列をBASE64にするコードを作ってみる。
とりあえず、書いてみた。
6bitごとにbyte配列を整形する 処理と 文字列化するコード を分けて記述。
進数変換と同様の考え方。今回のbit を bit mask で抽出し、前回余ったbit とを論理和で合成する。
今回余ったbit は 左シフトし、不用bitを落としておき、次回に持ち越す。
今回取り出すbitの右シフト量 ( v ) は 2, 4, 6 の3パターン。 (⇒ (((i % 3) + 1) * 2) )
次回に持ち越すbitの左シフト量は右シフト量 ( v ) に応じて求められ、 4, 2, 0 の3パターン。 (⇒ (6 - v) )
上位2bitは落としておく。 左シフトのため下位bitは無視して良い。
6bitごとにbyte配列を整形するメソッド
public static byte[] convert6bitBinary(byte[] data) {
if(data == null) {
return data;
} else {
byte mod = 0x00;
List<byte> list = new List<byte>();
byte[] lowMaskList = new byte[] {
0x3F, // [0] 00111111
0x0F, // [1] 00001111
0x03, // [2] 00000011
};
for(int i = 0; i < data.Length; i++) {
int j = i % 3;
int v = ((j + 1) * 2);
byte lowMask = lowMaskList[j];
byte b0 = data[i];
byte b6 = (byte)(mod | (lowMask & (b0 >> v)));
list.Add(b6);
mod = (byte)(0x3F & (b0 << (6 - v)));
if(j == 2) {
list.Add(mod);
mod = 0;
}
}
if((data.Length % 3) != 0) {
list.Add(mod);
}
return list.ToArray();
}
}
文字列化するコード
public static string ToBase64(byte[] data, bool isUrlSafe = false) {
StringBuilder sb = new StringBuilder();
if(data != null) {
string base64_map;
if(isUrlSafe) {
base64_map = BASE64_MAP_URLSAFE;
} else {
base64_map = BASE64_MAP_BASIC;
}
foreach(byte b in data) {
sb.Append(base64_map[b]);
}
if(!isUrlSafe) {
int max;
sb.Append("===");
max = (sb.Length / 4) * 4;
sb.Remove(max, sb.Length - max);
}
}
return sb.ToString();
}
STEP② 4文字ごと に着目した処理を考え実装する。
BASE64は比較的古くからある仕様であり、展開や実装の容易さと言うのも兼ね備えていたと考える。なので、 = を用いたパディング は何のために必要なのか疑問語を覚えるが。これは逆説的に、4文字ごと に作業を行うことで、最適化が行える を可能性を感じられる。特に 4文字 で表現される 24 bit は 8 と 6 の 最小公倍数 である。即ち、3byte ずつ 4文字 にする のである。この程度であれば、下手にベタ書きしたした方が可読性もあり、 if文 も減り、高速化が望めそうである。そんな訳で、処理を見直して、Encode処理とDecode処理を組み直してみる。
※ 現在のPCのCPUは計算機の発展であり、計算が得意で条件分岐が苦手である。レガシーなPCでは限られたリソースで速度を得るため、条件分岐を省き効率よく処理を作る必要性が高かった。
Encode (BASE64化)
3byteずつ、 int配列[3] にコピーする。byteの長さを超えた場合は、9bit目だけが立っている 0x0100 にする。
int配列[3] から byte配列[4] 6bitずつ格納していく。
(1) int配列[n]の9bit目 を byte配列[n]の7bit目 に論理和。
※ 6bitに伸長した場合は7-8bit目は使わないので、存在しないバイトはbitを立てて64以上の値にする。
(2) 上位ビットはカレントとして int配列[n]の該当bit を byte配列[n - 1]の該当bit へ 左シフト + マスク (抽出) して 論理和
(3) 下位ビットは不足分として int配列[n - 1]の該当bit を byte配列[n - 1]の該当bit へ 右シフト + マスク (抽出) して 論理和
存在するバイト値は 0 ~ 63 の値になる。
※ 0x0100 の下位8bitが0なので、不足分で使われても問題ない。
存在しないバイト値は 64 の値になる。
※ これも、0x0100 の効果。
byte配列 の値を文字化して格納。
通常の BASE64 では変換表の 64番目 に = を入れておき、条件分岐無くパディング行う。
URL安全の BASE64 URL 場合は、64番目 以上はスキップします。
encode処理抜粋
private static readonly string BASE64_MAP_BASIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
private static readonly string BASE64_MAP_URLSAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
public static string encode(byte[] data, bool isUrlSafe = false) {
StringBuilder sb = new StringBuilder();
for(int i = 0; i < data.Length; i += 3) {
int[] src = new int[3];
byte[] tgt = new byte[4];
//copy or default.
for(int j = 0; j < 3; j++) {
int k = i + j;
if(k < data.Length) {
src[j] = data[k];
} else {
//(bit: 0001 0000)
src[j] = 0x0100;
}
}
// reformat.
tgt[0] = (byte)((0x00) | (0x00) | (0x3F & (src[0] >> 2)));
tgt[1] = (byte)((0x00) | (0x3F & (src[0] << 4)) | (0x0F & (src[1] >> 4)));
tgt[2] = (byte)((0x40 & (src[1] >> 2)) | (0x3F & (src[1] << 2)) | (0x03 & (src[2] >> 6)));
tgt[3] = (byte)((0x40 & (src[2] >> 2)) | (0x3F & (src[2] << 0)) | (0x00));
// to character.
if(isUrlSafe) {
foreach(byte b in tgt) {
if(b < 0x40) {
sb.Append(BASE64_MAP_URLSAFE[b]);
}
}
} else {
sb.Append(BASE64_MAP_BASIC[tgt[0]]);
sb.Append(BASE64_MAP_BASIC[tgt[1]]);
sb.Append(BASE64_MAP_BASIC[tgt[2]]);
sb.Append(BASE64_MAP_BASIC[tgt[3]]);
}
}
return sb.ToString();
}
結構条件分岐が減って、良い感じ。また、符号化には最低でも2文字必要になるため、1-2文字目用のパディング判定処理は省いている。
Decode (BASE64からの複合)
通常の BASE64 と URL安全の BASE64 URL どちらでも複合ができる様にしている。
文字列 から byte配列[4] に値を格納する。存在しないbyte値は、64 にする。
通常の BASE64 では変換表の 64番目 に = を入れておき、こちらも 64 になる。
値が見つからなかった時は、URL安全の BASE64 URL の変換表も探索し、- _ を補完する。
byte配列[4] から int配列[3] へ 8bitずつ格納していく。
(1) byte配列[n + 1]の7bit目 を int配列[n]の9bit目 に論理和。
(2) 上位ビットはカレントとして byte配列[n]の該当bit を int配列[n]の該当bit へ 左シフト + マスク (抽出) して 論理和
(3) 下位ビットは不足分として byte配列[n + 1]の該当bit を int配列[n]の該当bit へ 右シフト + マスク (抽出) して 論理和
存在するバイト値の場合 byteの範囲を内の 0 ~ 255 の値になり、バイナリに追加する。
存在しないバイト値の場合、byteの範囲を超えた 256 (0x0100) の値になり、バイナリに追加しない。
decode処理抜粋
private static readonly string BASE64_MAP_BASIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
private static readonly string BASE64_MAP_URLSAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
public static byte[] decode(string base64) {
List<byte> list = new List<byte>();
if(!string.IsNullOrEmpty(base64)) {
for(int i = 0; i < base64.Length; i +=4) {
byte[] src = new byte[4];
int[] tgt = new int[3];
for(int j = 0; j < 4; j++) {
int k = i + j;
byte code;
if(k < base64.Length) {
char c = base64[k];
int index = BASE64_MAP_BASIC.IndexOf(c);
if(index < 0) {
index = BASE64_MAP_URLSAFE.IndexOf(c);
}
code = (byte)index;
} else {
// 01000000 (64)
code = 0x40;
}
src[j] = code;
}
tgt[0] = (0x00) | (0xFC & (src[0] << 2)) | (0x03 & (src[1] >> 4));
tgt[1] = (0xFF00 & (src[2] << 2)) | (0xF0 & (src[1] << 4)) | (0x0F & (src[2] >> 2));
tgt[2] = (0xFF00 & (src[3] << 2)) | (0xC0 & (src[2] << 6)) | (0x3F & (src[3] >> 0));
foreach(int val in tgt) {
if(val < 0xFF) {
list.Add((byte)val);
} else {
break;
}
}
}
}
return list.ToArray();
}
こちらも変換表の64番目に = を追加して64以上を使うことで、処理を少し簡略化出来ている感がある。
また、複合には最低でも2文字必要になるため、1byte目の複合には存在チェック用のbit処理を省いている。
本来 = 前の文字の場合は更にその前の余りを含んでいるので、
1文字の場合 ⇒ BASE64上ありえない (= で3文字パディング。)
2文字の場合 ⇒ = の 前の文字の下位4bitが0 (= で2文字パディング。)
3文字の場合 ⇒ = の 前の文字の下位2bitが0 (= で1文字パディング。)
4文字の場合 ⇒ 余りなし (= でパディングする必要もなし。)
な感じになる。
実行例
BASE64 scrap (C#) | Online editor and compiler
実行例抜粋
// check strs[0]-------------------------------------------------------
#source
str : Hellow C#
bin (hex) : 48 65 6C 6C 6F 77 20 43 23
bin (bit) : 01001000 01100101 01101100 01101100 01101111 01110111 00100000 01000011 00100011
#encode
base64 : SGVsbG93IEMj
base64url : SGVsbG93IEMj
#decode (basic) SGVsbG93IEMj
bin (hex) : 48 65 6C 6C 6F 77 20 43 23
bin (bit) : 01001000 01100101 01101100 01101100 01101111 01110111 00100000 01000011 00100011
str : Hellow C#
#decode (urlsafe) SGVsbG93IEMj
bin (hex) : 48 65 6C 6C 6F 77 20 43 23
bin (bit) : 01001000 01100101 01101100 01101100 01101111 01110111 00100000 01000011 00100011
str : Hellow C#
// check strs[1]-------------------------------------------------------
#source
str : BASE64-
bin (hex) : 42 41 53 45 36 34 2D
bin (bit) : 01000010 01000001 01010011 01000101 00110110 00110100 00101101
#encode
base64 : QkFTRTY0LQ==
base64url : QkFTRTY0LQ
#decode (basic) QkFTRTY0LQ==
bin (hex) : 42 41 53 45 36 34 2D
bin (bit) : 01000010 01000001 01010011 01000101 00110110 00110100 00101101
str : BASE64-
#decode (urlsafe) QkFTRTY0LQ
bin (hex) : 42 41 53 45 36 34 2D
bin (bit) : 01000010 01000001 01010011 01000101 00110110 00110100 00101101
str : BASE64-
// check strs[2]-------------------------------------------------------
#source
str : 𠮷🤔🍆👍
bin (hex) : F0 A0 AE B7 F0 9F A4 94 F0 9F 8D 86 F0 9F 91 8D
bin (bit) : 11110000 10100000 10101110 10110111 11110000 10011111 10100100 10010100 11110000 10011111 10001101 10000110 11110000 10011111 10010001 10001101
#encode
base64 : 8KCut/CfpJTwn42G8J+RjQ==
base64url : 8KCut_CfpJTwn42G8J-RjQ
#decode (basic) 8KCut/CfpJTwn42G8J+RjQ==
bin (hex) : F0 A0 AE B7 F0 9F A4 94 F0 9F 8D 86 F0 9F 91 8D
bin (bit) : 11110000 10100000 10101110 10110111 11110000 10011111 10100100 10010100 11110000 10011111 10001101 10000110 11110000 10011111 10010001 10001101
str : 𠮷🤔🍆👍
#decode (urlsafe) 8KCut_CfpJTwn42G8J-RjQ
bin (hex) : F0 A0 AE B7 F0 9F A4 94 F0 9F 8D 86 F0 9F 91 8D
bin (bit) : 11110000 10100000 10101110 10110111 11110000 10011111 10100100 10010100 11110000 10011111 10001101 10000110 11110000 10011111 10010001 10001101
str : 𠮷🤔🍆👍
// check 1st impl.
encode : 6A 05 39 => agU5
encode : 6A 05 => agU=
encode : 6A => ag==
encode : 6A 05 39 => agU5
encode : 6A 05 => agU
encode : 6A => ag
decode : agU5 => 6A 05 39
decode : agU= => 6A 05
decode : ag== => 6A
decode : a=== =>
decode : agU5 => 6A 05 39
decode : agU => 6A 05
decode : ag => 6A
decode : a =>
// check 2nd impl.
encode : 6A 05 39 => agU5
encode : 6A 05 => agU=
encode : 6A => ag==
encode : 6A 05 39 => agU5
encode : 6A 05 => agU
encode : 6A => ag
decode : agU5 => 6A 05 39
decode : agU= => 6A 05
decode : ag== => 6A
decode : a=== =>
decode : agU5 => 6A 05 39
decode : agU => 6A 05
decode : ag => 6A
decode : a =>
BASE64URL の活用法
私が今回使おうと思った背景では、Servlet のWEBアプリケーションで ChromeのAcrobat拡張 を利用するために、リクエストをURLに再現性のない POST から再現性のある GET に変更しようと思ったのがある。パラメータは複数あり、あまり生で見せたくないというのもあり、全パラメータをまとめて BASE64 での シリアライズ を行うことにした。
ChromeのAcrobat拡張 ではURLを再度開いているだけのため、再現性のあるURLである必要がある。これはfirefoxで内蔵ビューアで開いたPDFをオンザフライでAdobeReaderなどに開かせる際も同様かと思う。
GETによるアプリケーション連携が増えている昨今。一時的なリクエストにせよ、GET化することのメリットがそれなりに出てきている。
と言うか、私は パーマネント リンク大好きマン なので参照メインのサイトで パーマネント リンク 取れない時に結構キレてます。えぇ、最近のアニメ公式サイトとか多いですね。。。すっごいゴテゴテで、友人にshareしようとしたら、 パーマネント リンク が取れなくてTOPページしか送れないこととか。。。
ダイミダラーの公式サイト を見習って欲しい
(そういう意味ではFLASHが死に絶えたのはほんと、良かったのかも知れない。FLASHだけで作ってるサイトも度々あったので)
パーセント エンコーディング や URI エンコーディング じゃダメだったのか?
パーセント エンコーディング 及び URI エンコーディングはどんな文字列にもエンコード/デコードを処理をかけることができ、符号化/復号化がブラウザやWEBサーバーのリクエスト処理に含まれてしまっている。そのため、間に認証ページなどでジャンプが挟まる様な場合、ジャンプ数を加味して多重でパーセント エンコーディングをかけたりする必要がある場合がある。特に = が含まれている場合には、多段でジャンプ中にQueryStringとして前のページでのパラメータとして喰われてしまい、自分のページに届かない、なんてこともある。BASE64 の場合は + / により、多段ジャンプ自体が阻害される場合もある。
BASE64URL では自分で展開処理を行わなければいけないデメリット自体がそのままメリットとなり、他のページとの競合を避けることができる。誰も勝手に複合したりしないので、最後までそのまま自分のところに届く。
多段ジャンプ(笑) と思うこともあるかも知れないが。案外Twittterの認証など色々画面見てても、結構パラメーターを引きずっていることが多くあると思い、そんなにマイノリティでは無いかと思う。
HTTPのリクエストのセキュリティは、GET 以上のものが POST で得られるもんでも無いと思うので、もっと別の形で確保してください。
(だから認証でも GET を使っているのでしょう。)
総括
ベンチマークは取らないが、BASE64の実装をして理解を深めることが出来たと思います。とてもシンプルなロジックなので、一度自分で書いてみるのは、bit演算の訓練にもなって良いかと思った。あと、行数によってはベタに書くことも悪く無く、今回の BASE64 もその類なのかと思う。
なお、JavaとC#が混在しているのは単に気分です。( 当方、final教信者 の C#er です )。
参考 (謝辞)
Base64 - Wikipedia
https://ja.wikipedia.org/wiki/Base64
RFC 4648 - The Base16, Base32, and Base64 Data Encodings
https://tools.ietf.org/html/rfc4648
↧