在 .NET 要執行 DES/AES/RSA 加解密不是難事,遠從 .NET 3.5 時代,System.Security.Cryptography 命名空間都已內建相關 API 提供完整支援。不過這些安全相關 API 高度依賴作業系統的原生程式庫,從 .NET Core 開始,跨平台成為重要考量,綁死 Windows 將限制程式無法在 macOS 及 Linux 執行,無法邁向 Docker 新世界。

MS Docs 有篇文章 Cross-Platform Cryptography in .NET Core and .NET 5 整理了 System.Security.Cryptography 對於跨平台的支援狀況,基本的 AES、3DES、DES (CBC 及 ECB 模式) 還好,各平台都完整支援,但若用到 AES-CCM、AES-GCM、RSA、ECDSA、ECDH、DSA... 等演算法就得留意支援程度不一的問題。

另外一方面,.NET 安全 API 支援的加密應用不算完整,若要處理 PGP、TLS,得費不少工。評估之下,找個不依賴作業系統且功能完整的加解密程式庫,是較省事的抉擇,而 Bouncy Castle C# 是 .NET 社群最多人用的加解密開源程式庫。

由於系統存在一些利用 .NET DES、AES API 加密的資料,當然不能換了底層程式庫就解不開,因此我需要找到與 .NET 版完全相通的 Bouncy Castle DES、AES 加解密寫法,這就是這篇文章的終極目標,Let's Go!

以下是一段明朝(2002 年 .NET 1.1 時代)寫的 DESCryptoServiceProvider 加解密函式(.NET Core 起建議改用 Des.Create()),部分參數名還殘留匈牙利命名法的影子,原本有些太噁心太囉嗦的寫法我還是忍不住改掉了,並順手加上 AES 加解密,其中也有用到 SHA1、SHA256 由金鑰字串產生 Key 及 IV,剛好用來對照 Bouncy Castle 的 SHA1/SHA256 寫法: (註:DES、MD5、SHA1 目前都已被認定安全性不足,若應用與資安相關,建議改用 AES/SHA256 以上的演算法)

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace DesTest
{
    public class CodecNetFx
    {
        private class DesKeyIV
        {
            public Byte[] Key = new Byte[8];
            public Byte[] IV = new Byte[8];
            public DesKeyIV(string strKey)
            {
                var sha = new SHA1CryptoServiceProvider();
                var bpHash = sha.ComputeHash(Encoding.ASCII.GetBytes(strKey));
                for (int i = 0; i < 8; i++) Key[i] = bpHash[i];
                for (int i = 8; i < 16; i++) IV[i - 8] = bpHash[i];
            }
        }

        public static string DesEncrypt(string key, string rawString)
        {
            if (rawString.Length > 92160)
                return "Error. Data String too large. Keep within 90Kb.";
            var keyIv = new DesKeyIV(key);
            var rbData = UnicodeEncoding.Unicode.GetBytes(rawString);

            var descsp = new DESCryptoServiceProvider();
            var desEncrypt = descsp.CreateEncryptor(keyIv.Key, keyIv.IV);

            using (var mOut = new MemoryStream())
            {
                using (var cs = new CryptoStream(mOut, desEncrypt, CryptoStreamMode.Write))
                {
                    cs.Write(rbData, 0, rbData.Length);
                    cs.FlushFinalBlock();
                    if (mOut.Length == 0)
                        return string.Empty;
                    else
                    {
                        var buff = mOut.ToArray();
                        return Convert.ToBase64String(buff, 0, buff.Length);
                    }
                }
            }
        }

        public static string DesDecrypt(string key, string encString)
        {

            if (string.IsNullOrEmpty(encString)) return "ERROR: EncString is NULL!";

            var keyIv = new DesKeyIV(key);
            var descsp = new DESCryptoServiceProvider();
            var desDecrypt = descsp.CreateDecryptor(keyIv.Key, keyIv.IV);
            using (var mOut = new MemoryStream())
            {
                using (var cs = new CryptoStream(mOut, desDecrypt, CryptoStreamMode.Write))
                {
                    byte[] bPlain;
                    try
                    {
                        bPlain = Convert.FromBase64CharArray(encString.ToCharArray(), 0, encString.Length);
                    }
                    catch (Exception)
                    {
                        return "Error. Input Data is not base64 encoded.";
                    }
                    try
                    {
                        cs.Write(bPlain, 0, (int)bPlain.Length);
                        cs.FlushFinalBlock();
                        return Encoding.Unicode.GetString(mOut.ToArray());
                    }
                    catch (Exception e)
                    {
                        return "Error. Decryption Failed. Possibly due to incorrect Key or corrputed data: " + e.ToString();
                    }
                }
            }
        }

        private class AesKeyIV
        {
            public Byte[] Key = new Byte[16];
            public Byte[] IV = new Byte[16];
            public AesKeyIV(string strKey)
            {
                var sha = SHA256.Create();
                var hash = sha.ComputeHash(Encoding.ASCII.GetBytes(strKey));
                Array.Copy(hash, 0, Key, 0, 16);
                Array.Copy(hash, 16, IV, 0, 16);
            }
        }
        public static string AesEncrypt(string key, string rawString)
        {
            var keyIv = new AesKeyIV(key);
            var aes = Aes.Create();
            aes.Key = keyIv.Key;
            aes.IV = keyIv.IV;
            var rawData = Encoding.UTF8.GetBytes(rawString);
            using (var ms = new MemoryStream())
            {
                using (var cs = new CryptoStream(ms, aes.CreateEncryptor(aes.Key, aes.IV), CryptoStreamMode.Write))
                {
                    cs.Write(rawData, 0, rawData.Length);
                    cs.FlushFinalBlock();
                    return Convert.ToBase64String(ms.ToArray());
                }
            }
        }

        public static string AesDecrypt(string key, string encString)
        {
            var keyIv = new AesKeyIV(key);
            var aes = Aes.Create();
            aes.Key = keyIv.Key;
            aes.IV = keyIv.IV;
            var encData = Convert.FromBase64String(encString);
            byte[] buffer = new byte[8192];
            using (var ms = new MemoryStream(encData))
            {
                using (var cs = new CryptoStream(ms, aes.CreateDecryptor(aes.Key, aes.IV), CryptoStreamMode.Read))
                {
                    using (var sr = new StreamReader(cs))
                    {
                        using (var dec = new MemoryStream())
                        {
                            cs.CopyTo(dec);
                            return Encoding.UTF8.GetString(dec.ToArray());
                        }
                    }
                }
            }
        }
    }
}

若改用 Bouncy Castle API 實現,程式如下,其中的小眉角是 DES/AES 要搭配加密模式(CBC/ECB)跟填充模式(PKCS#5、PKCS#7)有多種組合,得跟 Windows 一樣才能互通。實測設定 DES/CBC/PKCS5Padding 跟 AES/CBC/PKCS7 加密結果便與 System.Security.Cryptography DES/AES 一致:

using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using System;
using System.Text;

namespace DesTest
{
    public class CodecBouncyCastle
    {
        private class DesKeyIV
        {
            public Byte[] Key = new Byte[8];
            public Byte[] IV = new Byte[8];
            public DesKeyIV(string strKey)
            {
                var sha = new Sha1Digest();
                var hash = new byte[sha.GetDigestSize()];
                var data = Encoding.ASCII.GetBytes(strKey);
                sha.BlockUpdate(data, 0, data.Length);
                sha.DoFinal(hash, 0);
                for (int i = 0; i < 8; i++) Key[i] = hash[i];
                for (int i = 8; i < 16; i++) IV[i - 8] = hash[i];
            }
        }


        public static string DesEncrypt(string key, string rawString)
        {
            if (rawString.Length > 92160)
                return "Error. Data String too large. Keep within 90Kb.";
            var keyIv = new DesKeyIV(key);
            // var engine = new DesEngine();
            // new PaddedBufferedBlockCipher(new CbcBlockCipher(engine));
            var cipher = CipherUtilities.GetCipher("DES/CBC/PKCS5Padding"); 
            cipher.Init(true, new ParametersWithIV(new KeyParameter(keyIv.Key), keyIv.IV));
            var rbData = UnicodeEncoding.Unicode.GetBytes(rawString);
            return Convert.ToBase64String(cipher.DoFinal(rbData));
        }

        public static string DesDecrypt(string key, string encString)
        {
            if (string.IsNullOrEmpty(encString)) return "ERROR: EncString is NULL!";
            var keyIv = new DesKeyIV(key);
            var cipher = CipherUtilities.GetCipher("DES/CBC/PKCS5Padding");
            cipher.Init(false, new ParametersWithIV(new KeyParameter(keyIv.Key), keyIv.IV));
            var encData = Convert.FromBase64String(encString);
            return Encoding.Unicode.GetString(cipher.DoFinal(encData));
        }


        private class AesKeyIV
        {
            public Byte[] Key = new Byte[16];
            public Byte[] IV = new Byte[16];
            public AesKeyIV(string strKey)
            {
                var sha = new Sha256Digest();
                var hash = new byte[sha.GetDigestSize()];
                var data = Encoding.ASCII.GetBytes(strKey);
                sha.BlockUpdate(data, 0, data.Length);
                sha.DoFinal(hash, 0);
                Array.Copy(hash, 0, Key, 0, 16);
                Array.Copy(hash, 16, IV, 0, 16);
            }
        }

        //REF: https://kashifsoofi.github.io/cryptography/aes-in-csharp-using-bouncycastle/
        public static string AesEncrypt(string key, string rawString)
        {
            var keyIv = new AesKeyIV(key);
            // Default - AES/GCM/NoPadding、System.Security.AES - AES/CBC/PKCS7
            var cipher = CipherUtilities.GetCipher("AES/CBC/PKCS7");
            cipher.Init(true, new ParametersWithIV(new KeyParameter(keyIv.Key), keyIv.IV));
            var rawData = Encoding.UTF8.GetBytes(rawString);
            return Convert.ToBase64String(cipher.DoFinal(rawData));
        }

        public static string AesDecrypt(string key, string encString)
        {
            var keyIv = new AesKeyIV(key);
            // Default - AES/GCM/NoPadding、System.Security.AES - AES/CBC/PKCS7
            var cipher = CipherUtilities.GetCipher("AES/CBC/PKCS7");
            cipher.Init(false, new ParametersWithIV(new KeyParameter(keyIv.Key), keyIv.IV));
            var encData = Convert.FromBase64String(encString);
            return Encoding.UTF8.GetString(cipher.DoFinal(encData));
        }
    }
}

測試程式:

using System;

namespace DesTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.ForegroundColor = ConsoleColor.Cyan;
            var raw = "Hello, World! 加密測試";
            var key = "#The3ncryp7Key";
            var desEnc = CodecNetFx.DesEncrypt(key, raw);
            var desDec = CodecNetFx.DesDecrypt(key, desEnc);
            var aesEnc = CodecNetFx.AesEncrypt(key, raw);
            var aesDec = CodecNetFx.AesDecrypt(key, aesEnc);
            Console.WriteLine("** System.Security.Cryptography **");
            Console.WriteLine($"raw = {raw}");
            Console.WriteLine($"DES enc = {desEnc}");
            Console.WriteLine($"DES dec = {desDec}");
            Console.WriteLine($"AES enc = {aesEnc}");
            Console.WriteLine($"AES dec = {aesDec}");


            Console.ForegroundColor = ConsoleColor.Yellow;
            desEnc = CodecBouncyCastle.DesEncrypt(key, raw);
            desDec = CodecBouncyCastle.DesDecrypt(key, desEnc);
            aesEnc = CodecBouncyCastle.AesEncrypt(key, raw);
            aesDec = CodecBouncyCastle.AesDecrypt(key, aesEnc);
            Console.WriteLine("** BouncyCastle **");
            Console.WriteLine($"raw = {raw}");
            Console.WriteLine($"DES enc = {desEnc}");
            Console.WriteLine($"DES dec = {desDec}");
            Console.WriteLine($"AES enc = {aesEnc}");
            Console.WriteLine($"AES dec = {aesDec}");

            Console.ResetColor();
            Console.ReadLine();

        }
    }

}

實測 Bouncy Castle 的演算結果與 System.Security.Cryptography 相同,可安心更換。

【延伸閱讀】

Example of using Bouncy Castle library to replace System.Security.Cryptography to DES/AES encrypt and decrypt data.


Comments

# by Vinix

我對加解密不是很熟,在分別使用BouncyCastle AES/GCM/NoPadding及C#內建的AesGcm來進行加解密時注意到,前者加密後的byte[]長度會比後者增加16,如下: Key:2C-7C-64-E1-BC-C1-C9-F5-41-53-FA-2E-0D-73-79-DE IV:66-C5-EC-32-DD-2B-A1-74-1B-79-D7-53 Original: This is Test Data!!! Original Bytes(Length: 20): 54-68-69-73-20-69-73-20-54-65-73-74-20-44-61-74-61-21-21-21 Encrypted using BouncyCastle(Length: 36): 49-3A-75-27-31-3E-E3-E1-95-B1-BC-09-22-97-4F-A6-94-57-D2-CB-80-71-75-92-5D-0B-E9-CA-2D-87-FF-47-AE-FD-22-9F Decrypted using BouncyCastle: This is Test Data!!! Encrypted using .NET(Length: 20): 49-3A-75-27-31-3E-E3-E1-95-B1-BC-09-22-97-4F-A6-94-57-D2-CB Decrypted using .NET: This is Test Data!!! 為什麼呢?

# by Vinix

後來發現,增加的長度剛好等於Tag Size,BouncyCastle的加密結果等於AesGcm的加密結果後面再接著Tag。

# by Jeffrey

to Vinix,我對加解密演算法細節研究有限,推測 BouncyCastle 結果較長的原因是它串接了 Authentication Tag: The main difference here (other than the low-level API) is that the resulting ciphertext comes pre-concatenated with the authentication tag. 參考:https://www.scottbrady91.com/c-sharp/aes-gcm-dotnet tagLength 預設 128 bit,換算等於 16 bytes,36-20=16,感覺挺吻合的。 參考:https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams 以上純屬不專業推測,正確性有待你驗證了。

Post a comment