关于密码技术你需要了解的必备知识
虹桥大宝剑
发表于 2022-12-10 03:31:20
144
0
0
密码技术1 l1 b3 p+ e; Z0 E
$ S0 Q5 y$ T0 v1 J2 B* k e
前两天看了《得到》里吴军的新专栏《吴军的谷歌方法论》,提到了工程思维中“极限”的概念,我们作为软件工程师,也要牢记,任何计算机技术都有其极限,我们要认识到其极限,才能更快地看清楚问题的本质。& n' h7 ?. Y2 R0 t
那么,回到密码技术上来,首先,我们必须清楚密码技术的极限在哪里。我们知道,密码技术是为了解决安全问题,再细化点,是为了解决信息安全问题。不同的密码技术,能够解决不同的信息安全问题,没有一种技术能解决所有信息安全问题。因此,我们为了能够解决尽量多的信息安全问题,都是通过组合多种密码技术来解决的。另外,高安全和高性能一般很难兼具,因此,也需要在两者间做好平衡。0 h- G% n( |; _1 E" g
对于密码技术所要解决的信息安全问题,除了信息保密问题,还涉及信息完整性校验、信息发布的不可抵赖性、以及在分布式计算中产生的来源于内部和外部的攻击的所有信息安全问题。为了解决这些信息安全问题的密码技术,从根本上来说,通常具有以下一个或多个特性:8 }6 i" O$ ?& z$ i! _/ ~. r
机密性:为了防止信息被窃听,因此需要对信息进行加密,对应的密码技术主要就是对称加密和非对称加密。
4 q& w: g8 \, x X- s* J
完整性:为了防止信息被篡改,因此需要对信息进行完整性校验,对应的密码技术有单向散列函数、消息认证码、数字签名。
$ j* @ H* U7 Q5 p+ ^
认证:为了防止攻击者伪装成真正的发送者,因此需要对信息进行鉴权,校验此消息是否来自合法的发送者,对应的密码技术有消息认证码、数字签名。
不可否认性:为了防止发送者发布信息后否认自己发布过,因此需要证据来证明信息是否由发送者发布,对应的密码技术为数字签名。. L4 a3 s6 q: ?0 g1 a2 h
上面已经提到了几种密码技术,包括对称加密、非对称加密、单向散列函数、消息认证码、数字签名,这些就是我们必须了解的几种密码技术。当然,这些可以说只是不同密码技术的类别,再看具体的算法,那就包括AES、RSA、MD5、SHA1、SHA256、HMAC等。有些对密码技术不了解的人,还会将BASE64也理解为一种加解密的密码技术。但实际上,BASE64只是一种编码方式,本质上其实和ASCII和UTF-8编码类似,主要用途就是将不可打印的二进制数据编码为可打印的字符串,它并不具备以上密码技术的四个特性中的任何一个。
' J6 L7 M* _0 V) L* N
对称加密
1 Q0 ~ D# R: r5 w: G
对消息加密和解密使用相同密钥的加密算法就叫对称加密,也称为私钥加密、密钥加密。常用的对称加密算法有:DES、3DES、AES、Blowfish、RC4、RC5、RC6等,现在的标准是AES,也是目前使用最广泛的对称加密算法,也是我们必备掌握的算法之一。
AES是一种分组密码,即将明文消息拆分为一定长度的N个分组,然后对每个分组进行加密。AES的分组长度固定为128比特,而密钥可以是128/192/256比特。既然是固定长度的分组,那我们要加密任意长度的明文,就涉及到如何将多个分组进行迭代加密的问题,因此,就有了分组模式。常用的分组模式有:ECB、CBC、CFB、OFB、CTR等。最常用的是ECB和CBC模式,因此,需要了解下这两种模式的用法和区别。3 H0 x; ?9 c! z
. Z* K! Q2 k1 h
ECB全称为ElectronicCodeBook,电子密码本模式,是最简单的一种模式,它直接将明文分割成多个分组并逐个加密,如下图:
这种模式的优点就是简单、快速,加密和解密都支持并行计算。而缺点也比较明显,因为每个明文分组都各自独立地进行加密和解密,如果明文中存在多个相同的明文分组,则这些分组最终会被转换为相同的密文分组。这样一来,只要观察一下密文,就可以知道明文中存在怎样的重复组合,并可以以此为线索来破译密码。另外,攻击者可以通过改变密文分组的顺序,或删除密文分组,或替换掉密文分组,就可以达到对明文操纵的目的,而无需破译密码。在实际应用中,很少需要进行并行计算的加解密场景,因此,一般情况下不会采用这种分组模式,而更推荐采用CBC模式。0 I: r, F; y# z* s0 x
p& u( z$ t6 F- Q: V4 `$ P. D
CBC全称为CipherBlockChanning,密文分组链接模式,是将前一个密文分组与当前明文分组的内容混合起来进行加密的,如下图:( r+ w) }# Y1 \7 Q0 R! o
在CBC模式中,首先将明文分组与前一个密文分组进行XOR运算,然后再进行加密。加密第一个明文分组时,由于不存在“前一个密文分组”,因此需要事先准备一个长度为一个分组的比特序列来代替“前一个密文分组”,这个比特序列称为初始化向量(initializationvector),通常缩写为IV。CBC模式避免了ECB模式的弱点,明文的重复排列不会反映在密文中。不过,相比ECB模式,CBC模式多了一个初始化向量IV。! v6 o0 E) ~0 M* W+ k, L7 L
) |' `9 F& I C) A' D0 h2 c/ F
另外,当最后一个明文分组的内容小于分组长度时,需要用一些特定的数据进行填充,填充方式也有很多种,常用的有两种:PKCS#5和PKCS#7。需要注意的就是,不同编程语言使用的填充方式可能会不同。比如,Java是使用PKCS#5,而iOS的Objective-C和Swift则采用PKCS#7。不过,对于AES来说,两种填充方式是一样的。6 x* k. y, g; W6 v! s
2 u; J$ {$ |/ ?4 o
在实际应用中,我们一般都是在前端对密码或其他敏感数据进行加密,然后在后端进行解密。因为前后端涉及到不同语言的实现,为了保证前后端经过加解密后的结果一致,有几个参数是需要保持一致的:
* m1 ^# f/ t: p; U9 i
密钥:密钥都要使用同一个,这点基本没有疑问,但需要注意的就是,密钥长度需要统一为128/192/256比特,即16/24/32字节。2 S/ O2 T/ b# l$ W3 K4 ?
) F- c9 l& a$ O1 V* a: D
分组模式:分组模式推荐统一为CBC模式,且要显式声明,因为不同语言的默认分组模式可能会不同。
初始化向量:加密和解密时的初始化向量IV也是要一致的,同样也不要使用默认设置,而要显式定义。; L9 s1 n7 a4 A3 e! g
0 d4 b E- Y* G/ }$ X1 h
填充方式:Java采用PKCS5Padding,iOS和JavaScript采用PKCS7,对于AES来说,两者是一样的。0 F3 [% T. k% C8 L& z
& X6 K) Z# C+ T! m
还有一点也需要注意,AES算法本身操作的都是byte字节数组,因此,加密后一般会使用BASE64编码将byte数组转为字符串,而解密之前则先用BASE64解码将字符串转回byte数组。
6 u" w* U# y* T" E& Y2 \4 \
使用对称加密最关键的就是要保证密钥的安全,一般不建议直接在网络上传输密钥,另外,在客户端也要做好密钥的安全存储。
非对称加密5 V; ^8 ~9 M6 S2 t1 p) [* A
4 S: ? ^# B5 f2 \( t+ Z* E/ h
非对称加密也称公钥加密,使用了一对密钥,用公钥进行加密,再用配对的私钥进行解密。公钥是公开的,而私钥是保密的。相比对称加密安全性提高了,但牺牲了性能,加解密的速度慢了几个数量级,消息越长,加密和解密的速度越慢。% }, W) O; Q+ h5 X1 k% U
2 ]2 Q3 C3 o2 i8 l' X
使用最广泛的非对称加密算法就是RSA,其原理是利用了大整数质因数分解问题的困难度,加密和解密其实就是非常简单的两条公式:
1 `, w3 q; X' s
加密:密文=明文^EmodN+ {8 I. ]" v( p& P
& F0 F* e; A0 @
解密:明文=密文^DmodN
即是说,加密就是对明文的E次方后除以N求余数的过程,其中E和N的组合就是公钥,即公钥=(E,N)。而解密过程就是对密文进行D次方后除以N得到余数,即是明文,D和N的组合就是私钥,即私钥=(D,N)。公钥和私钥共有的N称为module,即模数,E和D则分别是公钥指数和私钥指数。因为RSA是基于以上数学问题的,所以其明文、密钥和密文都是数字,我们平时看到的字符串其实都是二进制表示的数字经过BASE64编码的。
密钥长度越长越安全,推荐使用1024比特或更大的值,这里说的1024密钥长度其实是指模数的长度。还有,不同于对称密码可以加密任意长度的明文,RSA明文长度是不能超过密钥长度的。Java默认的RSA加密实现明文长度最长为密钥长度减去11字节,假如密钥长度设为1024比特,即128字节,那明文长度则不能超过128-11=117字节,如果超过该长度则会抛异常。如果想要加密的明文比较长,那就生成更长的密钥,如2048比特,那明文可以长达245字节,足够了。太长的明文也不推荐使用RSA进行加密,性能太低了。
另外,为了提高安全性,RSA加密时都会填充一些随机数。RSA加密填充方式主要有三种:NoPadding、PKCS1Padding、OAEPPadding。其中,最常用的就是PKCS1Padding,它会在明文前面填充11字节的随机数,因此,对同一明文每次加密产生的密文都会不一样。如果想让每次加密产生的密文都一样,那填充方式就采用NoPadding,即不填充,但这样无疑减低了安全性,所以一般不建议采用NoPadding。4 G& D% L7 K$ d2 p9 P% g" y' X- p
$ e" m8 d2 L. N- B. `1 F
实际应用中,我们不会直接对长消息进行非对称加密,而只会对一些安全性要求非常高的短消息进行加密,比如用户的密码、对称加密的密钥。SSL/TLS的加密方案就是用对称加密对请求消息进行加密,用公钥加密对对称加密的密钥进行加密。
N& O0 h; v2 W2 x) B- ]
单向散列函数" N, ?& A+ p) J% E
对称加密和非对称加密主要是用来解决消息的机密性问题的,即可以防止消息被窃听导致秘密泄露,但却无法校验消息是否被篡改。要校验消息是否被篡改,就要对消息进行完整性校验,有多种校验方案,最简单高效的就是单向散列函数。
8 P( E( i# G4 R3 ?+ x) k7 L
单向散列函数也称哈希函数、杂凑函数、消息摘要算法等,是能把任意长的输入消息串转变成固定长的输出串的一种函数,输出值称为“散列值”或“消息摘要”,也称为消息的“指纹”。使用单向散列函数,同一消息会生成同样的散列值,而只要改了消息,哪怕只改了1个字节,最终的散列值变化也很大,因此,很适合用这个散列值校验消息的完整性。
最常用的单向散列函数就是MD5和SHA,SHA其实包括了SHA-1、SHA-224、SHA-256、SHA-384和SHA-512,后四种并称为SHA-2。有时候,我们去下载一些软件的安装文件时,官方一般都会提供对应该文件的MD5和SHA-1的散列值,以便我们可以对下载后的文件自己生成散列值,再和官方提供的散列值进行比对,就知道这个文件有没有被修改过。( j: N! Q; u8 h" }( H, v. i
在我们平时的实际应用中,倒是很少会单独使用单向散列函数,一般都会结合其他技术一起使用。毕竟,单独使用的安全性不高。就举用户密码的安全来说吧,很多应用会将用户密码直接MD5之后传输给服务端。这种方案主要存在两个安全隐患,一是对于一些不够复杂的密码难以防范彩虹表,二是不同用户如果设置了相同密码那散列值无疑会一样。8 z. Z" u$ e- _$ x
先说第一点,首先,先了解下什么是彩虹表。彩虹表是一个用于单向散列函数逆运算的预先计算好的表,为了破解密码的散列值而准备。我们可以简单理解为彩虹表就是在明文和密文之间建立起对应关系的字典表,可以通过已知的密文反查出明文,虽然实际上其原理远比想象中复杂得多。比如说,密码“123456”的MD5结果是“E10ADC3949BA59ABBE56E057F20F883E”,那我监听到用户登录的请求,拿到“E10ADC3949BA59ABBE56E057F20F883E”这个密码串时,从准备好的彩虹表中就可以反查出原密码是“123456”。5 Y9 S3 s+ L) m! [; a
3 x, m* J. d$ n) i; N" }4 e
再说第二点,设置了相同密码的不同用户,由于他们MD5后的散列值全都一样,那么只要破解了其中一个密码,就等于破解了多个用户的密码。
1 a0 n5 n+ ^+ s$ V1 S
为了应对以上两个问题,比较好的方案就是MD5+salt,也称MD5加盐,即将原密码拼上一串盐值salt之后再进行MD5。盐值salt是一个随机字符串,每个用户的salt值一般都是不同的,这样就可以保证不同用户最终MD5出来的散列值不一样,而且因为有一串随机字符串,彩虹表也很难发挥作用了。5 D% |+ @! p4 q- {, h* V' ^/ B
: A& n# O# }- I% F2 `0 J; r5 |
消息认证码) A5 A+ J5 T. ^( }+ z# p
虽然单向散列函数可以用来对消息进行完整性校验,但无法校验消息是否来自合法的发送者,即无法解决认证问题。要解决发送者的认证问题,最常用的有两种方案,一是采用消息认证码,二是使用数字签名。这一小节我们先来了解下消息认证码。
消息认证码(messageauthenticationcode)是一种确认完整性并进行认证的技术,简称为MAC。消息认证码的输入包括任意长度的消息和一个发送者与接收者之间共享的密钥,它可以输出固定长度的数据,这个数据称为MAC值。
% N" E6 }) V% n( V: j0 w/ K
消息认证码的实现方式有很多种,最常用的实现方式就是HMAC,再具体点,根据使用哪种单向散列函数可分为:HMAC-MD5、HMAC-SHA1、HMAC-SHA256等等。HMAC简单理解就是带有密钥的散列函数,因为有了密钥,就可以对发送者进行认证;也因为使用了散列函数,也具有完整性校验的性质。8 `+ t5 S7 ^2 b h+ @' m
认证的基本流程就是:5 @1 `3 \* R6 K1 y% ]' f! K% x
发送者使用共享密钥对消息计算MAC值;1 a; o( j8 S& k5 N' p9 m
发送者将消息和MAC值一起发送给接收者;
接收者收到消息和MAC值后,使用同一个共享密钥对消息计算MAC值;' a3 o& I2 I/ W/ l2 S
- J* @9 r: r6 Q* }$ w; S9 p
对比计算出来的MAC值和接收到的MAC值是否一致,一致则认证成功。
# Y3 j7 f- j% R% b! H! h
现在,很多接口所添加的URL签名机制,其实就是对请求做MAC认证,我常用的也是HMAC。具体的设计细节后面的文章再详细说明。
不过,因为使用了共享密钥,因此也存在和对称加密一样的密钥安全问题。
数字签名3 t6 O# k# J q
8 a- k* I. t( o* o! i8 ^& @
数字签名也可以解决发送者的认证问题,而且,数字签名还具有不可抵赖性。数字签名的原理也非常简单,其实就是将非对称加密反过来用。我们知道,非对称加密是用公钥加密,然后用私钥解密。而数字签名则是用私钥加密,生成的密文就是数字签名,再用公钥解密。用私钥进行加密这一行为只能由持有私钥的人完成,正是基于这一事实,才可以将用私钥加密的密文作为签名来对待。而由于公钥是对外公开的,因此任何人都可以用公钥进行解密,即任何人都能够对签名进行验证。
另外,我们也知道,非对称加密本身加密和解密是非常慢的,消息越长,性能越慢,因此,一般不用来加密和解密长消息。同样的,一般也不会直接对长消息签名,通常的做法是对消息的散列值进行签名,因为散列值比较短,所以加密签名相对就会快很多。因此,你会看到数字签名有类似MD5withRSA、SHA1withRSA这样的实现。3 l9 {) s6 {: D6 J* Z3 x4 I3 J
1 M4 ?/ y% n, g* R8 p' j6 y
最后,需要注意一点,像MD5withRSA和SHA1withRSA这样的数字签名实现可以校验消息完整性、对发送者进行认证、还可防止抵赖,但却不能解决机密性的问题,不要妄想用一种密码技术就能解决所有问题。
# @3 B4 o2 s" n( N) i# V
不过,数字签名其实不太适合直接用在客户端上。因为客户端要对消息签名,那么客户端就需要保存私钥,那依然有私钥的安全配送和存储问题。数字签名使用最广泛的应该就是用在数字证书上了,这还涉及到SSL/TLS和CA等,后面的文章再聊这个话题。
总结8 I+ \- g! n9 X" d7 L% m/ |- r
密码技术其实非常多,还包括各种单一技术的组合,我们本篇文章所学的只是最基础的一些知识,包括了对称加密、非对称加密、单向散列函数、消息认证码、数字签名这些密码技术的一些必备知识点,只有掌握了这些,才能理解和设计一些安全性更高的应用。7 g" j. n' I9 l/ O" A u
对称加密和非对称加密用来解决机密性问题,对称加密的速度快,适合用来加密长消息,但密钥在安全配送和客户端的存储是个难点;而非对称加密避免了共享密钥的安全配送和存储问题,但对长消息的加密速度非常慢,只适合用来加密短消息。单向散列函数可以用来对消息进行完整性校验,但很少单独使用。消息认证码简单理解就是带密钥的单向散列函数,既能校验完整性,还能对发送者进行认证,但因为使用了共享密钥,也存在和对称密码一样的共享密钥的安全配送和存储问题。数字签名能解决完整性校验、认证和防止抵赖等问题,最广泛的应用是在数字证书上。
$ M- y( Z! s$ X q" e* ]$ p
思考和实践
在实际应用中,当进行一些安全性的设计时,如何在以上这些密码技术之间进行选型?如何设计一套安全的API?在Android、iOS、Web前端、Java后端,实现以上所有常用密码技术的工具代码库吧。
成为第一个吐槽的人