寫給開發人員的實用密碼學(六)—— 對稱金鑰加密演算法

於清樂發表於2022-03-14

本文主要翻譯自 Practical-Cryptography-for-Developers-Book,筆者補充了部分程式碼示例。

《寫給開發人員的實用密碼學》系列文章目錄:

零、術語介紹

兩個常用動詞:

  • 加密:cipher 或者 encrypt
  • 解密:decipher 或者 decrypt

另外有幾個名詞有必要解釋:

  • cipher: 指用於加解密的「密碼演算法」,有時也被直接翻譯成「密碼」
  • cryptographic algorithm: 密碼學演算法,泛指密碼學相關的各類演算法
  • ciphertext: 密文,即加密後的資訊。對應的詞是明文 plaintext
  • password: 這個應該不需要解釋,就是我們日常用的各種字元或者數字密碼,也可稱作口令。
  • passphrase: 翻譯成「密碼片語」或者「密碼片語」,通常指用於保護金鑰或者其他敏感資料的一個 password
    • 如果你用 ssh/gpg/openssl 等工具生成或使用過金鑰,應該對它不陌生。

在密碼學裡面,最容易搞混的詞估計就是「密碼」了,cipher/password/passphrase 都可以被翻譯成「密碼」,需要注意下其中區別。

一、什麼是對稱加密

在密碼學中,有兩種加密方案被廣泛使用:「對稱加密」與「非對稱加密」。

對稱加密是指,使用相同的金鑰進行訊息的加密與解密。因為這個特性,我們也稱這個金鑰為「共享金鑰(Shared Secret Key)」,示意圖如下:

現代密碼學中廣泛使用的對稱加密演算法(ciphers)有:AES(AES-128、AES-192、AES-256)、ChaCha20、Twofish、IDEA、Serpent、Camelia、RC6、CAST 等。
其中絕大多數都是「塊密碼演算法(Block Cipher)」或者叫「分組密碼演算法」,這種演算法一次只能加密固定大小的塊(例如 128 位);
少部分是「流密碼演算法(Stream Cipher)」,流密碼演算法將資料逐位元組地加密為密文流。

通過使用稱為「分組密碼工作模式」的技術,可以將「分組密碼演算法」轉換為「流密碼演算法」。

量子安全性

即使計算機進入量子時代,仍然可以沿用當前的對稱密碼演算法。因為大多數現代對稱金鑰密碼演算法都是抗量子的quantum-resistant),這意味當使用長度足夠的金鑰時,強大的量子計算機無法破壞其安全性。
目前來看 256 位的 AES/Twofish 在很長一段時間內都將是 量子安全 的。

二、對稱加密方案的結構

我們在第一章「概覽」裡介紹過,單純使用資料加密演算法只能保證資料的安全性,並不能滿足我們對訊息真實性、完整性與不可否認性的需求,因此通常我們會將對稱加密演算法跟其他演算法組合成一個「對稱加密方案」來使用,這種多個密碼學演算法組成的「加密方案」能同時保證資料的安全性、真實性、完整性與不可否認性。

一個分組加密方案通常會包含如下幾種演算法:

  • 將密碼轉換為金鑰的金鑰派生演算法 KDF(如 Scrypt 或 Argon2):通過使用 KDF,加密方案可以允許使用者使用字元密碼作為「Shared Secret Key」,並使密碼的破解變得困難和緩慢
  • 分組密碼工作模式(用於將分組密碼轉換為流密碼,如 CBC 或 CTR)+ 訊息填充演算法(如 PKCS7):分組密碼演算法(如 AES)需要藉助這兩種演算法,才能加密任意大小的資料
  • 分組密碼演算法(如 AES):使用金鑰安全地加密固定長度的資料塊
    • 大多數流行的對稱加密演算法,都是分組密碼演算法
  • 訊息認證演算法(如HMAC):用於驗證訊息的真實性、完整性、不可否認性

而一個流密碼加密方案本身就能加密任意長度的資料,因此不需要「分組密碼模式」與「訊息填充演算法」。

如 AES-256-CTR-HMAC-SHA256 就表示一個使用 AES-256 與 Counter 分組模式進行加密,使用 HMAC-SHA256 進行訊息認證的加密方案。
其他流行的對稱加密方案還有 ChaCha20-Poly1305 和 AES-128-GCM 等,其中 ChaCha20-Poly130 是一個流密碼加密方案。我們會在後面單獨介紹這兩種加密方案。

三、分組密碼工作模式

前面簡單介紹了「分組密碼工作模式」可以將「分組密碼演算法」轉換為「流密碼演算法」,從而實現加密任意長度的資料,這裡主要就具體介紹下這個分組密碼工作模式(下文簡稱為「分組模式」或者「XXX 模式」)。

加密方案的名稱中就帶有具體的「分組模式」名稱,如:

  • AES-256-GCM - 具有 256 位加密金鑰和 GCM 分組模式的 AES 密碼
  • AES-128-CTR - 具有 128 位加密金鑰和 CTR 分組模式的 AES 密碼
  • Serpent-128-CBC - 具有 128 位加密金鑰和 CBC 分組模式的 Serpent 密碼

「分組密碼工作模式」背後的主要思想是把明文分成多個長度固定的組,再在這些分組上重複應用分組密碼演算法進行加密/解密,以實現安全地加密/解密任意長度的資料。

某些分組模式(如 CBC)要求將輸入拆分為分組,並使用填充演算法(例如新增特殊填充字元)將最末尾的分組填充到塊大小。
也有些分組模式(如 CTR、CFB、OFB、CCM、EAX 和 GCM)根本不需要填充,因為它們在每個步驟中,都直接在明文部分和內部密碼狀態之間執行異或(XOR)運算.

使用「分組模式」加密大量資料的流程基本如下:

  • 初始化加密演算法狀態(使用加密金鑰 + 初始向量 IV)
  • 加密資料的第一個分組
  • 使用加密金鑰和其他引數轉換加密演算法的當前狀態
  • 加密下一個分組
  • 再次轉換加密狀態
  • 再加密下一分組
  • 依此類推,直到處理完所有輸入資料

解密的流程跟加密完全類似:先初始化演算法,然後依次解密所有分組,中間可能會涉及到加密狀態的轉換。

下面我們來具體介紹下 CTR 與 GCM 兩個常見的分組模式。

0. 初始向量 IV

介紹具體的分組模式前,需要先了解下初始向量 IV(Initialization Vector)這個概念,它有時也被稱作 Salt 或者 Nonce。
初始向量 IV 通常是一個隨機數,主要作用是往密文中新增隨機性,使同樣的明文被多次加密也會產生不同的密文,從而確保密文的不可預測性。

IV 的大小應與密碼塊大小相同,例如 AES、Serpent 和 Camellia 都只支援 128 位密碼塊,那麼它們需要的 IV 也必須也 128 位。

IV 通常無需保密,但是應當足夠隨機(無法預測),而且不允許重用,應該對每條加密訊息使用隨機且不可預測的 IV。

一個常見錯誤是使用相同的對稱金鑰和相同的 IV 加密多條訊息,這使得針對大多數分組模式的各種加密攻擊成為可能。

1. CTR (Counter) 分組模式 {#counter_mode}

參考文件: https://csrc.nist.gov/publications/detail/sp/800-38a/final

下圖說明了「CTR 分組工作模式」的加密解密流程,基本上就是將明文/密文拆分成一個個長度固定的分組,然後使用一定的演算法進行加密與解密:

可以看到兩圖中左邊的第一個步驟,涉及到三個引數:

  • Nonce,初始向量 IV 的別名,前面已經介紹過了。
  • Counter: 一個計數器,最常用的 Counter 實現是「從 0 開始,每次計算都自增 1」
  • Key: 對稱加密的金鑰
  • Plaintext: 明文的一個分組。除了最後一個分組外,其他分組的長度應該跟 Key 相同

CTR 模式加解密的演算法使用公式來表示如下:

\[\begin{alignedat}{2} C_i &= P_i \oplus O_i, \ &\text{for } i &= 1, 2 ... n-1 \\\\ P_i &= C_i \oplus O_i, \ &\text{for } i &= 1, 2 ... n-1 \\\\ O_i &= \text{CIPH}_{key}(\text{Nonce} + I_i), \ &\text{for } i &= 1, 2 ... n-1 \end{alignedat} \]

公式的符號說明如下

  • \(C_i\) 表示密文的第 \(i\) 個分組
  • \(P_i\) 表示明文的第 \(i\) 個 分組
  • \(I_i\) 表示計數器返回的第 \(i\) 個值,其長度應與分組的長度相同
  • \(\text{CIPH}_{key}\) 表示使用金鑰 \(key\) 的對稱加密演算法

上面的公式只描述了 $ 0 \ge i \le n-1$ 的場景,最後一個分組 \(i = n\) 要特殊一些——它的長度可能比 Key 要短。
CTR 模式加解密這最後這個分組時,會直接忽略掉 \(O_n\) 末尾多餘的 bytes.
這種處理方式使得 CTR 模式不需要使用填充演算法對最後一個分組進行填充,而且還使密文跟明文的長度完全一致。
我們假設最後一個分組的長度為 \(u\),它的加解密演算法描述如下(\(MSB_u(O_n)\) 表示取 \(O_n\) 的 u 個最高有效位):

\[\begin{alignedat}{2} C_{n} &= P_{n} \oplus {MSB_u}(O_n) \\\\ P_{n} &= C_{n} \oplus {MSB_u}(O_n)\\\\ O_n &= \text{CIPH}_{key}(\text{Nonce} + I_n) \end{alignedat} \]

可以看到,因為異或 XOR 的對稱性,加密跟解密的演算法是完全相同的,直接 XOR \(O_i\) 即可。

Python 中最流行的加密庫是 cryptographyrequests/flask 底層就使用了它,下面我們使用這個庫來演示下 AES-256-CTR 演算法:

# pip install cryptography==36.0.1
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

plaintext = b"this is a test message, hahahahahaha~"

# 使用 32bytes 的 key,即使用演算法 AES-256-CTR
key = os.urandom(32)
# key => b'\x96\xec.\xc7\xd5\x1b/5\xa1\x10s\x9d\xd5\x10z\xdc\x90\xb5\x1cm">x\xfd \xd5\xc5\xaf\x19\xd1Z\xbb'

# AES 演算法的 block 大小是固定的 128bits,即 16 bytes, IV 長度需要與 block 一致
iv = os.urandom(16)
# iv => b'\x88[\xc9\n`\xe4\xc2^\xaf\xdc\x1e\xfd.c>='

# 1. 傳送方加密資料
## 構建 AES-256-CTR 的 cipher,然後加密資料,得到密文
cipher = Cipher(algorithms.AES(key), modes.CTR(iv))
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
# ciphertext => b'\x9b6(\x1d\xfd\xde\x96S\x8b\x8f\x90\xc5}ou\x9e\xb1\xbd\x9af\xb8\xdc\xec\xbf\xa3"\x18^\xac\x14\xc8s2*\x1a\xcf\x1d'

# 2. 傳送方將 iv + ciphertext 傳送給接收方

# 3. 接收方解密資料
# 接收方使用自己的 key + 接收到的 iv,構建 cipher,然後解密出原始資料
cipher = Cipher(algorithms.AES(key), modes.CTR(iv))
decryptor = cipher.decryptor()
decryptor.update(ciphertext) + decryptor.finalize()

從上面的演算法描述能感覺到,CTR 演算法還蠻簡單的。下面我使用 Python 寫一個能夠 work 的 CTR 實現:

def xor_bytes(a, b):
    """Returns a new byte array with the elements xor'ed.
       if len(a) != len(b), extra parts are discard.
    """
    return bytes(i^j for i, j in zip(a, b))

def inc_bytes(a):
    """ Returns a new byte array with the value increment by 1 """
    out = list(a)
    for i in reversed(range(len(out))):
        if out[i] == 0xFF:
            out[i] = 0
        else:
            out[i] += 1
            break
    return bytes(out)

def split_blocks(message, block_size, require_padding=True):
    """
    Split `message` with fixed length `block_size`
    """
    assert len(message) % block_size == 0 or not require_padding
    return [message[i:i+16] for i in range(0, len(message), block_size)]

def encrypt_ctr(block_cipher, plaintext, iv):
    """
    Encrypts `plaintext` using CTR mode with the given nounce/IV.
    """
    assert len(iv) == 16

    blocks = []
    nonce = iv
    for plaintext_block in split_blocks(plaintext, block_size=16, require_padding=False):
        # CTR mode encrypt: plaintext_block XOR encrypt(nonce)
        o = bytes(block_cipher.encrypt(nonce))
        block = xor_bytes(plaintext_block, o)  # extra parts of `o` are discard in this step
        blocks.append(block)
        nonce = inc_bytes(nonce)

    return b''.join(blocks)

# 加密與解密的演算法完全一致
decrypt_ctr = encrypt_ctr

接下來驗證下演算法的正確性:

# Python 官方庫未提供 AES 實現,因此需要先裝下這個庫:
# pip install pyaes==1.6.1
from pyaes import AES

# AES-256-CTR - plaintext key 都與前面的測試程式碼完全一致
plaintext = b"this is a test message, hahahahahaha~"
key = b'\x96\xec.\xc7\xd5\x1b/5\xa1\x10s\x9d\xd5\x10z\xdc\x90\xb5\x1cm">x\xfd \xd5\xc5\xaf\x19\xd1Z\xbb'

# 1. 傳送方加密資料
# 首先生成一個隨機 IV,為了對比,這裡使用前面生成好的資料
iv = b'\x88[\xc9\n`\xe4\xc2^\xaf\xdc\x1e\xfd.c>='
aes_cipher = AES(key)
ciphertext = encrypt_ctr(aes_cipher, plaintext, iv)
print("ciphertext =>", bytes(ciphertext)) # 輸出應該與前面用 cryptography 計算出來的完全一致
# ciphertext => b'\x9b6(\x1d\xfd\xde\x96S\x8b\x8f\x90\xc5}ou\x9e\xb1\xbd\x9af\xb8\xdc\xec\xbf\xa3"\x18^\xac\x14\xc8s2*\x1a\xcf\x1d'

# 2. 傳送方將 ciphertext + iv 傳送給接收方

# 3. 接收方使用自己的 key 解密資料
aes_cipher = AES(key)
decrypted_bytes = decrypt_ctr(aes_cipher, ciphertext, iv)
print("decrypted_bytes =>", bytes(decrypted_bytes))
# decrypted_bytes => b"this is a test message, hahahahahaha~"

2. GCM (Galois/Counter) 分組模式

GCM (Galois/Counter) 模式在 CTR 模式的基礎上,新增了訊息認證的功能,而且同時還具有與 CTR 模式相同的平行計算能力。因此相比 CTR 模式,GCM 不僅速度一樣快,還能額外提供對訊息完整性、真實性的驗證能力。

下圖直觀地解釋了 GCM 塊模式(Galois/Counter 模式)的工作原理:

GCM 模式新增的 Auth Tag,計算起來會有些複雜,我們就直接略過了,對原理感興趣的可以看下 Galois/Counter_Mode_wiki.

3. 如何選用塊模式

一些 Tips:

  • 常用的安全塊模式是 CBC(密碼塊連結)、CTR(計數器)和 GCM(伽羅瓦/計數器模式),它們需要一個隨機(不可預測的)初始化向量 (IV),也稱為 noncesalt
  • CTR(Counter)」塊模式在大多數情況下是一個不錯的選擇,因為它具有很強的安全性和並行處理能力,允許任意輸入資料長度(無填充)。但它不提供身份驗證和完整性,只提供加密
  • GCM(Galois/Counter Mode)塊模式繼承了 CTR 模式的所有優點,並增加了加密訊息認證能力。GCM 是在對稱密碼中實現認證加密的快速有效的方法,強烈推薦
  • CBC 模式在固定大小的分組上工作。因此,在將輸入資料拆分為分組後,應使用填充演算法使最後一個分組的長度一致。大多數應用程式使用 PKCS7 填充方案或 ANSI X.923. 在某些情況下,CBC 阻塞模式可能容易受到「padding oracle」攻擊,因此最好避免使用 CBC 模式
  • 眾所周知的不安全塊模式是 ECB(電子密碼本),它將相等的輸入塊加密為相等的輸出塊(無加密擴散能力)。不要使用 ECB 塊模式!它可能會危及整個加密方案。
  • CBC、CTR 和 GCM 模式等大多數塊都支援「隨機訪問」解密。比如在視訊播放器中的任意時間偏移處尋找,播放加密的視訊流

總之,建議使用 CTR (Counter) 或 GCM (Galois/Counter) 分組模式。
其他的分組在某些情況下可能會有所幫助,但很可能有安全隱患,因此除非你很清楚自己在做什麼,否則不要使用其他分組模式!

CTR 和 GCM 加密模式有很多優點:它們是安全的(目前沒有已知的重大缺陷),可以加密任意長度的資料而無需填充,可以並行加密和解密分組(在多核 CPU 中)並可以直接解密任意一個密文分組。
因此它們適用於加密加密錢包、文件和流視訊(使用者可以按時間查詢)。
GCM 還提供訊息認證,是一般情況下密碼塊模式的推薦選擇。

請注意,GCM、CTR 和其他分組模式會洩漏原始訊息的長度,因為它們生成的密文長度與明文訊息的長度相同。
如果您想避免洩露原始明文長度,可以在加密前嚮明文新增一些隨機位元組(額外的填充資料),並在解密後將其刪除。

四、對稱加密演算法與對稱加密方案

前面囉嗦了這麼多,下面進入正題:對稱加密演算法

1. 安全的對稱加密演算法

目前應用最廣泛的對稱加密演算法,是 AES 跟 Salsa20 / ChaCha20 這兩個系列。

1. AES (Rijndael)

wiki: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard

AES(高階加密標準,也稱為 Rijndael)是現代 IT 行業中最流行和廣泛使用的對稱加密演算法。AES 被證明是高度安全、快速且標準化的,到目前為止沒有發現任何明顯的弱點或攻擊手段,而且幾乎在所有平臺上都得到了很好的支援。 AES 是 128 位分組密碼,使用 128、192 或 256 位金鑰。它通常與分組模式組合成分組加密方案(如 AES-CTR 或 AES-GCM)以處理流資料。
在大多數分組模式中,AES 還需要一個隨機的 128 位初始向量 IV。

Rijndael (AES) 演算法可免費用於任何用途,而且非常流行。很多站點都選擇 AES 作為 TLS 協議的一部分,以實現安全通訊。
現代 CPU 硬體基本都在微處理器級別實現了 AES 指令以加速 AES 加密解密操作。

這裡有一個純 Python 的 AES 實現可供參考: AES encryption in pure Python - boppreh

我們在前面的 CTR 分組模式中已經使用 Python 實踐了 AES-256-CTR 加密方案。
而實際上更常用的是支援整合身份驗證加密(AEAD)的 AES-256-GCM 加密方案,它的優勢我們前面已經介紹過了,這裡我們使用 Python 演示下如何使用:

# pip install cryptography==36.0.1
import os
from cryptography.hazmat.primitives.ciphers import (
    Cipher, algorithms, modes
)

def encrypt(key, plaintext, associated_data):
    # Generate a random 96-bit IV.
    iv = os.urandom(12)

    # Construct an AES-GCM Cipher object with the given key and a
    # randomly generated IV.
    encryptor = Cipher(
        algorithms.AES(key),
        modes.GCM(iv),
    ).encryptor()

    # associated_data will be authenticated but not encrypted,
    # it must also be passed in on decryption.
    encryptor.authenticate_additional_data(associated_data)

    # Encrypt the plaintext and get the associated ciphertext.
    # GCM does not require padding.
    ciphertext = encryptor.update(plaintext) + encryptor.finalize()

    return (iv, ciphertext, encryptor.tag)

def decrypt(key, associated_data, iv, ciphertext, tag):
    # Construct a Cipher object, with the key, iv, and additionally the
    # GCM tag used for authenticating the message.
    decryptor = Cipher(
        algorithms.AES(key),
        modes.GCM(iv, tag),
    ).decryptor()

    # We put associated_data back in or the tag will fail to verify
    # when we finalize the decryptor.
    decryptor.authenticate_additional_data(associated_data)

    # Decryption gets us the authenticated plaintext.
    # If the tag does not match an InvalidTag exception will be raised.
    return decryptor.update(ciphertext) + decryptor.finalize()


# 接下來進行演算法驗證

plaintext = b"this is a paintext, hahahahahaha~"
key = b'\x96\xec.\xc7\xd5\x1b/5\xa1\x10s\x9d\xd5\x10z\xdc\x90\xb5\x1cm">x\xfd \xd5\xc5\xaf\x19\xd1Z\xbb'
associated_data = b"authenticated but not encrypted payload"  # 被用於訊息認證的關聯資料

# 1. 傳送方加密訊息
iv, ciphertext, tag = encrypt(
    key,
    plaintext,
    associated_data
)

# 2. 傳送方將 associated_data iv ciphertext tag 打包傳送給接收方

# 3. 接收方使用自己的 key 驗證並解密資料
descrypt_text = decrypt(
    key,
    associated_data,
    iv,
    ciphertext,
    tag
)

2. Salsa20 / ChaCha20

wiki: https://en.wikipedia.org/wiki/Salsa20#ChaCha_variant

Salsa20 及其改進的變體 ChaCha(ChaCha8、ChaCha12、ChaCha20)和 XSalsa20 是由密碼學家 Daniel Bernstein 設計的現代、快速的對稱流密碼家族。 Salsa20 密碼是對稱流密碼設計競賽 eSTREAM(2004-2008)的決賽選手之一,它隨後與相關的 BLAKE 雜湊函式一起被廣泛採用。 Salsa20 及其變體是免版稅的,沒有專利。

Salsa20 密碼將 128 位或 256 位對稱金鑰 + 隨機生成的 64 位隨機數(初始向量)和無限長度的資料流作為輸入,並生成長度相同的加密資料流作為輸出輸入流。

ChaCha20-Poly1305

Salsa20 應用最為廣泛的是認證加密方案:ChaCha20-Poly1305,即組合使用 ChaCha20 與訊息認證演算法 Poly1305,它們都由密碼學家 Bernstein 設計。

ChaCha20-Poly1305 已被證明足夠安全,不過跟 GCM 一樣它的安全性也依賴於足夠隨機的初始向量 IV,另外 ChaCha20-Poly1305 也不容易遭受計時攻擊。

在沒有硬體加速的情況下,ChaCha20 通常比 AES 要快得多(比如在舊的沒有硬體加速的移動裝置上),這是它最大的優勢。

以下是一個 ChaCha20 的 Python 示例:

# pip install cryptography==36.0.1
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

plaintext = b"this is a paintext, hahahahahaha~"
key = b'\x96\xec.\xc7\xd5\x1b/5\xa1\x10s\x9d\xd5\x10z\xdc\x90\xb5\x1cm">x\xfd \xd5\xc5\xaf\x19\xd1Z\xbb'
nonce = os.urandom(16)

algorithm = algorithms.ChaCha20(key, nonce)
# ChaCha20 是一個流密碼,mode 必須為 None
cipher = Cipher(algorithm, mode=None)

# 1. 加密
encryptor = cipher.encryptor()
ct = encryptor.update(plaintext)

# 2. 解密
decryptor = cipher.decryptor()
decryptor.update(ct)

3. 其他流行的對稱加密演算法

還有一些其他的現代安全對稱密碼,它們的應用不如 AES 和 ChaCha20 這麼廣泛,但在程式設計師和資訊保安社群中仍然很流行:

  • Serpent - 安全對稱金鑰分組密碼(金鑰大小:128、192 或 256 位),公眾所有(Public Domain),完全免費
  • Twofish - 安全對稱金鑰分組密碼(金鑰大小:128、192 或 256 位),公眾所有(Public Domain),完全免費
  • Camellia - 安全對稱金鑰分組密碼(分組大小:128 位;金鑰大小:128、192 和 256 位),專利演算法,但完全免費
    • 該演算法由三菱和日本電信電話(NTT)在 2000 年共同發明
  • RC5 - 安全對稱金鑰分組密碼(金鑰大小:128 到 2040 位;分組大小:32、64 或 128 位;輪數:1 ... 255),短金鑰不安全(56 位金鑰已被暴力破解) , 專利在 2015 年到期,現在完全免費
  • RC6 - 安全對稱金鑰分組密碼,類似於 RC5,但更復雜(金鑰大小:128 到 2040 位;分組大小:32、64 或 128 位;輪數:1 ... 255),專利在 2017 年到期,現在完全免費
  • IDEA - 安全對稱金鑰分組密碼(金鑰大小:128 位),所有專利在均 2012 年前過期,完全免費
  • CAST (CAST-128 / CAST5, CAST-256 / CAST6) - 安全對稱金鑰分組密碼系列(金鑰大小:40 ... 256 位),免版稅
  • ARIA - 安全對稱金鑰分組密碼,類似於 AES(金鑰大小:128、192 或 256 位),韓國官方標準,免費供公眾使用
  • SM4 - 安全對稱金鑰分組密碼,類似於 AES(金鑰大小:128 位),中國官方標準,免費供公眾使用
    • 由中國國家密碼管理局於 2012 年 3 月 21 日釋出

具體的演算法內容這裡就不介紹了,有興趣或者用得到的時候,可以再去仔細瞭解。

2. 不安全的對稱加密演算法

如下這些對稱加密演算法曾經很流行,但現在被認為是不安全的或有爭議的安全性,不建議再使用

  • DES - 56 位金鑰大小,可以被暴力破解
  • 3DES(三重 DES, TDES)- 64 位密碼,被認為不安全,已在 2017 年被 NIST 棄用.
  • RC2 - 64 位密碼,被認為不安全
  • RC4 - 流密碼,已被破解,網上存在大量它的破解資料
  • Blowfish - 舊的 64 位密碼,已被破壞
  • GOST - 俄羅斯 64 位分組密碼,有爭議的安全性,被認為有風險

對稱認證加密演算法 AE / AEAD

我們在前面第三篇文章「MAC 與金鑰派生函式 KDF」中介紹過 AE 認證加密及其變體 AEAD.

一些對稱加密方案提供整合身份驗證加密(AEAD),比如使用了 GCM 分組模式的加密方案 AES-GCM,而其他加密方案(如 AES-CBC 和 AES-CTR)自身不提供身份驗證能力,需要額外新增。

最流行的認證加密(AEAD)方案有如下幾個,我們在之前已經簡單介紹過它們:

  • ChaCha20-Poly1305
    • 具有整合 Poly1305 身份驗證器的 ChaCha20 流密碼(整合身份驗證 AEAD 加密)
    • 使用 256 位金鑰和 96 位隨機數(初始向量)
    • 極高的效能
    • 在硬體不支援 AES 加速指令時(如路由器、舊手機等硬體上),推薦使用此演算法
  • AES-256-GCM
    • 我們在前面的 GCM 模式一節,使用 Python 實現並驗證了這個 AES-256-GCM 加密方案
    • 使用 256 位金鑰和 128 位隨機數(初始向量)
    • 較高的效能
    • 在硬體支援 AES 加速時(如桌面、伺服器等場景),更推薦使用此演算法
  • AES-128-GCM
    • 跟 AES-256-GCM 一樣,區別在於它使用 128 位金鑰,安全性弱於 ChaCha20-Poly1305 與 AES-256-GCM.
    • 目前被廣泛應用在 HTTPS 等多種加密場景下,但是正在慢慢被前面兩種方案取代

今天的大多數應用程式應該優先選用上面這些加密方案進行對稱加密,而不是自己造輪子。
上述方案是高度安全的、經過驗證的、經過良好測試的,並且大多數加密庫都已經提供了高效的實現,可以說是開箱即用。

目前應用最廣泛的對稱加密方案應該是 AES-128-GCM,
而 ChaCha20-Poly1305 因為其極高的效能,也越來越多地被應用在 TLS1.2、TLS1.3、QUIC/HTTP3、Wireguard、SSH 等協議中。

五、AES 演算法案例:以太坊錢包加密

在這一小節我們研究一個現實中的 AES 應用場景:以太坊區塊鏈的標準加密錢包檔案格式。
我們將看到 AES-128-CTR 密碼方案如何與 Scrypt 和 MAC 相結合,通過字元密碼安全地實現經過身份驗證的對稱金鑰加密。

以太坊 UTC / JSON 錢包

在比特幣和以太坊等區塊鏈網路中,區塊鏈資產持有者的私鑰儲存在稱為加密錢包的特殊金鑰庫中。
通常,這些加密錢包是本地硬碟上的檔案,並使用字元密碼加密。

在以太坊區塊鏈中,加密錢包以一種特殊的加密格式在內部儲存,稱為「UTC / JSON 錢包(金鑰庫檔案)」或「Web3 祕密儲存定義」。
這是一種加密錢包的檔案格式,被廣泛應用在 geth 和 Parity(以太坊的主要協議實現)、MyEtherWallet(流行的線上客戶端以太坊錢包)、MetaMask(廣泛使用的瀏覽器內以太坊錢包)、ethers.js 和 Nethereum 庫以及許多其他與以太坊相關的技術和工具中。

以太坊 UTC/JSON 金鑰庫將加密的私鑰、加密資料、加密演算法及其引數儲存為 JSON 文字文件。

UTC / JSON 錢包的一個示例如下:

{
  "version": 3,
  "id": "07a9f767-93c5-4842-9afd-b3b083659f04",
  "address": "aef8cad64d29fcc4ed07629b9e896ebc3160a8d0",
  "Crypto": {
    "ciphertext": "99d0e66c67941a08690e48222a58843ef2481e110969325db7ff5284cd3d3093",
    "cipherparams": { "iv": "7d7fabf8dee2e77f0d7e3ff3b965fc23" },
    "cipher": "aes-128-ctr",
    "kdf": "scrypt",
    "kdfparams": {
      "dklen": 32,
      "salt": "85ad073989d461c72358ccaea3551f7ecb8e672503cb05c2ee80cfb6b922f4d4",
      "n": 8192,
      "r": 8,
      "p": 1
      },
    "mac": "06dcf1cc4bffe1616fafe94a2a7087fd79df444756bb17c93af588c3ab02a913"
  }
}

上述 json 內容也是認證對稱加密的一個典型示例,可以很容易分析出它的一些組成成分:

  • kdf: 用於從字元密碼派生出金鑰的 KDF 演算法名稱,這裡用的是 scrypt
    • kdfparams: KDF 演算法的引數,如迭代引數、鹽等...
  • ciphertext: 錢包內容的密文,通常這就是一個被加密的 256 位私鑰
  • cipher + cipherparams: 對稱加密演算法的名稱及引數,這裡使用了 AES-128-CTR,並給出了初始向量 IV
  • mac: 由 MAC 演算法生成的訊息認證碼,被用於驗證解密密碼的正確性
    • 以太坊使用擷取派生金鑰的一部分,拼接上完整密文,然後進行 keccak-256 雜湊運算得到 MAC 值
  • 其他錢包相關的資訊

預設情況下,金鑰派生函式是 scrypt 並使用的是弱 scrypt 引數(n=8192 成本因子,r=8 塊大小,p=1 並行化),因此建議使用長而複雜的密碼以避免錢包被暴力解密。

參考

相關文章