Base64 encoding/decoding常見于各種authentication和防盜鏈的實現當中。徹底搞懂它絕對提升團隊troubleshooting的底氣。我們從純手工方式編碼解碼開始,然后看看學到的技能怎么樣應用在實際的troubleshooting 中。
準備工作:我們應知道一個byte有8個bits,并且知道怎么表示它。
我們應該comfortable with hex, binary and decimal 之間的conversion.
有一張現成的ASCII table. (網上有很多,我下面附一個直接帶binary value的)
Base64 Table
Base64 Encoding的原理
大白話是這樣。給定一個bits序列,從最左開始,按順序每6-bit 為一組進行分組,這樣每一組可以表示最多2的6次方,也就是64個字符。這是為什么它叫Base64的原因。
這樣分組,最后即最右邊的一組如果正好有6-bit, 分組就完成。如果不夠6-bit, 就要做一個alignment / padding的處理。有兩步。第一步,用binary 0 (bit 的值為0)補齊到6-bit. 如果補過的bit 序列正好落在byte boundary,則分組完畢;如若不然,繼續第二步,補到下一個最近的byte boundary, 不過不是用0去pad, 而是用等號字符“=”, 用1個或者2個等號去補, 別忘了,一個等號占6個bits.
用數學的方式正規一點的描述是這樣。
給定一個長度為L的bit 序列,L可以被8整除 ( 這也是byte boundary含義), 我們需要找到兩個正整數M和N, 使得: L <= M <= N, 其中 M要能夠被6整除, N is either L or L + 8.
根據這個描述可以得出下面這些結論:最后得到的長度為N的bit 序列也是落在byte boundary上,即N可以被8 整除。
L除以6的余數只有三種可能: 0,2, 4, 對應于三種不同的padding scenarios.
M - L 的 值只可能為: 0, 2, 4, 道理同上
N – M的值只可能為: 0, 6,12, 同樣的,這是對應于三種不同的padding scenarios.
純手工Base64編碼
Given an input string “a”,
Step 1: 將它convert 成 bit 序列
查ASCII table, 我們得到它的binary value 即 bit 序列: 01100001
這是一個長度為8的初始序列。
Step 2: 按6個bits 一組分組
011000 01
Step 3: 最右組只有2 個bit, 不夠6 bits, 需要補4個0
011000 010000
至此我們可以得到兩個字符:
011000 = 2 ** 4 + 2 ** 3 = 16 + 8 = 24, 查Base64 table, 對應于24的字符是 Y.
010000 = 2 ** 4 = 16, 同樣查Base64 table, 對應于16的字符是 Q
很多Base64的實現,到這就結束了。小寫 a, 經過Base64編碼之后就是YQ.
Step 4: 這原始bit 序列長度為8, Step 3補了4個0, 變成12,不能被8整除,繼續補。
我們知道從12開始,下一個被8整除的值是24, 所以我們要補24 – 12 = 12個bits. 在這種情況下我們補等號字符,一個字符占6 bits,所以我們補兩個”= “.
如果做了這一步,小寫 a, 經過Base64編碼之后就是YQ== .
純手工Base64 解碼
Given YQ==,
Step 1: 去掉“=”的padding, 得到 YQ, 同時知道pad了12 bits 到byte boundary.
Step 2: 根據Base64 table convert “YQ” to bit 序列 011000 010000
Step 3: 去掉 最右邊pad的4個0
因為原始字符也就是要decode過去的字符是跳 8的,現在有12 bits, 很快可以確定最右4 bits 是padding.
從這種工作方式我們可以看到,既然最右4 bits無論如何都是要去掉的,這4 bits 可以是任何2 ** 4 = 16個值中的一個,而不影響decoding的結果。舉幾個例子:
0110000 010001 = YR
0110000 010010 = YS
0110000 010011 = YT
…
0110000 011110 = Ye
0110000 011111 = Yf
下面是用Python Bas64 module decoding的結果,驗證上面所講。
>>> base64.b64decode("YR==")
'a'
>>> base64.b64decode("YT==")
'a'
>>> base64.b64decode("YU==")
'a'
>>> base64.b64decode("YV==")
'a'
>>> base64.b64decode("YW==")
'a'
>>> base64.b64decode("YY==")
'a'
>>> base64.b64decode("YZ==")
'a'
>>> base64.b64decode("Ya==")
'a'
>>> base64.b64decode("Yb==")
'a'
>>> base64.b64decode("Yf==")
'a'
>>> base64.b64decode("Ye==")
'a'
>>>
Step 4: 按8-bit 分組, 得到 011000 01, 這個就是”a”.
實例分析
Linkedin發現如果他們修改防盜鏈token的最后一位,authentication仍然可以通過,要我們解釋。
Token in question是這樣的:
bs3xODAExQFZthY2LF1EqbuVtq4veLftiHILz3pXn13Ai5DzRYmdCZ8O9FAQwI4zceyBpEJO6secbKz9rGNMfBVeNaplwLNVqQwAXSR-grVQGJkz91pqdmVBbrwCpGto5IazBV8XDMJUIGCGZOdld2REqemD4cvDoLdeN3itZirB2FGfMHPxhP0
如果把末尾0換成1,2, 3, authentication 仍舊可以通過。
前面學到的東西可以馬上應用。思路是這樣的:
Token 長度為183, 所以bit 序列的長度為183 * 6 = 1098. 因為1098 沒有落在byte boundary, 我們可以立即確定encoding 的Step 4省略了,即沒有pad “=”.
因為1098 / 8 = 137 r2, 我們可以確定 最右的2 bit 是padding. 如前所述, 這兩個bits 可以是 任何 2 ** 2 = 4個值中的一個,而不影響decoding的結果。
00 = 0
01 = 1
10 = 2
11 = 3
至此Linkedin 的問題徹底解釋清楚。
讀了這一篇,你應該可以回答為什么有的Base64-encoded的values 末尾沒有等號,有的有一個等號,有的有兩個等號,以及為什么多個values會decode到相同的value. If necessary, you should be able to base64 encode and decode pretty much anything MANUALLY.