0、摘要
今天看到吉日嘎拉的一篇关于管理软件中信息加密和安全的文章,感觉非常有实际意义。文中作者从实践经验出发,讨论了信息管理软件中如何通过哈希和加密进行数据保护。但是从文章评论中也可以看出很多朋友对这个方面一些基本概念比较模糊,这样就容易“照葫芦画瓢”,不能根据自身具体情况灵活选择和使用各种哈希和加密方式。本文不对哈希和加密做过于深入的讨论,而是对哈希和加密的基本概念和原理进行阐述、比较,并结合具体实践说明如何选择哈希和加密算法、如何提高安全性等问题,使朋友们做到“知其然,知其所以然”,这样就能通过分析具体情况,灵活运用哈希和加密保护数据。
1、哈希(Hash)与加密(Encrypt)的区别
在本文开始,我需要首先从直观层面阐述哈希(Hash)和加密(Encrypt)的区别,因为我见过很多朋友对这两个概念不是很清晰,容易混淆两者。而正确区别两者是正确选择和使用哈希与加密的基础。
概括来说,哈希(Hash)是将目标文本转换成具有相同长度的、不可逆的杂凑字符串(或叫做消息摘要),而加密(Encrypt)是将目标文本转换成具有不同长度的、可逆的密文。
具体来说,两者有如下重要区别:
1、哈希算法往往被设计成生成具有相同长度的文本,而加密算法生成的文本长度与明文本身的长度有关。
例如,设我们有两段文本:“Microsoft”和“Google”。两者使用某种哈希算法得到的结果分别为:“140864078AECA1C7C35B4BEB33C53C34”和“8B36E9207C24C76E6719268E49201D94”,而使用某种加密算法的到的结果分别为“Njdsptpgu”和“Hpphmf”。可以看到,哈希的结果具有相同的长度,而加密的结果则长度不同。实际上,如果使用相同的哈希算法,不论你的输入有多么长,得到的结果长度是一个常数,而加密算法往往与明文的长度成正比。
2、哈希算法是不可逆的,而加密算法是可逆的。
这里的不可逆有两层含义,一是“给定一个哈希结果R,没有方法将E转换成原目标文本S”,二是“给定哈希结果R,即使知道一段文本S的哈希结果为R,也不能断言当初的目标文本就是S”。其实稍微想想就知道,哈希是不可能可逆的,因为如果可逆,那么哈希就是世界上最强悍的压缩方式了——能将任意大小的文件压缩成固定大小。
加密则不同,给定加密后的密文R,存在一种方法可以将R确定的转换为加密前的明文S。
这里先从直观层面简单介绍两者的区别,等下文从数学角度对两者做严谨描述后,读者朋友就知道为什么会有这两个区别了。
2、哈希(Hash)与加密(Encrypt)的数学基础
从数学角度讲,哈希和加密都是一个映射。下面正式定义两者:
一个哈希算法是一个多对一映射,给定目标文本S,H可以将其唯一映射为R,并且对于所有S,R具有相同的长度。由于是多对一映射,所以H不存在逆映射
使得R转换为唯一的S。
一个加密算法是一个一一映射,其中第二个参数叫做加密密钥,E可以将给定的明文S结合加密密钥Ke唯一映射为密文R,并且存在另一个一一映射,可以结合Kd将密文R唯一映射为对应明文S,其中Kd叫做解密密钥。
下图是哈希和加密过程的图示:
有了以上定义,就很清楚为什么会存在上文提到的两个区别了。由于哈希算法的定义域是一个无限集合,而值域是一个有限集合,将无限集合映射到有限集合,根据“鸽笼原理(Pigeonhole principle)”,每个哈希结果都存在无数个可能的目标文本,因此哈希不是一一映射,是不可逆的。
而加密算法是一一映射,因此理论上来说是可逆的。
但是,符合上面两个定义的映射仅仅可以被叫做哈希算法和加密算法,但未必是好的哈希和加密,好的哈希和加密往往需要一些附加条件,下面介绍这些内容。
一个设计良好的哈希算法应该很难从哈希结果找到哈希目标文本的碰撞(Collision)。那么什么是碰撞呢?对于一个哈希算法H,如果,则S1和S2互为碰撞。关于为什么好的哈希需要难以寻找碰撞,在下面讲应用的时候会详解。另外,好的哈希算法应该对于输入的改变极其敏感,即使输入有很小的改动,如一亿个字符变了一个字符,那么结果应该截然不同。这就是为什么哈希可以用来检测软件的完整性。
一个设计良好的加密算法应该是一个“单向陷门函数(Trapdoor one-way function)”,单向陷门函数的特点是一般情况下即使知道函数本身也很难将函数的值转换回函数的自变量,具体到加密也就是说很难从密文得到明文,虽然从理论上这是可行的,而“陷门”是一个特殊的元素,一旦知道了陷门,则这种逆转换则非常容易进行,具体到加密算法,陷门就是密钥。
顺便提一句,在加密中,应该保密的仅仅是明文和密钥。也就是说我们通常假设攻击者对加密算法和密文了如指掌,因此加密的安全性应该仅仅依赖于密钥而不是依赖于假设攻击者不知道加密算法。
3、哈希(Hash)与加密(Encrypt)在软件开发中的应用
哈希与加密在现代工程领域应用非常广泛,在计算机领域也发挥了很大作用,这里我们仅仅讨论在平常的软件开发中最常见的应用——数据保护。
所谓数据保护,是指在数据库被非法访问的情况下,保护敏感数据不被非法访问者直接获取。这是非常有现实意义的,试想一个公司的安保系统数据库服务器被入侵,入侵者获得了所有数据库数据的查看权限,如果管理员的口令(Password)被明文保存在数据库中,则入侵者可以进入安保系统,将整个公司的安保设施关闭,或者删除安保系统中所有的信息,这是非常严重的后果。但是,如果口令经过良好的哈希或加密,使得入侵者无法获得口令明文,那么最多的损失只是被入侵者看到了数据库中的数据,而入侵者无法使用管理员身份进入安保系统作恶。
3.1、哈希(Hash)与加密(Encrypt)的选择
要实现上述的数据保护,可以选择使用哈希或加密两种方式。那么在什么时候该选择哈希、什么时候该选择加密呢?
基本原则是:如果被保护数据仅仅用作比较验证,在以后不需要还原成明文形式,则使用哈希;如果被保护数据在以后需要被还原成明文,则需要使用加密。
例如,你正在做一个系统,你打算当用户忘记自己的登录口令时,重置此用户口令为一个随机口令,而后将此随机口令发给用户,让用户下次使用此口令登录,则适合使用哈希。实际上很多网站都是这么做的,想想你以前登录过的很多网站,是不是当你忘记口令的时候,网站并不是将你忘记的口令发送给你,而是发送给你一个新的、随机的口令,然后让你用这个新口令登录。这是因为你在注册时输入的口令被哈希后存储在数据库里,而哈希算法不可逆,所以即使是网站管理员也不可能通过哈希结果复原你的口令,而只能重置口令。
相反,如果你做的系统要求在用户忘记口令的时候必须将原口令发送给用户,而不是重置其口令,则必须选择加密而不是哈希。
3.2、使用简单的一次哈希(Hash)方法进行数据保护
首先我们讨论使用一次哈希进行数据保护的方法,其原理如下图所示:
对上图我想已无需多言,很多朋友应该使用过类似的哈希方法进行数据保护。当前最常用的哈希算法是MD5和SHA1,下面给出在.NET平台上用C#语言实现MD5和SHA1哈希的代码,由于.NET对于这两个哈希算法已经进行很很好的封装,因此我们不必自己实现其算法细节,直接调用相应的库函数即可(实际上MD5和SHA1算法都十分复杂,有兴趣的可以参考维基百科)。
02 |
using System.Web.Security; |
04 |
namespace HashAndEncrypt |
09 |
public sealed class HashHelper |
14 |
/// <param name="source">源字串</param> |
15 |
/// <returns>杂凑字串</returns> |
16 |
public static string MD5Hash( string source) |
18 |
return FormsAuthentication.HashPasswordForStoringInConfigFile(source, "MD5" ); |
24 |
/// <param name="source">源字串</param> |
25 |
/// <returns>杂凑字串</returns> |
26 |
public static string SHA1Hash( string source) |
28 |
return FormsAuthentication.HashPasswordForStoringInConfigFile(source, "SHA1" ); |
3.3、对简单哈希(Hash)的攻击
下面我们讨论上述的数据保护方法是否安全。
对于哈希的攻击,主要有寻找碰撞法和穷举法。
先来说说寻找碰撞法。从哈希本身的定义和上面的数据保护原理图可以看出,如果想非法登录系统,不一定非要得到注册时的输入口令,只要能得到一个注册口令的碰撞即可。因此,如果能从杂凑串中分析出一个口令的碰撞,则大功告成。
不过我的意见是,对这种攻击大可不必担心,因为目前对于MD5和SHA1并不存在有效地寻找碰撞方法。虽然我国杰出的数学家王小云教授曾经在国际密码学会议上发布了对于MD5和SHA1的碰撞寻找改进算法,但这种方法和很多人口中所说的“破解”相去甚远,其理论目前仅具有数学上的意义,她将破解MD5的预期步骤数从2^80降到了2^69,虽然从数学上降低了好几个数量级,但2^69对于实际应用来说仍然是一个天文数字,就好比以前需要一亿年,现在需要一万年一样。
不过这并不意味着使用MD5或SHA1后就万事大吉了,因为还有一种对于哈希的攻击方法——穷举法。通俗来说,就是在一个范围内,如从000000到999999,将其中所有值一个一个用相同的哈希算法哈希,然后将结果和杂凑串比较,如果相同,则这个值就一定是源字串或源字串的一个碰撞,于是就可以用这个值非法登录了。
例如,下文是对MD5的穷举攻击的代码(设攻击范围为000000到999999):
02 |
using System.Web.Security; |
04 |
namespace HashAndEncrypt |
09 |
public sealed class MD5AttackHelper |
14 |
/// <param name="hashString">杂凑串</param> |
15 |
/// <returns>杂凑串的源串或源串碰撞(攻击失败则返回null)</returns> |
16 |
public static string AttackMD5( string hashString) |
18 |
for ( int i = 0; i <= 999999; i++) |
20 |
string testString = i.ToString(); |
21 |
while (testString.Length < 6) |
22 |
testString = "0" + testString; |
24 |
if (FormsAuthentication.HashPasswordForStoringInConfigFile(testString, "MD5" ) == hashString) |
这种看似笨拙的方法,在现实中爆发的能量却是惊人的,目前几乎所有的MD5破解机或MD5在线破解都是用这种穷举法,但就是这种“笨”方法,却成功破解出很多哈希串。纠其缘由,就是相当一部分口令是非常简单的,如“123456”或“000000”这种口令还有很多人在用,可以看出,穷举法是否能成功很大程度上取决于口令的复杂性。因为穷举法扫描的区间往往是单字符集、规则的区间,或者由字典数据进行组合,因此,如果使用复杂的口令,例如“ASDF#$%uiop.8930”这种变态级口令,穷举法就很难奏效了。
3.4、对一次哈希(Hash)的改进——多重混合哈希(Hash)
上面说过,如果口令过于简单,则使用穷举法可以很有效地破解出一次哈希后的杂凑串。如果不想这样,只有让用户使用复杂口令,但是,很多时候我们并不能强迫用户,因此,我们需要想一种办法,即使用户使用诸如“000000”这种简单密码,也令穷举法难奏效。其中一种办法就是使用多重哈希,所谓多重哈希就是使用不同的哈希函数配合自定义的Key对口令进行多次哈希,如果Key很复杂,那么穷举法将变得异常艰难。
例如,如果使用下面的混合公式进行哈希:
如果将Key设为一个极为复杂的字符串,那么在攻击者不知道Key的情况下,几乎无法通过穷举法破解。因为即使S很简单,但是Key的MD5值几乎是无法在合理时间内穷举完的。下面是这种多重混合哈希的代码实现:
02 |
using System.Web.Security; |
04 |
namespace HashAndEncrypt |
09 |
public sealed class HashHelper |
11 |
private static readonly String hashKey = "qwer#&^Buaa06" ; |
15 |
/// <param name="source">待处理明文</param> |
16 |
/// <returns>Hasn后的数据</returns> |
17 |
public static String Hash(String source) |
19 |
String hashCode = FormsAuthentication.HashPasswordForStoringInConfigFile(source, "MD5" ) + |
20 |
FormsAuthentication.HashPasswordForStoringInConfigFile(hashKey, "MD5" ); |
21 |
return FormsAuthentication.HashPasswordForStoringInConfigFile(hashCode, "SHA1" ); |
3.5、使用加密(Encrypt)方法进行数据保护
加密方法如果用于口令保护的话,与上述哈希方法的流程基本一致,只是在需要时,可以使用解密方法得到明文。关于加密本身是一个非常庞大的系统,而对于加密算法的攻击更是可以写好几本书了,所以这里从略。下面只给出使用C#进行DES加密和解密的代码。
02 |
using System.Security.Cryptography; |
04 |
using System.Web.Security; |
06 |
namespace HashAndEncrypt |
11 |
public sealed class EncryptHelper |
13 |
private static readonly Byte[] DesKey = {5, 7, 8, 9, 0, 2, 1, 6}; |
14 |
private static readonly Byte[] DesVi = { 6, 9, 8, 5, 1, 6, 2, 8 }; |
18 |
/// <param name="data">待加密数据</param> |
19 |
/// <returns>密文</returns> |
20 |
public static String Encrypt(String data) |
22 |
DESCryptoServiceProvider des = new DESCryptoServiceProvider(); |
23 |
Encoding utf = new UTF8Encoding(); |
24 |
ICryptoTransform encryptor = des.CreateEncryptor(DesKey, DesVi); |
26 |
byte [] bData = utf.GetBytes(data); |
27 |
byte [] bEnc = encryptor.TransformFinalBlock(bData, 0, bData.Length); |
28 |
return Convert.ToBase64String(bEnc); |
34 |
/// <param name="data">待解密数据</param> |
35 |
/// <returns>明文</returns> |
36 |
public static String Decrypt(String data) |
38 |
DESCryptoServiceProvider des = new DESCryptoServiceProvider(); |
39 |
Encoding utf = new UTF8Encoding(); |
40 |
ICryptoTransform decryptor = des.CreateDecryptor(DesKey, DesVi); |
42 |
byte [] bEnc = Convert.FromBase64String(data); |
43 |
byte [] bDec = decryptor.TransformFinalBlock(bEnc, 0, bEnc.Length); |
44 |
return utf.GetString(bDec); |
4、总结
密码学本身是一个非常深奥的数学分支,对于普通开发者,不需要了解过于深入的密码学知识。本文仅仅讲述哈希与加密的基础内容,并对两者做了比较,帮助读者明晰概念,另外,对一些实际应用情况进行了简单的讨论。希望本文对大家有所帮助。看了下时间,零点刚过,祝大家十一快乐!玩得开心!