# API資料串接
## 流程說明
此筆記紀錄與銀行互相用api傳遞資料,其中包含
* **AES加密演算法(EncryptorService )**
* **資料存取txt檔**
* **讀txt檔(ProcessAllTxtFilesInDirectory)**
* **寄信功能**
* **使用引數自動執行專案**
## UI設計
2個Combobox、1個DateTimtPicker、2個button以及textBoxStatus

## Form.cs
在排程自動化下,無法藉由手動來按下"執行"按鈕,因此可以藉由**引數**帶值,自動執行專案,同時在執行專案的開始及結束這兩個執行點用Log做紀錄。
### Form1_Load_1 事件處理函數
#### **功能:**
* 當 Form1 載入時會自動觸發這個函數。
* 依照命令列參數來判斷是否手動執行或是批次執行。
#### **程式碼解析**:
1. 日誌記錄
* BatchLogService.WriteLog("執行開始"),紀錄「執行開始」到日誌。
2. 隱藏表單
* this.Visible = false;: 預設讓 Form1 不顯示。
3. 取得命令列參數
* string[] args = Environment.GetCommandLineArgs();: 獲得命令列參數。
4. 判斷執行類型
* 如果 args.Count() == 1,代表是手動執行。
- 表單會變為可見。
- 初始化下拉選單(comboBox)。
* 否則,代表是批次執行。
- 嘗試執行特定操作。
- 如果操作成功,寫入「執行結束」到日誌。
- 如果操作失敗,寫入「排程執行失敗」和錯誤訊息到日誌。
```csharp=
private void Form1_Load_1(object sender, EventArgs e)
{
BatchLogService.WriteLog("執行開始");
//// 不顯示表單
this.Visible = false;
//// 取得命令列參數
string[] args = Environment.GetCommandLineArgs();
//// args.Count == 1, 代表是手動執行
//// args.Count != 1, 代表是批次執行
if (args.Count() == 1)
{
//// 顯示表單
this.Visible = true;
comboBox1.Items.Add(new ComboItem("收檔結果查詢服務", "TxFileResult"));
comboBox2.Items.Add(new ComboItem("指示", "I"));
comboBox2.Items.Add(new ComboItem("取消", "C"));
}
else
{
try
{
string action = args[1].ToUpper();
ExecAction(action, today);
BatchLogService.WriteLog("執行結束");
}
catch (Exception ex)
{
BatchLogService.WriteLog("排程執行失敗" + ex.ToString(), 2);
}
}
}
```
### ExecAction 函數
#### **功能:**
* 根據傳入的 action(引數) 和日期 yyyyMMdd,執行相對應的動作。
* 這個函數是由 Form1_Load_1 呼叫的,當 args.Count() != 1(批次執行)時。
#### **程式碼解析**:
1. 動作判斷
* 使用傳入的 action 字串來判斷要執行哪一種動作。
2. 收檔結果查詢服務 ( TXFILERESULT )
* 寫入 "執行收檔結果查詢服務" 到日誌。
* 讀取特定目錄下的所有 txt 檔案。
* 如果有找到 txt 檔案,則進行相應處理。
* 如果沒有找到,則顯示和寫入 "無txt檔可查詢"。
3. 信託指示付款 - 指示 ( I )
* 寫入 "執行信託指示付款_指示" 到日誌。
* 讀取和處理 txt 檔案。
* 更新狀態為 "執行結束"。
4. 信託指示付款 - 取消 ( C )
* 寫入 "執行信託指示付款_取消" 到日誌。
* 讀取和處理 txt 檔案。
* 更新狀態為 "執行結束"。
5. 錯誤處理
* 若在任何階段發生錯誤,寫入具體的錯誤訊息到日誌。
6. 程式結束
* Application.Exit( ); 結束應用程式。
```csharp=
private void ExecAction(string action, string yyyyMMdd)
{
try
{
if (action == "TXFILERESULT")
{
BatchLogService.WriteLog("執行收檔結果查詢服務");
TxtFileResultService _txtFileResultService = new TxtFileResultService();
try
{
string folderPath = "D:\\ftproot\\SinoPacsftp\\Instruction";
var txtFiles = GetAllTxtFiles(folderPath);
if (txtFiles.Count > 0)
{
_txtFileResultService.ProcessAllTxtFilesInDirectory(txtFiles);
}
else
{
textBoxStatus.Text = "無txt檔可查詢";
BatchLogService.WriteLog("無txt檔可查詢");
}
}
catch (Exception ex)
{
textBoxStatus.Text = "執行失敗";
BatchLogService.WriteLog("發生錯誤:" + ex.ToString(), 2);
}
}
else if (action == "I")
{
TxFileCheckService _txtFileCheckService = new TxFileCheckService(action);
BatchLogService.WriteLog("執行信託指示付款_指示");
_txtFileCheckService.ReadTxtFileResult();
textBoxStatus.Text = "執行結束";
}
else if (action == "C")
{
TxFileCheckService _txtFileCheckService = new TxFileCheckService(action);
BatchLogService.WriteLog("執行信託指示付款_取消");
_txtFileCheckService.ReadTxtFileResult();
textBoxStatus.Text = "執行結束";
}
Application.Exit();
}
catch (Exception ex)
{
BatchLogService.WriteLog("ExecAction 內部發生錯誤" + ex.ToString(), 2);
}
}
```
### GetAllTxtFiles 函數 (獲取當天txt檔)
#### **功能:**
* 從指定的資料夾中讀取所有 .txt 結尾的文件。
* 只返回包含今日日期(格式為 "yyyyMMdd")在文件名中的 .txt 文件。
#### **程式碼解析**:
1. 初始化變數
* 創建一個 List<string> 來存儲符合條件的 .txt 文件的路徑。
2. 讀取文件
* 使用 Directory.GetFiles( ) 從 folderPath 讀取所有 .txt 文件。
3. 獲取今天日期
* 使用 DateTime.Now.ToString("yyyyMMdd") 獲取今天的日期字符串。
4. 過濾文件
* 遍歷所有 .txt 文件。
* 判斷文件名是否包含今天的日期。
* 如果包含,則添加到 txtFilesToday 這個 List 中。
5. 返回結果
* 返回 txtFilesToday,這個 List 包含了所有符合條件的 .txt 文件的路徑。
```csharp=
private List<string> GetAllTxtFiles(string folderPath)
{
List<string> txtFilesToday = new List<string>();
string[] txtFiles = Directory.GetFiles(folderPath, "*.txt");
string todayDateString = DateTime.Now.ToString("yyyyMMdd");
foreach (var filePath in txtFiles)
{
string fileName = Path.GetFileName(filePath);
if (fileName.Contains(todayDateString))
{
txtFilesToday.Add(filePath);
}
}
return txtFilesToday;
}
```
### button1_Click 事件處理函數(執行)
#### **功能:**
* 當使用者點擊 button1 時觸發這個事件(不用排程執行專案時,就可以觸發Button_Click執行專案)。
* 根據日期選擇器(dateTimePicker1)和兩個下拉選單(comboBox1 和 comboBox2)的選擇來執行相對應的動作。
#### **程式碼解析**:
1. 獲取選擇日期
* 使用 dateTimePicker1.Value.ToString("yyyyMMdd") 獲取使用者選擇的日期。
2. 獲取下拉選單選擇
* 從 comboBox1 和 comboBox2 獲取使用者選擇的動作(execAction 和 execAction2)。
3. 執行動作
* 調用 ExecAction 函數,將 execAction 和 yyyyMMdd 作為參數。
* 再次調用 ExecAction 函數,但這次使用 execAction2 和 yyyyMMdd 作為參數。
4. 錯誤處理
* 若在任何階段發生錯誤,使用 BatchLogService.WriteLog 寫入具體的錯誤訊息到日誌。
```csharp=
private void button1_Click(object sender, EventArgs e)
{
try
{
//// 執行的日期
string yyyyMMdd = dateTimePicker1.Value.ToString("yyyyMMdd");
//// 執行選擇的動作
string execAction = ((ComboItem)comboBox1.Items[comboBox1.SelectedIndex]).Value;
ExecAction(execAction, yyyyMMdd);
string execAction2 = ((ComboItem)comboBox2.Items[comboBox2.SelectedIndex]).Value;
ExecAction(execAction2, yyyyMMdd);
}
catch (Exception ex)
{
BatchLogService.WriteLog("執行失敗" + ex.ToString(), 2);
}
}
```
### button2_Click 事件處理函數(清除)
#### **功能:**
* 當使用者點擊 button2 時觸發這個事件。
* 這個函數主要用於清除或重置表單上的某些控件到其預設狀態。
#### **程式碼解析**:
1. 清除下拉選單(ComboBox)
* 將 comboBox1 和 comboBox2 的 SelectedIndex 設為 -1,這樣就不會有選定的項目。
2. 重置日期選擇器(DateTimePicker)
* 將 dateTimePicker1.Value 設為當前的日期和時間(DateTime.Now)。
```csharp=
private List<string> GetAllTxtFiles(string folderPath)
{
List<string> txtFilesToday = new List<string>();
string[] txtFiles = Directory.GetFiles(folderPath, "*.txt");
string todayDateString = DateTime.Now.ToString("yyyyMMdd");
foreach (var filePath in txtFiles)
{
string fileName = Path.GetFileName(filePath);
if (fileName.Contains(todayDateString))
{
txtFilesToday.Add(filePath);
}
}
return txtFilesToday;
}
```
## Service
### TxFileCheckService(讀txt檔->組ResqestBody->Api)
#### **ProcessAllTxtFilesInDirectory(List<string> txtFiles)**
這個方法做了以下幾件事:
* 遍歷傳入的TXT文件列表。
* 對於每一個文件:
* 讀取文件內容。
* 判斷是否有 "沒有需要送出的資料" 的文本,如果有,則設置 noDataToSend 為 true。
* 生成 TxFilerequestBodyList。
* 上傳到API。
**參數**
* List<string> txtFiles:一個字符串列表,每個字符串都是一個TXT文件的本地路徑。
**變量**
* bool noDataToSend = false:用於標記是否有數據需要發送。
* string lastProcessedFilePath = null:存儲最後一個被處理的文件路徑。
**主體**
* foreach (var localFilePath in txtFiles):遍歷每個TXT文件的路徑。
* DateTime txDate = DateTime.Now:獲取當前的時間。
* string fileNameOnly = Path.GetFileName(localFilePath):獲取不包含路徑的檔名。
* string textFromFile = File.ReadAllText(localFilePath):讀取文件內容。
* 判斷文本是否包含 "沒有需要送出的資料"。
* 如果是,設置 noDataToSend = true,並跳出循環。
* 如果不是,則處理文本並上傳到API。
**處理完成後**
* 如果 noDataToSend 是 true,則觸發 UploadStatusChanged 事件,發送電子郵件,並寫日誌。
* 移動處理過的文件到備份目錄。
**其他細節**
* 使用了 ConfigurationManager.AppSettings 來讀取配置文件中的資料。
```csharp=
public void ProcessAllTxtFilesInDirectory(List<string> txtFiles)
{
bool noDataToSend = false;
string lastProcessedFilePath = null;
foreach (var localFilePath in txtFiles)
{
DateTime txDate = DateTime.Now;
//自身產生的txt檔(不包含路徑)
string fileNameOnly = Path.GetFileName(localFilePath);
//讀檔
string textFromFile = File.ReadAllText(localFilePath);
lastProcessedFilePath = localFilePath;
if (textFromFile.Contains("沒有需要送出的資料"))
{
noDataToSend = true;
lastProcessedFilePath = localFilePath;
break;
}
var TxFilerequestBodyList = TxFileResultRequestBody(txDate, textFromFile);
// 上傳檔案到API
foreach (var TxFilerequestBody in TxFilerequestBodyList)
{
string TxtfileName = TxFilerequestBody.OriFileName;
UploadFileDirectlyFromSftpToApi(localFilePath, TxtfileName, selectedApiEndpoint, TxFilerequestBody);
}
}
if (noDataToSend)
{
UploadStatusChanged?.Invoke("無提領檔可查詢");
string subject = "【aa & bb】aa信託收檔結果查詢服務通知" + DateTime.Now.ToString("yyyy/MM/dd");
string body = "無提領檔可查詢";
emailService.SendEmail(subject, body);
#region 所有操作成功後,移動文件
// 原本txt的路徑檔
string sourceDirectory = ConfigurationManager.AppSettings["SinoPacsftp_Instruction"];
//要移置txt的路徑檔
string targetDirectory = ConfigurationManager.AppSettings["SinoPacInstructionBackUp"];
//確保資料夾存在
if (!Directory.Exists(targetDirectory))
{
Directory.CreateDirectory(targetDirectory);
}
string fileName = Path.GetFileName(lastProcessedFilePath);
string datepart = DateTime.Now.ToString("yyyyMMddHHmmss");
string newFileName = datepart + "_" + fileName;
string sourceFilePath = Path.Combine(sourceDirectory, fileName);
string targetFilePath = Path.Combine(targetDirectory, newFileName);
File.Move(sourceFilePath, targetFilePath);
#endregion
BatchLogService.WriteLog("無提領檔可查詢");
}
}
```
#### **TxFileResultRequestBody(DateTime txDate, string textFromFile)**
* 基於交易日期和TXT文件內容生成request body列表。
* 將TXT文件解析成多行,並對每一行進行分割,以獲取文件名。
* 為每個文件名生成一個模型並加入到列表中。
**參數**
* DateTime txDate:交易日期。
* string textFromFile:從文件讀取的文本。
**返回值**
* List<GetPayTxFileResultRequestBodyModel>:一個由 GetPayTxFileResultRequestBodyModel 對象組成的列表。
**主要步驟**
1. 初始化一個空的 GetPayTxFileResultRequestBodyModel 對象列表 requestBodyList。
2. 格式化 txDate 為 "yyyyMMdd"。
3. 使用 Split('\n') 將 textFromFile 分割為多行。
4. 遍歷這些行,對每一行使用 Split('\t') 進行分割,並提取第一個標記(在這個情境中,應該是檔案名)。
5. 對每一個提取出的 TxtfileName,創建一個 GetPayTxFileResultRequestBodyModel 對象,然後將其添加到 requestBodyList。
6. 為每個 TxtfileName,創建一個電子郵件主題和正文,然後使用 emailService.SendEmail() 方法來發送郵件。
7. 最後,使用 BatchLogService.WriteLog() 方法寫日誌。
#### **UploadFileDirectlyFromSftpToApi(...)**
這個方法做了以下幾件事:
* 初始化API端點和加密金鑰。
* 檢查本地文件是否存在。
* 序列化 TxFilerequestBody 到JSON。
* 調用API。
* 根據API的響應進行後續操作,例如寫入日誌或移動文件。
**參數**
* string localFilePath:本地檔案路徑。
* string TxtfileName:檔案名。
* string selectedApiEndpoint:選定的 API 端點。
* GetPayTxFileResultRequestBodyModel TxFilerequestBody:要上傳的請求主體。
**主要流程**
1. 設定 API 端點:selectedApiEndpoint 用於存儲API的端點URL。
2. 金鑰和向量初始化:
* k1:讀取配置中的密鑰,並將其從 Base64 格式解碼。
* iv:初始化向量,生成隨機 12 字節數據。
3. JSON 序列化和加密:
* TxFilerequestBody 被序列化為 JSON 格式。
* 使用 k1 和 iv 對 JSON 進行加密。
4. API 調用:
* 這個步驟會調用一個 API 並獲取響應。
* 如果 API 調用成功,執行以下操作:
* 將返回的數據寫入一個 .txt 檔案。
* 移動原始的 Intruction.txt 文件到備份目錄。
5. 異常處理:
* 如果 API 調用失敗或出現異常,相應的錯誤將被寫入日誌並發送通過電子郵件。
```csharp=
public void UploadFileDirectlyFromSftpToApi(string localFilePath, string TxtfileName, string selectedApiEndpoint, GetPayTxFileResultRequestBodyModel TxFilerequestBody)
{
selectedApiEndpoint = "https://apisbx............";
// 解碼Base64格式的k1金鑰
string key = ConfigurationManager.AppSettings["MySecretKey"];
byte[] k1 = Convert.FromBase64String(key);
//初始化向量(IV)
byte[] iv = new byte[12];
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(iv);
}
try
{
if (File.Exists(localFilePath))
{
//序列化為 JSON
string jsonPayload = JsonConvert.SerializeObject(TxFilerequestBody);
//調用加密
EncryptorService.FlattenedJose flattenedJose = EncryptorService.EncodeFlattenedJoseContainer(jsonPayload, k1, iv);
string flattenedJoseJson = JsonConvert.SerializeObject(flattenedJose);
//先調用查詢api
List<GetPayTxFileResultResponseBodyModel> TxFileResultApi = new List<GetPayTxFileResultResponseBodyModel>();
apiService apiService = new apiService();
var apiResponse = apiService.CallApi(selectedApiEndpoint, flattenedJoseJson);
if (apiResponse.Success == true)
{
//TxFileResultResponse寫入到txt
string folderPath = ConfigurationManager.AppSettings["TxFileResultResponsePath"];
string TxFileResultfileName = "TxFileResultResponse_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".txt";
string fullPath = Path.Combine(folderPath, TxFileResultfileName);
if (!Directory.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
}
File.WriteAllText(fullPath, apiResponse.Data);
// Intruction.txt檔丟backup資料夾
string sourceDirectory = ConfigurationManager.AppSettings["SinoPacsftp_Instruction"];
string targetDirectory = ConfigurationManager.AppSettings["SinoPacInstructionBackUp"];
if (!Directory.Exists(targetDirectory))
{
Directory.CreateDirectory(targetDirectory);
}
string fileName = Path.GetFileName(localFilePath);
string datepart = DateTime.Now.ToString("yyyyMMddHHmmss");
string newFileName = datepart + "_" + fileName;
string sourceFilePath = Path.Combine(sourceDirectory, fileName);
string targetFilePath = Path.Combine(targetDirectory, newFileName);
System.IO.File.Move(sourceFilePath, targetFilePath);
}
else
{
UploadStatusChanged?.Invoke("API 1呼叫失敗");
string errorMessage = "API 呼叫失敗 " + apiResponse.StatusCode;
//寄信
string subject = "【aa & bb】aaa信託收檔結果查詢服務_" + DateTime.Now.ToString("yyyy/MM/dd");
string body = "API 呼叫失敗,請至AllPayLog查看原因";
emailService.SendErrorEmail(subject, body);
BatchLogService.WriteLog(errorMessage);
apiResponse.Success = false;
}
}
}
catch (Exception ex)
{
UploadStatusChanged?.Invoke("未預期的異常");
BatchLogService.WriteLog("發生錯誤: " + ex.ToString(), 2);
}
}
```
### ApiService (API接應)
**建構子 (Constructor)**
在建構子中,你設定了 HttpClient 的一些預設請求頭,並初始化了 HttpClient 實例。這個設計有助於所有的 API 請求都使用相同的 HTTP 頭資訊。
```csharp=
public apiService()
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("X-xxxxxx", "x");
httpClient.DefaultRequestHeaders.Add("X-xxxxx", "xxxxx");
httpClient.DefaultRequestHeaders.Add("xxxx", "xxxxxxxxxxxxxxxx");
}
```
**CallApi 方法**
這個方法接受兩個參數:API 的 endpoint (selectedApiEndpoint) 和加密過的請求體 (encryptedResquestBody)。
1. 發送 API 請求:使用 HttpClient 的 PostAsync 方法來發送一個 POST 請求。
2. 處理 API 響應:
* 如果 API 請求成功並且返回了資料,將返回的資料解密並儲存。
* 如果 API 請求成功但查無資料,寫入 Log 並返回一個不成功的響應。
3. 異常處理:對於 HttpRequestException、IOException,和一般的 Exception,分別進行了處理。
4. 日誌和通知:利用 BatchLogService 和 EmailService 記錄事件和發送通知。
```csharp=
public ResponseBodyModel CallApi(string selectedApiEndpoint, string encryptedResquestBody)
{
ResponseBodyModel apiResponse = new ResponseBodyModel();
try
{
StringContent content = new StringContent(encryptedResquestBody, Encoding.UTF8, "application/json");
HttpResponseMessage apiresponse = Task.Run(() => httpClient.PostAsync(selectedApiEndpoint, content)).GetAwaiter().GetResult();
if (apiresponse.IsSuccessStatusCode == true)
{
string responseBody = apiresponse.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (responseBody.Contains("查無資料"))
{
// 寫入 Log 並終止後續程式
BatchLogService.WriteLog("API 請求成功但查無資料. Response Body:" + responseBody,2);
apiResponse.Success = false;
apiResponse.Data = "查無資料";
}
else
{
// 正常處理
string key = ConfigurationManager.AppSettings["MySecretKey"];
byte[] k1 = Convert.FromBase64String(key);
EncryptorService.FlattenedJose encryptedData = JsonConvert.DeserializeObject<EncryptorService.FlattenedJose>(responseBody);
string decodedData = DecodeDataService.DecodeFlattenedJoseContainer(encryptedData, k1);
apiResponse.Success = true;
apiResponse.Data = decodedData;
BatchLogService.WriteLog("API 請求成功. Response Body:" + apiResponse.Data);
}
}
else
{
string responseBody = apiresponse.Content.ReadAsStringAsync().GetAwaiter().GetResult();
string statusCode = ((int)apiresponse.StatusCode).ToString();
string errorMessage = "API 請求失敗,狀態碼: " + statusCode + ", reason " + apiresponse.ReasonPhrase + ", and response body " + responseBody;
#region 寄信
string subject = "【aa & bb】aa信託信件通知" + DateTime.Now.ToString("yyyy/MM/dd");
string body = "API 呼叫失敗,請至AllPayLog查看原因";
EmailService emailService = new EmailService();
emailService.SendErrorEmail(subject, body);
#endregion
BatchLogService.WriteLog(errorMessage, 2);
apiResponse.Success = false;
apiResponse.Data = errorMessage;
}
}
catch (HttpRequestException httpRequestException)
{
apiResponse.Success = false;
BatchLogService.WriteLog("HTTP請求失敗: " + httpRequestException.Message, 2);
if (httpRequestException.InnerException != null)
{
BatchLogService.WriteLog("內部異常:"+ httpRequestException.InnerException, 2);
}
}
catch (IOException ioException)
{
apiResponse.Success = false;
BatchLogService.WriteLog("IO異常:" +ioException.Message, 2);
if (ioException.InnerException != null)
{
BatchLogService.WriteLog("內部異常:"+ioException.InnerException, 2);
}
}
catch (Exception ex)
{
apiResponse.Success = false;
BatchLogService.WriteLog("一般異常:" + ex.ToString(), 2);
}
return apiResponse;
}
```
### EncryptorService(AES加密演算法)
**FlattenedJose 類別**
這個內嵌類別用於保存 Jose 容器的各個部分:_protected、iv(初始化向量)、ciphertext(密文)和 tag
```csharp=
public class FlattenedJose
{
[JsonProperty(propertyName: "protected")]
public string _protected { get; set; }
public string iv { get; set; }
public string ciphertext { get; set; }
public string tag { get; set; }
}
```
**EncodeFlattenedJoseContainer 函數**
接受三個參數:
* jsonPayload: 要加密的原始文本。
* key: 用於加密的金鑰。
* iv: 初始化向量。
該函數使用 GCM(Galois/Counter Mode)進行加密,這是一種使用對稱密鑰進行加密和附加認證數據(AAD)的模式。
**Base64UrlEncode 函數**
負責將輸入的字節數組進行 Base64 Url 編碼。
```csharp=
public static string Base64UrlEncode(byte[] input)
{
string output = Convert.ToBase64String(input);
output = output.Split('=')[0]; // 移除任何尾隨的 '='
output = output.Replace('+', '-'); // '+' => '-'
output = output.Replace('/', '_'); // '/' => '_'
return output;
}
```
**流程**
**1. 初始化保護頭部(_protected)**
這部分定義了加密的參數,如加密算法(alg)、金鑰識別符(kid)和加密模式(enc)。這些參數被序列化為一個 JSON 對象,並進行 Base64Url 編碼。
```csharp=
var _protected = JsonConvert.SerializeObject(new Dictionary<string, string>
{
{ "alg","dir"},
{ "kid","K1"},
{ "enc","A128GCM"}
});
_protected = Base64UrlEncode(Encoding.UTF8.GetBytes(_protected));
```
**2. 附加認證數據(AAD )**
_protected 數據會被轉換為 ASCII 字節數組,這將作為 GCM 加密中的附加認證數據(AAD)。
```csharp=
var aad = Encoding.ASCII.GetBytes(_protected);
```
**3. 準備要加密的文本**
輸入的 JSON payload(jsonPayload)被轉換為一個 UTF-8 字節數組。
```csharp=
var text = Encoding.UTF8.GetBytes(jsonPayload);
```
**4. 初始化 GCM cipher**
這裡使用了 BouncyCastle 的 GcmBlockCipher 和 AesEngine。AES 加密算法和 GCM 模式是在這一步中設置的
```csharp=
GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine());
AeadParameters parameters = new AeadParameters(new KeyParameter(key), 128, iv, aad);
cipher.Init(true, parameters);
```
**5.執行加密**
在初始化後,cipher 對象用於對文本(text)進行加密。
```csharp=
byte[] ciphertext = new byte[cipher.GetOutputSize(text.Length)];
int len = cipher.ProcessBytes(text, 0, text.Length, ciphertext, 0);
cipher.DoFinal(ciphertext, len);
```
**6.提取認證標記(Tag)**
最後 16 個字節是 GCM 的認證標記,它用於後續的消息完整性檢查。
```csharp=
byte[] tag = new byte[16];
Array.Copy(ciphertext, ciphertext.Length - 16, tag, 0, 16);
```
**7.封裝結果**
所有這些數據(_protected,iv,ciphertext 和 tag)被封裝到一個 FlattenedJose 對象中,然後返回。
```csharp=
return new FlattenedJose
{
_protected = _protected,
iv = Base64UrlEncode(iv),
ciphertext = Base64UrlEncode(ciphertext.Take(ciphertext.Length - 16).ToArray()),
tag = Base64UrlEncode(tag)
};
```
**完整版**
```csharp=
public class EncryptorService
{
public static FlattenedJose EncodeFlattenedJoseContainer(string jsonPayload, byte[]key,byte[]iv)
{
var _protected = JsonConvert.SerializeObject(new Dictionary<string, string>
{
{ "alg","dir"}, Encryption)
{ "kid","K1"},
{ "enc","A128GCM"}
});
_protected = Base64UrlEncode(Encoding.UTF8.GetBytes(_protected));
var aad = Encoding.ASCII.GetBytes(_protected);
var text = Encoding.UTF8.GetBytes(jsonPayload);
GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine());
AeadParameters parameters = new AeadParameters(new KeyParameter(key), 128, iv, aad);
cipher.Init(true, parameters);
byte[] ciphertext = new byte[cipher.GetOutputSize(text.Length)];
int len = cipher.ProcessBytes(text, 0, text.Length, ciphertext, 0);
cipher.DoFinal(ciphertext, len);
byte[] tag = new byte[16];
Array.Copy(ciphertext, ciphertext.Length - 16, tag, 0, 16);
return new FlattenedJose
{
_protected = _protected,
iv = Base64UrlEncode(iv),
ciphertext = Base64UrlEncode(ciphertext.Take(ciphertext.Length - 16).ToArray()),
tag = Base64UrlEncode(tag)
};
}
}
```
### DecodeDataService(AES解密演算法)
**流程:**
**1. 解碼和驗證 _protected 字段**
使用 Base64Url 解碼 _protected 字段並將其解析為字典。
**2. 算法和金鑰標識符驗證**
驗證解碼後的字典是否包含 "alg", "kid", 和 "enc" 字段,以及它們是否具有預期的值。
**3. 初始化變數**
* aad: 由 _protected 字段轉換為 ASCII 字節數組。
* iv: 初始化向量,由 encyryptedData.iv 解碼。
* ciphertext: 密文,由 encyryptedData.ciphertext 解碼。
* tag: 標記(或許可字元),由 encyryptedData.tag 解碼。
* key: 解密金鑰,就是輸入的 k1。
**4. 初始化 GCM cipher**
使用 AesEngine 初始化 GcmBlockCipher,並設置參數。
**5. 進行解密**
* 結合密文和標記以形成一個連續的字節數組。
* 使用 cipher 進行解密。
**6. 返回解密後的文本**
將解密後的字節數組轉換為 UTF-8 字符串。
```csharp=
class DecodeDataService
{
public static string DecodeFlattenedJoseContainer(FlattenedJose encyryptedData, byte[] k1)
{
var _prot = JsonConvert.DeserializeObject<Dictionary<string, string>>(
Encoding.UTF8.GetString(DecodeBase64Url(encyryptedData._protected)));
if (!(_prot.ContainsKey("alg") && _prot.ContainsKey("kid") && _prot.ContainsKey("enc")))
throw new InvalidCastException("Invalid protected header");
if (!(_prot["alg"] == "dir" && _prot["enc"] == "A128GCM"))
throw new InvalidCastException("Unsupported algorithms");
var aad = Encoding.ASCII.GetBytes(encyryptedData._protected);
var iv = DecodeBase64Url(encyryptedData.iv);
var ciphertext = DecodeBase64Url(encyryptedData.ciphertext);
var tag = DecodeBase64Url(encyryptedData.tag);
byte[] key = k1;
var cipher = new GcmBlockCipher(new AesEngine());
var parameters = new AeadParameters(new KeyParameter(key), 128, iv, aad);
cipher.Init(false, parameters);
byte[] ciphertextWithTag = ciphertext.Concat(tag).ToArray();
byte[] plaintextBytes = new byte[cipher.GetOutputSize(ciphertext.Length + tag.Length)];
int length = cipher.ProcessBytes(ciphertextWithTag, 0, ciphertextWithTag.Length, plaintextBytes, 0);
cipher.DoFinal(plaintextBytes, length);
return Encoding.UTF8.GetString(plaintextBytes).TrimEnd('\0');
}
public static byte[] DecodeBase64Url(string base64Url)
{
base64Url = base64Url.Replace('-', '+').Replace('_', '/');
int padding = (4 - (base64Url.Length % 4)) % 4;
base64Url += new string('=', padding);
return Convert.FromBase64String(base64Url);
}
}
```