Quantcast
Channel: 黑暗執行緒
Viewing all articles
Browse latest Browse all 2311

裝飾者模式(Decorator Pattern)實例:壓縮加密一次完成

$
0
0

手邊有個自訂傳輸管道的加密需求,預期資料量可能高達數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[]
publicstatic Tuple<byte[], byte[]> GetKeyIV(string keyString)
        {
            MD5 md5 = MD5.Create();
            var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(keyString));
byte[] key = newbyte[8];
byte[] iv = newbyte[8];
            System.Buffer.BlockCopy(hash, 0, key, 0, 8);
            System.Buffer.BlockCopy(hash, 8, iv, 0, 8);
returnnew Tuple<byte[],byte[]>(key, iv);
        }
publicstaticbyte[] 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();
            }
        }
publicstaticbyte[] 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 = newbyte[8192];
int len = 0;
//由於不知結果資料長度,持續讀取
//由Read()傳回長度偵測是否已到結尾
while ((len = gzip.Read(buff, 0, buff.Length)) > 0)
                            {
                                msDec.Write(buff, 0, len);
                            }
return msDec.ToArray();
                        }
                    }
                }
            }
        }
staticvoid 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,解密、解壓縮還原後與原始內容比對一致,測試成功。


Viewing all articles
Browse latest Browse all 2311

Trending Articles