.NET 加密简化
一个简单、面向字符串的类,用于对称加密、非对称加密和哈希处理。
引言
Microsoft 的 .NET 框架在 System.Security.Cryptography 命名空间中提供了强大的加密支持。执行加密所需的一切都可以在该类中找到,但除非您对密码学理论有深刻的理解,否则很难理解。在过去的四个月里,我一直在努力理解加密和解密数据的概念和理论。我将我推导出的所有知识都封装到一个我称之为 Encryption
的类中。这个类有大量的文档,面向字符串,最重要的是,它很简单!非常适合学习加密。
背景
Encryption
命名空间代表了三个基本的密码学概念。在继续深入之前,每个开发人员都必须理解这些概念
- 哈希
哈希本身不是加密,但它们是所有其他加密操作的基础。哈希是数据的指纹 - 一小块字节,代表更大块字节的唯一性。就像指纹一样,没有两个哈希应该是相同的,并且匹配的指纹是身份的决定性证据。对哈希的完整讨论超出了本文的范围,但我强烈推荐 Steve Friedl 的 《加密哈希图解指南》 以获取更多背景知识。
- 对称加密
在对称加密中,使用单个密钥来加密和解密数据。这种加密方式非常快,但有一个严重的问题:为了与某人共享秘密,他们必须知道您的密钥。这意味着共享秘密的人之间存在非常高的信任水平;如果一个不诚实的人拥有您的密钥 - 或者您的密钥被间谍截获 - 他们可以解密您使用该密钥发送的所有消息!
- 非对称加密
非对称加密通过使用两个不同的密钥来解决对称加密中固有的信任问题:一个公钥用于加密消息,一个私钥用于解密消息。这使得与您不完全信任的人进行秘密通信成为可能。如果一个不诚实的人拥有您的公钥,那又如何呢?公钥只能用于加密;它无法用于解密。他们无法解密您的任何消息!但是,非对称加密*非常慢*。不建议将其用于处理超过约 1KB 的数据。
这三个概念在现代密码学中紧密交织,并且总是同时出现。它们各有优缺点;将它们结合起来可以提供比单独使用任何一种方法高得多的安全性。例如,当您通过数字方式向银行发送支票时,这三种方法都会被使用。
图片转载自 Entrust 的《加密学和数字签名简介》PDF。
- 计算支票的哈希值。
- 使用非对称加密,用我们的公钥加密哈希值。
- 将加密后的哈希值附加到文档中。
- 使用唯一的、一次性的对称加密密钥加密文档。
- 使用非对称加密,用接收者的公钥加密一次性对称加密密钥。
- 将加密的密钥和加密的文档传输给接收者。
为了打开支票,接收者只需按相反的顺序执行这些步骤。请注意,如果其中任何一步丢失,交易将存在严重的漏洞,可能被利用!
Encryption.Hash
让我们从最简单的操作开始 - 哈希字符串 "Hash Browns"
Dim h As New Encryption.Hash(Encryption.Hash.Provider.CRC32)
Dim d As New Encryption.Data("Hash Browns")
h.Calculate(d)
Console.WriteLine(".ToHex = '" & h.Value.ToHex & "'")
Console.WriteLine(".ToBase64 = '" & h.Value.ToBase64 & "'")
Console.WriteLine(".ToString = '" & h.Value.ToString & "'")
使用 CRC32 算法,字符串 "Hash Browns" 的唯一数据指纹为 32 位或 4 字节长。我们有一个自定义数据类型 Encryption.Data
,用于帮助我们将在这些 4 字节与熟悉的字符串表示形式之间进行转换。
.ToHex = 'FDBFBC6D'
.ToBase64 = '/b+8bQ=='
.ToString = 'y¿¼m'
使用 .ToString
方法显示原始字节数组意义不大;这仅用于说明目的。您需要以十六进制或 Base64 编码 的形式显示原始字节值。如果需要,可以通过 Encryption.Data.Bytes
数组获取原始字节表示。
CRC32 哈希不是安全工作的最佳选择;它针对速度和检测机器传输错误进行了优化。对于有知识的人类黑客来说,很容易生成一个产生相同 CRC32 哈希的字符串。让我们看看一个更慢但更安全的哈希:SHA1。
Dim h As New Encryption.Hash(Encryption.Hash.Provider.SHA1)
Dim d As New Encryption.Data("Hash Browns")
Dim salt As New Encryption.Data("NaCl")
h.Calculate(d, salt)
Console.WriteLine(h.Value.ToHex)
Console.WriteLine(h.Value.ToBase64)
SHA1 生成一个更长、更防篡改的 160 位哈希码。
.ToHex = '95CF26B3BB0.F377347B6D414951456A16DD0CF5F'
.ToBase64 = 'lc8ms7sPN3NHttQUlRRWoW3Qz18='
注意到我添加的盐了吗?哈希通常用于避免在数据库中以明文形式存储密码。您计算密码的哈希值,并存储哈希值而不是实际密码。当用户输入密码时,对其进行哈希处理,然后与数据库中存储的哈希值进行比较。这很巧妙,但存在一个漏洞:您仍然可以通过哈希英文字典并与数据库中存储的哈希值进行匹配来发起字典攻击。我们可以通过将盐 - 一个唯一的字符串 - 添加到每个密码中,然后再对其进行哈希处理来防止这种情况。您通常会使用同一记录中的某个任意值作为盐,例如记录 ID、用户的生日或 GUID。盐值是什么并不重要,只要它能使值唯一即可。通过如上所示添加盐,我们实际上是在哈希字符串 "NaClHash Browns",而不是 "Hash Browns"。要找到字典中的 "NaClHash" 就祝你好运了!
另外请注意,字符串表示形式效率不高;使用十六进制表示 160 位(20 字节)哈希需要 40 个字符,使用 Base64 编码 表示相同的哈希需要 28 个字符。如果您不需要以半人类可读的格式显示数据,请坚持使用二进制格式。但是,文本表示形式在 XML 或 .config 文件中使用非常方便!
我们不局限于固定长度的 Encryption.Data
字节数组。我们还可以计算任意大小 IO.Stream
的哈希值。
Dim sr As New IO.StreamReader("c:\test.txt")
Dim h As New Encryption.Hash(Encryption.Hash.Provider.MD5)
Console.WriteLine(".ToHex = '" & h.Calculate(sr.BaseStream).ToHex & "'")
sr.Close()
因此,文件test.txt 的 MD5 哈希值为:
.ToHex = '92C7C0F251D98DEA2ACC49B21CF08070'
让我们看看如果我们向 test.txt 添加一个空格字符,然后再次对其进行哈希处理会发生什么。
.ToHex = 'FADECF02C2ABDC7B65EBF2382E8AC756'
哈希的一个定义属性是源字节的微小变化会在生成的哈希字节中产生巨大的差异。
所有哈希的目的都相同:数字指纹代码。但是,每个 Hash.Provider
在速度和安全性方面都有不同的权衡。
提供商 | 长度(位) | 安全 | 速度 |
Hash.Provider.CRC32 | 32 | 低 | 快速 |
Hash.Provider.SHA1 | 160 | 中等 | medium |
Hash.Provider.SHA256 | 256 | 高 | 慢 |
Hash.Provider.SHA384 | 384 | 高 | 慢 |
Hash.Provider.SHA512 | 512 | 极慢 | 慢 |
Hash.Provider.MD5 | 128 | 中等 | medium |
Encryption.Symmetric
对称加密是最熟悉的加密类型;您有一个秘密密钥,用于加密和解密。
Dim sym As New Encryption.Symmetric(Encryption.Symmetric.Provider.Rijndael)
Dim key As New Encryption.Data("My Password")
Dim encryptedData As Encryption.Data
encryptedData = sym.Encrypt(New Encryption.Data("Secret Sauce"), key)
Dim base64EncryptedString as String = encryptedData.ToBase64
我们现在有了一些 Rijndael 加密后的字节,表示为 Base64 字符串。让我们解密它们。
Dim sym As New Encryption.Symmetric(Encryption.Symmetric.Provider.Rijndael)
Dim key As New Encryption.Data("My Password")
Dim encryptedData As New Encryption.Data
encryptedData.Base64 = base64EncryptedString
Dim decryptedData As Encryption.Data
decryptedData = sym.Decrypt(encryptedData, key)
Console.WriteLine(decryptedData.ToString)
与 Encryption.Hash
类一样,它也适用于任何任意大小的 IO.Stream
以及固定大小的 Encryption.Data
。
Dim sym As New Encryption.Symmetric(Encryption.Symmetric.Provider.TripleDES)
Dim key As New Encryption.Data("My Password")
Dim fs As New IO.FileStream("c:\test.txt", IO.FileMode.Open,
IO.FileAccess.Read)
Dim br As New IO.BinaryReader(fs)
Dim encryptedData As Encryption.Data
encryptedData = sym.Encrypt(br.BaseStream, key)
br.Close()
Dim sym2 As New Encryption.Symmetric(Encryption.Symmetric.Provider.TripleDES)
Dim decryptedData As Encryption.Data
decryptedData = sym2.Decrypt(encryptedData, key)
在使用 Encryption.Symmetric
类时,有几点需要记住:
- 目前所有对称加密都在内存中执行。加密非常大的文件时要小心!
- .NET 默认总是选择最大的可用密钥大小。如果您想手动指定较小的密钥大小,请使用
.KeySizeBytes
或.KeySizeBits
属性。 - 在
.Encrypt
方法中,密钥是可选的。如果您不提供密钥,将为您自动生成一个适当长度的密钥,您可以通过.Key
属性检索它。它可能不容易发音,因为它将是随机生成的字节数组,但它很难猜测! .InitializationVector
属性是完全可选的。对称算法是块导向的,并将下一个块的种子与前一个块的结果相关联。这意味着第一个块没有种子,所以 IV 就派上用场了。记住密码和初始化向量来解密数据很麻烦,而且我认为这不是一个严重的弱点,所以我建议接受默认的初始化向量。
.NET 提供了四种不同的 Symmetric.Provider
算法;我建议避免那些密钥较短且存在已知弱点的算法。
提供商 | 长度(位) | 已知漏洞 |
Symmetric.Provider.DES | 64 | 是 |
Symmetric.Provider.RC2 | 40-128 | 是 |
Symmetric.Provider.Rijndael | 128, 192, 256 | 否 |
Symmetric.Provider.TripleDES | 128, 192 | 否 |
Encryption.Asymmetric
非对称加密需要使用两个密钥:一个公钥,一个私钥,合称为“密钥对”。让我们生成一个新的密钥对并加密一些数据。
Dim asym As New Encryption.Asymmetric
Dim pubkey As New Encryption.Asymmetric.PublicKey
Dim privkey As New Encryption.Asymmetric.PrivateKey
asym.GenerateNewKeyset(pubkey, privkey)
Dim secret As String = "ancient chinese"
Dim encryptedData As Encryption.Data
encryptedData = asym.Encrypt(New Encryption.Data(secret), pubkey)
Dim decryptedData As Encryption.Data
Dim asym2 As New Encryption.Asymmetric
decryptedData = asym2.Decrypt(encryptedData, privkey)
请注意,我们使用了公钥来*加密*,使用私钥来*解密*。
虽然您可以生成任意数量的新公钥/私钥对,但通常您会加载一个现有的密钥对。为了方便加载和保存密钥,Encryption.Asymmetric.PublicKey
和 Encryption.Asymmetric.PrivateKey
类通过 .ToXml
和 .FromXml
方法支持 XML 序列化。它们还通过 .ToConfigSection
方法支持导出到config 文件格式,该方法返回一个适合剪切并粘贴到您的*.config 文件的 <appSettings>
部分的字符串。
<appSettings>
<add key="PublicKey.Modulus"
value="3uWxbWSnlL2ntr/gcJ0NQeiWRfzj/72zIDuBW/TmegeodMdPUvI5vXur0fKp
6RbSU112oPf9o7hoAF8bdR9YOiJg6axZYKh+BxEH6pUPLbrtn1dPCUgTxlMeo0IhKvi
h1Q90Bz+ZxCp/V8Hcf86p+4LPeb1o9EOa01zd0yUwvkE=" />
<add key="PublicKey.Exponent"
value="AQAB" />
<add key="PrivateKey.P"
value="76iHZusdN1TYrTqf1gExNMMWbiHS7zSB/bi/xeUR0F3fjvnvsayn6s5ShM0jx
YHVVkRyVoH16PwLW6Tt2gpdYw==" />
<add key="PrivateKey.Q"
value="7hiVRmx0z1KERw+Zy86MmlvuODUsn2kuM06kLsSHbznSkYl5lekH9RFxFemNk
GGMBg8OT5+EVtWAOdto8KTJCw==" />
<add key="PrivateKey.DP"
value="ksvo/EqBn9XRzvH826npSQdCYv1G5gyEnzQeC4qPidEmUb6Yan12cWYlt4CsK
5umYGwWmRSL20Ufc+gnZQo6Pw==" />
<add key="PrivateKey.DQ"
value="QliLUCJsslDWF08blhUqTOENEpCOrKUMgLOLQJT3AGFmcbOTM9jJpNqFXovEL
NVhxVZwsHNM1z2LC5Q+O8BPXQ==" />
<add key="PrivateKey.InverseQ"
value="pjEtLwYB4yeDpdORNFxhFVXWZCqoky86bmAnrrG4+FvwkH/2dNe65Wmp62JvZ
7dwgPBIA+uA/LF+C1LXcXe9Aw==" />
<add key="PrivateKey.D"
value="EmuZBhlTYA9sVMX2nlfcSJ4YDSChFvluXDOOtTK/+UW4vi3aeFhcPTSDNo5/T
Cv+pbULoLHd3DHZJm61rjAw8jV5n09Trufg/Z3ybzUrAOzT3iTR2rvg7mNS2IBmaTyJg
emNKQDeFW81UOELVszUXNjhVex+k67Ma4omR6iTHSE=" />
</appSettings>
私钥是公钥的超集;它可以用于加密和解密,而公钥只能用于加密。一旦密钥被放入您.config 文件的 <appSettings>
部分,它将被自动使用;您不再需要在 .Decrypt
方法中指定私钥。
Dim encryptedData As Encryption.Data
Dim decryptedData As Encryption.Data
Dim asym As New Encryption.Asymmetric
Dim asym2 As New Encryption.Asymmetric
Dim secret As String = "Michael Bolton"
encryptedData = asym.Encrypt(New Encryption.Data(secret))
decryptedData = asym2.Decrypt(encryptedData)
Console.WriteLine(decryptedData.ToString)
请注意,我们没有在这里指定任何密钥;所有内容都已从config 文件的 <appSettings>
部分自动获取。
使用 Encryption.Asymmetric
时有几点需要注意:
- Microsoft 的非对称加密实现不提供提供商选择:您将获得 RSA,并且必须接受!不过,您确实可以进行密钥大小的选择 - 从 384 位到 16,384 位,步长为 8 位。如果您在构造函数中未指定大小,默认将获得 1,024 位。这对于大多数用途来说应该足够了。
- 非对称加密设计用于小型输入。 部分原因是由于非对称加密非常慢,但这也是设计使然:根据您选择的密钥大小,如果您尝试加密过大的内容,您将收到一个异常!有解决方法,但我*不*建议使用它们。遵循本文开头定义的最佳实践;使用非对称加密来保护简短内容,例如对称密码或哈希。
Encryption.Asymmetric 中的烦人的文件依赖
不幸的是,Microsoft 选择通过现有的基于 COM 的 CryptoAPI 提供一些 System.Security.Cryptography 功能。通常这没什么大不了的;.NET 中的许多功能都是通过 COM 接口实现的。然而,在这种情况下有一个破坏性的副作用:*在我看来,非对称加密应该是一个完全在内存中进行的操作,但它却有一个文件系统的“密钥容器”依赖项:*
更糟糕的是,这个奇怪的小“密钥容器”文件通常会存放在当前用户的文件夹中!我已经按照 此 Microsoft 知识库文章 中的说明指定了一个机器文件夹。每次我们执行非对称加密操作时,都会在 C:\Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys 文件夹中创建一个文件,然后将其删除。这是完全不可避免的,您可以通过打开此文件夹并观察在进行非对称加密调用时文件夹发生的变化来亲自验证。请确保 .NET 正在运行的帐户(ASP.NET 等)对此文件夹具有权限!
结论
加密是一个深刻而复杂的主题。我希望本文以及附带的类至少能让它更容易理解一些。
请随时提供反馈,无论好坏!如果您喜欢这篇文章,您可能还会喜欢 我的其他文章。
历史
- 2005 年 4 月 19 日,星期二
- 发布。
- 2005 年 5 月 1 日,星期日
- 文章代码的小错误修复。
- 在 C# 中修复了字节数组 null 和
Encoding.GetString
的问题。
- 2007 年 1 月 29 日,星期一
- 直接移植到 .NET 2.0 和 Visual Studio 2005。