手邊有個自訂傳輸管道的加密需求,預期資料量可能高達數MB,為提升效能,先壓縮再加密是不錯的做法,既可減少加密時間及成本,又能節省頻寬,一舉兩得。

過去用C#寫過DES加密,也寫過GZip壓縮,把兩個結合起來不是難事。如果不要想太多,取得待處理資料(byte[]),用GZipStream壓縮可得壓縮後資料(byte[]),再以壓縮後資料當成輸入參數交給CryptoStream進行DES加密,就得到壓縮並加密的結果(byte[])。

事實上,有更輕巧簡潔的做法-.NET的Stream支援設計模式(Design Pattern)裡的裝飾者模式(Decorator Pattern,也有人翻成修飾模式),裝飾者模式的精神在於「不使用繼承,而以組裝方式動態地為物件加入新功能」。常見的做法是設計一個裝飾類別,以被裝飾物件A做為建構式參數,而裝飾類別提供與被裝飾對象相同的方法,讓呼叫端使用跟原來相同的介面呼叫,得到裝飾類別加工後的結果。以GZip壓縮使用的GZipStream為例,假設原本有個FileStream fs,呼叫fs.Write()可以將byte[]寫入檔案。若要加入壓縮功能,我們可以建立一個GZipStream gzip = new GZipStream(fs, CompressionMode.Compress),寫入時改呼叫gzip.Write(),資料使會先經GZip壓縮再寫入檔案。如果要做到動態決定要不要壓縮,則可將變數stream宣告成抽象型別Stream(FileStream、GZipStream共同繼承的繼承來源),一開始stream = new FileStream(…),若決定壓縮,就讓stream = new GZipStream(stream, CompressionMode.Compress),寫入資料時則一律呼叫stream.Write(),便能實現執行期間動態切換是否壓縮,展現裝飾者模式的優勢。

解釋完概念,以下是完整程式範例:

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
 
namespace Demo
{
    class Program
    {
        //以金鑰字串進行雜湊運算產生DES所需的Key及IV byte[]
        public static Tuple<byte[], byte[]> GetKeyIV(string keyString)
        {
            MD5 md5 = MD5.Create();
            var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(keyString));
            byte[] key = new byte[8];
            byte[] iv = new byte[8];
            System.Buffer.BlockCopy(hash, 0, key, 0, 8);
            System.Buffer.BlockCopy(hash, 8, iv, 0, 8);
            return new Tuple<byte[],byte[]>(key, iv);
        }
        public static byte[] Encrypt(string key, byte[] data, bool compress = false)
        {
 
            using (MemoryStream ms = new MemoryStream())
            {
                DESCryptoServiceProvider des = new DESCryptoServiceProvider();
                var keyIv = GetKeyIV(key);
                //先建立加密用的CryptoStream
                using (CryptoStream crypto = new CryptoStream(ms,
                    des.CreateEncryptor(keyIv.Item1, keyIv.Item2), CryptoStreamMode.Write))
                {
                    //依compress參數將Stream gzip設成GzipStream
                    //若不需壓縮,gzip直接等於CryptoStream crypto
                    using (Stream gzip =
                        compress ?
                        (Stream)new GZipStream(crypto, CompressionMode.Compress) :
                        (Stream)crypto)
                    {
                        //不管是CryptoStream或GZipStream
                        //一視同仁寫入byte[]就對了
                        gzip.Write(data, 0, data.Length);
                    }
                }
                return ms.ToArray();
            }
        }
        public static byte[] Decrypt(string key, byte[] crypted, bool decompress = false)
        {
            //用MemoryStream將傳入的byte[]包裝成Stream物件
            using (MemoryStream ms = new MemoryStream(crypted))
            {
                DESCryptoServiceProvider des = new DESCryptoServiceProvider();
                var keyIv = GetKeyIV(key);
                //解密兼解壓時,CryptoStream一樣在外層
                using (CryptoStream crypto = new CryptoStream(ms,
                    des.CreateDecryptor(keyIv.Item1, keyIv.Item2), CryptoStreamMode.Read))
                {
                    using (Stream gzip =
                        decompress ?
                        (Stream)new GZipStream(crypto, CompressionMode.Decompress) :
                        (Stream)crypto)
                    {
                        //另外宣告一個MemoryStream用來存結果
                        using (MemoryStream msDec = new MemoryStream())
                        {
                            byte[] buff = new byte[8192];
                            int len = 0;
                            //由於不知結果資料長度,持續讀取
                            //由Read()傳回長度偵測是否已到結尾
                            while ((len = gzip.Read(buff, 0, buff.Length)) > 0)
                            {
                                msDec.Write(buff, 0, len);
                            }
                            return msDec.ToArray();
                        }
                    }
                }
            }
        }
 
 
        static void Main(string[] args)
        {
            WebClient wc = new WebClient();
            byte[] testData = wc.DownloadData(
                "http://cdn.kendostatic.com/2014.1.318/js/kendo.all.min.js");
            var encoding = Encoding.UTF8;
 
            int displayLen = Math.Min(testData.Length, 128);
            Console.WriteLine("來源長度={0:n0}", testData.Length);
            Console.WriteLine("內容預覽: {0}...", 
                encoding.GetString(testData, 0, displayLen));
            Console.WriteLine();
 
            var encKey = "SECRET";
            byte[] encrypted = Encrypt(encKey, testData);
            Console.WriteLine("加密後長度={0:n0}", encrypted.Length);
            Console.WriteLine("內容預覽: {0}...", 
                encoding.GetString(encrypted, 0, displayLen));
            Console.WriteLine();
 
            byte[] decrypted = Decrypt(encKey, encrypted);
            string srcB64 = Convert.ToBase64String(testData);
            Console.WriteLine("解密還原驗證: {0}",
                srcB64 == Convert.ToBase64String(decrypted));
 
            byte[] compressEncrypted = Encrypt(encKey, testData, true);
            Console.WriteLine("壓縮加密後長度={0:n0}", compressEncrypted.Length);
            Console.WriteLine("內容預覽: {0}...", 
                encoding.GetString(compressEncrypted, 0, displayLen));
            Console.WriteLine();
 
 
            byte[] decompressDecrypted = Decrypt(encKey, compressEncrypted, true);
            Console.WriteLine("解密還原驗證: {0}",
                srcB64 == Convert.ToBase64String(decompressDecrypted));
 
            Console.Read();
        }
    }
}

以Kendo UI Libaray為測試對象,原本長度為1,493,858,加密後大小為1,493,864,啟用壓縮再加密後大小為442,504,解密、解壓縮還原後與原始內容比對一致,測試成功。


Comments

# by kuanpak

有沒有試過先壓縮後加密? 感覺壓縮率應該會更高.

# by Jeffrey

to kuanpak, 同意,文章採用的做法也是先壓縮再加密,加密後的內容形同亂碼,壓縮率不會太好。

# by 別問我是誰

大大分享的範例真是"完整" 在網頁看了看還是不能體會,實際貼到 VS 上跑一次,在上面看程式碼也比較習慣。 謝謝了

# by Vanson

很喜歡 decorator pattern 例如 iOS UIAlertView 和 Android AlertView 等等

# by 胡忠晞

如果是 Decorator Pattern 應該要實作 WebClient 吧! 這篇講解 Decorator Pattern 比較清楚。 http://www.dotblogs.com.tw/pin0513/archive/2010/01/04/12779.aspx

Post a comment