Java 25 中的鍵派生函數 API
1. 引言
在採用 Java 進行安全應用程式開發時,加密金鑰管理一直是核心問題。
Java 25 在 JDK 24 中透過JEP 478首次預覽了金鑰派生函數 (KDF) API 之後,正式引入了該 API。這個新的 API 引入了一個簡潔、可擴展的模型,用於使用成熟的演算法從初始金鑰材料派生加密金鑰。
在本文中,我們將探討金鑰衍生函數 (KDF) API 背後的動機、新 API 的架構及其核心元件。
2. KDF 背後的動機
金鑰衍生函數接受一些初始金鑰材料(IKM),並從中衍生出一個或多個密碼學上安全的金鑰。
我們可以將 IKM 理解為輸入到函數中的原始加密種子。 IKM 可以是網路協商的共用金鑰、使用者提供的密碼,或是金鑰協商協定產生的熵。
讓我們看看KDF在哪些方面有用:
- TLS 和協定握手:在 Diffie-Hellman 或 ECDH 金鑰交換之後,不應直接使用原始共用金鑰。金鑰衍生函數 (KDF) 會將其轉換為具有所需長度和熵的會話金鑰。
- 基於密碼的加密:原始密碼是弱密鑰。像
PBKDF2這樣的演算法會對密碼進行拉伸和加鹽處理,從而產生安全的加密材料。 - 關鍵多樣化:一把主鑰匙可以安全地擴展為多把特定用途的子鑰匙,而不會損害原鑰匙。
在 Java 25 之前,沒有統一的 API 來表達這些用例。因此,開發人員要麼依賴底層基於 MAC 的結構、特定供應商提供的 API,要麼依賴嵌入式第三方函式庫,例如 Bouncy Castle。
新的 KDF API 透過提供一個標準的、與 JCA 整合的介面來解決這個問題,該介面既與演算法無關,又可擴展到提供者。
3. 新KDF API的架構
KDF API 位於javax.crypto套件中,並遵循 Java 現有加密 API 使用的相同工廠方法模式:
KDF kdf = KDF.getInstance("HKDF-SHA256");
工廠模式是一種創建型設計模式。它提供了一種建立物件的方法,通常使用getInstance()方法,而無需指定要建立的物件的類別。
在本例中, getInstance()方法接受一個演算法名稱,以及一個可選的Provider 。這使得 API 與更廣泛的 JCA 架構保持一致,並允許在不更改應用程式程式碼的情況下插入其他實作。
一旦獲得 KDF 實例,即可如下衍生出類型化的SecretKey或原始位元組材料:
SecretKey key = kdf.deriveKey("AES", paramSpec);
byte[] rawKeyMaterial = kdf.deriveData(paramSpec);
4. 推導方法
讓我們來討論一下 KDF 類別如何公開主要的衍生方法:
4.1. deriveKey(String alg, AlgorithmParameterSpec params)
第一個方法deriveKey(),顧名思義,會為指定的目標演算法衍生出一個 SecretKey 。在內部,提供者會使用參數來決定所需的金鑰長度和衍生輸入。傳回的金鑰隨後即可用於對應的 JCA 演算法:
SecretKey aesKey = kdf.deriveKey("AES", hkdfParams);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmSpec);
4.2. deriveData(AlgorithmParameterSpec params)
另一方面, deriveData()方法直接傳回派生的位元組byte[] 。當下游使用者不是 JCA 演算法時,例如將輸出饋送到 MAC 或自訂協定時,此方法非常有用:
byte[] okm = kdf.deriveData(hkdfParams);
需要注意的是,如果參數規範與所選演算法不相容,則這兩種方法都會拋出InvalidAlgorithmParameterException異常;如果任何已載入的提供者都無法提供該演算法,則NoSuchAlgorithmException會從getInstance()方法傳播。
5. 輸入參數
在本節中,我們將了解應用程式和使用 KDF API 需要哪些輸入參數。
KDF API 使用AlgorithmParameterSpec實作來承載派生輸入。
對於 HKDF,JDK 提供了一個類別來模擬 HKDF 規範(RFC 5869)的三個不同階段: Extract 、 Expand以及Extract-then-Expand的組合階段。 HKDFParameterSpec HKDFParameterSpec是其中的核心。
每種方法都包含一組不同的鍊式方法,並且需要一組不同的參數。
5.1. 先提取後展開
第一種方法是先提取,再進行擴充:
HKDFParameterSpec params = HKDFParameterSpec
.ofExtract()
.addIKM(ikm)
material.addSalt(salt) // optional, randomizes extraction
.thenExpand(info, 32);
它公開了一個extract() 方法,該方法傳回一個建構器。然後我們對其呼叫thenExpand()方法並密封規範。這表明兩個階段應該同時運行。
info 參數是一個上下文綁定位元組數組,它將派生金鑰與特定用途(例如加密與身份驗證)綁定在一起。
5.2. 僅萃取物
在這種方法中, HKDFParameterSpec僅利用提取:
HKDFParameterSpec extractOnly = HKDFParameterSpec
.ofExtract()
.addIKM(ikm)
.addSalt(salt)
.extractOnly();
這樣就可以從 IKM 和鹽值產生偽隨機金鑰 (PRK),而無需對其進行擴展。然後,我們可以儲存 PRK 或將其傳遞給後續的僅擴展步驟。
5.3. 僅展開
最後,我們來談談僅展開的情況:
HKDFParameterSpec expandOnly = HKDFParameterSpec
.expandOnly(prk, info, 64);
這裡, prk是先前推導出的SecretKey 。這在將提取和擴展階段分開到握手的不同階段的協議中非常有用。
6. Java 25 支援的演算法
Java 25 透過預設的SunJCE提供者內建了對 HKDF 系列演算法的支援。
支援的演算法名稱有:
- 香港防務局-SHA256
- 香港防務局-SHA384
- 香港防務局-SHA512
這三種方法都遵循 RFC 5869 中定義的基於 HMAC 的提取和擴展結構。每種變體的最大輸出長度是哈希輸出長度的 255 倍,這是規範中規定的。
7. 新舊對比
在 KDF API 出現之前,使用 HKDF 衍生金鑰需要使用javax.crypto.Mac手動實作Extract和Expand步驟:
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(salt, "HmacSHA256"));
byte[] prk = mac.doFinal(ikm);
mac.init(new SecretKeySpec(prk, "HmacSHA256"));
byte[] t = new byte[0];
byte[] okm = new byte[32];
byte counter = 1;
mac.update(t);
mac.update(info);
mac.update(counter);
t = mac.doFinal();
System.arraycopy(t, 0, okm, 0, 32);
SecretKey aesKey = new SecretKeySpec(okm, "AES");
這種方法既脆弱又冗長,而且很容易出錯。此外,它還將加密邏輯與應用程式程式碼混為一談,增加了稽核難度。
新的 API 將相同的操作簡化為一個自文檔化的三行程式碼:
KDF hkdf = KDF.getInstance("HKDF-SHA256");
SecretKey aesKey = hkdf.deriveKey("AES",
HKDFParameterSpec.ofExtract()
.addIKM(ikm)
.addSalt(salt)
.thenExpand(info, 32));
演算法名稱在實例化時進行驗證,而不是隱藏在傳遞給Mac.getInstance()的字串中。
最後,我們來談談錯誤處理。參數錯誤會以類型異常的形式拋出,而不是靜默地導致資料損壞。這是舊方法中缺少的一環。
由於 KDF 採用提供者支援的實現,安全團隊無需修改應用程式代碼即可更換底層演算法實現,例如更換為符合 FIPS 標準的提供者。這使得新的 KDF 規範設計更加穩健。
8. 結論
在本文中,我們探討了 Java 25 中引入的金鑰派生函數 API。我們研究了為什麼 KDF 在現代加密工作流程中至關重要,KDF 類別如何融入現有的 JCA 架構,以及HKDFParameterSpec如何對 HKDF 規範的提取和擴展階段進行建模。
我們也看到,新的 API 消除了開發人員以前必須編寫的手動、容易出錯的變通方法,取而代之的是簡潔、與演算法無關且可擴展的介面。
本文範例的完整原始程式碼可在 GitHub 上找到。