无需解密到磁盘即可使用加密文件






4.84/5 (21投票s)
演示如何在内存中解密文件以维护数据安全。
引言
加密和解密通常是基于文件的...也就是说,一个加密的文件被解密到磁盘上的另一个文件,应用程序从解密的文件中读取。假设使用加密的初衷是为了维护安全和/或限制对受版权保护或敏感材料的访问,那么解密到磁盘文件就违背了最初使用加密的目的。
举例来说,假设一个应用程序使用受版权保护的音频文件,这些文件可以在应用程序中使用,但绝不能在应用程序外部使用。为了限制这种访问,音频文件被加密了。让应用程序将文件解密到磁盘是一个问题,因为即使文件在播放后立即被自动删除,它仍然存在于磁盘上,并且理论上可以被恢复。
一个更好的解决方案是让应用程序在内存中解密文件,这样未加密的数据就不会在磁盘上留下任何痕迹。(关于此声明的一个警告,请参阅“要点”部分。)
背景
假定读者了解基本输入/输出操作,即使用 FileStream 和相关类来读写文件。
使用代码
示例解决方案中的所有加密/解密代码都包含在 EncryptionEngine 类中,该类仅使用 .NET 原生类。只需将该类包含在项目中并使用可用的方法即可。
本文提供的示例解决方案包含两个 Windows 窗体应用程序。第一个应用程序使用 EncryptionEngine 允许用户选择和加密文本和 wav 文件。第二个应用程序允许用户选择一个加密的 wav 文件并使用 .NET Media.Soundplayer 播放它,或者选择一个文本文件并将其内容显示在一个弹出窗口中。
让我们从加密过程开始。对于这个示例应用程序,我选择使用 DES 加密,尽管代码可以很容易地修改为使用更强大的加密方法。
首先,原始文件被读取到一个内部缓冲区。
// Open the file and prepare the buffer
FileInfo info = new FileInfo(sourceFile);
FileStream input = new FileStream(sourceFile, FileMode.Open, FileAccess.Read);
byte[] dta = new byte[info.Length];
// Read the raw file into the buffer and close it
input.Read(dta, 0, (int)info.Length);
input.Close();
接下来,数据缓冲区被加密并写入输出文件。
FileStream output = new FileStream(destFile, FileMode.Create, FileAccess.Write);
// Setup the encryption object
DESCryptoServiceProvider cryptic = new DESCryptoServiceProvider();
cryptic.Key = ASCIIEncoding.ASCII.GetBytes("NOWISTHE");
cryptic.IV = ASCIIEncoding.ASCII.GetBytes("WINTEROF");
// Create a crptographic output stream for the already open file output stream
CryptoStream crStream = new CryptoStream(output, cryptic.CreateEncryptor(), CryptoStreamMode.Write);
// Write the data buffer to the encryption stream, then close everything
crStream.Write(dta, 0, dta.Length);
crStream.Close();
output.Close();
此时,文件已被加密并以新名称保存。完整的 EncryptBinaryFile 方法如下所示:
public void EncryptBinaryFile(String sourceFile, String destFile)
{
// Get file info, used to know the size of the file
FileInfo info = new FileInfo(sourceFile);
// Open the file for input
FileStream input = new FileStream(sourceFile, FileMode.Open, FileAccess.Read);
// Create a data buffer large enough to hold the file -- Could be modified to read
// the file in smaller chunks.
byte[] dta = new byte[info.Length];
// Read the raw file into the buffer and close it
input.Read(dta, 0, (int)info.Length);
input.Close();
// Open the file for output
FileStream output = new FileStream(destFile, FileMode.Create, FileAccess.Write);
// Setup the encryption object
DESCryptoServiceProvider cryptic = new DESCryptoServiceProvider();
cryptic.Key = ASCIIEncoding.ASCII.GetBytes("NOWISTHE");
cryptic.IV = ASCIIEncoding.ASCII.GetBytes("WINTEROF");
// Create a crptographic output stream for the already open file output stream
CryptoStream crStream = new CryptoStream(output, cryptic.CreateEncryptor(),
CryptoStreamMode.Write);
// Write the data buffer to the encryption stream, then close everything
crStream.Write(dta, 0, dta.Length);
crStream.Close();
output.Close();
}
现在文件已加密,我们来看问题的核心,即在读取文件的同时解密文件以便使用。为此,我们将查看两个示例:显示文本文件和播放 wav 文件。
LoadEncryptedTextFile 方法将加密的文件解密并加载到字符串数组中,以便应用程序根据需要进行处理。在这种情况下,它最终将被传递到一个弹出窗口并在列表框中显示。
第一步是设置加密对象。
DESCryptoServiceProvider cryptic = new DESCryptoServiceProvider();
cryptic.Key = ASCIIEncoding.ASCII.GetBytes("NOWISTHE");
cryptic.IV = ASCIIEncoding.ASCII.GetBytes("WINTEROF");
请注意,它的设置与加密方法中的设置完全相同。下一步是打开文件作为 FileStream,然后使用它来建立 CryptoStream。然后,由于这是一个文本文件,并且我们希望以文本方式读取它,因此我们使用 CryptoStream 来建立 StreamReader。
FileStream input = new FileStream(sourceFile, FileMode.Open);
CryptoStream cryptoStream = new CryptoStream(input, cryptic.CreateDecryptor(),
CryptoStreamMode.Read);
StreamReader vStreamReader = new StreamReader(cryptoStream, Encoding.ASCII);
此时,我们像处理任何其他输入文本文件一样处理它,使用 Read() 和 ReadLine() 方法读取数据。整个方法如下所示:
public List<String> LoadEncryptedTextFile(String sourceFile)
{
List<String> lines = new List<string>();
DESCryptoServiceProvider cryptic = new DESCryptoServiceProvider();
cryptic.Key = ASCIIEncoding.ASCII.GetBytes("NOWISTHE");
cryptic.IV = ASCIIEncoding.ASCII.GetBytes("WINTEROF");
// Open the file, decrypt the data stream, and send it to XmlTextReader
FileStream input = new FileStream(sourceFile, FileMode.Open);
CryptoStream cryptoStream = new CryptoStream(input, cryptic.CreateDecryptor(),
CryptoStreamMode.Read);
StreamReader vStreamReader = new StreamReader(cryptoStream, Encoding.ASCII);
while (!vStreamReader.EndOfStream)
{
String line = vStreamReader.ReadLine();
lines.Add(line);
}
vStreamReader.Close();
cryptoStream.Close();
input.Close();
return lines;
}
请注意,加载字符串列表并非严格必要……每行可以在读取时立即进行处理,而不是保存起来稍后处理。
播放加密的 wav 文件过程类似,将文件解密并读取到内部缓冲区,然后将该缓冲区作为 MemoryStream 传递给 .NET SoundPlayer() 方法。我们首先打开文件并设置解密器。
FileInfo info = new FileInfo(sourceFile);
FileStream input = new FileStream(sourceFile, FileMode.Open, FileAccess.Read);
DESCryptoServiceProvider cryptic = new DESCryptoServiceProvider();
cryptic.Key = ASCIIEncoding.ASCII.GetBytes("NOWISTHE");
cryptic.IV = ASCIIEncoding.ASCII.GetBytes("WINTEROF");
// Implement the decryptor
CryptoStream crStream = new CryptoStream(input, cryptic.CreateDecryptor(),
CryptoStreamMode.Read);
此时文件已打开,解密准备开始。所以我们构建一个 BinaryReader 并将文件读取到缓冲区中。
BinaryReader rdr = new BinaryReader(crStream);
byte[] dta = new byte[info.Length];
rdr.Read(dta, 0, (int)info.Length);
将数据存储在字节数组中后,我们构建一个 MemoryStream 并将其提供给 .NET SoundPlayer。
Stream stream = new MemoryStream(dta);
var MainPlayer = new SoundPlayer(stream);
MainPlayer.Play();
完整的 PlayEncWav 方法如下所示:
public void PlayEncWav(String sourceFile)
{
if (sourceFile.Length == 0)
return;
// Get the encrypted file and setup the decryption engine
FileInfo info = new FileInfo(sourceFile);
FileStream input = new FileStream(sourceFile, FileMode.Open, FileAccess.Read);
DESCryptoServiceProvider cryptic = new DESCryptoServiceProvider();
cryptic.Key = ASCIIEncoding.ASCII.GetBytes("NOWISTHE");
cryptic.IV = ASCIIEncoding.ASCII.GetBytes("WINTEROF");
// Implement the decryptor
CryptoStream crStream = new CryptoStream(input, cryptic.CreateDecryptor(),
CryptoStreamMode.Read);
// Read the decrypted file into memory and convert to a memory stream
BinaryReader rdr = new BinaryReader(crStream);
byte[] dta = new byte[info.Length];
rdr.Read(dta, 0, (int)info.Length);
rdr.Close();
crStream.Close();
input.Close();
Stream stream = new MemoryStream(dta);
var MainPlayer = new SoundPlayer(stream);
MainPlayer.Play();
stream.Close();
}
实际上就这么简单。使用加密会增加应用程序的复杂性,但为了以更安全的方式处理数据文件,额外的努力通常是值得的。
我曾将这些方法用于各种目的,包括解析应用程序使用的加密 XML 文件。(提示:使用 StreamReader(在 CryptoStream 上打开)来创建 XmlTextReader。)每个用例的关键点在于,文件的加密从未在其他人可以访问的地方(即磁盘)被破解。即使应用程序自由解密数据以供自身使用,加密数据的完整性仍然得以保持。
关注点
首先也是最明显的一点是,DESCryptoServiceProvider 的密钥和 IV 必须与加密应用程序中的相同,与解密应用程序中的相同。它们不必与示例中的值匹配,但如果没有匹配的密钥数据,则无法解密。
由于 DES 加密的明显限制,即加密密钥的 8 个字符限制,在实际将其用于应用程序之前,请随意修改代码以使用更强大的加密方法。
严格来说,我不需要将解密后的音频文件加载到缓冲区并将其作为 MemoryStream 传递给 SoundPlayer……该播放器很简单,不允许在音频流中搜索,因此将其全部加载到内存中并非严格必要。但是,如果升级到更强大的播放器,例如 NAudio,则必须将音频文件全部加载到内存中才能进行搜索。
更新 2014 年 9 月 2 日: 将文件解密到内存流并不能完全消除解密后的文件出现在磁盘上的可能性;如果在系统 GC 处置之前应用程序被分页到磁盘,则解密的内存缓冲区将存储在磁盘上。不幸的是,我们无法阻止应用程序被分页,我们能做的就是限制解密缓冲区的使用时间。一旦程序完成使用解密缓冲区,最好擦除并释放内存缓冲区。这并不能“修复”问题,但它极大地减少了分页捕获解密缓冲区的机会。即便存在这种潜在问题,将文件解密到内存流仍然是一个好主意,并且是维护数据安全的一种强大方法。