PHP 實現 Base64 加密演算法

Dennis_Ritchie發表於2019-11-16

多看看外面的世界

對於現在很多的php程式設計師而言,絕大部分時間都是在做業務有關的程式碼,其它方面可能涉及的比較少,因此今天準備和大家講講不一樣的知識,Base64加密演算法,上午花了一點兒時間用PHP重新實現了一遍,因為之前使用c寫的,中間也出現了一些bug,但是很快修復了,程式碼我已經上傳到了碼雲php-base64-implemention,希望大家下載下來仔細的分析一哈。

PHP實現Base64加密演算法

知識儲備

如果對位操作不熟悉的讀者,建議先看一下這方面的內容,非常簡單,幾分鐘就可以了。

友情連結

ASCII圖

Base64作用

base64的作用是把任意的字元序列轉換為只包含特殊字符集的序列,那麼base64加密之後的文字包含哪些字元呢?

  • A-Z
  • a-z
  • 0-9
  • +和/

上面總共包含64個字元,所以每個字元都使用6位來表示,下面有一張表,可以清晰的說明這個問題

PHP實現Base64加密演算法

這個是我在維基百科的截圖,舉個例子,對於Base64加密之後的字元A,對應的數值為0,二進位制表示就是000000,如果你現在不懂,沒關係,後面我會仔細的講解加密和解密的過程。

Base64加密

上面我已經提到了,每個Base64字元用6位來表示,但是一個位元組是8位,所以3個位元組剛好可以生成4個Base64字元,這應該很容易計算出來,下面我給大家舉個例子,假如說現在有個字串為"123",1的ASCII為49,那麼轉換為二進位制就是 00110001,2的ASCII為50,那麼轉換為二進位制就是00110010,3的ASCII為51,那麼轉換為二進位制就是00110011,這三個二進位制組合在一起就是這樣:001100010011001000110011
上面的二進位制位總共24位,從左到右依次取6位,對應關係如下:

  • 第一個6位,001100,查閱上面的圖示,對應M
  • 第二個6位,010011,同樣的操作,對應T
  • 第三個6位,001000,對應I
  • 第四個6位,110011,對應z

所以經過上面的分析,123轉換為Base64之後,就是MTIz,是不是很簡單?正常情況下都是很美好的,但是我們剛才的分析建立在加密之前的位元組數是3的倍數,那麼如果不是呢,比如剩下一個位元組,或者是2個,別急,下面來一一分析。

補齊

如果剩下一個位元組,那麼也就是說剩下8位,因為6位才能組合成一組啊,所以我們需要給它補上,補多少呢?只要4位就行了,12位剛好可以湊成2個Base64字元,那麼補什麼呢?很簡單,補0000就可以了,還是以上面的123為例,但是我們給它加上一個4,所以現在是“1234”,根據上面的分析,123剛好可以轉換為4個Base64字元,所以不管它,和上面的一模一樣,。現在我們只需要分析後面的4,4的ASCII為52,轉換為二進位制就是00110100,我們給它加上4個0,那麼結果就是001101000000,再對它進行6位分割,001101和000000,查表得到N和A,沒錯,這就是正確答案,但是為了後面的解碼,我們需要在加密後的字串末尾加上2個“=”,就是“MTIzNA==”。

如果剩下2個位元組的話,2個位元組剛好16位,6位一組的話,也就是說,少了2位,這樣就可以組合成18位了(3個Base64字元),這裡我們以字串“12”為例,1的ASCII轉換為二進位制是00110001,2的ASCII轉換為二進位制是00110010,我們將它組合在一起然後補齊之後(加上2個0),就是001100010011001000,按照6位一組進行分割,然後查表求得,結果是MTI,但是為了後面的解碼,我們需要在加密後的字串末尾加上1個“=”,就是“MTI=”。

Base64解密

有了加密的基礎,解密就很簡單了,以上面的加密結果為例 “MTIzNA==”,下面我們分別分析:

  • 我們首先判斷字串末尾是否有“=”,如果沒有的話,那麼也就是說,原始字串沒有補位操作,按照4個Base64字元轉換為3個8位的位元組演算法就可以了,4個字元組合起來就是24位,按照8位一個位元組,就是三個位元組。
  • 如果末尾有2個等於號“==”,也就是說之前進行了補位操作,通過上面加密的流程可以知道,原始位元組流中,剩餘1個位元組,補了4個0,得到了2個Base64字元,所以加密字串中,除了最後2個字元,其餘按照沒有補位的轉換操作就可以了,對於最後的2個Base64字元,我們把他們對應的二進位制位組合起來,然後再進行 右移 4位,就得到了一個8位的位元組。
  • 如果末尾有一個等於號“=”,也就是說未加密之前,剩餘2個位元組,所以按照上面所說的,加密的時候,需要補齊2個0,這樣就形成了三個Base64字元,那麼除了最後的三個字元,其餘的按照正常的轉換就可以了,對於最後的三個Base64字元,我們把他們的二進位制位組合起來總共18位,然後右移2位,就得到了16位的2個位元組。

Base64解密的時候,需要查上面的表,進行反向操作,舉個例子,對於Base64字元M,查表得到它對應的6位二進位制位為001100,一定要謹記這一點

Base64 程式碼實現

上面講解了Base64的加密和解密方法,說起來容易做起來難啊,在PHP裡面尤其如此

6位數字 轉換為Base64字元(參考上圖)

function normalToBase64Char($num)
{
    if ($num >= 0 && $num <= 25) {
        return chr(ord('A') + $num);
    } else if ($num >= 26 && $num <= 51) {
        return chr(ord('a') + ($num - 26));
    } else if ($num >= 52 && $num <= 61) {
        return chr(ord('0') + ($num - 52));
    } else if ($num == 62) {
        return '+';
    } else {
        return '/';
    }
}

上面的程式碼就是截圖的PHP程式碼實現,這裡我提醒大家不要把Base64的a字元和ASCII的a字元混淆起來,兩種情況下存在著上圖的對映關係,再次提醒一下,這個函式傳入的是6位的資料。

Base64字元轉換為 6位數字

這個過程就是 6位數字 轉換為Base64字元的逆過程,程式碼如下:

function base64CharToInt($num)
{
    if ($num >= 65 && $num <= 90) {
        return ($num - 65);
    } else if ($num >= 97 && $num <= 122) {
        return ($num - 97) + 26;
    } else if ($num >= 48 && $num <= 57) {
        return ($num - 48) + 52;
    } else if ($num == 43) {
        return 62;
    } else {
        return 63;
    }
}

對於任意一個Base64字元,我們首先要獲取到它對應的ASCII值,再根據這個值,通過上面的表的對映關係,求出它對應Base64數值,這個資料就是未加密資料的真實位元組資料。

加密程式碼實現

function encode($content)
{
    $len = strlen($content);
    $loop = intval($len / 3);//完整組合
    $rest = $len % 3;//剩餘位元組數,需要補齊
    $ret = "";
    //首先計算完整組合
    for ($i = 0; $i < $loop; $i++) {
        $base_offset = 3 * $i;
        //每三個位元組組合成一個無符號的24位的整數
        $int_24 = (ord($content[$base_offset]) << 16)
            | (ord($content[$base_offset + 1]) << 8)
            | (ord($content[$base_offset + 2]) << 0);
        //6位一組,每一組都進行Base64字串轉換
        $ret .= self::normalToBase64Char($int_24 >> 18);
        $ret .= self::normalToBase64Char(($int_24 >> 12) & 0x3f);
        $ret .= self::normalToBase64Char(($int_24 >> 6) & 0x3f);
        $ret .= self::normalToBase64Char($int_24 & 0x3f);
    }
    //需要補齊的情況
    if ($rest == 0) {
        return $ret;
    } else if ($rest == 1) {
        //剩餘1個位元組,此時需要補齊4位
        $int_12 = ord($content[$loop * 3]) << 4;
        $ret .= self::normalToBase64Char($int_12 >> 6);
        $ret .= self::normalToBase64Char($int_12 & 0x3f);
        $ret .= "==";
        return $ret;
    } else {
        //剩餘2個位元組,需要補齊2位
        $int_18 = ((ord($content[$loop * 3]) << 8) | ord($content[$loop * 3 + 1])) << 2;
        $ret .= self::normalToBase64Char($int_18 >> 12);
        $ret .= self::normalToBase64Char(($int_18 >> 6) & 0x3f);
        $ret .= self::normalToBase64Char($int_18 & 0x3f);
        $ret .= "=";
        return $ret;
    }
}

上面的程式碼和我之前分析的一模一樣。

解密程式碼實現

解密的過程複雜一點兒,但是隻要你看懂上面我所說的,肯定沒問題。

function decode($content)
{
    $len = strlen($content);
    if ($content[$len - 1] == '=' && $content[$len - 2] == '=') {
        //說明加密的時候,剩餘1個位元組,補齊了4位,也就是左移了4位,所以除了最後包含的2個字元,前面的所有字元可以4個字元一組
        $last_chars = substr($content, -4);
        $full_chars = substr($content, 0, $len - 4);
        $type = 1;
    } else if ($content[$len - 1] == '=') {
        //說明加密的時候,剩餘2個位元組,補齊了2位,也就是左移了2位,所以除了最後包含的3個字元,前面的所有字元可以4個字元一組
        $last_chars = substr($content, -4);
        $full_chars = substr($content, 0, $len - 4);
        $type = 2;
    } else {
        $type = 3;
        $full_chars = $content;
    }

    //首先處理完整的部分
    $loop = strlen($full_chars) / 4;
    $ret = "";
    for ($i = 0; $i < $loop; $i++) {
        $base_offset = 4 * $i;
        $int_24 = (self::base64CharToInt(ord($full_chars[$base_offset])) << 18)
            | (self::base64CharToInt(ord($full_chars[$base_offset + 1])) << 12)
            | (self::base64CharToInt(ord($full_chars[$base_offset + 2])) << 6)
            | (self::base64CharToInt(ord($full_chars[$base_offset + 3])) << 0);
        $ret .= chr($int_24 >> 16);
        $ret .= chr(($int_24 >> 8) & 0xff);
        $ret .= chr($int_24 & 0xff);
    }
    //緊接著處理補齊的部分
    if ($type == 1) {
        $l_char = chr(((self::base64CharToInt(ord($last_chars[0])) << 6)
                | (self::base64CharToInt(ord($last_chars[1])))) >> 4);
        $ret .= $l_char;
    } else if ($type == 2) {
        $l_two_chars = ((self::base64CharToInt(ord($last_chars[0])) << 12)
                | (self::base64CharToInt(ord($last_chars[1])) << 6)
                | (self::base64CharToInt(ord($last_chars[2])) << 0)) >> 2;
        $ret .= chr($l_two_chars >> 8);
        $ret .= chr($l_two_chars & 0xff);
    }
    return $ret;
}

告誡

任何程式碼都不能缺少理論的支撐,所以在看程式碼前,請仔細的閱讀Base64的基本原理,一旦原理看懂了,閱讀程式碼就不是那麼難了,任何時候閱讀別人的程式碼,這都是應該謹記的地方,之前就已經告訴大家了,程式碼已經上傳到碼雲,php-base64-implemention,程式碼沒有問題,完全可以執行,如果有問題可以找我,博文的最後面有我的聯絡方式,祝您假期愉快。

交流學習

我建了一個qq群,大家平時可以交流學習,我也會給大家講解Laravel的底層知識和其它程式設計知識。

PHP 實現 Base64 加密演算法

如果有不懂的地方,可以加我的qq:1174332406,或者是微信:itshardjs,公眾號:LearnCodeHard

相關文章