使用 AES 加密数据集,包括压缩
本文介绍了如何使用 AES 加密数据集。可以选择在加密之前对数据集进行压缩。
引言
许多应用程序使用 DataSet 将数据本地存储在客户端计算机的硬盘上。
根据应用程序的不同,数据可能非常敏感。虽然保存的数据集可以通过文件夹和文件权限等各种方式进行保护,但如果文件被错误地复制到错误的位置,这些机制将无法提供保护。
增强安全性的一种方法是在保存数据集时对其进行加密,并在读取时再次解密。本文演示了使用 AES 进行 128 位加密来加密数据集。此外,还可以在加密前压缩数据集。
应用程序
在开始编写代码之前,让我们先看看应用程序。演示项目同时提供了 C# 和 VB.NET 版本。演示项目存储联系人信息,其中包括:
- 姓名
- 姓
- 电话号码
- 电子邮件地址
主窗口用于操作数据。窗口非常简单。首先需要创建一个加密的数据集。然后,您可以添加联系人并保存或取消更改。您可以选择压缩文件。
您可以随时打开另一个现有数据集或创建一个新数据集。
创建或打开加密数据集时,会显示身份验证窗口。
此窗口要求提供用于加密/解密的凭据以及存储数据集的文件名。
身份验证窗口还会显示密码强度的大致估计。这将在后面更详细地解释。因此,身份验证如下所示:
写入和读取数据集
数据集使用 WriteXml 和 ReadXml 方法进行写入和读取。加密(或解密)在写入或读取数据时完成。为了提供一种一致的使用这些方法的方式,我已经为这两个操作定义了扩展方法,它们需要三个参数:
fileName
,要写入或读取的文件名userName
,用于加密或解密的用户姓名password
,要使用的密码compress
,是否应在加密前压缩数据
扩展方法位于静态 Cryptography
类中。WriteXml
的扩展方法如下:
/// <summary>
/// Extension method for a dataset to define WriteXml method with encryption
/// </summary>
/// <param name="dataSet">The dataset</param>
/// <param name="fileName">File name to read</param>
/// <param name="userName">Username for encryption</param>
/// <param name="password">Password for encryption</param>
/// <param name="compress">Should the file be compressed</param>
public static void WriteXml(this System.Data.DataSet dataSet,
string fileName,
string userName,
string password,
bool compress) {
// Check the parameters
if (dataSet == null
|| string.IsNullOrEmpty(userName)
|| string.IsNullOrEmpty(password)
|| string.IsNullOrEmpty(fileName)) {
throw new System.ArgumentNullException("All arguments must be supplied.");
}
// Encrypt and save the dataset
Cryptography.EncryptDataSet(dataSet, userName, password, fileName, compress);
}
''' <summary>
''' Extension method for a dataset to define WriteXml method with encryption
''' </summary>
''' <param name="dataSet">The dataset</param>
''' <param name="fileName">File name to read</param>
''' <param name="userName">Username for encryption</param>
''' <param name="password">Password for encryption</param>
''' <param name="compress">Should the file be compressed</param>
<System.Runtime.CompilerServices.Extension()>
Public Sub WriteXml(dataSet As System.Data.DataSet,
fileName As String,
userName As String,
password As String,
compress As Boolean)
' Check the parameters
If dataSet Is Nothing _
Or String.IsNullOrEmpty(userName) _
Or String.IsNullOrEmpty(password) _
Or String.IsNullOrEmpty(fileName) Then
Throw New System.ArgumentNullException("All arguments must be supplied.")
End If
' Encrypt and save the dataset
Cryptography.EncryptDataSet(dataSet, userName, password, fileName, compress)
End Sub
ReadXml
的扩展方法看起来非常相似:
/// <summary>
/// Extension method for a dataset to define ReadXml method with decryption
/// </summary>
/// <param name="dataSet">The dataset</param>
/// <param name="fileName">File name to read</param>
/// <param name="userName">Username for decryption</param>
/// <param name="password">Password for decryption</param>
/// <param name="compressed">Is the file compressed</param>
/// <returns>XmlReadMode used for reading</returns>
public static System.Data.XmlReadMode ReadXml(this System.Data.DataSet dataSet,
string fileName,
string userName,
string password,
bool compressed) {
// Check the parameters
if (dataSet == null
|| string.IsNullOrEmpty(userName)
|| string.IsNullOrEmpty(password)
|| string.IsNullOrEmpty(fileName)) {
throw new System.ArgumentNullException("All arguments must be supplied.");
}
// Decrypt the saved dataset
return Cryptography.DecryptDataSet(dataSet, userName, password, fileName, compressed);
}
''' <summary>
''' Extension method for a dataset to define ReadXml method with decryption
''' </summary>
''' <param name="dataSet">The dataset</param>
''' <param name="fileName">File name to read</param>
''' <param name="userName">Username for decryption</param>
''' <param name="password">Password for decryption</param>
''' <param name="compressed">Is the file compressed</param>
''' <returns>XmlReadMode used for reading</returns>
<System.Runtime.CompilerServices.Extension()>
Public Function ReadXml(dataSet As System.Data.DataSet,
fileName As String,
userName As String,
password As String,
compressed As Boolean) As System.Data.XmlReadMode
' Check the parameters
If dataSet Is Nothing _
Or String.IsNullOrEmpty(userName) _
Or String.IsNullOrEmpty(password) _
Or String.IsNullOrEmpty(fileName) Then
Throw New System.ArgumentNullException("All arguments must be supplied.")
End If
' Decrypt the saved dataset
Return Cryptography.DecryptDataSet(dataSet, userName, password, fileName, compressed)
End Function
如您所见,实际工作是在 Cryptography
类中的其他方法中完成的。我们将在下面进行研究。
加密和解密数据
InitAes 方法
首先,让我们看一下 InitAes
方法。此方法创建 AesManaged
类的实例,该类负责加密。该方法接收用户名和密码,并将它们用于 Key
和 IV
。
此方法如下所示:
/// <summary>
/// This method initializes the Aes used to encrypt or decrypt the dataset.
/// </summary>
/// <param name="username">Username to use for the encryption</param>
/// <param name="password">Password to use for the encryption</param>
/// <returns>New instance of Aes</returns>
private static System.Security.Cryptography.Aes InitAes(string username, string password) {
System.Security.Cryptography.Aes aes = new System.Security.Cryptography.AesManaged();
System.Security.Cryptography.Rfc2898DeriveBytes rfc2898
= new System.Security.Cryptography.Rfc2898DeriveBytes(
password,
System.Text.Encoding.Unicode.GetBytes(username));
aes.Padding = System.Security.Cryptography.PaddingMode.PKCS7;
aes.KeySize = 128;
aes.Key = rfc2898.GetBytes(16);
aes.IV = rfc2898.GetBytes(16);
return aes;
}
''' <summary>
''' This method initializes the Aes used to encrypt or decrypt the dataset.
''' </summary>
''' <param name="username">Username to use for the encryption</param>
''' <param name="password">Password to use for the encryption</param>
''' <returns>New instance of Aes</returns>
Private Function InitAes(username As String, password As String)
As System.Security.Cryptography.Aes
Dim aes As System.Security.Cryptography.Aes
= New System.Security.Cryptography.AesManaged()
Dim rfc2898 As System.Security.Cryptography.Rfc2898DeriveBytes
= New System.Security.Cryptography.Rfc2898DeriveBytes(
password,
System.Text.Encoding.Unicode.GetBytes(username))
aes.Padding = System.Security.Cryptography.PaddingMode.PKCS7
aes.KeySize = 128
aes.Key = rfc2898.GetBytes(16)
aes.IV = rfc2898.GetBytes(16)
Return aes
End Function
用于加密的密钥大小为 128 位,即 16 字节。Key
和初始化向量 (IV
) 都通过 Rfc2898DeriveBytes
从提供的密码派生。用户名用作派生的 salt。
密钥分配为派生密码的前 16 个字节,IV 为接下来的 16 个字节。
如果密钥的长度不匹配 16 字节,则会使用填充来填充必要的数据。在本例中,使用 PKCS7
,这意味着数据的末尾会用重复的“缺失”字节数进行填充。
此实例化的 Aes 类用于 EncryptDataSet
和 DecryptDataSet
方法。
EncryptDataSet 方法
顾名思义,此方法负责将加密数据写入磁盘。
该方法如下所示:
/// <summary>
/// Saves the dataset encrypted in specified file
/// </summary>
/// <param name="dataset">Dataset to save</param>
/// <param name="username">Username for encryption</param>
/// <param name="password">Password for encryption</param>
/// <param name="fileName">File name where to save</param>
/// <param name="compress">Should the file be compressed</param>
internal static void EncryptDataSet(System.Data.DataSet dataset,
string username,
string password,
string fileName,
bool compress) {
// Check the parameters
if (dataset == null
|| string.IsNullOrEmpty(username)
|| string.IsNullOrEmpty(password)
|| string.IsNullOrEmpty(fileName)) {
throw new System.ArgumentNullException("All arguments must be supplied.");
}
// Save the dataset as encrypted
using (System.Security.Cryptography.Aes aes
= Cryptography.InitAes(username,
password)) {
using (System.IO.FileStream fileStream
= new System.IO.FileStream(fileName,
System.IO.FileMode.Create,
System.IO.FileAccess.ReadWrite,
System.IO.FileShare.None)) {
using (System.Security.Cryptography.CryptoStream cryptoStream
= new System.Security.Cryptography.CryptoStream(fileStream,
aes.CreateEncryptor(),
System.Security.Cryptography.CryptoStreamMode.Write)) {
if (compress) {
// when compression is requested, use GZip
using (System.IO.Compression.GZipStream zipStream
= new System.IO.Compression.GZipStream(cryptoStream,
System.IO.Compression.CompressionMode.Compress)) {
dataset.WriteXml(zipStream, System.Data.XmlWriteMode.WriteSchema);
zipStream.Flush();
}
} else {
dataset.WriteXml(cryptoStream, System.Data.XmlWriteMode.WriteSchema);
cryptoStream.FlushFinalBlock();
}
}
}
}
}
''' <summary>
''' Saves the dataset encrypted in specified file
''' </summary>
''' <param name="dataSet">Dataset to save</param>
''' <param name="username">Username for encryption</param>
''' <param name="password">Password for encryption</param>
''' <param name="fileName">File name where to save</param>
''' <param name="compress">Should the file be compressed</param>
Friend Sub EncryptDataSet(dataSet As System.Data.DataSet, username As String, password As String, fileName As String, compress As Boolean)
' Check the parameters
If dataSet Is Nothing _
Or String.IsNullOrEmpty(username) _
Or String.IsNullOrEmpty(password) _
Or String.IsNullOrEmpty(fileName) Then
Throw New System.ArgumentNullException("All arguments must be supplied.")
End If
' Save the dataset as encrypted
Using aes As System.Security.Cryptography.Aes
= Cryptography.InitAes(username,
password)
Using fileStream As System.IO.FileStream
= New System.IO.FileStream(fileName,
System.IO.FileMode.Create,
System.IO.FileAccess.ReadWrite,
System.IO.FileShare.None)
Using cryptoStream As System.Security.Cryptography.CryptoStream
= New System.Security.Cryptography.CryptoStream(fileStream,
aes.CreateEncryptor(),
System.Security.Cryptography.CryptoStreamMode.Write)
If (compress) Then
' when compression is requested, use GZip
Using zipStream As System.IO.Compression.GZipStream
= New System.IO.Compression.GZipStream(cryptoStream,
System.IO.Compression.CompressionMode.Compress)
dataSet.WriteXml(zipStream, System.Data.XmlWriteMode.WriteSchema)
zipStream.Flush()
End Using
Else
dataSet.WriteXml(cryptoStream, System.Data.XmlWriteMode.WriteSchema)
cryptoStream.FlushFinalBlock()
End If
End Using
End Using
End Using
End Sub
FileStream
,以便将数据集的内容写入文件。写入本身通过 CryptoStream
完成。CryptoStream 使用先前创建的 AesManaged 实例处理流的加密。
如果请求压缩,则会创建一个 GZipStream
以压缩发送到 CryptoStream
的数据。我选择 GZip 而不是 deflate 是因为 CRC 校验(循环冗余校验)。这有助于检测代码或文件本身中可能存在的潜在问题。
要实际将数据集的内容写入文件,会调用本地的 WriteXml
。根据参数,数据集将被写入 CryptoStream
或 GZipStream
。
所以发生的情况是:
- WriteXml 将数据集的内容以 XML 格式写入 CryptoStream 或 GZipStream。
- 如果涉及 GZipStream,它会压缩 XML 格式的数据,并将此流的内容发送到 CryptoStream。
- CryptoStream 使用 AES 加密流中的数据。
- FileStream 读取 CryptoStream 并将加密数据写入磁盘。
需要注意的是,我在写入数据后调用 FlushFinalBlock
(或对 GZipStream 调用 Flush
)。否则,流的最后几个字节将不会写入文件,这将导致加密数据无法解密。
可选的压缩是针对 XML 数据进行的。这比对加密数据进行压缩能获得更好的压缩率。
DecryptDataSet 方法
此方法负责解密。基本上,它使用与 EncryptDataSet 相同的方法。代码:
/// <summary>
/// Reads and decrypts the dataset from the given file
/// </summary>
/// <param name="dataset">Dataset to read</param>
/// <param name="username">Username for decryption</param>
/// <param name="password">Password for decryption</param>
/// <param name="fileName">File name to read</param>
/// <param name="compressed">Is the file compressed</param>
/// <returns>XmlReadMode used for reading</returns>
internal static System.Data.XmlReadMode DecryptDataSet(System.Data.DataSet dataset,
string username,
string password,
string fileName,
bool compressed) {
System.Data.XmlReadMode xmlReadMode;
// Check the parameters
if (dataset == null
|| string.IsNullOrEmpty(username)
|| string.IsNullOrEmpty(password)
|| string.IsNullOrEmpty(fileName)) {
throw new System.ArgumentNullException("All arguments must be supplied.");
}
// Read the dataset and encrypt it
using (System.Security.Cryptography.Aes aes
= Cryptography.InitAes(username,
password)) {
using (System.IO.FileStream fileStream
= new System.IO.FileStream(fileName,
System.IO.FileMode.Open)) {
using (System.Security.Cryptography.CryptoStream cryptoStream
= new System.Security.Cryptography.CryptoStream(fileStream,
aes.CreateDecryptor(),
System.Security.Cryptography.CryptoStreamMode.Read)) {
if (compressed) {
// when decompression is requested, use GZip
using (System.IO.Compression.GZipStream zipStream
= new System.IO.Compression.GZipStream(cryptoStream,
System.IO.Compression.CompressionMode.Decompress)) {
xmlReadMode = dataset.ReadXml(zipStream, System.Data.XmlReadMode.ReadSchema);
}
} else {
xmlReadMode = dataset.ReadXml(cryptoStream, System.Data.XmlReadMode.ReadSchema);
}
}
}
}
return xmlReadMode;
}
''' <summary>
''' Reads and decrypts the dataset from the given file
''' </summary>
''' <param name="dataset">Dataset to read</param>
''' <param name="username">Username for decryption</param>
''' <param name="password">Password for decryption</param>
''' <param name="fileName">File name to read</param>
''' <param name="compressed">Is the file compressed</param>
''' <returns>XmlReadMode used for reading</returns>
Friend Function DecryptDataSet(dataset As System.Data.DataSet,
username As String,
password As String,
fileName As String,
compressed As Boolean) As System.Data.XmlReadMode
Dim xmlReadMode As System.Data.XmlReadMode
' Check the parameters
If dataset Is Nothing _
Or String.IsNullOrEmpty(username) _
Or String.IsNullOrEmpty(password) _
Or String.IsNullOrEmpty(fileName) Then
Throw New System.ArgumentNullException("All arguments must be supplied.")
End If
' Read the dataset and encrypt it
Using aes As System.Security.Cryptography.Aes
= Cryptography.InitAes(username,
password)
Using fileStream As System.IO.FileStream
= New System.IO.FileStream(fileName,
System.IO.FileMode.Open)
Using cryptoStream As System.Security.Cryptography.CryptoStream
= New System.Security.Cryptography.CryptoStream(fileStream,
aes.CreateDecryptor(),
System.Security.Cryptography.CryptoStreamMode.Read)
If (compressed) Then
' when decompression is requested, use GZip
Using zipStream As System.IO.Compression.GZipStream
= New System.IO.Compression.GZipStream(cryptoStream,
System.IO.Compression.CompressionMode.Decompress)
xmlReadMode = dataset.ReadXml(zipStream, System.Data.XmlReadMode.ReadSchema)
End Using
Else
xmlReadMode = dataset.ReadXml(cryptoStream, System.Data.XmlReadMode.ReadSchema)
End If
End Using
End Using
End Using
Return xmlReadMode
End Function
因此,工作流程与 EncryptDataSet
方法相同。
- 数据集使用本地的
ReadXml
方法进行读取。 ReadXml
从CryptoStream
读取,该流使用先前创建的AesManaged
实例。- 如果文件被压缩,则在
CryptoStream
之前使用GZipStream
。 CryptoStream
从FileStream
获取数据,该FileStream
读取磁盘上的文件。
PasswordStrength 方法
一项重要的事情是测试密码的强度。强度测试返回一个介于 0 和 100 之间的整数。数字越高,密码越强。此数字将在身份验证窗口中以图形方式显示。
密码强度的测试非常简单,请根据您的需求进行调整。我使用了以下逻辑:
测试 |
增加的分数 |
密码长度大于 5 个字符 | 5 |
密码长度大于 10 个字符 | 15 |
密码至少包含一个数字 | 5 |
密码至少包含三个数字 | 15 |
密码至少包含一个特殊字符 | 5 |
密码至少包含三个特殊字符 | 15 |
密码至少包含一个大写字母 | 5 |
密码至少包含三个大写字母 | 15 |
密码至少包含一个小写字母 | 5 |
密码至少包含三个小写字母 | 15 |
大多数测试是通过检查 RegEx
匹配的数量来完成的。因此,代码如下:
private static System.Text.RegularExpressions.Regex passwordDigits
= new System.Text.RegularExpressions.Regex(@"\d",
System.Text.RegularExpressions.RegexOptions.Compiled);
private static System.Text.RegularExpressions.Regex passwordNonWord
= new System.Text.RegularExpressions.Regex(@"\W",
System.Text.RegularExpressions.RegexOptions.Compiled);
private static System.Text.RegularExpressions.Regex passwordUppercase
= new System.Text.RegularExpressions.Regex(@"[A-Z]",
System.Text.RegularExpressions.RegexOptions.Compiled);
private static System.Text.RegularExpressions.Regex passwordLowercase
= new System.Text.RegularExpressions.Regex(@"[a-z]",
System.Text.RegularExpressions.RegexOptions.Compiled);
/// <summary>
/// This method calculates the strength of the password
/// </summary>
/// <param name="password">Password to check</param>
/// <returns>Password strength between 0 and 100</returns>
internal static int PasswordStrength(string password) {
int strength = 0;
if (password.Length > 5) strength += 5;
if (password.Length > 10) strength += 15;
if (passwordDigits.Matches(password).Count >= 1) strength += 5;
if (passwordDigits.Matches(password).Count >= 3) strength += 15;
if (passwordNonWord.Matches(password).Count >= 1) strength += 5;
if (passwordNonWord.Matches(password).Count >= 3) strength += 15;
if (passwordUppercase.Matches(password).Count >= 1) strength += 5;
if (passwordUppercase.Matches(password).Count >= 3) strength += 15;
if (passwordLowercase.Matches(password).Count >= 1) strength += 5;
if (passwordLowercase.Matches(password).Count >= 3) strength += 15;
return strength;
}
Private passwordDigits As System.Text.RegularExpressions.Regex
= New System.Text.RegularExpressions.Regex("\d",
System.Text.RegularExpressions.RegexOptions.Compiled)
Private passwordNonWord As System.Text.RegularExpressions.Regex
= New System.Text.RegularExpressions.Regex("\W",
System.Text.RegularExpressions.RegexOptions.Compiled)
Private passwordUppercase As System.Text.RegularExpressions.Regex
= New System.Text.RegularExpressions.Regex("[A-Z]",
System.Text.RegularExpressions.RegexOptions.Compiled)
Private passwordLowercase As System.Text.RegularExpressions.Regex
= New System.Text.RegularExpressions.Regex("[a-z]",
System.Text.RegularExpressions.RegexOptions.Compiled)
''' <summary>
''' This method calculates the strength of the password
''' </summary>
''' <param name="password">Password to check</param>
''' <returns>Password strength between 0 and 100</returns>
Friend Function PasswordStrength(password As String) As Integer
Dim strength As Integer = 0
If (password.Length > 5) Then strength += 5
If (password.Length > 10) Then strength += 15
If (passwordDigits.Matches(password).Count >= 1) Then strength += 5
If (passwordDigits.Matches(password).Count >= 3) Then strength += 15
If (passwordNonWord.Matches(password).Count >= 1) Then strength += 5
If (passwordNonWord.Matches(password).Count >= 3) Then strength += 15
If (passwordUppercase.Matches(password).Count >= 1) Then strength += 5
If (passwordUppercase.Matches(password).Count >= 3) Then strength += 15
If (passwordLowercase.Matches(password).Count >= 1) Then strength += 5
If (passwordLowercase.Matches(password).Count >= 3) Then strength += 15
Return strength
End Function
其他可能有趣的程序代码
演示项目中可能还有其他一些有趣的东西。
首次创建数据集时,应用程序会先将新创建的数据集写入文件,然后再从文件读取数据集。虽然这并不必要,因为数据集已在内存中,但我希望像这样进行初始化,因为这可以确保写入的信息能够正确读取。如果加密的数据集包含大量数据并被损坏,那将不是一个好的情况。
密码强度进度条:进度条使用 LinearGradientBrush
来指示密码强度。颜色从红色变为黄色再变为绿色。在编写此内容时,我使用了一个转换器来计算颜色的偏移量以及使用的颜色。如果我只使用了带有三种颜色的渐变画笔,即使密码不强,进度条也会始终显示所有颜色。我想即使进度条的值在变化,也要保持颜色变化的偏移量恒定。您可以查看 ForegroundConverter
类中的转换器。颜色变化仍然有些笨拙,也许有一天我会修复它。
注意:打开演示项目时,请记住先进行编译,因为下载中不包含二进制文件。否则,在设计模式下打开窗口等时会遇到不必要的错误。
参考文献
在阅读本文时,您可能会觉得有趣的参考文献:
这次就到这里。一如既往,非常欢迎您的评论 " />
历史
- 2012 年 9 月 9 日:创建。
- 2012 年 9 月 14 日:添加了压缩。
- 2012 年 12 月 4 日:修正了拼写错误。
- 2015 年 8 月 21 日:格式化了代码块。