了解 Base64
在了解 base64
之前,我們先來了解一下編碼是什麼。
編碼是什麼?
編碼是將一種將資料轉換成另一種資料的過程,而這個過程是可以被反轉的。
舉個例子,我們可以透過一張字母對應表,將一串英文字轉換成一串數字。
字母對應表:
a b c d e f g h i j k l m n o p q r s t u v w x y z
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ... 26
這樣我們就可以將 hello
轉換成 8 5 12 12 15
,
而這個過程是可以被反轉的,也就是說我們可以透過這張字母對應表,
將 8 5 12 12 15
轉換回 hello
。
這個轉換的過程就是編碼,而這張字母對應表就是這個轉換的規則。
並且從 hello
轉換成 8 5 12 12 15
的過程,我們稱之為 encode,
而從 8 5 12 12 15
轉換回 hello
的過程,我們稱之為 decode。
常見的編碼方式有 base64
、hex
、binary
、ascii
等等,
它們都是將資料轉換成另一種資料的規則,且具備不同的用途。
為什麼要編碼?
舉個例子,我們的電腦是以二進位的方式儲存資料的,
也就是說,我們的電腦只能儲存 0
跟 1
,
所以當我們要儲存一個字元時,我們必須將這個字元轉換成二進位的資料,
且轉換必須是可以被反轉的,這樣我們才能將這個二進位的資料轉換回字元。
諸如此類因為儲存資料的方式不同,所以必須要轉換的情況,
就會需要編碼來幫助我們將資料轉換成另一種資料。
這邊我們也可以發現到資料轉換能夠成立的條件,
是必須要基於共同的規則,例如前面提到的字母對應表,
這樣我們才能將資料轉換回來,也才能進而將資料用於儲存或溝通。
所以如果有人說他的檔案編碼跑掉 (或是說 出現亂碼 之類的),
那其實就是指他用了跟你當初撰寫時不同的編碼 (字母對應表) 來開啟檔案。
編碼適用於任何資料
雖然上面舉了文字的例子,但是編碼這個概念適用在任何資料上,
例如. 圖片、影片、音樂、檔案等等。
任何牽涉到資料轉換的過程,都稱之為編碼,
所以不用太疑惑為什麼處理圖像或影片的函式庫會出現 encode 或 decode 之類的函式,
例如. go-image-jpeg,rust-video。
Base64 是什麼?
base64
是一種將二進位資料編碼成 ASCII 字元的編碼方式,
它的名稱來自於它的編碼字元表。
雖然 base64
的名稱是 base64
,但是它的編碼字元表有 65 個字元,
=
是 pending 字元,它的用途是用來填補資料長度不足的情況。
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=
詳細的對照表可以參考這裡。
它被設計出來的原因,就是想透過文字的方式來呈現二進位資料,
畢竟並不是所有的地方都能夠直接儲存二進位資料,
例如. HTML、JSON,這些檔案格式並沒辦法直接儲存二進位資料,
所以才會需要透過 base64
透過文字的方式來儲存二進位資料。
正常不會直接去看 base64
來理解資訊,它一開始就不是要設計來給人看的。
Base64 的應用場景
base64
通常用來解決儲存或是傳輸時,
只能使用 ASCII 字元 (包含它的超集 UTF-8) 這類無法接受任意二進位資料的情況,
已確保資料在傳輸或儲存的過程中保持資訊的完整而不會被破壞。
舉個例子,
HTML 是純文本的檔案類型,所以我們無法直接嵌入二進位資料,
但是透過 base64
我們可以將二進位資料轉換成文字,
搭配 data:URL 來直接嵌入二進位資料。
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIA..." />
Base64 不是加密法
這是一個很常見也常被誤解的問題,
加密是為了保密而對資訊進行轉換的過程,所以它也屬於編碼的一種,
但是編碼不一定是為了保密而進行的,
例如. base64
並不是為了保密而進行的編碼,
它只是原封不動的將二進位資料轉換成 ASCII 字元,
所以你可能不會希望看到 base64
這種編碼方式被用在加密上。
如果你是想設計一張只有你跟你的朋友們才能看懂的字母對應表,
這種方式叫做 凱薩加密法 (Caesar cipher)。
Base64 的變體 base64url
base64
有一個變體叫做 base64url
,
它是為了讓 base64
可以用於 URL 或是 檔案名稱 而設計的。
因為 base64
會使用 +
跟 /
這兩個字元,
而這兩個字元在 URL 中有特殊的意義,
所以 base64url
將 +
跟 /
分別替換成 -
跟 _
。
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=
詳細的對照表可以參考這裡。
如何在 base64 跟 base64url 之間轉換
這裡提供 JavaScript 的實作方式。
const base64Tobase64url = (text) =>
text
.replace(/\//g, "_")
.replace(/\+/g, "-")
.replace(/=/g, "");
base64Tobase64url("8J+mhA=="); // 8J-mhA
const base64urlTobase64 = (text) =>
text
.replace(/_/g, "/")
.replace(/-/g, "+")
.padEnd(text.length + (text.length % 4), "=");
base64urlTobase64("8J-mhA"); // 8J+mhA==
Base64 會增加資料的大小
常見的誤解認為 base64
可以將資料壓縮,
但是事實上,base64
會增加資料的大小。
根據 RFC 4648,
base64
裡的一個字元只能替代原始資料的 6 個位元,
但一個字元是卻是 8 個位元組成的。
所以,假設我們有筆資料長度為 3 個 bytes (3x8 bits = 24 bits)
,
base64
就會將資料拆分成 4 個 6-bit 為一組 (4x6 bits = 24 bits)
,
而一個字元是由 8 個位元組成的 (4x8 bits = 32 bits)
,
32 / 24 = 1.333
,
也就是說 base64
大概會增加 33.3% 的資料大小 (a.k.a 大概大了 1/3
)。
無論任何資料形式,都是由二進位資料組成的,計算大小的單位都是位元 (bits),
一個 bit 只能儲存0
或1
。
Base64 去除不可視字元
這個說法其實倒果為因了,
應該說它在設計階段就刻意避開了非英文單字 (Non-Alphabet) 的字元。
這樣做可以在理論上避開的兩個資安相關的風險:
但本文主要是在講 base64
的 encode/decode,在此就不多做說明。
JavaScript 中的 Base64
btoa 跟 atob
在 JavaScript 中,我們可以透過 btoa 跟 atob 來進行 base64
的 encode 跟 decode。
btoa 就是 binary to ascii
的縮寫,用來將字串轉換成 base64
。
btoa("hello");
// 'aGVsbG8='
atob 就是 ascii to binary
的縮寫,就是用來將 base64
轉換回字串。
atob("aGVsbG8=");
// 'hello'
但這兩個函式其實並沒有辦法完全將任意的字串轉換成 base64
,
他們能處理的只有 0x00
到 0xFF
這個範圍而已,
所以像是拋入 🦄 這類超過範圍的字元就會拋出 InvalidCharacterError。
btoa("🦄");
// Uncaught DOMException: Failed to execute 'btoa' on 'Window':
// The string to be encoded contains characters outside of the Latin1 range.
這兩個函式並沒有支援轉換任意二進位資料的原因是
因為他們誕生的比 web 平台支援二進制資料型別的時間還早,
早期並沒有人覺得 web 平台需要支援二進制資料型別。
在現今大部分平台都支援 UTF-8 這個範圍字元集的情況下,
直接使用 btoa 跟 atob 來進行 base64
的 encode 跟 decode 其實並不是很理想。
Encoding API
Encoding API 被設計用於處理各種字元編碼轉換,包括 UTF-8 範圍字元集,
以下介紹 Encoding API 中的 TextEncoder 跟 TextDecoder。
TextEncoder 主要用於將字串轉換成二進位資料,
const encoder = new TextEncoder();
const encoded = encoder.encode("🦄");
console.log(encoded);
// Uint8Array(4) [240, 159, 166, 132]
Uint8Array 是一種固定長度的陣列,每格由 8 個位元組成的,
是 JavaScript 處理二進位資料的一種資料型別。
TextDecoder 則用於將 二進位資料轉換成字串,
const decoder = new TextDecoder();
const decoded = decoder.decode(new Uint8Array([240, 159, 166, 132]));
console.log(decoded);
// 🦄
上面雖然可以將 UTF-8 範圍字元集的字串轉換成二進位資料,但是還不是 base64
。
但搭配 btoa, atob 跟 String.fromCodePoint 就可以進行 base64
的 encode 跟 decode 了。
用 🦄 舉個例子,我們要將其轉換成 base64
,整個轉換邏輯演算如下:
- 將 UTF-8 字元轉換成二進位,讓 🦄 展現原貌變成
Uint8Array(4)
。
( 🦄 =>Uint8Array(4)[240, 159, 166, 132]
)
這時,單看每格裏面的數值已落在0x00
到0xFF
的範圍了。
(Uint8
的範圍 0 ~ 255 就是0x00
~0xFF
) - 將每格裡面的數值拆出來,個別轉換成文字。
(Uint8Array(4)
=>[240, 159, 166, 132]
)
(String.fromCodePoint(240)
=>ð
)
(String.fromCodePoint(159)
=>\x9F
)
(String.fromCodePoint(166)
=>¦
)
(String.fromCodePoint(132)
=>\x84
) - 因為分別轉換的文字已經落在
0x00
到0xFF
的範圍了 (0 ~ 255),
所以可以透過 btoa 來進行base64
的 encode。
(btoa("ð\x9F¦\x84")
=>8J+mhA==
)
const encoder = new TextEncoder();
const encoded = encoder.encode("🦄");
const binString = String.fromCodePoint(...encoded);
const base64 = btoa(binString);
console.log(base64);
// 8J+mhA==
反之,要將 base64
轉換回 🦄,整個轉換邏輯演算如下:
- 透過 atob 將
base64
轉換成個別文字。
(atob('8J+mhA==')
=> "ð\x9F¦\x84" ) - 將個別文字資料轉換成
Uint8Array(4)
。
( "ð\x9F¦\x84" =>Uint8Array(4)[240, 159, 166, 132]
) - 再將
Uint8Array(4)
透過 TextDecoder 轉換成 🦄。
(Uint8Array(4)[240, 159, 166, 132]
=> 🦄 )
const binString = atob("8J+mhA==");
const bytes = Uint8Array.from(binString, (char) => char.codePointAt(0));
const decoder = new TextDecoder();
const decoded = decoder.decode(bytes);
console.log(decoded);
// 🦄
在現今大部分系統都支援 UTF-8 範圍字元集的情況下,
在處理文字與二進位資料轉換的時候,
使用上述方法來進行 base64
的 encode 跟 decode 會比 btoa 跟 atob 更理想。
轉換任意二進制資料
在 Encoding 中提到的方法其實就可以用在任意二進位資料的轉換上,
const byteArray = new Uint8Array([240, 159, 166, 132]);
const binString = String.fromCodePoint(...byteArray);
const base64 = btoa(binString);
除此之外,我們也可以透過 FileReader.readAsDataURL() 來將檔案轉換成 base64
,
readAsDataURL 會回傳 data:URL 而它的內容 就是 base64
。
function bytesToBase64DataUrl(bytes, type) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(new File([bytes], "", { type }));
});
}
const bytes = new Uint8Array([240, 159, 166, 132]);
const dataUrl = await bytesToBase64DataUrl(bytes, "text/plain");
// 'data:text/plain;base64,8J+mhA=='
// regexp match dataurl base64 content
const dataUrlRegex = /^data:(.+\/.+);base64,(.*)$/;
const extractBase64 = (url) => dataUrlRegex.exec(url)?.[2] ?? "";
const base64 = extractBase64(dataUrl);
// '8J+mhA=='
Node.js 的 Buffer
在 Node.js,暫時無法使用 Encoding API,
但是 Encoding API 是 WHATWG 提出的標準規格,
在未來應該會被 Node.js 支援。 (此文章發布時,Node.js LTS v18.18.0)
所以在 Node.js 中,我們可以透過 Buffer 來進行 base64
的 encode 跟 decode。
const encoded = Buffer.from("🦄").toString("base64");
console.log(encoded);
// 8J+mhA==
const decoded = Buffer.from("8J+mhA==", "base64").toString();
console.log(decoded);
// 🦄