# API 使用動態金鑰加密資料

> 在 API 通訊加密中,使用 **固定金鑰 (Static Key Wrapping)** 雖然實作簡單,但一旦金鑰外洩,後果嚴重。若能在每次登入時動態產生金鑰 (**Dynamic Key Wrapping**),可有效縮短攻擊面並降低風險。本文將介紹固定金鑰與動態金鑰的差異,並示範如何在 `Bee.NET` 框架中設定與實作動態金鑰。
📦 範例程式:[jsonrpc-sample](https://github.com/jeff377/jsonrpc-sample)
---
## 1️⃣ 固定金鑰 vs 動態金鑰差異
### 固定金鑰 (Static Key Wrapping)
- **概念**:所有用戶端共用同一把 API 加密金鑰,並儲存在後端設定檔。
- **優點**
- 架構簡單,不需維護 Session 狀態。
- 適合內網或低風險系統。
- **缺點**
- 金鑰一旦外洩,歷史封包可被回溯解密。
- 必須人工輪換金鑰,維運成本高。
---
### 動態金鑰 (Dynamic Key Wrapping)
- **概念**:每次用戶登入時,系統隨機產生新的 API 加密金鑰,並與 Session 綁定。
- **優點**
- 金鑰外洩僅影響當前 Session,歷史封包不易回溯解密。
- 金鑰自動輪換,安全性高。
- **缺點**
- 必須維護 Session 狀態,實作稍複雜。
- 登入流程多一次金鑰產生與加解密。
---
## 2️⃣ `Bee.NET` 動態金鑰設定方式
透過 [BeeSettingsEditor](https://github.com/jeff377/bee-library/releases/download/BeeSettingsEditor-v1.0.3/BeeSettingsEditor-v1.0.3.zip) 工具編輯 **SystemSettings.xml**,只需設定 `<ApiEncryptionKeyProvider>`:
```xml
<SystemSettings>
<BackendConfiguration>
<!--
ApiEncryptionKeyProvider:
- Bee.Business.StaticApiEncryptionKeyProvider, Bee.Business (靜態金鑰)
- Bee.Business.DynamicApiEncryptionKeyProvider, Bee.Business (動態金鑰)
-->
<ApiEncryptionKeyProvider>
Bee.Business.DynamicApiEncryptionKeyProvider, Bee.Business
</ApiEncryptionKeyProvider>
</BackendConfiguration>
</SystemSettings>
```
設定 `DynamicApiEncryptionKeyProvider` 後,系統會在登入時自動產生 **Session 專屬金鑰**,並於 API 加解密時自動套用。
---
## 3️⃣ Login 方法支援靜態與動態金鑰
`Bee.NET` 的 `Login` 方法在登入時都會產生金鑰並存入 `SessionInfo`,差別在於後續是否使用:
- **靜態金鑰模式**:使用設定檔共用金鑰,`SessionInfo` 的金鑰不會被使用。
- **動態金鑰模式**:登入時產生隨機金鑰並寫入 `SessionInfo`,API 透過 AccessToken 取出對應金鑰。
### Login 方法範例
```csharp
public virtual LoginResult Login(LoginArgs args)
{
// 1. 驗證帳密
if (!AuthenticateUser(args))
throw new UnauthorizedAccessException("Invalid username or password.");
// 2. 依設定產生靜態或動態金鑰
byte[] encryptionKey = BackendInfo.ApiEncryptionKeyProvider.GenerateKeyForLogin();
// 3. 建立 SessionInfo 並存入快取
var sessionInfo = new SessionInfo
{
AccessToken = Guid.NewGuid(),
UserID = args.UserId,
ExpiredAt = DateTime.UtcNow.AddHours(1),
ApiEncryptionKey = encryptionKey
};
CacheFunc.SetSessionInfo(sessionInfo);
// 4. 用 RSA 公鑰加密金鑰回傳
string encryptedKey = RsaCryptor.EncryptWithPublicKey(
Convert.ToBase64String(encryptionKey),
args.ClientPublicKey
);
return new LoginResult
{
AccessToken = sessionInfo.AccessToken,
ExpiredAt = sessionInfo.ExpiredAt,
ApiEncryptionKey = encryptedKey
};
}
```
---
## 4️⃣ 金鑰提供者實作比較
### 動態金鑰提供者 DynamicApiEncryptionKeyProvider
```csharp
public class DynamicApiEncryptionKeyProvider : IApiEncryptionKeyProvider
{
public byte[] GetKey(Guid accessToken)
{
if (BaseFunc.IsEmpty(accessToken))
throw new UnauthorizedAccessException("Access token is required.");
var sessionInfo = CacheFunc.GetSessionInfo(accessToken);
return sessionInfo?.ApiEncryptionKey
?? throw new UnauthorizedAccessException("Session key not found or expired.");
}
public byte[] GenerateKeyForLogin()
{
return AesCbcHmacKeyGenerator.GenerateCombinedKey();
}
}
```
---
### 靜態金鑰提供者 StaticApiEncryptionKeyProvider
```csharp
public class StaticApiEncryptionKeyProvider : IApiEncryptionKeyProvider
{
public byte[] GetKey(Guid accessToken)
{
return BackendInfo.ApiEncryptionKey
?? throw new InvalidOperationException("BackendInfo.ApiEncryptionKey is not initialized.");
}
public byte[] GenerateKeyForLogin()
{
return GetKey(Guid.Empty);
}
}
```
---
## 5️⃣ JSON-RPC 請求參數範例
以下為 **未加密** 與 **加密** 格式的範例,示範如何傳遞 `Employee.Hello` 方法的參數:
### (1) 未加密格式
```json
{
"jsonrpc": "2.0",
"method": "Employee.Hello",
"params": {
"value": {
"$type": "Custom.Define.HelloArgs, Custom.Define",
"userName": "Jeff"
}
},
"id": "85b2c2c3-9854-4eb9-b6dc-6a82a9165fc3"
}
```
### (2) 加密格式 (Encrypted)
```json
{
"jsonrpc": "2.0",
"method": "Employee.Hello",
"params": {
"format": "Encrypted",
"value": {
"$type": "System.Byte[], System.Private.CoreLib",
"$value": "EAAAAFw44iP8acRR8V6gK6A4g8UwAAAAU6rvBwZkdXaboglEilgv8rSX2gaxK3phfZUW1tJCiNsPoCNt9hPJ1VLO8hnJe9eqJ8NanxPrstyOssDJV8GQjderfsBtBfGer1WcdgnGYy0="
},
"type": "Custom.Define.HelloArgs, Custom.Define"
},
"id": "85b2c2c3-9854-4eb9-b6dc-6a82a9165fc3"
}
```
> 當 `format` 為 `Encrypted` 時,`value` 會變成經 AES + HMAC 加密後的 Base64 字串,並透過動態金鑰進行解密。
---
## 6️⃣ 動態金鑰完整流程圖
```mermaid
sequenceDiagram
participant Client
participant Server
participant Cache
Note over Client,Server: 使用者登入流程
Client->>Server: 1. 提交帳號密碼 + 公鑰 (Login)
Server->>Server: 2. 驗證帳密
Server->>Server: 3. 產生隨機 Session 專屬金鑰
Server->>Cache: 4. 建立 SessionInfo (包含金鑰)
Server-->>Client: 5. 回傳 AccessToken + 金鑰 (RSA 公鑰加密)
Note over Client,Server: 後續 API 請求
Client->>Server: 6. 提交 AccessToken + 加密資料
Server->>Cache: 7. 依 AccessToken 取得 SessionInfo
Server->>Server: 8. 使用 SessionInfo.ApiEncryptionKey 解密
Server-->>Client: 9. 回傳 API 執行結果
```
---
## ✨ 總結
- `Login` 方法透過 `IApiEncryptionKeyProvider` 介面即可同時支援 **靜態**與**動態金鑰**,由設定檔決定模式。
- **動態金鑰模式**:登入時自動產生 Session 專屬金鑰,降低回溯解密風險。
- **靜態金鑰模式**:實作簡單,但外洩影響全面,建議僅用於低風險或內網系統。
- 搭配 JSON-RPC 加密傳輸,可有效保護 API 傳遞的參數資料。
---
**📢 歡迎轉載,請註明出處**
**📬 歡迎追蹤我的技術筆記與實戰經驗分享**
[Facebook](https://www.facebook.com/profile.php?id=61574839666569) | [HackMD](https://hackmd.io/@jeff377) | [GitHub](https://github.com/jeff377) | [NuGet](https://www.nuget.org/profiles/jeff377)