# API資料串接 ## 流程說明 此筆記紀錄與銀行互相用api傳遞資料,其中包含 * **AES加密演算法(EncryptorService )** * **資料存取txt檔** * **讀txt檔(ProcessAllTxtFilesInDirectory)** * **寄信功能** * **使用引數自動執行專案** ## UI設計 2個Combobox、1個DateTimtPicker、2個button以及textBoxStatus ![](https://hackmd.io/_uploads/SyWhdZHzT.png) ## 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); } } ```