iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
Software Development

我是Java工程師,關於密碼學我想懂的不多系列 第 28 篇

Day27 - 非對稱式加密系統:RSA與Padding 機制 (OAEP)

  • 分享至 

  • xImage
  •  

前言

在上一章中,我們推導了 RSA 的數學原理與大質數分解的安全性基石。然而,數學上的純粹並不等於工程上的安全。直接使用數學公式 c ≡ m^e mod n 執行的教科書式 RSA (Textbook RSA),在真實工程環境中極度脆弱,易遭受確定性分析、同態攻擊與 Padding Oracle 攻擊。

本章將揭示 RSA 填充機制(Padding)的演進,深入剖析現代非對稱加密的黃金標準——RSA-OAEP (Optimal Asymmetric Encryption Padding),並透過 Java JCA 實作具備抗攻擊能力的生產級程式碼。


一、 為什麼教科書式 RSA 與 PKCS#1 v1.5 絕對不能在現代工程中使用?

在 RSA 加密中,直接對原始位元組進行指數運算是致命的。為了解決安全性漏洞,密碼學界引入了填充(Padding)機制,但填充演算法本身的演進也充滿了血淚史。

1. 教科書式 RSA (Textbook RSA) 的致命缺陷

  • 確定性加密(Deterministic Encryption):相同的明文 m 永遠產生相同的密文 c。攻擊者若能猜測明文範圍(例如 HTTP 狀態碼或金額),可直接建立字典進行比對。
  • 乘法同態性(Multiplicative Homomorphism):給定兩密文 c1 = m1^e mod n 與 c2 = m2^e mod n,攻擊者可直接計算 c1 * c2 ≡ (m1 * m2)^e mod n,在不解密的前提下構造出被篡改的合法密文。
  • 小明文漏洞:若明文數值 m 很小,使得 m^e < n,則模運算 m^e mod n 失去作用,攻擊者直接對密文 c 開 e 次方根就能取回明文。

2. PKCS#1 v1.5 Padding 的落日與 Bleichenbacher 攻擊

為解決 Textbook RSA 的問題,早期推出了 PKCS#1 v1.5 填充格式:在明文前加入固定格式與隨機非零位元組:

EB = 0x00 || 0x02 || Random Padding || 0x00 || Plaintext

然而,1998 年密碼學家 Daniel Bleichenbacher 提出了著名的 Million Message Attack(Padding Oracle 攻擊):

  • 伺服器在解密時,若傳回「Padding 格式錯誤」與「解密失敗」的不同錯誤回饋,攻擊者可透過傳送數百萬個經過精心修改的密文,根據伺服器回應的時間與錯誤訊息,利用概率收斂逐步反推算出整份明文。
  • 現狀:NIST 與各大資安標準已明確要求停用 PKCS#1 v1.5 加密填充。

二、 現代黃金標準:RSA-OAEP 運作原理

RSA-OAEP (Optimal Asymmetric Encryption Padding, RFC 8017 / PKCS#1 v2.2) 是一種結合了非對稱加工與對稱 Feistel 網路結構的安全填充協議。

                  +------------------+       +------------------+
                  |  Message (m)     |       |  Optional Label  |
                  +------------------+       +------------------+
                            |                         |
                            v                         v
                       [ Padding ]              [ Hash (L) ]
                            |                         |
                            +------------+------------+
                                         |
                                         v
   +------------------+         +------------------+
   |   Random Seed    |         |   Data Block (DB)|
   +------------------+         +------------------+
            |                            |
            |     +---------------+      |
            +---->| MGF1 (Hash)   |----->(+)  (XOR)
            |     +---------------+      |
            |                            v
            |                      MaskedDB
            |                            |
            |     +---------------+      |
           (+)<---| MGF1 (Hash)   |<-----+
            |     +---------------+
            v
       MaskedSeed
            |                            |
            +----------------------------+
                         |
                         v
        EM = 0x00 || MaskedSeed || MaskedDB  --->  執行 RSA 模次方加密 (c = EMᵉ mod n)

OAEP 的三大核心安全特性

  1. 全域隨機化(Probabilistic Encryption):引入動態生成的高熵隨機種子(Random Seed),即使加密一模一樣的明文 100 次,產出的密文也完全不同。
  2. 雙向遮蔽與擴散(Mask Generation Function, MGF1):透過兩輪基於密碼學雜湊(如 SHA-256)的 MGF1 遮蔽運算,確保明文的每一個 bit 與 Seed 的每一個 bit 都相互擴散,徹底摧毀同態結構。
  3. 拒絕 Padding Oracle(明文完整性檢查):解密時,若發現 DB 內部的 Hash 標籤不匹配,系統會統一拋出通用的無差別例外,不洩漏任何關於 Padding 結構的側信道資訊。

三、 RSA-OAEP 加密容量限制與計算公式

RSA 是一種分組大小受限的演算法,其最大可加密的明文長度 L_max 由以下公式嚴格限制:

L_max = KeyByteLen - 2 * HashByteLen - 2

其中:

  • KeyByteLen 為 RSA 金鑰位元數轉換為的位元組數(如 2048 bits = 256 bytes)。
  • HashByteLen 為 OAEP 內部所選用雜湊函數的輸出位元組數(如 SHA-256 為 32 bytes)。

RSA-OAEP 最大容量對照表

RSA 金鑰長度 (bits) 模數長度 (Bytes) OAEP 內部 Hash Hash 長度 (Bytes) 最大明文容量L_max
2048 bits 256 bytes SHA-256 32 bytes 190 Bytes
3072 bits 384 bytes SHA-256 32 bytes 318 Bytes
4096 bits 512 bytes SHA-256 32 bytes 446 Bytes

工程原則:RSA 絕不適合用來直接加密大檔案或長文字。如果資料長度超過 L_max,必須採用 混合加密(Hybrid Encryption) 架構:用 AES-GCM 加密大資料,再用 RSA-OAEP 加密 AES 的對稱金鑰。

四、 Java JCA 生產級 RSA-OAEP 實作範例

在 Java JCA 中,為了確保主雜湊演算法與 MGF1 雜湊演算法皆使用強大的 SHA-256(避免預設退回 SHA-1),必須使用 OAEPParameterSpec 進行精確配置。

import javax.crypto.Cipher;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.MGF1ParameterSpec;
import java.util.Base64;

public class RsaOaepProductionExample {

    private static final String RSA_ALGORITHM = "RSA/ECB/OAEPPadding";
    private static final int RSA_KEY_SIZE = 3072; // 推薦 3072 bits

    public static void main(String[] args) throws Exception {
        // 1. 生成 3072-bit RSA KeyPair
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
        keyPairGen.initialize(RSA_KEY_SIZE);
        KeyPair keyPair = keyPairGen.generateKeyPair();
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();

        String originalMessage = "Day 26: Secure RSA-OAEP with SHA-256 and MGF1-SHA256";
        System.out.println("原始明文: " + originalMessage);

        // 2. RSA-OAEP 加密
        byte[] ciphertext = encryptOaep(originalMessage.getBytes(StandardCharsets.UTF_8), publicKey);
        System.out.println("加密後密文 (Base64): " + Base64.getEncoder().encodeToString(ciphertext));

        // 3. RSA-OAEP 解密
        byte[] decryptedBytes = decryptOaep(ciphertext, privateKey);
        String decryptedMessage = new String(decryptedBytes, StandardCharsets.UTF_8);
        System.out.println("解密成功: " + decryptedMessage);
    }

    /**
     * 生產級 RSA-OAEP 加密 (明確指定 SHA-256 + MGF1-SHA256)
     */
    public static byte[] encryptOaep(byte[] plaintext, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);

        // 顯式配置 OAEP 參數:Primary Hash = SHA-256, MGF1 Hash = SHA-256
        OAEPParameterSpec oaepParams = new OAEPParameterSpec(
                "SHA-256",
                "MGF1",
                MGF1ParameterSpec.SHA256,
                PSource.PSpecified.DEFAULT
        );

        cipher.init(Cipher.ENCRYPT_MODE, publicKey, oaepParams);
        return cipher.doFinal(plaintext);
    }

    /**
     * 生產級 RSA-OAEP 解密
     */
    public static byte[] decryptOaep(byte[] ciphertext, PrivateKey privateKey) throws Exception {
        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);

        // 配置與加密端完全相同的 OAEP 參數
        OAEPParameterSpec oaepParams = new OAEPParameterSpec(
                "SHA-256",
                "MGF1",
                MGF1ParameterSpec.SHA256,
                PSource.PSpecified.DEFAULT
        );

        cipher.init(Cipher.DECRYPT_MODE, privateKey, oaepParams);
        return cipher.doFinal(ciphertext);
    }
}


上一篇
Day26 - 非對稱金鑰加密系統:RSA演算法
下一篇
Day28 - 橢圓曲線密碼學 (ECC) 數學基礎:有限域、群論與離散對數
系列文
我是Java工程師,關於密碼學我想懂的不多 共 29 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言