這是今天碰到的有趣問題。一般我們在使用對稱 (或非對稱) 加密演算法加密資料時,很習慣的就用 CryptoStream,然後用 Write 來加密,用 Read 來解密,而且平常也是用的好好的,一些字串加密的工作其實很容易就做完了。但是如果遇到了檔案型的資料時,很容易會因為檔案大小在加密時發生變化,而導致解密時檔案無法被打開 (ex: Office 2007 的檔案)。如果觀察一下,可以發現原始資料和加密過的資料大小會有不同。
這是今天碰到的有趣問題。一般我們在使用對稱 (或非對稱) 加密演算法加密資料時,很習慣的就用 CryptoStream,然後用 Write 來加密,用 Read 來解密,而且平常也是用的好好的,一些字串加密的工作其實很容易就做完了。但是如果遇到了檔案型的資料時,很容易會因為檔案大小在加密時發生變化,而導致解密時檔案無法被打開 (ex: Office 2007 的檔案)。如果觀察一下,可以發現原始資料和加密過的資料大小會有不同。
症狀 (Symptoms)
當使用密碼演算法時,加密過的檔案大小會增加,以致解密時檔案資料可能會被檔案的處理程式視為損毀或無法開啟。
原因 (Cause)
其實這和密碼演算法在演算過程中的機制有關,以 AES (Advanced Encryption Standard,在 .NET 中會以 RijndaelManaged 類別實作) 為例,AES 演算法是以資料區塊 (block) 進行加密計算的,如下圖 (Source: 維基百科的 AES 條目):
在加密進行時,資料會依據所給定的區塊大小 (AES 為 128 位元,即 16 bytes) 進行資料切割,再進行演算程序,而當資料的大小不足區塊大小 (ex: 10 bytes) 時,AES 演算法會在不足的資料中加入填補位元 (padding bytes),以補足資料的區塊大小後再進行演算程序,當資料加密完成後,這些增補資料會保存在已加密的資料中,這時已加密的資料通常會比原始資料來的大,而且大小會等於區塊大小的倍數。而增補資料並不會因為解密就會自動由檔案資料中移除,導致檔案在解密後會因為大小的不同而可能會無法開啟,或是可開啟但會有錯誤訊息 (ex: Word 2007, Excel 2007, Acrobat Reader)。
解決方法 (Solution)
要解決這個問題,就要在加密時取得有多少位元組是被增補進去的,然後在解密時扣除這些增補的位元組即可,例如下列程式碼:
int paddingByteSize = 0;
FileStream fsSource = new FileStream(args[1], FileMode.Open, FileAccess.Read);
string encFileName = Path.GetFileName(fsSource.Name) + ".enc";
plainData = new byte[fsSource.Length];
stream = new CryptoStream(
new FileStream(Environment.CurrentDirectory + @"\" + encFileName, FileMode.Create, FileAccess.Write),
algorithm.CreateEncryptor(),
CryptoStreamMode.Write);
fsSource.Read(plainData, 0, plainData.Length);
fsSource.Close();
stream.Write(plainData, 0, plainData.Length);
stream.FlushFinalBlock();
stream.Flush();
stream.Close();
paddingByteSize = ((int)(new FileInfo(Environment.CurrentDirectory + @"\" + encFileName)).Length) - plainData.Length;
加密的流程都和原始的 .NET 加密完全一樣,唯一不同的是最後會由加密後檔案的大小與原始檔案大小相減,以取得增補位元的資料數。
而解密的部份如下:
stream = new CryptoStream(new FileStream(args[1], FileMode.Open, FileAccess.Read), algorithm.CreateDecryptor(), CryptoStreamMode.Read);
plainData = new byte[((int)(new FileInfo(args[1])).Length)];
stream.Read(plainData, 0, plainData.Length);
stream.Close();
FileStream fsDec = new FileStream(Environment.CurrentDirectory + @"\" + args[3], FileMode.Create, FileAccess.Write);
fsDec.Write(plainData, 0, (plainData.Length - Convert.ToInt32(args[2])));
fsDec.Flush();
fsDec.Close();
在解密之後取得的資料寫入檔案前,要先將增補的位元數扣除後,再寫入檔案,而增補位元都是放在資料的最後面,所以直接裁切掉最後多的位元組即可。
Sample Code: https://dotblogsfile.blob.core.windows.net/user/regionbbs/1105/201151916252243.rar
Reference:
維基百科 AES 演算法條目。
MSDN RijndaelManaged 類別說明。