[.NET] 使用密碼演算法加解密檔案時,會出現檔案開啟失敗或是大小增加的解法

這是今天碰到的有趣問題。一般我們在使用對稱 (或非對稱) 加密演算法加密資料時,很習慣的就用 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-SubBytes.svg

在加密進行時,資料會依據所給定的區塊大小 (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 類別說明。