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