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/NoPaddingRSA/NONE/PKCS1PaddingRSA/NONE/OAEPWithMD5AndMGF1PaddingRSA/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.html ”Unlimited 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) { throw new RuntimeException(e); } } try { //处理分段加密 int offSet = 0; int i = 0; ByteArrayOutputStream out = new ByteArrayOutputStream(); 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) { throw new RuntimeException(e); }
3.3 关于加密后的数据长度问题对于AES,如果使用NoPadding,则加密后的数据长度不会有变化,并且对同一原文加密后的密文都一样如果使用了一些Padding的话,密文数据长度会因为填充而变长,并且例如像AES/CBC/PKCS7Padding这样的方式的话多次对同一原文进行加密后得到密文会变化,安全性会更高对于RSA来说,其单次加密后的密文长度是和密钥长度相关的,即不管一次加密的原文是1字节还是最大的blockSize,其密文长度是固定的,和有没有使用Padding没有联系,所以如果有数据要进行RSA分段加密的话要使得单段数据尽可能和blockSize一样大如果使用Padding的话可以实现对同一原文的加密结果不同的效果,从而提高安全性3.4 对AES和RSA性能的理解使用以下简单的方法测试出来一点数据,让大家可以对不同算法性能有个直观一点的了解,以下测试使用200字节原文,加密和解密各循环10000次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客户端使用不当存在的安全性问题)这个时候就需要数字签名来完善方案了:首先服务器侧拿着公钥和host域名等核心信息去CA机构申请一个使用CA私钥签名的数字证书,CA机构会对服务提供商的域名、资质等进行校验,其他服务提供商不能去CA上签发有冲突的内容(比如a服务商没有持有.b.com域名,就不能去CA上为这个域名签发证书),确保了整个技术环境的有序,这个数字证书在SSL握手的时候会被分发给客户端同时客户端所运行的操作系统里维护有权威CA机构签发的带有公钥的证书,SSL握手拿到服务器返回的数字证书后会首先让操作系统验证这个证书是不是受信的CA机构签发的,如果是,则信任上面的内容,如果不是则可以终止请求这是整个SSL/TLS工作的核心流程,实际实现比这个更加详细,涉及到CA证书链、密钥协商、加密算法协商等内容通过上述的解释,我们在使用HTTPS的时候,只要做到对数字证书合法性的校验以及对证书上签发的域名通配符校验,就可以做到安全的单向的安全通信了,即客户端请求到的服务器一定是证书里指向的那个服务提供商,任何第三方不能劫持到请求 但是这两个校验也是开发经常容易疏忽的地方,如果由于配置错误,在识别到某个元素校验不通过,但是仍然进行通信的话,就会存在中间人劫持漏洞(图片来源网络,侵删)
0 评论