base64加密演算法C++實現
base64編碼原理:維基百科 - Base64
其實編碼規則很簡單,將字串按每三個字元組成一組,因為每個字元的 ascii 碼對應 0~127 之間(顯然,不考慮其他字符集編碼),即每個字元的二進位制以 8 bit 儲存,$ 3 \times 8 = 4 \times 6 $,這樣就可以很方便的轉為 4 個 6 bit 的字元,當一組中的字元(最後一組會出現這樣的情況)少於3個字元,則用"="字元填充。
解碼也就是一個逆過程,也不難。
既然是二進位制,顯然應該想到利用位操作。。。
注意到:
1. 6 bit 的二進位制組成的數的十進位制一定小於 64,$ (111111)_{2} = (63)_{10} $。這就是那張 base64 表的原因。
2.將前一個字元的多餘的二進位制,填充到後一位字元的二進位制的前面以填充滿 6 位,每 3 個字元為 1 週期 。
如果對位操作很熟練,那麼這個演算法會很簡單(核心部分):
unsigned bit[6] = {0u}; size_t ens = 0, i = 2u; for (unsigned j = 0u; inputs[j]; j++) { int asc_ = int(inputs[j]); unsigned inx = asc_ >> i; for (unsigned k = 0u; k < i - 2; k++) inx += (bit[k] & 1) << (8 + k - i); // min: 0, max:{1, 2, 4, 8, 16, 32} encode[ens++] = base64_table[inx]; if (i != 6) { for (unsigned k = 0u, n = asc_; k < i; n >>= 1, k++) bit[k] = n & 1; i += 2; } else { i = asc_; inx = i >= 64 ? (i >= 128 ? i - 128 : i - 64) : i; encode[ens++] = base64_table[inx]; if (inputs[j + 1] != '\0') i = 2; } }
下面從分析時間複雜度方向來解釋演算法:
unsigned bit[6] = {0u}; size_t ens = 0, i = 2u;
首先我們需要維護一個大小為6的無符號整形陣列 bit 來儲存6個二進位制數位;
ens 是用來維護 encode 字元陣列的棧下標;
變數 i 扮演了幾個小而十分重要的角色:判斷週期變化,維護掃描到的每一個字元當前應該做多少次的位移數,當 $ i = 6 $ ,意味著可以通過當前分組中的第3個字元的後 6 bit 編碼得到第 4 個字元,這裡為了減少區域性變數冗餘以及便利,我使用了 i 來多做了一點本不屬於它的任務。
然後進入迴圈主體
for (unsigned j = 0u; inputs[j]; j++)
迴圈次數為我們需要編碼的字串的長度。
int asc_ = int(inputs[j]); unsigned inx = asc_ >> i; for (unsigned k = 0u; k < i - 2; k++) inx += (bit[k] & 1) << (8 + k - i); // min: 0, max:{1, 2, 4, 8, 16, 32} encode[ens++] = base64_table[inx];
上面程式碼片段的主要作用是:
1.將字元轉為十進位制ascii碼
2.將 asc_ 右移 i 得到 6 bit,因為這裡不用手動計算二進位制,它本身就得到了一個 0~63 的十進位制數,將其作為 base64 表的下標索引得到第一個字元(i 的所有取值情況為 { 2, 4, 6 } ,這也是 bit 陣列為 6 的原因)。
3.如果程式進行到當前分組的第一個字元,那麼迴圈
for (unsigned k = 0u; k < i - 2; k++) inx += (bit[k] & 1) << (8 + k - i);
將不會進行,否則,計算該分組前一個字元儲存在 bit 中的對應的 i 位上的值(計算字元移除的 bit 程式碼在後面,因為每組的第一個字元不需要計算加上 bit 的結果)。時間複雜度 $ O(1) $
例如(該例來自wikipedia - Base64):
第一個字元右移出的2個bit(01)儲存到bit陣列,剩餘的編碼為 T 字元,掃描到字元 a 時,先將其值大小右移 4 位,變為 6(0110),將 bit 中的 2 個 bit 加到 0110 前面,也就是 010110,這裡 bit 中的 1 的位為 4($ 1 \times 2^4 = 16 $ ),0的位為 5($ 0 \times 2^5 = 0 $ ), 可以根據 8 + k - i 來計算應該位移多少(註釋中列舉了可能情況)。
4.利用計算出的結果作為 base64 表的索引,取出對應的字元,並存儲到 encode 棧中。
下一個片段:
if (i != 6) { for (unsigned k = 0u, n = asc_; k < i; n >>= 1, k++) bit[k] = n & 1; i += 2; }
當程式還在週期中進行時,程式就會進入到該片段,目的就是將當前字元後 i 個 bit 儲存到 bit 陣列中,以維護並填充下一個字元的前 i 個 bit。
否則執行片段:
else { i = asc_; inx = i >= 64 ? (i >= 128 ? i - 128 : i - 64) : i; encode[ens++] = base64_table[inx]; if (inputs[j + 1] != '\0') i = 2; }
當掃描到分組中第 3 個字元時,一個週期就結束了,因為 $ 4 \times 6 = 3 \times 8 $,所以,最後一個字元不需要位移,更準確的說,最後一個字元可以編碼成兩個字元,前 2 bit 結合前一個字元的後 4 bit,得到一個字元;後 6 bit 可以直接編碼為一個字元。但如何將 8 bit 的前 2 個 bit 去掉呢?思考了一下,第7位二進位制有效的最小的十進位制值為64,第8位二進位制有效的最小的十進位制值為128,所以,就有了上面程式碼片段中第3行的程式碼,實際上,我們的輸入是 ascii 字符集,不可能有大於等於 128 的情況,所以,可以寫成:
inx = i >= 64 ? i - 64 : i;
然後將 inx 作為 base64 表的索引,取出字元並加入到 encode 棧頂。注意:inx 值是合法的,且一定不會導致越界發生。
最後如果下一個字元不是結束符,則將 i 重新置為 2,以開始新一輪的編碼。當下一個字元為結束符時,i 的值一定大於 6(對於字母和數字的字元部分)。
核心部分基本就是這些了,但還有一些細節沒處理。
對於上面的程式,當輸入的字串長度不能整除 3 時,最後一個字元的後面一部分一定會沒有被編碼出來,以及沒有填充 "=" 來完成 base64 的編碼規則。
所以,繼續完善細節,見下面片段:
if (i <= 6) { unsigned inx = 0; for (unsigned k = 0u; k < i - 2; k++) inx += (bit[k] & 1) << (8 + k - i); encode[ens++] = base64_table[inx]; } while (i <= 6) encode[ens++] = '=', i += 2;
前面的程式的結束情況一定在 if 之後就完成迴圈了,最後一個字元的 8 bit 後面 i 部分存入 bit 中的後,沒有用上,於是,將其取出來後面全填充為 0 並計算出值即可,然後判斷迴圈結束前執行到週期的第幾個字元(一定是1或2),填充上"="。時間複雜度 $ O(1) $
於是,該演算法的總時間複雜度為 $ O(n) $ ,n 為字串長度。
最後
完整實現程式碼:
#include <iostream> #include <cstdio> #include <cstring> const char *base64_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; char* base64encode(char* encode, const char* inputs) { unsigned bit[6] = {0u}; size_t ens = 0, i = 2u; for (unsigned j = 0u; inputs[j]; j++) { int asc_ = int(inputs[j]); unsigned inx = asc_ >> i; for (unsigned k = 0u; k < i - 2; k++) inx += (bit[k] & 1) << (8 + k - i); // min: 0, max:{1, 2, 4, 8, 16, 32} encode[ens++] = base64_table[inx]; if (i != 6) { for (unsigned k = 0u, n = asc_; k < i; n >>= 1, k++) bit[k] = n & 1; i += 2; } else { i = asc_; inx = i >= 64 ? i - 64 : i; encode[ens++] = base64_table[inx]; if (inputs[j + 1] != '\0') i = 2; } } if (i <= 6) { unsigned inx = 0; for (unsigned k = 0u; k < i - 2; k++) inx += (bit[k] & 1) << (8 + k - i); encode[ens++] = base64_table[inx]; } while (i <= 6) encode[ens++] = '=', i += 2; encode[ens] = 0; return encode; } char* base64encode(char* encode, std::string inputs) { return base64encode(encode, inputs.c_str()); } int main(int argc, char* argv[]) { if (argc <= 1) { std::cout << "usage -i <input text>" << std::endl; return 0; } if ( !strcmp(argv[1], "-i") ) { std::string doc = argv[2]; int len = doc.size(); char* encode = new char[len * (4 / 3)]; std::cout << base64encode(encode, argv[2]) << std::endl; delete[] encode; encode = nullptr; } return 0; }
測試一下(來自wiki - Base64):
base64encode -i "Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure."
Output:
TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=