摘要:学习如何利用 .NET Framework 的加密功能创建类似本文所述的包装程序来保护您的数据。
下载与本文相关的CryptoSampleCSSample.msi和 CryptoSampleVBSample.msi 代码示例。(请注重,在示例文件中,程序员的注释使用的是英文,本文中将其译为中文是为了便于读者理解。)
您希望在计算机上保存一些机密信息吗?假如是,本文就为您介绍如何进行加密!加密技术就是将有意义的字符编码成无意义的字符,使不应该访问这些数据的人员无法读取它们。加密技术已经存在很多年了,甚至远在计算机诞生之前就已经存在了。随着计算机的出现,在计算机领域应用加密技术可以生成几乎牢不可破的代码。Microsoft 在 Windows 95 中开发并分发了加密 API。使用 Microsoft .NET,新创建的类可以将这些复杂的算法打包到非常易于使用的属性和方法中。
散列简介
假如您只是不想让别人窃取您的密码,那么可以为密码数据创建一个散列。散列是一种单向算法,一旦数据被转换,将无法再获得其原始值。大多数开发人员使用数据库存储密码。但是,在数据库中查找用户数据的人员也能够看到这些密码。不过,您可以使用散列算法对密码进行加密,然后再将其存储在数据库中。用户输入密码后,您可以再次使用散列算法对其进行解密,然后将其与存储在数据库中的散列进行比较。散列的缺点之一是,即使原始数据只发生一个小小的改动,数据的散列也会发生非常大的变化。Pork 和 Porky 这两个单词非常相似,但使用散列算法加密后的结果却相去甚远。您可能根本看不出二者之间有什么相似之处。
.NET 开发人员可以使用多种散列算法类。最常用的是 SHA1 和 md5。下面我们看一下如何为“Paul”这样的普通字符串生成散列,使任何人都无法识别它。
使用 SHA1 生成散列
我们创建一个新例程,然后使用它为字符串“Paul”生成散列。在 Visual Studio? .NET 中打开一个新的 Windows 应用程序,在窗体上放置一个命令按钮。当该命令按钮上发生 Click 事件时,将调用名为 HashText() 的方法。您可以将以下代码添加到该窗体中,看一下此散列算法的实际效果。编写下列代码之前,需要导入命名空间 System.Security.Cryptography。
您可以传递不同的字符串值来调用该例程,查看散列值的变化。例如,假如将字符串“Paul”传递给该例程,Debug(调试)窗口将显示以下文本:
w2h6uYgMJt/nq5ZqihcBTeAXwv8=
现在,将此过程中的输入值更改为“Pauly”。您将看到以下输出结果:
PRoywxJ0znMpGF5sbB18+7GSAsM=
如您所见,输入字符串的一个小小变化就会产生完全不同的字符组合。这正是散列算法之所以有效的原因,它使我们很难找到输入字符串的规律,也很难根据加密后的字符弄清楚字符串原来的模样。
使用 MD5 也可以生成散列
了解一种散列类的使用方法后,基本上就了解了所有的散列类。下面的方法用于 MD5 散列算法。注重,除了 CryptoServiceProvider 类不同外,代码是完全相同的。
输入“Paul”之后,MD5 散列算法的输出结果如下所示:
nVWBsHh1MKNctPioSyqyTQ==
同样,加密后的字符串看起来也与原始输入相去甚远。这些散列算法对于创建没有任何意义的密码来说非常有用,也使黑客很难猜出这些密码。之所以使用散列算法,是因为可以用这种算法对密码进行加密并将其存储在数据库中。然后,当用户输入真实密码时,您先对密码进行解密,然后通过网络发送到数据库中,比较它与数据库中的密码是否匹配。请记住,散列是单向操作。使用散列算法对原始密码加密后将无法再恢复。
如何选择算法
本文介绍的两种散列算法都执行同一种操作。不同之处只在于生成散列的密钥大小以及使用的算法。使用的密钥越大,加密就越安全。例如,MD5 使用的加密密钥比 SHA1 使用的密钥大,因此 MD5 散列较难破解。
对于散列算法要考虑的另外一点是,从实践或理论的角度上看是否存在冲突的可能性。冲突是我们所不希望的,因为两个不同的单词可能会生成相同的散列。例如,SHA1 从实践或理论上来讲没有发生冲突的可能性。MD5 从理论上讲有发生冲突的可能性,但从实践上讲没有发生冲突的可能性。因此,选择哪种算法归根结底取决于您所需要的安全级别。
创建示例散列项目
本文包含两个示例散列项目,以更普通的方式说明如何使用不同的散列算法加密任意字符串。这两个示例项目的名称分别为 CryptoSampleVB.sln 和 CryptoSampleCS.sln。前者是 Visual Basic .NET 解决方案,后者是 C# 解决方案。两个解决方案都包括一个类似图 1 的窗体,该窗体答应您输入要通过散列算法为其加密的原始字符串,还提供一个用来选择散列算法的选项按钮和一个显示散列结果的文本框。
图 1:创建一个通用散列屏幕来尝试两种散列算法
单击此屏幕上的 Hash(散列)按钮时,将运行该按钮的 Click 事件过程。此事件过程将调用一个名为 HashString() 的例程。
HashString() 方法接受输入的值并调用 SetHash() 方法。此方法将根据窗体上选项按钮的设置来决定使用哪个加密服务提供程序创建该方法的实例并返回该方法。将为该窗体创建一个名为 mHash 的 HashAlgorithm 类型的成员变量。HashAlgorithm 类型是创建所有散列加密服务提供程序的基类。
SetHash() 方法如下所示:
根据您在窗体上选择的选项按钮,此方法将创建并返回一个不同的 HashAlgorithm 类型。HashString() 方法在该窗体上执行实际的数据加密:
在 HashString 方法中,我们创建了两个字节数组。第一个数组用来保存用户的原始字符串输入。我们使用 System.Text.Encoding.UTF8.GetBytes() 方法将该字符串转换成字节数组。将原始字符串转换成字节数组后,现在使用服务提供程序的 ComputeHash() 方法计算该字符串的散列值。此方法接受字节数组作为输入,然后返回该字符串加密格式的字节数组。
注重:完成之后清除散列变量是一个好的做法。因此,您看到计算该字符串的散列后,我们调用了 Clear 方法。
现在我们已经获得了加密的字节数组,这正是要从该方法返回的数组。因为我们要将原始值和加密值都作为字符串数据类型而不是字节数组进行处理,所以要通过使用 Convert.ToBase64String 方法返回加密的字节。此方法负责将字节数组转换成 Base64 编码的字符串。Base64 编码的使用非常重要,因为有可能需要将此字符串推到 Web 页上或将其存储到数据库中。假如不进行转换,加密字符串中的某些高阶 ASCII 字符将无法正确显示或存储。
在散列中添加一些“盐”值
到目前为止,散列算法暴露出来的问题之一是,假如两个用户碰巧使用相同的密码,那么散列值将完全相同。假如黑客看到您存储密码的表格,会从中找到规律并明白您很可能使用了常见的词语,然后黑客会开始词典攻击以确定这些密码。要确保任何两个用户密码的散列值都不相同,一种方法是在加密密码之前,在每个用户的密码中添加一个唯一的值。这个唯一值称为“盐”值。在进行此操作时,需要确保将使用的盐值存储为用户记录的一部分。假如您使用表格存储用户 ID 和密码,我建议您使用不同的表格来存储盐值。这样,即使数据库泄漏,盐值也可以为您提供一层额外的安全保护。
在用户密码中添加盐值的方法有多种。最简单的方法是摘取用户的某些信息(例如姓、名、电子邮件地址或员工 ID)并将其添加到用户密码中,然后再进行加密。这种方法的缺点是,因为您需要存储盐值,所以假如黑客找到该值,将会对您所做的一切操作了如指掌。当然,黑客需要花费额外的时间来破解盐值,但这对黑客来说简直是易如反掌。
另外一种方法是使用 .NET Framework 类 RNGCryptoServiceProvider 创建一个随机的数字字符串。RNG 表示随机数生成器。该类可以创建一个任意长度的随机字节数组,长度由您指定。您可以使用此随机字节数组作为散列算法的盐值。要采用这种方法,必须安全地存储该盐值。
该示例与本文中的上一个示例基本相同,不同之处在于创建盐值的例程。在此屏幕上的按钮的 Click 事件下,首先调用一个名为 CreateSalt() 的方法来生成一个唯一的盐值,然后将该值存储到 txtSalt 文本框中。获得唯一的盐值后,再调用 HashString() 方法,将这两个值结合起来。
CreateSalt() 方法的代码非常简单。它首先创建一个长度为 8 个字节的字节数组,然后您创建一个新的 RNGCryptoServiceProvider 类实例。使用该对象的 GetBytes() 方法,将生成的随机字符集填充到字节数组中。然后将此字节数组转换成 Base64 编码字符串并从该函数返回。
数据加密是一个双行道
假如需要在两个