[.Net] 對稱式加解密,使用AES演算法的Sample Code

.Net Framework AES SymmetricAlgorithm Sample Code (include .Net Core)

前言

之前開發的Web網站被白箱工具掃出Parameter Tampering(參數竄改)的漏洞,直到最近去恆逸上CASE.Net課程才知道原來解法要對傳遞的關鍵參數先做加密,接收頁收到後做解密

然後拿解密後的值和資料庫的資料再比對驗證資料是否正確、完整。

我以前只有實作DB資料驗證,沒有加解密,難怪仍然被工具掃描出Parameter Tampering漏洞

※關鍵參數:例如訂單編號、折價卷序號,如果透過Web前端QueryString、表單hidden field來傳遞資料並且有新刪修動作至資料庫,參數有被駭客竄改的風險,而且一被竄改成功會造成商業上的損失(瞧瞧之前有人0元買到高鐵票的新聞),此時最好對關鍵參數加解密+接收端再和DB資料驗證確認資料完整性,不然就得把被敏感資料儲存在Session中傳遞,減少駭客接觸敏感資料的機率。

※至於有人會竄改QueryString來查詢、進入不該進入的畫面,這個則是驗證、授權相關問題

實作

本文程式碼適用.Net Framework 4.7.1以上、.Net Core 2.1.0以上

自己寫的類別 MyAesCryptography.cs ↓ 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
/*引用這兩個命名空間*/
using System.Security.Cryptography;
using System.Text;

namespace Console_MyPrj
{
    public class MyAesCryptography
    {
        /// <summary>
        /// 驗證key和iv的長度(AES只有三種長度適用)
        /// </summary>
        /// <param name="key"></param>
        /// <param name="iv"></param>
        private static void Validate_KeyIV_Length(string key,string iv)
        {
            //驗證key和iv都必須為128bits或192bits或256bits
            List<int> LegalSizes = new List<int>() { 128, 192, 256 };
            int keyBitSize = Encoding.UTF8.GetBytes(key).Length * 8;
            int ivBitSize = Encoding.UTF8.GetBytes(iv).Length * 8;
            if (!LegalSizes.Contains(keyBitSize) || !LegalSizes.Contains(ivBitSize))
            {
                throw new Exception($@"key或iv的長度不在128bits、192bits、256bits其中一個,輸入的key bits:{keyBitSize},iv bits:{ivBitSize}");
            }
        }
        /// <summary>
        /// 加密後回傳base64String,相同明碼文字編碼後的base64String結果會相同(類似雜湊),除非變更key或iv
        /// 如果key和iv忘記遺失的話,資料就解密不回來
        /// base64String若使用在Url的話,Web端記得做UrlEncode
        /// </summary>
        /// <param name="key"></param>
        /// <param name="iv"></param>
        /// <param name="plain_text"></param>
        /// <returns></returns>
        public static string Encrypt(string key,string iv,string plain_text)
        {
           
            
            Validate_KeyIV_Length(key,iv);
            Aes aes = Aes.Create();
            aes.Mode = CipherMode.CBC;//非必須,但加了較安全
            aes.Padding = PaddingMode.PKCS7;//非必須,但加了較安全

            ICryptoTransform transform = aes.CreateEncryptor(Encoding.UTF8.GetBytes(key), Encoding.UTF8.GetBytes(iv));

            byte[] bPlainText = Encoding.UTF8.GetBytes(plain_text);//明碼文字轉byte[]
            byte[] outputData = transform.TransformFinalBlock(bPlainText, 0, bPlainText.Length);//加密
            return Convert.ToBase64String(outputData);  
        }
        /// <summary>
        /// 解密後,回傳明碼文字
        /// </summary>
        /// <param name="key"></param>
        /// <param name="iv"></param>
        /// <param name="base64String"></param>
        /// <returns></returns>
        public static string  Decrypt(string key, string iv, string base64String)
        {
            
            
            Validate_KeyIV_Length(key,iv);
            Aes aes = Aes.Create();
            aes.Mode = CipherMode.CBC;//非必須,但加了較安全
            aes.Padding = PaddingMode.PKCS7;//非必須,但加了較安全

            ICryptoTransform transform = aes.CreateDecryptor(Encoding.UTF8.GetBytes(key), Encoding.UTF8.GetBytes(iv));
            byte[] bEnBase64String = null;
            byte[] outputData = null;
            try
            {
                bEnBase64String = Convert.FromBase64String(base64String);//有可能base64String格式錯誤
                outputData = transform.TransformFinalBlock(bEnBase64String, 0, bEnBase64String.Length);//有可能解密出錯
            }
            catch (Exception ex)
            {
                //todo 寫Log
                throw new Exception($@"解密出錯:{ex.Message}"); 
            }
           
             //解密成功
            return Encoding.UTF8.GetString(outputData);
            
        }
    }
}

使用方式↓

using System;
using Console_MyPrj;

namespace ConsoleApp1PWDTest
{
    class Program
    {
        /// <summary>
        /// ↓自己決定,可以的話,最好使用  RNGCryptoServiceProvider 來產生key 和 IV
        /// </summary>
        private static string myKey = "1234567812345678";//必須至少16個字元,最大32個字元,因為使用AES演算法,可以的話,最好給32個字比較安全
        private static string myIV = "1234567812345678";//必須至少16個字元,最大32個字元,因為使用AES演算法,可以的話,最好給32個字比較安全
        static void Main(string[] args)
        {
            //原文
            string plain_text = "Hello World!!";
            //加密
            string base64String = MyAesCryptography.Encrypt(myKey, myIV, plain_text);
            Console.WriteLine($@"加密後:{base64String}");
            //解密
            string decrypt_text = MyAesCryptography.Decrypt(myKey, myIV, base64String);
            Console.WriteLine($@"解密後:{decrypt_text}");
        }
    }
}

執行結果↓

※使用本文需留意

如果你每次加密的key與iv都是給相同值的話,那麼相同原始明文加密後產生出來的密文每次都會一樣

事實上在每次加密時,IV都要給不同值,讓每次加密後的密文值都不相同,這樣會比較安全

結語

會選擇對稱式演算法是因為加解密我只想固定同一把金鑰,然後對稱式演算法DES、RC2、Triple-DES、Rijndael、AES當中,又以AES演算法最強健

Rijndael是AES前身,由於KeySize和IV (BlockSize)長度關係,微軟官方建議使用AES替代Rijndael演算法,詳見→:Rijndael ClassThe Differences Between Rijndael and AES

AES演算法使用方式參考微軟官網說明:Aes Class,本文程式碼我自認已經寫得很短很淺顯易懂XD

最後,本文只是個備忘錄,還沒經過白箱工具的考驗,說不定正式應用工作上又有其它安全性不足問題XD

 

2021.3.24 追記

加密v.s.雜湊 的不同

(和系統查詢比對資料有關↓)
相同原文在每次加密後,密文都不相同(密文無法比對)
相同原文在每次雜湊後,雜湊值都相同(雜湊值可以比對)=>會員登入密碼常使用這個

(和系統想要瀏覽資料有關↓)
加密需要金鑰,且可以透過解密得到原始明文。(加密可逆)
雜湊不需金鑰,無法逆向解出原始明文。(雜湊不可逆)

 

其它補充文章

亂數產生器:Random 與 RNGCryptoServiceProvider by Will保哥