---
title: "Encoding Hashing Encryption"
url: "https://laigary.com/interview/system-design/encoding-hashing-encryption"
type: "note"
section: "system-design"
date: "2026-08-10"
updated: "2026-08-10"
---

# Encoding Hashing Encryption

這三個詞在後端和系統設計面試裡出現的頻率很高，而且很常被混用。最常見的是把「密碼加密存起來」講成加密，但那件事該做的其實是雜湊。

會踩到它們的題目也滿固定的：短網址要用什麼編碼、使用者密碼怎麼存、webhook 或 API token 怎麼簽章、主鍵要用什麼 ID。這幾題的難處通常不在演算法本身，而在有沒有辦法一句話講清楚這三個東西是在解不同的問題。

|  | 可逆？ | 需要金鑰？ | 目的 | 典型場景 |
| --- | --- | --- | --- | --- |
| Encoding（Base64／Base62／hex） | 可以，任何人都能還原 | 否 | 換一種表示法，方便傳輸或儲存 | 短網址、附件、把二進位塞進 JSON |
| Hashing（SHA-256／bcrypt） | 不行，單向而且有損 | 否 | 指紋、比對、完整性 | 密碼儲存、檔案校驗、分片 |
| Encryption（AES／RSA） | 可以，但要金鑰 | 是 | 保密 | 傳輸中的資料、靜態資料 |

要先講清楚的是 encoding 不是安全機制。Base64 誰都解得開，它解決的是「這段位元組能不能安全地放進一個只吃文字的通道」，跟保密完全沒有關係。看到有人說某個東西「有經過 Base64 編碼所以很安全」，那就是搞錯了。

## Encoding

### Base64

最常見的一個。Email 附件、把圖片塞進 HTML、把二進位放進 JSON，用的都是它。字元集是 A–Z、a–z、0–9 再加上 `+` 和 `/`，總共 64 個，另外有一個補位用的 `=`。

規則是每 3 個 byte 換成 4 個字元。因為 3 個 byte 是 24 bit，剛好可以切成 4 段 6 bit，而 6 bit 剛好能表示 64 個值。拿 `Man` 來看，先把三個字元攤成 24 個 bit：

|  | M | a | n |
| --- | --- | --- | --- |
| ASCII | 77 | 97 | 110 |
| 二進位 | `01001101` | `01100001` | `01101110` |

然後把這 24 個 bit 重新切成 4 段。這裡的切點跟原本的 byte 邊界是錯開的，那大概是整個 Base64 唯一需要動腦的地方：

| 6 bit 區塊 | `010011` | `010110` | `000101` | `101110` |
| --- | --- | --- | --- | --- |
| 十進位 | 19 | 22 | 5 | 46 |
| 查表 | `T` | `W` | `F` | `u` |

所以 `Man` 會變成 `TWFu`。

因為 `+` 和 `/` 在網址裡有特殊意義，要放進 URL 或 cookie 的時候得用 Base64URL 這個變體，把 `+` 換成 `-`、`/` 換成 `_`，通常也把 `=` 省掉。JWT 用的就是這個版本。

### Base62

字元集是 0–9、A–Z、a–z。說穿了就是 Base64 拿掉 `+` 和 `/`，所以天生就是 URL-safe，也不用補位。短網址和對外的 ID 幾乎都選它。

換算方式就是一直除以 62 取餘數，最後把餘數反過來讀。以十進位的 125 為例：

| 步驟 | 算式 | 商 | 餘數 |
| --- | --- | --- | --- |
| 1 | 125 ÷ 62 | 2 | 1 |
| 2 | 2 ÷ 62 | 0 | 2 |

餘數反過來是 2、1，查表得到 `21`。解回去驗算 $(2 \times 62) + 1 = 125$。

### 其他幾種

Base58 是 Base62 再拿掉 `0`、`O`、`I`、`l` 這幾個看起來會混淆的字元，目的是讓人手抄的時候不會抄錯，比特幣的錢包地址就是這樣來的。

Base36 只有 0–9 和 A–Z，好處是大小寫不敏感，適合那種要念出來、或是要用鍵盤敲進去的短代碼。

Base32 更保守，只用 A–Z 和 2–7，一樣大小寫不敏感而且容錯性好，所以 QR code、DNS、還有 2FA 的 TOTP 金鑰都用它。

Base85 是這裡面最省空間的，代價是幾乎沒辦法讀，只有 PostScript 和少數壓縮工具在用。

### 到底省多少

編碼一定會讓資料變大，差別只在變大多少。每個 byte 需要幾個字元就是 $8 / \log_2(\text{base})$：

| 編碼 | 每 byte 需要的字元數 | 膨脹 |
| --- | --- | --- |
| Base16（hex） | 2.000 | +100% |
| Base32 | 1.600 | +60% |
| Base36 | 1.547 | +55% |
| Base58 | 1.366 | +37% |
| Base62 | 1.344 | +34% |
| Base64 | 1.333 | +33% |
| Base85 | 1.248 | +25% |

這裡有個常見的誤傳要修正。很多文章會說 Base85 比 Base64 小 25%，但那個 25% 是 Base85 相對於原始資料的膨脹率，不是它跟 Base64 的差距。實際算一下 $1.25 / 1.333$ 大約是 0.938，也就是只小了 6% 左右。要多付出可讀性和實作複雜度才換到這 6%，大概就是它一直沒有普及的原因。

另外一件從這張表看得出來的事情是 Base62 和 Base64 幾乎一樣長，1.344 對 1.333。所以為了 URL-safe 從 Base64 換成 Base62，長度上等於不用付代價，短網址選它就是這個理由。

### 什麼時候用哪一個

| 場景 | 選擇 | 理由 |
| --- | --- | --- |
| 短網址 | Base62 或 Base58 | 短、可讀、URL-safe、不用補位 |
| Email 附件（MIME） | Base64 | 把二進位安全地放進純文字協定，標準支援 |
| API 或 JSON 裡的二進位 | Base64URL | 放進網址或 header 不會被特殊字元弄壞 |
| 對外的訂單或使用者 ID | Base62 或 Base36 | 短、不連號、好念好抄 |
| 區塊鏈地址 | Base58 | 排除視覺上容易混淆的字元 |
| QR code、2FA 金鑰 | Base32 | 大小寫不敏感、容錯性好 |
| 追求傳輸效率的管線 | Base85 | 最省，但只有在機器對機器的時候才划算 |

## Hashing

雜湊在面試裡其實是兩個不太相干的題目，連用的函式都不一樣。一種是拿它當指紋，看檔案有沒有被改過、兩份內容是不是一樣、或者決定資料要分到哪一個分片，這一類要快。另一種是拿它存密碼，這一類要慢。把第一類的函式拿去做第二類，是最經典的錯誤。

### 常見的雜湊函式

| 函式 | 輸出長度 | 現況 |
| --- | --- | --- |
| MD5 | 128 bits | 已破，碰撞可以在幾秒內造出來，只能當非安全用途的 checksum |
| SHA-1 | 160 bits | 已破，2017 年 SHAttered 造出了真實的碰撞，已經淘汰 |
| SHA-2（SHA-256／512） | 256／512 bits | 目前的主流，沒有已知的實用攻擊 |
| SHA-3 | 可變 | 結構跟 SHA-2 完全不同，當作 SHA-2 萬一被破的備案 |
| BLAKE3 | 可變 | 非常快，適合大檔案校驗，但不是 NIST 標準 |

講「已破」的時候要說清楚破的是什麼。MD5 和 SHA-1 被破的是碰撞攻擊，也就是可以造出兩份雜湊值相同的不同內容，並不是可以從雜湊值反推回原文。不過對簽章和憑證那種場景來說，能造出碰撞就已經足夠致命了。

### 密碼不能用一般的雜湊

用 SHA-256 存密碼看起來很合理，反正它不可逆。問題不是它不安全，是它太快，而快本來就是它被設計出來的目的。這個優點放到密碼上就變成災難：現代 GPU 一秒可以算幾十億次，拿一份常見密碼的字典跑一輪，幾分鐘就能把大部分的人還原出來。

所以密碼要用刻意設計得慢的那幾個。bcrypt 算老牌，有 cost 參數可以調慢；scrypt 除了慢還刻意吃記憶體，讓 GPU 和 ASIC 難以平行化；Argon2id 是 2015 年 Password Hashing Competition 的冠軍，新專案大概直接選它就好。

它們都會處理 salt，每個使用者一組隨機值，所以兩個人用同樣的密碼也會存出不同的結果，彩虹表就沒用了。cost 則是留給未來的，硬體變快就把它調高，不用換演算法。慢在這裡是特性不是缺點。

### HMAC

一般的雜湊誰都算得出來，所以它只能證明內容沒被改過，不能證明是誰送來的。HMAC 的作法是把一把只有雙方知道的金鑰混進雜湊裡，這樣就同時證明了兩件事。GitHub 和 Stripe 送 webhook 過來時的簽章、JWT 的 HS256、各種 API 請求簽名，用的都是它。

它是雜湊不是加密，收到的人做的事情是拿同樣的金鑰重算一次然後比對，而不是把它解開來看。

### 順帶一提

系統設計裡常出現的 consistent hashing 跟這一節的其他東西沒什麼關係，它只是拿雜湊來做分片，取的是分布均勻這個性質，跟安全無關。放在一起講很容易讓人搞混。

## Encryption

|  | 金鑰 | 速度 | 代表 | 主要用途 |
| --- | --- | --- | --- | --- |
| 對稱加密 | 加解密同一把 | 快 | AES-256 | 真正拿來加大量資料 |
| 非對稱加密 | 公鑰私鑰一對 | 慢很多 | RSA、ECC（X25519、Ed25519） | 交換金鑰、數位簽章 |

對稱加密的模式建議直接用 AES-GCM，因為它是 authenticated encryption，機密性和完整性一起顧到。用 CBC 的話得自己再接一個 MAC，那一步很容易做錯。

非對稱有兩個方向，不要搞混。公鑰加密、私鑰解密是為了保密，只有拿著私鑰的人看得到內容；私鑰簽名、公鑰驗證則是為了證明來源，誰都可以驗，但只有私鑰持有者簽得出來。

### 實務上都是混合的

非對稱太慢，所以真實系統不會拿它來加大量資料。TLS 的做法是先用非對稱協商出一把臨時的對稱金鑰，之後整條連線的資料都用這把對稱金鑰加密。

靜態資料也是同一套，叫做信封加密。資料本身用一把隨機產生的資料金鑰加密，這把資料金鑰再用 KMS 裡的主金鑰加密之後存在旁邊。這樣要輪替主金鑰的時候，只要重新加密那些很短的資料金鑰就好，不用把所有資料重新加密一次。

### 傳輸中和靜態要分開回答

面試問加密通常會分成兩個面向。傳輸中就是 TLS，基本上是預設答案；靜態則是磁碟或儲存層的加密，或者敏感欄位在應用層就先加密起來。要分開講是因為它們防的威脅不一樣，前者防的是路上被竊聽，後者防的是儲存媒介或備份外流。

### 真正麻煩的是金鑰

演算法都是現成的，實務上會出事的幾乎都在金鑰上：放在哪裡（KMS 或 HSM，不是原始碼裡）、多久輪替一次、外洩之後怎麼撤銷、誰有權限拿。被問到加密卻只答得出演算法名字，會顯得沒有真的做過。

## UUID 為什麼常跟前面混在一起講

很多時候我看到會有人把上面的內容跟 UUID 放在一起講，但是它們並不是同一件事。UUID 回答的是怎麼產生一個唯一的值，encoding 回答的是這個值要怎麼寫成字串。

會混在一起是因為同一個 UUID 可以用不同的編碼表示，長度差滿多的：

| 表示法 | 長度 |
| --- | --- |
| hex 加上連字號，也就是平常看到的樣子 | 36 字元 |
| hex | 32 字元 |
| Base32 | 26 字元 |
| Base62 或 Base64URL | 22 字元 |

短網址那種題目會同時碰到這兩件事，既要唯一又要短，所以兩個主題就被綁在一起講了。分開來看的話它們是兩層，先決定要產生什麼值，再決定怎麼表示它。

### 常見的版本

| 版本 | 組成 | 特性 |
| --- | --- | --- |
| v1 | 時間戳加上網卡 MAC | 可以排序，但會洩漏機器資訊和產生時間 |
| v4 | 122 bits 隨機 | 最常見，不洩漏資訊，但完全無序 |
| v7 | 毫秒時間戳前綴加上隨機 | 2024 年 RFC 9562 納入標準，可以排序又不洩漏機器資訊 |

### v4 當主鍵會痛在哪裡

這大概是實務上最值得記住的一點。v4 是隨機的，所以每次插入都會落在 B-tree 索引的隨機位置，造成頁面分裂、索引膨脹、快取命中率變差，資料量一大寫入效能就會明顯掉下來。

自增整數沒有這個問題，它永遠插在尾端，但它會洩漏你有多少筆資料，而且很容易被猜。

v7 就是為了同時解決這兩件事。前綴是毫秒時間戳，所以整體是遞增的、對索引友善；後面接隨機，所以猜不到也不會洩漏機器資訊。新專案要用 UUID 當主鍵的話，現在應該直接選 v7。

### 相近的東西

ULID 的想法跟 v7 一樣，時間戳加隨機，一樣是 128 bit，但它規定要用 Crockford Base32 編成 26 個字元，所以字串本身就可以直接拿來排序。

Snowflake 是 Twitter 的做法，只有 64 bit，切成時間戳、機器編號和序號三段。長度只有 UUID 的一半而且嚴格遞增，代價是需要協調機器編號，也就是得有一套發號的基礎設施。
