JavaScript >> Javascript 文檔 >  >> JavaScript

JavaScript 中的計算機科學:Base64 編碼

不久前,我寫了一篇關於數據 URI 的文章,並發布了一些工具來幫助生成它們。數據 URI 等式的一個關鍵部分是 base64 編碼。 RFC 3548 中描述了 Base64 編碼,以及 base16 和 base 32 編碼,所有這些都是用有限字符集表示單字節數據的方法。一個常見且不幸的誤解是,base64 編碼是一種旨在掩蓋底層數據的加密算法。 Base64 編碼不提供加密,它只是按照單一算法將一種形式的數據轉換為另一種形式。

定義了所有三種編碼,base16、base32 和 base64,以便數據在通過 7 位系統時能夠以安全的方式傳輸,而沒有數據丟失的風險。電子郵件傳統上是一個 7 位系統,最初定義 MIME base64 編碼是為了使數據能夠安全地從一個系統傳輸到下一個系統。簡而言之,創建這些編碼是為了防止在 8 位數據可能通過 7 位系統或通過 7 位系統傳輸時防止數據損壞。

它是如何工作的?

Base64 編碼直接作用於數據的底層二進製表示。您實際上並不是對字符串進行 base64 編碼,而是對錶示構成字符串的字符的字節進行 base64 編碼。字符串中的每個字符由單個 8 位字節表示; base64 編碼字符串中的每個字符僅由 6 位組成。 Base64編碼其實無非就是進行這種轉換。

base64 字母表中有 65 個可能的字符:字母 A 到 Z、a 到 z、數字 0 到 9、加號 (+) 和斜杠 (/)。第 65 個字符是等號 (=),用於表示填充(稍後討論)。因此,6 位數字 0 由 base64 編碼字符串中的字母 A 表示,6 位數字 1 由 B 表示,依此類推。

為了對數據進行 base64 編碼,您至少需要 24 位(可以被 6 和 8 整除的最小數字),因此任何三字符 ASCII 序列都可以乾淨地用 base64 編碼。考慮字符串“hat”。字母“h”用二進製表示的104或01101000,“a”表示97或01100001,“t”表示116或01110100。如果放在一起,你會得到:

01101000-01100001-01110100

要轉換成base64編碼,取這個並將邊界重新定義為6位:

011010-000110-000101-110100

之後,將每 6 位轉換為一個數字。

26-6-5-52

然後,用 base64 字母表中的字符替換每個數字。

a-G-F-0

所以“hat”的base64編碼形式是“aGF0”。這很有效,因為正好有 24 位或三個 ASCII 字符要編碼。由於並非所有字符串的長度都可以被三整除,因此當沒有足夠的字節進行編碼時,base64 編碼需要一些填充。

請注意,由於字符串的每三個字節最終表示為四個字節,因此生成的 base64 編碼字符串總是比原始字符串大 33% 左右。對於數據 URI,這通常是可以的,因為 base64 編碼也使 gzip 更有效,因此您實際上最終通過線路傳輸的字節數大致相同。

填充

編碼繼續進行,轉換每個 24 位,直到不再有 24 位要轉換。此時,可能出現三種情況:

  1. 沒有其他位需要轉換(原始字符串可以被三整除)。
  2. 有 8 個額外的位。在這種情況下,右填充 0 到 12 位。
  3. 有 16 個額外的位。在這種情況下,右填充 0 到 18 位。

請注意,對於第二個和第三個條件,右填充僅填充到最接近的可被 6 整除的位數。每個 6 位段轉換為一個字符,然後分別在末尾附加兩個或一個等號。每個等號表示添加了兩個額外的填充位。這些字符並不代表原始 ASCII 字符串中的任何內容;它們只是指示填充是必要的,以便解碼器知道如何處理 base64 編碼的字符串。

例如,考慮孵化這個詞。字母“h”用二進製表示的 104 或 01101000,“a”表示 97 或 01100001,“t”表示 116 或 01110100,“c”表示 99 或 01100011,“h”表示 104 或 01101000。生成的二進制表示是:

01101000-01100001-01110100-01100011-01101000

要轉換成 base64 編碼,創建 6 位組:

(011010-000110-000101-110100)(011000-110110-1000)

請注意,此序列的開頭只有一組完整的 24 位。序列的第二部分僅由 16 位組成。在這種情況下,最後一組用兩個零填充以創建一個 18 位組:

(011010-000110-000101-110100)(011000-110110-100000)

然後將 6 位組轉換為字符:

(a-G-F-0)(Y-2-g)

所以結果字符串是“aGF0Y2g”。但這不是最終的 base64 編碼字符串。由於有兩位填充添加,因此必須在末尾附加一個等號,使得結果為“aGF0Y2g=”。

JavaScript 編碼

許多語言中的 Base64 編碼直接處理字節和字節數組。由於 JavaScript 都沒有原生數據類型,因此位運算符對這個過程非常重要。位運算符直接作用於數字的底層位表示。儘管 JavaScript 數字在技術上以 64 位存儲,但只要涉及按位運算符,整數值就會被視為 32 位。問題中最複雜的部分是將三個 8 位數字轉換為四個 6 位數字,這就是位運算符的用武之地。

位運算

假設您有三個 8 位數字:

AAAAAAAA-BBBBBBBB-CCCCCCCC

6 位等效項是:

AAAAAA-AABBBB-BBBBCC-CCCCCC

請注意 6 位第一個數字是如何由 8 位數字的最高有效 6 位組成的。從本質上講,您想切斷最後兩個位並將它們視為從未存在過。這正是右移 (») 運算符所做的。取二進制數 240 或 11110000。如果將此數字右移兩位,則最終得到二進制 (60) 中的 00111100。所有位都向右移動了兩個點,當沒有足夠的空間時,剩餘的位“脫落”到末端並被淘汰。左邊的所有位都用零填充。因此,要從 8 位數字組中獲取第一個 6 位數字,您可以執行以下操作:

var first6bitNum = first8bitNum >> 2;    //right shift by two bits

第二個 6 位數字有點棘手,因為它是由第一個 8 位數字和第二個 8 位數字組合而成的。最簡單的部分是從第二個 8 位數字中獲取四個最高有效位,因為這又是一次右移。將第二個 8 位數字右移四位將使所有這些位都位於正確的位置。要獲取前兩位,需要對前 8 位數字執行幾個操作。

您想要的第一個 8 位數字的唯一部分是最低有效位,其他所有內容都需要變為零。這樣做的方法是對數字 3(二進制 00000011)使用按位與運算。按位與從兩個操作數逐位創建一個數字。如果每個數字中的對應位具有相同的值,則生成的數字在同一位中具有該值。例如:

    01100001
AND 00000011
------------
    00000001

請注意,結果數字 (1) 在兩個最低有效位中的值與原始數字 (97) 完全相同。通過對 3 執行按位與運算,您可以消除所有剩餘位的零值,只剩下兩個最低有效位。要將這些位放入第二個 6 位數字的正確位置,您需要左移四個空格(以便為第二個 8 位數字中已經可用的四個位留出空間)。然後,您使用按位 OR 運算符來組合這兩個組。因此,第二個6位數為:

var second6bitNum = (first8bitNum & 3) << 4 | (second8bitNum >> 4); 

對於第三個 6 位數字,過程幾乎完全相同。這個數字由第二個和第三個 8 位數字的位組成,因此還有另一個按位與和移位的過程。這一次,您需要第二個 8 位數的四個最低有效位和第三個 8 位數的兩個最高有效位。再一次,6 位數的最低有效位是最簡單的,因為您只需將第三個 8 位數右移 6 位。要獲得 6 位數的四個最高有效位,請與 15(二進制 00001111,十六進制 0F)執行按位與運算,將最高有效四位清零,然後將結果左移兩個點以留出空間還有兩個位:

var third6bitNum = (second8bitNum & 0x0f) << 2 | (third8bitNum >> 6); 

最後一個 6 位數字也很簡單,因為您只需從最後一個 8 位數字中刪除兩個最高有效位。為此,請對 63 執行按位與(二進制為 00111111,十六進制為 3F):

var fourth6bitNum = third8bitNum & 0x3f; 

確定所有 6 位數字後,您可以分配一個 base64 數字來表示該值。這通常是通過列出字符索引為 6 位數字的字符串中的所有 base64 數字來完成的。示例:

var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var firstBase64Digit = digits.charAt(first6bitNum);   //get first digit

這是從ASCII轉換為base64編碼的基本過程。

base64Encode() 函數

在嘗試對字符串進行 base64 編碼之前,您應該檢查字符串是否僅包含 ASCII 字符。由於 base64 編碼每個輸入字符需要 8 位,因此任何編碼高於 255 的字符都無法準確表示。規範指出在這種情況下應該拋出錯誤:

function base64Encode(text){

    if (/([^\u0000-\u00ff])/.test(text)){
        throw new Error("Can't base64 encode non-ASCII characters.");
    } 

    //more code here
}

此檢查使用一個簡單的正則表達式來檢查不在 0-255 範圍內的任何字符。如果其中一個字符在字符串中,則存在無法編碼的非 ASCII 字符並引發錯誤。

下一節的主要工作是使用按位運算符將每個 3 個 8 位序列轉換為 4 個 6 位序列。由於字符串中的每個字符都代表一個 8 位字節,因此您可以逐個字符地遍歷字符串:

function base64Encode(text){

    if (/([^\u0000-\u00ff])/.test(text)){
        throw new Error("Can't base64 encode non-ASCII characters.");
    } 

    var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
        i = 0,
        cur, prev, byteNum,
        result=[];      

    while(i < text.length){

        cur = text.charCodeAt(i);
        byteNum = i % 3;

        switch(byteNum){
            case 0: //first byte
                result.push(digits.charAt(cur >> 2));
                break;

            case 1: //second byte
                result.push(digits.charAt((prev & 3) << 4 | (cur >> 4)));
                break;

            case 2: //third byte
                result.push(digits.charAt((prev & 0x0f) << 2 | (cur >> 6)));
                result.push(digits.charAt(cur & 0x3f));
                break;
        }

        prev = cur;
        i++;
    }

    //more code here

    return result.join("");
}

由於三字節序列的每個字節的處理方式略有不同,byteNum 變量跟踪正在處理三字節序列的哪個字節。當byteNum 為 0 表示第二個字節的第一個字節,1 表示第二個字節,2 表示第三個字節。這很容易使用模數運算符計算出來。

該算法使用兩個變量來跟踪字符串的進度,cur 跟踪當前字符和 prev 跟踪前一個字符。這是必要的,因為第二個和第三個字節需要有關前一個字節的信息才能正確地進行 base64 編碼。一個 switch 語句用於確定如何解釋字節,然後應用位運算符。計算出 base64 值後,將其用作對 digits 的查找 多變的。 digits 變量是所有 base64 數字的列表,按使用順序排列。因此,您可以使用 digits 通過 charAt() 作為 base64 位的查找表 .結果是使用數組result建立的 ,後面會加入。

最後一步是填充字節數不正確的字符串。

function base64Encode(text){

    if (/([^\u0000-\u00ff])/.test(text)){
        throw new Error("Can't base64 encode non-ASCII characters.");
    } 

    var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
        i = 0,
        cur, prev, byteNum,
        result=[];      

    while(i < text.length){

        cur = text.charCodeAt(i);
        byteNum = i % 3;

        switch(byteNum){
            case 0: //first byte
                result.push(digits.charAt(cur >> 2));
                break;

            case 1: //second byte
                result.push(digits.charAt((prev & 3) << 4 | (cur >> 4)));
                break;

            case 2: //third byte
                result.push(digits.charAt((prev & 0x0f) << 2 | (cur >> 6)));
                result.push(digits.charAt(cur & 0x3f));
                break;
        }

        prev = cur;
        i++;
    }

    if (byteNum == 0){
        result.push(digits.charAt((prev & 3) << 4));
        result.push("==");
    } else if (byteNum == 1){
        result.push(digits.charAt((prev & 0x0f) << 2));
        result.push("=");
    }

    return result.join("");
}

由於 byteNum,這部分非常簡單 多變的。如果 byteNum 主循環完成後為 2,這意味著有適當數量的字節來完成編碼。如果 byteNum 是任何其他數字,這意味著填充是必要的。所以,如果 byteNum 為 0,這意味著有一個額外的字節,您需要填充四位。這是使用左移運算符完成的,然後在末尾添加兩個等號。如果 byteNum 是 1,這意味著有兩個額外的字節,你需要填充兩個位。

之後,將結果連接在一起並返回。這是原始字符串的 base64 編碼版本。

在 JavaScript 中解碼

一旦您知道如何對字符串進行 base64 編碼,就可以通過反轉過程輕鬆完成 base64 解碼。幾乎完全相同的算法可以用於相同的調整。

位運算

首先,假設您有以下四個 6 位數字的序列:

AAAAAA-BBBBBB-CCCCCC-DDDDDD

將其轉換為三個 8 位數字,結果將是:

AAAAAABB-BBBBCCCC-CCDDDDDD

所以第一個 8 位字節是前兩個 6 位數字的組合,使用第一個數字的六個位和第二個數字的兩個位。為了實現這一點,第一個數字必須左移兩位,以便為第二個數字的兩位留出空間。然後第二個數字必須向右移動四位以消除其四個最低有效位。代碼中的結果:

var first8bitNum= (first6bitNum << 2) | (second6bitNum >> 4); 

對於第二個 8 位數字,您只希望該數字的最右邊四位,因此第一步是對數字 15(二進制 00001111,十六進制 0F)應用按位與。之後,需要將這些位向左移動四個位置以將它們放置在正確的位置。然後需要第三個 6 位數字的前四位,因此將該數字向右移動兩個點並使用按位或將其與先前的結果相結合:

var second8bitNum= ((second6bitNum & 0x0f) << 4) | (third6bitNum >> 2); 

最後一個 8 位數字遵循類似的方法。您只需要第三個 6 位數的最後兩位,因此執行與 3(二進制 00000011)的按位與以隔離這些位。然後,左移六個點將這些位移動到位。由於第四個 6 位數字中的所有位都已經在正確的位置,因此您只需在此結果與前一個結果之間執行按位或即可得到第三個 8 位數字:

var third8bitNum= ((third6bitNum & 3) << 6) | fourth6bitNum; 

要將這些值中的每一個轉換為 ASCII 字符,請使用 String.fromCharCode() 方法:

var firstChar = String.fromCharCode(first8bitNum); 

您可能想知道在位填充的情況下會發生什麼,因為本節沒有涉及。關於 base64 解碼的有趣之處在於,您可以完全忽略填充並最終得到正確的值。因此,如果您使用 base64 解碼“aGF0Y2g”,您將得到與使用 base64 解碼“aGF0Y2g=”時相同的結果。原因在於數字是如何確定的。

回想一下,“hatch”這個詞在 base64 二進制中表示如下:

(011010-000110-000101-110100)(011000-110110-100000)

添加了兩位填充,所以原始表示實際上是這樣的:

(011010-000110-000101-110100)(011000-110110-1000)

現在,比較每個轉換後得到的 8 位序列:

(01101000-01100001-01110100)(01100011-01101000-00)
(01101000-01100001-01110100)(01100011-01101000)

請注意,帶有填充的原始文件右側有兩個額外的零位。這將構成第三個數字的最後兩位,但沒有足夠的位來完全創建第三個 ASCII 字符。每當最後一個序列有四個或更少位的 0 時,您可以忽略它。

base64Decode() 函數

與編碼一樣,第一步應該始終是驗證輸入。這裡有幾件事要記住。首先,空白在 base64 編碼的數據中並不重要,因此應該忽略它。其次,字符串的長度應該是 4 的倍數,如果不是,這不是一個有效的 base64 編碼字符串。牢記這一點,您可以想出一個合理的數據驗證方法:

function base64Decode(text){

    text = text.replace(/\s/g,"");

    if(!(/^[a-z0-9\+\/\s]+\={0,2}$/i.test(text)) || text.length % 4 > 0){
        throw new Error("Not a base64-encoded string.");
    }   

    //more code here
}

由於空白並不重要,因此第一步是在進行任何進一步驗證之前將其刪除。正則表達式檢查文本中沒有無效字符,然後驗證長度。如果所有這些條件都通過,那麼就該進入函數的解碼部分了。

如前所述,填充在解碼中並不重要,因此去掉等號以避免混淆。然後,採用與 base64 編碼類似的過程:逐個字符地跟踪前一個字符,因為它是計算所必需的。

function base64Decode(text){

    text = text.replace(/\s/g,"");

    if(!(/^[a-z0-9\+\/\s]+\={0,2}$/i.test(text)) || text.length % 4 > 0){
        throw new Error("Not a base64-encoded string.");
    }   

    //local variables
    var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
        cur, prev, digitNum,
        i=0,
        result = [];

    text = text.replace(/=/g, "");

    while(i < text.length){

        cur = digits.indexOf(text.charAt(i));
        digitNum = i % 4;

        switch(digitNum){

            //case 0: first digit - do nothing, not enough info to work with

            case 1: //second digit
                result.push(String.fromCharCode(prev << 2 | cur >> 4));
                break;

            case 2: //third digit
                result.push(String.fromCharCode((prev & 0x0f) << 4 | cur >> 2));
                break;

            case 3: //fourth digit
                result.push(String.fromCharCode((prev & 3) << 6 | cur));
                break;
        }

        prev = cur;
        i++;
    }

    return result.join("");
}

再次,digits 變量用於幫助轉換。在這種情況下,indexOf() 方法用於定位 base64 數字並返回其位置。然後將其用於執行解碼。 digitNum 變量跟踪您在一組四位中評估的 6 位數字。請注意,第一個數字,即數字 0,最初必須被忽略,因為沒有足夠的信息來進行任何解碼。當遇到數字 1 時,您可以回頭查看數字 0 以檢索必要的信息。剩下的就是對每個數字應用適當的按位運算並存儲結果,最終返回連接的字符串。

如果輸入文本中有任何填充,則循環將在數字 1 或 2 處停止,不計算填充的零。無需創建特殊情況來解決填充問題。

原生 base64 支持

一些瀏覽器實際上默認內置了 base64 編碼和解碼。 Firefox、Safari、Chrome 都支持兩個功能:

  • btoa(text) – base64 對文本進行編碼。
  • atob(text) – base64 解碼文本。

Internet Explorer 和 Opera 本身並不支持這些方法,因此您仍然需要其他實現,例如本文中的實現,以便在這些瀏覽器中執行 base64 編碼。

結論

Base64 編碼最初旨在通過 7 位系統安全地傳輸 8 位數據。現在,它在瀏覽器中的數據 URI 中使用起來更受歡迎。儘管有一些瀏覽器原生支持base64編碼和解碼,但並不是所有的瀏覽器都支持,所以需要有一些代碼可以在任何地方工作。

我不能說的一件事是base64編碼是不是 一種加密算法。不要誤以為編碼的數據是安全的,而實際上它只是轉換成另一種易於解碼的形式。

您可以從我的 GitHub 項目“JavaScript 中的計算機科學”下載源代碼。如果您想在您的 YUI 3 代碼中使用它,請查看 YUI Gallery 上的 Base64 實用程序。


Tutorial JavaScript 教程
  1. 調整動畫 GIF 的大小

  2. 使用 Utopiops 免費構建、部署和託管您的 React.js 應用程序

  3. 函數 - 參數與參數

  4. 如何將 TypeScript 包發佈到 NPM

  5. 作為前端開發人員需要使用的 10 個資源/庫/框架

  6. Webpack:第 3 部分

  7. 今天潛入反應!

  1. 在 Angular 11 中使用熱模塊替換

  2. 使用 Apollo-express 構建一個 graphql 服務器

  3. 在瀏覽器 <script type=module> 中使用 sql.js-httpvfs

  4. 使用 React-Bootstrap 創建響應式導航欄

  5. $.getJSON 返回 undefined 並產生 XML 解析錯誤

  6. 如何安裝反應?

  7. 如何在 Chart JS 欄中隱藏值

  1. 常用 React 組件庫指南

  2. 使用 VS Code 為 Vue.Js 項目設置 ESLint

  3. 以最簡單的方式將適合移動設備的交互式地圖添加到您的應用程序🗺️

  4. JS Firebase 數據庫錯誤 db.ref 不是函數