0


常用加解密算法理解及应用

1 背景

随着大环境对隐私、数据安全的要求越来越高,我们日常开发工作中遇到数据安全处理的需求也越来越多,多数情况下都会有专门的安全团队提供完整的解决方案,我们按照对应的文档处理就能很好地解决问题。但是有这样的安全团队支持,并不代表我们不需要对安全知识有一定的了解。作为一名优秀的程序员,还是要适当对别人封装好的技术方案有一定的专研精神,一方面是可以拓宽自己的技术边界,另一方面也可以帮助我们在和安全团队对接方案时有更多的共同认知,提高沟通和接入效率。本文首先会对常见的加解密算法及其特性做一个简单介绍,然后结合工作中的高频需求来分析如何综合运用这些算法来实现安全要求。

2 常见加解密算法简介

在处理数据安全问题时,接触到最多的技术应该是加解密了,下图直观地体现了加解密算法的工作流。

请添加图片描述

根据加解密过程中用到的密钥是否相同,可以把加解密算法分为两类:

对称加密: 加解密用的是同一把密钥,在业界中最常见的算法是AES(Advanced Encryption Standard)

非对称加密: 加解密用的是两把不同的密钥,一把叫公钥(Public Key),一把叫私钥(Private Key),在业界中最常见的算法是RSA(三位发明人的姓氏首字母)

在业界中使用加解密算法遵循的一个原则是:算法公开,密钥私有。 我们在设计的时候,基本都是采用业界公开被论证为足够安全的算法,然后自己保管好密钥,而不是尝试自己设计一套加解密算法(在一些政企业务中可能会被要求使用国产安全算法)。因此保护数据安全的问题,其实就转化成了保护密钥安全的问题了。

我们在设计信息安全机制的时候,一定要基于业务所需要的安全等级来。比如银行系统,那一定是怎么增加机制的复杂度都不为过;但是像类似新闻、天气等一些只读的公开信息接口,甚至都可以不使用安全技术来保护。所以一切脱离业务要求的安全等级谈安全机制设计都是耍流氓。但是实际上在具体落地的时候我们能经常看到安全机制设计得远远超过了安全等级要求,比如我只是一个简单的开放新闻只读接口,却用了一套很复杂的加密算法。这也要求我们对安全技术有个基本的了解,这样才能对安全团队为我们提供的方案有个清晰的认识,我们站在业务的角度还能有仔细推敲、取舍的能力,选择一套确实符合我们需要的方案,而不至于不小心“杀鸡用了牛刀”,所以下面对加解密算法的一些参数作简单解释。

密钥长度

AES算法最常见的密钥长度是128位、192位、256位。RSA常见的密钥长度(模数位数)1024位和2048位,由于RSA是一对密钥,这里的长度是指模数位数,并不就是说公钥或者私钥的长度就是2048位这么长(实际上私钥会比公钥长很多),这一点需要注意。但是至于什么是模数位数则可以不用了解那么细,这里涉及到密码学的知识,我们只要了解密钥长度越长,理论上安全性越高,算法性能越差,我们在确定密钥长度的时候就需要根据我们的业务要求选择合适的密钥长度。

填充模式

在Java中,加解密相关功能被封装成以下代码所示的API进行操作

Key k =toKey(key);Cipher cipher;
cipher =Cipher.getInstance(algorithm);   
cipher.init(Cipher.ENCRYPT_MODE, k);
cipher.doFinal(data);

其中“algorithm”参数传入的是类似“AES/ECB/NoPadding”、“AES/CBC/PKCS7Padding”这样格式的字符串。类似的取值还可能有

RSA/NONE/NoPadding
RSA/NONE/PKCS1Padding
RSA/NONE/OAEPWithMD5AndMGF1Padding
RSA/NONE/OAEPWithSHA256AndMGF1Padding

其中第一个参数表示的是具体的加解密算法,第二个参数是加密的模式,第三个参数是填充模式,这里我们先讲填充模式。简单理解就是一些加密算法首先会对原文进行分块,每一次处理固定长度的内容(block size),比如16个字节,那么如果原文长度不是16字节的整数倍的话会对不足的那些部分进行填充。

加密模式

从上面举例来看,加密模式在AES算法中可以有不同的选择,比如有ECB、CBC、CTR、OCF、CFB、XTS。实际应用中CBC是最常见的(ECB安全性不太够),这种模式是分块进行加密,所以会跟着一种填充模式,对于AES而言,“AES/CBC/PKCS7Padding“是比较常用的加密方式。

初始向量iv

这个也是AES里面特有的参数,在CBC这种分块的算法中,为了增加复杂度,还使用了前一个分块加密后的密文作为一个向量来和后一个待加密的原文进行异或(异或本身可以当成一种很初级的加密方式),对异或后的结果再进行加密。这样就有个问题,第一个分块的前面还没有可以用来异或的向量,因此我们可以指定一个初始向量来解决这个问题,如果没有指定的话,可能会抛异常。在实际应用中iv会随机生成,并需要分发给解密方。

经过以上参数的解释,相信大家在代码中如何使用加解密算法有了个初步的认识。在这里值得强调的一点是理论上我们选择的算法越复杂、密钥越长、加密模式越复杂、填充模式越高级,安全性越高,但是所带来的系统开销也就越大,在实际应用过程中要根据业务要求来选择合适的组合,才能为我们的业务带来最大化的收益。

3 加解密算法使用中常见的问题

3.1 Java中使用AES 256位密钥长度加密时抛"Illegal key size"异常

在JDK 1.8.0_161之前由于美国技术出口限制,JCE里面限制了使用256位密钥的AES加解密,如果在使用过程中报错可以尝试检查JDK版本,详情可以参考https://www.oracle.com/technetwork/java/javase/8u161-relnotes-4021379.htmlUnlimited cryptography enabled by default“ 部分。

3.2 关于能够加密的数据长度问题

使用AES 加密(例如CBC模式),算法里会对原文进行分块后加密,因此原文的数据长度理论上可以不受限制。但是如果选择了NoPadding的话,由于没有填充,原文则必须是16字节的整数倍(算法按照16字节分块),不然会抛”Input length not multiple of 16 bytes“ 异常。

RSA加密算法一次能够加密的数据长度受密钥长度的限制,例如1024位的密钥一次最多只能加密1024个bit的数据(128字节),实际上由于还有填充算法,能够加密的数据还要减去填充所占用的长度。Java中RSA不会像AES那样在算法里面对原文进行分块,所以如果原文超过限制长度的话则需要自行处理数据分段加密的问题,可以参考如下示例代码:

PublicKey pk =PEM_READER.readRSAPublicKey(publicKey);//自己封装的一个根据byte型公钥获取PublicKey实例的方法Cipher cipher =Cipher.getInstance(algorithm,PROVIDER);//初始化Cipher为指定加密算法
        cipher.init(Cipher.ENCRYPT_MODE, pk);int inputLen = data.length;int blockSize = cipher.getBlockSize();//Java有提供对应API来获取加密算法能够处理的块大小,因此可以动态计算当前算法一次能够处理的数据长度if(inputLen <= blockSize){//如果待加密数据长度小于blockSize,则直接加密try{return cipher.doFinal(data);}catch(BadPaddingException e){thrownewRuntimeException(e);}}try{//处理分段加密int offSet =0;int i =0;ByteArrayOutputStream out =newByteArrayOutputStream();byte[] cache;while(inputLen - offSet >0){if(inputLen - offSet > blockSize){
                                   cache = cipher.doFinal(data, offSet, blockSize);}else{
                                   cache = cipher.doFinal(data, offSet, inputLen - offSet);}
                               out.write(cache,0, cache.length);
                               i++;
                               offSet = i * blockSize;}byte[] encryptedData = out.toByteArray();return encryptedData;}catch(BadPaddingException e){thrownewRuntimeException(e);}

3.3 关于加密后的数据长度问题

对于AES,如果使用NoPadding,则加密后的数据长度不会有变化,并且对同一原文加密后的密文都一样。如果使用了一些Padding的话,密文数据长度会因为填充而变长,并且例如像AES/CBC/PKCS7Padding这样的方式的话多次对同一原文进行加密后得到密文会变化,安全性会更高。

对于RSA来说,其单次加密后的密文长度是和密钥长度相关的,即不管一次加密的原文是1字节还是最大的blockSize,其密文长度是固定的,和有没有使用Padding没有联系,所以如果有数据要进行RSA分段加密的话要使得单段数据尽可能和blockSize一样大。如果使用Padding的话可以实现对同一原文的加密结果不同的效果,从而提高安全性。

3.4 对AES和RSA性能的理解

使用以下简单的方法测试出来一点数据,让大家可以对不同算法性能有个直观一点的了解,以下测试使用200字节原文,加密和解密各循环10000次。
*算法***加密耗时(毫秒)****解密耗时(毫秒)**RSA/NONE/PKCS1Padding 2048bit155041931RSA/NONE/NoPadding 2048bit125440291RSA/NONE/OAEPWithSHA256AndMGF1Padding 2048bit255584241AES/CBC/PKCS7Padding 256 bit259202AES/CBC/NoPadding 256 bit (原文208字节)275159AES/ECB/PKCS7Padding 256 bit228147
从这个简单性能对比可以看到,RSA和AES相比,性能差太多。测试中还没有对比CPU负载情况,实际根据以往经验来看,RSA对CPU占用也很高,综合来看,RSA是一个开销比较大的算法,通常在我们使用到RSA算法的nginx的服务器(开启了SSL)是有支持特殊指令集或者有硬件加速能力的机器,以提升SSL的性能。

3.5 JDK对RSA算法的支持

JDK对RSA算法支持比较有限,但是JCE提供了SPI机制,我们可以自由地选择三方加解密扩展,使用如下代码可以把三方的加解密算法库加入到JVM中:

Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());    //BouncyCastleProvider 简称BC库,是Java中使用的比较广的RSA算法库

4 加解密在业务中的应用

在C/S和B/S应用结构中涉及到网络通信,会涉及到一些常见的安全问题需要考虑:

篡改: 对网络请求或者响应进行改写,或者对客户端本地数据文件进行改写

欺骗: 使用技术手段模拟正常端的请求,扰乱正常服务秩序

劫持: ”劫持“连接,将请求转到其他位置

重放: 获取一段正常的端侧请求参数,使用该参数多次请求服务

例如在秒杀业务中,需要防止自动化的脚本模拟用户请求来扰乱正常业务流程;在支付场景中要防止支付信息被篡改或者重放;在银行业务中要防止请求被劫持而泄漏个人信息等。下面我们来讨论下如何使用已有的技术来构建合理的安全体系。

4.1 使用对称加密

由于对称加密使用的是同一个密钥进行加解密,按照上述第2节提及的密钥私有原则就比较难做到。在C/S和B/S架构中,应用会被广泛分发,这样密钥也会跟随客户端代码一起被分发,特别是B/S中的H5,所有代码都能直接暴露。所以直接使用对称加密算法应用场景有限,如果采取在客户端中预埋对称加密密钥的方式,那么安全级别在反编译级别,可以起到初步的防御作用,例如客户端的本地数据库,配置文件等可以不以明文的行式出现,防止被篡改。不过一旦应用被反编译,定位到硬编码的密钥的话,则该机制就失去了防御作用。因此该方案受限于反编译的难度,可以使用代码混淆、加密逻辑使用低级语言编写、代码加固等方式增加逆向工程的难度。

4.2 使用非对称加密

非对称使用了一对密钥,可以使用私钥加密,公钥解密,也可以反过来进行,这样又有更高的设计空间了。在实际应用中,我们通常是保证私钥私有,公钥跟随应用一起分发,这样可以实现在应用侧使用公钥加密,密文只能由我们自己来解密,其他任何三方都无法查看数据的原文,可以实现一条从客户端到服务端的单向安全通道,这样的能力对于像埋点数据上报这样的业务场景非常适用。这里我们可以解决篡改劫持的问题,但是我们仍然没有办法解决欺骗,对于欺骗我们似乎很难百分百解决,就像秒杀业务中我们很难仅仅使用数据加密的方式来识别欺骗,可能还会结合一些应用操作行为事件、ip等其他因素。

请添加图片描述

4.3 使用数字签名

其实在实际场景中,我们并非一定要使用加密技术来确保数据安全,在更多的时候我们需要做的是互相校验对方的身份,如果互相都确认了是自己可信的那个人,那么双方通信的内容只要不被第三方篡改,就能满足一般业务要求了。那么如何进行身份验证呢?我们可以参考文件签属的这一社会机制,某一段数据被签上Bob的大名,Tim收到这段数据和签名,仔细和Bob的”笔迹“对比是不是他签属的就可以决定是否相信这段数据。所以我们现在要解决的问题就是生成Bob的签名和比对他的”笔迹“。

Hash算法是天然的摘要生成算法,但是算法公开,谁都可以对一段数据计算摘要,不具备”笔迹“的唯一性。但是如果在要计算摘要的数据中埋入一段私有的字符串的话,那么这个摘要就可以防止伪造了。下图举例了一个简单的数字签名策略,这种方式被广泛用在Server对Server端的接口调用上,常见于各种系统的开放平台文档中。因为被拼接的唯一字符串是通过一种确定渠道”私下“沟通的,所以在这个字符串没有被泄漏的情况下,Tim一定能确定某个请求是不是Bob发送的。这样我们可以解决欺骗篡改的问题。
请添加图片描述

上面的方案是一种简单有效的方式,不过在RSA算法应用中,还有一种更加高级且正规的方案:
请添加图片描述

这就是我们所看到的电子证书的技术原理,因为私钥是私有的,任何其他人不能模仿,实现持有公钥的一侧可以验证数据是不是来自受信一方的单向验证通道。比如我们作为用户可以用银行颁发给我们的公钥来验证我们当前请求的服务器是不是银行的。早期我们办理网银业务银行会为我们颁发一个U key,这里面就包含了一个和实名信息绑定的私钥,这样一来银行系统就可以实现高安全性的双向身份校验和双向数据加解密了。

由此我们可以总结得出要基于非对称加密技术实现双向的安全通路,必须要确保双方都各自持有对方认可的私钥A和对方颁发的公钥B,且私钥A的分发途径是安全受信的。

4.4 对称、非对称、数字签名综合运用

经过前面章节的介绍,我们可以认识到对称加密不能很好地处理密钥分发问题,非对称加密能天然做到单向的安全通路,但是性能差。在实际应用中,很少有方案直接使用RSA对数据进行加密的,下面我们来看看经典的SSL/TLS是如何综合运用这些技术来解决网络通信安全问题的,这也是面试中的高频知识点。

首先我们来看如果把这对称和非对称结合起来使用,会有什么样的效果:请添加图片描述

核心思想就是使用随机生成的AES密钥加密数据,然后使用公钥RSA加密AES密钥,然后把密文和密钥密文一起传输给服务器,这样可以利用性能较好的AES加密大数据量的数据,利用RSA的单向安全通道能力隐藏AES密钥,综合利用了两者的优点。这样的设计在埋点上报类业务很有用,并且是单向安全的,数据上报接口都可以不使用HTTPS接口,从而避免HTTPS的二次加密开销。

上面这样的设计对可以预埋公钥的客户端来说有一定的实用空间,但是对于像面向浏览器这种通用性的客户端,就存在一个很大的问题。浏览器怎么信任服务器颁发的公钥?如果直接信任服务器颁发下来的公钥,那么任何服务方都可以使用上图的机制来和浏览器通信,虽然数据被加密了,但是请求被劫持转到中间商模拟的一个一样的流程还是可以对数据进行解密,这就是中间人劫持问题(这是常见的HTTPS客户端使用不当存在的安全性问题)。这个时候就需要数字签名来完善方案了:
存在劫持风险的方案

存在劫持风险的方案

引入CA中间机构的认证方案

引入CA中间机构的认证方案

首先服务器侧拿着公钥和host域名等核心信息去CA机构申请一个使用CA私钥签名的数字证书,CA机构会对服务提供商的域名、资质等进行校验,其他服务提供商不能去CA上签发有冲突的内容(比如a服务商没有持有*.b.com域名,就不能去CA上为这个域名签发证书),确保了整个技术环境的有序,这个数字证书在SSL握手的时候会被分发给客户端。同时客户端所运行的操作系统里维护有权威CA机构签发的带由公钥的证书,SSL握手拿到服务器返回的数字证书后会首先让操作系统验证这个证书是不是受信的CA机构签发的,如果是,则信任上面的内容,如果不是则可以终止请求。这是整个SSL/TLS工作的核心流程,实际实现比这个更加详细,涉及到CA证书链、密钥协商、加密算法协商等内容。感兴趣的同学可以自行了解更完善的技术实现。

通过上述的解释,我们在使用HTTPS的时候,只要做到对数字证书合法性的校验以及对证书上签发的域名通配符校验,就可以做到安全的单向的安全通信了,即客户端请求到的服务器一定是证书里指向的那个服务提供商,任何第三方不能劫持到请求。 但是这两个校验也是开发经常容易疏忽的地方,如果由于配置错误,在识别到某个元素校验不通过,但是仍然进行通信的话,就会存在中间人劫持漏洞。

5 总结

其实安全涉及到的范围非常广泛,本文所介绍的常用加解密算法及其应用只是比较基础的知识点,希望大家通过不断地积累,丰富自己的技术边界,在业务系统设计中越来越灵活使用各种技术来加固自己的系统!

标签: java 安全 算法

本文转载自: https://blog.csdn.net/ystgx/article/details/127970743
版权归原作者 Tison-田 所有, 如有侵权,请联系我们删除。

“常用加解密算法理解及应用”的评论:

还没有评论