Skip to content
~/notes
Go back

ChaCha20-Poly1305 加密演算法介紹

11 min read

簡介

ChaCha20 是一種對稱加密演算法,由 Daniel J. Bernstein 在 2008 年提出。它是基於 Salsa20 的改進版本。

而ChaCha20-Poly1305 是一種 AEAD(Authenticated Encryption with Associated Data)加密模式,結合了 ChaCha20 加密演算法與 Poly1305 認證碼(Message Authenticate Code),用於提供機密性和完整性保護。

ChaCha20-Poly1305 的特性

ChaCha20-Poly1305 有以下的特性:

stream cipher 跟 block cipher 的差異
  • Stream Cipher(串流加密) 是一次處理 1 個 Bit 或 1 個 Byte,將明文與逐字生成的金鑰流(Keystream)進行 XOR 運算;
  • Block Cipher(區塊加密) 則是將明文切成固定大小的區塊(如 128 bits),再整塊進行複雜的混淆與代換,如果明文長度不是區塊大小的整數倍,則需要填充(Padding)來補足。

ChaCha20-Poly1305 的應用

目前,ChaCha20-Poly1305 已被廣泛應用於網路協議中,例如:


ChaCha20-Poly1305 的運作原理可以分為兩個部分:加密和認證。
首先我們先來看加密的部分:

ChaCha20 的運作原理

1. 初始化狀態矩陣

ChaCha20 使用一個 4x4 的狀態矩陣(共 16 個 32-bit 的 word),初始化如下:

(c0c1c2c3k0k1k2k3k4k5k6k7bn0n1n2)\begin{pmatrix} c_0 & c_1 & c_2 & c_3 \\ k_0 & k_1 & k_2 & k_3 \\ k_4 & k_5 & k_6 & k_7 \\ b & n_0 & n_1 & n_2 \end{pmatrix}

其中:

nonce 的重要性

nonce 必須是唯一的,否則會導致加密安全性下降。因為 ChaCha20 屬於Stream Cipher,其運作原理是利用 Key 與 Nonce 經由演算法生成一段偽隨機的Keystream,再將Keystream與明文進行 XOR 來產生密文。

如果在相同的 Key 下重複使用相同的 Nonce(Nonce Reuse),將會產生一模一樣的Keystream。攻擊者只要取得兩份用相同 Nonce 加密的密文 C1C_1 與 C2C_2:

C1=P1⊕KC_1 = P_1 \oplus K C2=P2⊕KC_2 = P_2 \oplus K

兩者進行 XOR 後, KK 就會被抵銷掉:

C1⊕C2=(P1⊕K)⊕(P2⊕K)=P1⊕P2C_1 \oplus C_2 = (P_1 \oplus K) \oplus (P_2 \oplus K) = P_1 \oplus P_2

這會直接洩漏兩份明文相互 XOR 的結果(P1⊕P2P_1 \oplus P_2)。攻擊者便能利用頻率分析、已知明文攻擊(Known-Plaintext Attack)或語言統計特性,極容易地還原出原始明文內容,甚至篡改訊息。因此 Nonce 絕對不能重複使用(Nonce 即代表 Number used once)。

我們接下來將這個矩陣的元素位置用以下的方式標記:

(0123456789101112131415)\begin{pmatrix} 0 & 1 & 2 & 3 \\ 4 & 5 & 6 & 7 \\ 8 & 9 & 10 & 11 \\ 12 & 13 & 14 & 15 \end{pmatrix}

2. Quarter Round 函數

我們定義一個Quarter Round 函數(QRQR),它接受四個 32-bit 的字作為輸入,並對他們進行混合/混淆操作:

QR(a,b,c,d):a←a+b,d←(d⊕a)⋘16c←c+d,b←(b⊕c)⋘12a←a+b,d←(d⊕a)⋘8c←c+d,b←(b⊕c)⋘7\begin{aligned} &QR(a, b, c, d): \\ &a \leftarrow a + b, && d \leftarrow (d \oplus a) \lll 16 \\ &c \leftarrow c + d, && b \leftarrow (b \oplus c) \lll 12 \\ &a \leftarrow a + b, && d \leftarrow (d \oplus a) \lll 8 \\ &c \leftarrow c + d, && b \leftarrow (b \oplus c) \lll 7 \end{aligned}
Note
  • <<< 表示左旋轉(Left Rotate),例如 1101 <<< 2 會變成 0111
  • 這邊的加法都是在 mod 2322^{32} 下

圖片說明如下:

QR 函數的運作示意圖

C語言實現如下:

void QuarterRound(uint32_t *a, uint32_t *b, uint32_t *c, uint32_t *d) {
    *a += *b; *d ^= *a; *d = (*d << 16) | (*d >> (32 - 16));
    *c += *d; *b ^= *c; *b = (*b << 12) | (*b >> (32 - 12));
    *a += *b; *d ^= *a; *d = (*d << 8) | (*d >> (32 - 8));
    *c += *d; *b ^= *c; *b = (*b << 7) | (*b >> (32 - 7));
}

3. 20 輪變換

ChaCha20 名字中的 “20” 代表它會對狀態矩陣進行 20 輪的變換,每一輪包含四個 Quarter Round 操作,分別作用於不同的列和對角線。

奇數輪作直行變換(Column Round)

QR(0,4,8,12)QR(1,5,9,13)QR(2,6,10,14)QR(3,7,11,15)\begin{aligned} &\text{QR}(0, 4, 8, 12) \\ &\text{QR}(1, 5, 9, 13) \\ &\text{QR}(2, 6, 10, 14) \\ &\text{QR}(3, 7, 11, 15) \end{aligned}

偶數輪作對角線變換(Diagonal Round)

QR(0,5,10,15)QR(1,6,11,12)QR(2,7,8,13)QR(3,4,9,14)\begin{aligned} &\text{QR}(0, 5, 10, 15) \\ &\text{QR}(1, 6, 11, 12) \\ &\text{QR}(2, 7, 8, 13) \\ &\text{QR}(3, 4, 9, 14) \end{aligned}

4. 最終輸出

最後把 20 輪變換後的狀態與初始狀態逐 word 相加,得到 ChaCha20 的輸出。

extStatefinal[i]=(State20−rounds[i]+Stateinitial[i]) mod 232,for i=0,1,…,15\begin{aligned} ext{State}_{final}[i] &= \bigl(\text{State}_{20-rounds}[i] + \text{State}_{initial}[i]\bigr) \bmod 2^{32}, \\ &\quad \text{for } i = 0, 1, \dots, 15 \end{aligned}
為什麼要加回初始狀態?

這是 ChaCha20 的 feed-forward 步驟,用來避免整個 20 輪變換只是單純的可逆置換。

之後將最終狀態矩陣轉換為 64 個Bytes的 Keystream,並與明文進行 XOR 運算,產生密文。


Poly1305 的運作原理

Poly1305 是一種訊息認證碼(MAC)演算法,由 Daniel J. Bernstein 在 2005 年提出。它的主要目的是確保訊息的完整性和來源的真實性。

Poly1305 的運作原理可以分為兩個部分:生成認證碼和驗證認證碼。

生成認證碼

Poly1305 是在一個有限域(finite field) 上運作的,使用一個 130-bit 的質數 p=2130−5p = 2^{130} - 5。它接受兩個輸入:訊息 MM 和一個 256-bit 的金鑰 KK,其中 KK 被分成兩個部分:rr 和 ss。兩個部分都是128-bit 的數字:

分割訊息

將訊息分成最多 16 Bytes 的區塊。每個區塊以 little-endian 方式視為整數,並在每個區塊的資料末端上附加一個 0x01,因此每個區塊會被視為一個最多 129-bit 的整數。

Example

以”ABC”為例,ASCII 編碼為 0x41, 0x42, 0x43,附加 0x01 後變成 0x41, 0x42, 0x43, 0x01,視為整數為 0x01434241

多項式求值

設訊息被切成 qq 個區塊,對應整數為 n1,n2,…,nqn_1, n_2, \dots, n_q。Poly1305 會維護一個累加器 aa(初始為 0),每次處理一個區塊:

a←(a+ni)⋅r mod p,p=2130−5a \leftarrow (a + n_i) \cdot r \bmod p, \quad p = 2^{130}-5

全部區塊處理完後,再把 ss 加上去並截成 128-bit:

tag=(a+s) mod 2128\text{tag} = (a + s) \bmod 2^{128}

也就是說,Poly1305 本質上是在有限域上計算一個多項式:

tag=(∑i=1qniri)+s(mod2128)\text{tag} = \left(\sum_{i=1}^{q} n_i r^i\right) + s \pmod{2^{128}}
為什麼每個區塊要附加 0x01?

這是為了讓不同長度但前綴相同的區塊序列不會映射到同一個多項式值,避免模糊邊界造成碰撞風險。

驗證認證碼

接收端拿到訊息 MM 與 tag\text{tag} 後,會用同一把一次性金鑰(同一組 r,sr,s)重新計算一次 Poly1305:

  1. 對 MM 做同樣的分塊與附加 0x01。
  2. 依序更新累加器並得到新的 tag′\text{tag}'。
  3. 以常數時間比較 tag′\text{tag}' 與收到的 tag\text{tag}。

若相同則驗證成功;否則代表資料可能被竄改或金鑰不正確。

為什麼要常數時間比較?

若用一般字串比較,可能因為「第一個不相等的位置」提早返回,洩漏時間差資訊,進而被逐 byte 猜測 tag。


ChaCha20-Poly1305 的整合運作 (RFC 8439)

根據 RFC 8439,ChaCha20-Poly1305 的運作流程如下:

加密與生成tag

  1. 生成金鑰與 nonce:
    • 使用 256-bit 的金鑰 KK。
    • 使用 96-bit 的 nonce NN(不可重複)。
  2. 生成 Poly1305 金鑰:
    • 使用 ChaCha20 以 nonce NN 和 block counter = 0 生成第一個 32 Bytes 的 keystream,前 16 Bytes 作為 rr,後 16 Bytes 作為 ss。後面32 Bytes 不使用。
為什麼要用 ChaCha20 生成 Poly1305 金鑰?

這是為了確保 Poly1305 的金鑰是一次性的,避免重複使用同一把金鑰,從而增強安全性。

r 的clamp

為了防止攻擊者利用 Poly1305 的數學特性進行攻擊,r 需要進行 clamp 操作: 將 r 的 128-bit 二進位表示中:第 3、7、11、15 個 bit 設為 0

  1. 加密明文:
    • 使用 ChaCha20 以 nonce NN 和 block counter = 1 開始生成 keystream,與明文進行 XOR 運算,產生密文。
  2. 組裝AAD:
    • AAD(Associated Data)是額外的資料,會被 Poly1305 認證,但不會被加密。AAD 可以是任何長度的資料。
AAD 的組成

AAD (Additional Authenticated Data,附加驗證資料) 是 AEAD 機制中「僅驗證真實性與完整性、不進行加密」的明文資料。

  1. 實務上的內容組成 (Protocol Headers & Metadata)

在網路傳輸協定(如 TLS 1.3、QUIC)中,AAD 通常包含不宜加密但必須防範竄改與重放攻擊的資料:

  • 網路標頭資訊:來源/目的 IP 位址、Port 號碼、標頭 Flag。

  • 控制與狀態資訊:封包序號 (Sequence Number)、Session ID、協定版本號。

  • 結構與長度:訊息長度 (Length)、Payload 類型。

  1. RFC 8439 規範下的計算結構組成 (Poly1305 Input)

在 ChaCha20-Poly1305 構建 mac_data 輸入給 Poly1305 計算 MAC Tag 時,AAD 在位元組串流中的物理組成如下:

  • AAD 原始資料:長度範圍為 00 至 264−12^{64}-1 位元組。
  • AAD Padding:若長度非 16 位元組倍數,末端以 0x00 補滿至 16 位元組對齊(補 0∼150 \sim 15 位元組)。
  • aad_len 標記:於 Poly1305 輸入串流末尾,包含一個以 64 位元無符號小端序 (uint64_le) 編碼的 8 位元組欄位,記錄 AAD 的原始未填充長度。
  1. 計算 Poly1305 認證碼:

解密與驗證tag

  1. 生成 Poly1305 金鑰:
    • 與加密時相同,使用 ChaCha20 以 nonce NN 和 block counter = 0 生成 Poly1305 金鑰。
  2. 驗證 Poly1305 認證碼:
    • 將收到的 AAD、密文以及它們的長度組裝成訊息,使用 Poly1305 計算認證碼,並與收到的 tag 比較。
    • 若驗證失敗,則拒絕解密並丟棄資料。
  3. 解密密文:
    • 若驗證成功,使用 ChaCha20 以 nonce NN 和 block counter = 1 生成 keystream,與密文進行 XOR 運算,還原出明文。
為甚麼要先驗證 tag 再解密?

為了防止攻擊者利用解密過程中的錯誤訊息或時間差,進行側信道攻擊(Side-channel Attack)。先驗證 tag 可以確保資料的完整性與真實性,避免在不安全的情況下進行解密操作。

tag 的偽造成功率

Poly1305 的 tag 是 128-bit,所以如果攻擊者只是隨機猜一個 tag,單次猜中的機率大約是:

Pr⁡[guess success]≈12128\Pr[\text{guess success}] \approx \frac{1}{2^{128}}

這代表在不知道金鑰的情況下,要直接偽造出一個可通過驗證的 tag 幾乎是不可能的。不過在真實安全分析裡,這個數字通常還要考慮訊息長度與查詢次數。對於長度為 qq 個區塊的訊息,RFC 8439 給出的偽造成功率上界大致可寫成:

Pr⁡[forge]≤q2106\Pr[\text{forge}] \le \frac{q}{2^{106}}

這個上界比 2−1282^{-128} 大,原因是 Poly1305 的 rr 會經過 clamp,而訊息越長,攻擊者可利用的結構也越多。因此,Poly1305 的安全性不是依賴完全不能偽造,而是依賴偽造成本高到可以忽略


參考資料


Share this post:

Next Post
SSL/TLS 憑證簡介