文章有点长(一共2300字)
但最后一个故事最有意思
看不完的话可以直接拉到底
== 1 ==
从面试题说起好了。
在考察到网络这一块的时候,可能会问问http协议,聊安全相关问题时,就顺便聊聊 https。
大多数候选人知道非对称加密,了解客户端会用 RSA 公钥进行加密。
那么,服务器在返回响应报文之前,会用什么来进行加密呢?
有些候选人回答:“用服务器私钥进行加密”。
我内心呵呵一笑
接着问,那服务器返回的信息岂不是可以被中间人拦截并解密吗?
候选人一般就放弃挣扎,只能强颜欢笑了。
有进一步了解过 https 的同学,能够说出在SSL/TLS握手以后,会生成一个对称加密密钥。
那么,既然有非对称加密,为什么还需要使用对称加密呢?
有些候选人就回答不上来了,只能强颜欢笑+1。
实际上这是因为非对称加密的性能通常比对称加密算法差几个数量级。
以 RSA 为例,在加解密的时候,需要对大整数(典型值是2048bit,256字节)做大量乘法、取模等运算;相比之下如 AES 这样的非对称加密算法会简单很多,一些XOR、移位,以及在4x4的矩阵上做些变换,还可以通过查表来加速。
此外,由于AES的广泛应用,主流 CPU(Intel, AMD, ARM)都有相应的扩展指令集,可以将性能提升一个数量级,实际每秒能处理的数据在数百 MB 这个量级上。
有些硬盘号称有全盘加密功能,实际上就是硬盘的主控芯片在写入前通过AES进行加密,在电脑启动时BIOS会要求输入密码。这样即使电脑丢了,或者硬盘被人拆下挂到其他机器上也不用担心数据泄露。
关于 RSA 算法的实现细节,推荐阮一峰写的《RSA算法原理》,在文末点击 “阅读原文” 可以查看。
== 2 ==
另一个有趣的事情是 2017 年,当时在钱厂,对接某银行系统的时候,在通信协议的加密这块,对方给了一个 jar 包(不给源码),以及不知什么编码的公钥、私钥文件,既不是PEM也不是X509,是个奇怪的二进制文件。
然鹅,钱厂用的是PHP,这就有点尴尬了。
幸好这个jar包没有经过混淆,用安卓开发小伙伴提供的反编译工具,得到了源码,并经过一番努力重写成了PHP的源码。
然后发现那个公私钥是 java object 序列化后得到的字节码。
更有趣的还在后面。
为了方便测试,我们按银行给的API写了一套mock系统,这样就可以在不依赖银行在内部完成全流程自测,大幅提高了开发效率。
在部署mock系统的时候,没想太多,就用银行提供的这对公私钥,然后竟然调通了
也就是说,银行给的公钥和私钥文件竟然是一对
我猜,应该是银行的安全审计部门在项目需求中要求用非对称加密,但是又没有对最终代码进行审查吧
顺便一提,正式上线时,对方给的API url是https的,但是url中的域名是IP,钱厂在代码中只能把 CURLOPT_SSL_VERIFYHOST 设为 false。
过了一段时间,他们决定用个正式的域名,才给安上了https证书。
又过了一段时间,他们的证书过期了。
并且在故障期间不允许我们忽略证书进行访问。
== 3 ==
故事2里提到,把那段java代码“经过一番努力”重写成了PHP,其实中间还是遇到了个不大不小的麻烦。
Java代码里用了一个叫 bouncycastle 的库来进行 RSA 的加解密,而我用 PHP 的 openssl_private_encrypt 加密的文本,并不能被他们提供的java代码正常解密。
经过多次尝试,我发现了一个现象:对同一个消息,java代码加密生成的密文,每次都一样,而PHP生成的密文,则总是在变化。
作为一个信息安全专业的毕业生,我竟然不知道这是为什么,真是愧对国家愧对党,只好默默点开桌面上的小飞机,在一些不存在的网站上摸索。
根据这个现象,我在stackoverflow找到了 "data encrypted with openssl_public_encrypt is different every time?" 这个问题,答案中给了个线索:
The PKCS#1 encryption algorithm uses some random seed to make the cipher-text different every time. This protects the cipher-text against several attacks, like frequency analysis, ciphertext matching.
stackoverflow.com/questions/36627518
经过进一步的搜索,终于找到了如何在PHP中解决这个问题。
具体解决办法后面说,这里先介绍一些背景知识。
RSA加密的基本流程是:将一个和密钥长度相同的输入(明文),通过一系列运算(加密),得到一个和密钥长度相同的输出(密文)。。
以 1024 bit 的RSA密钥为例,每次输入128字节,输出128字节。
对于超过128字节的情况,就需要将原始数据切成128字节的块,分别加密后再拼起来;解密时,按128字节拆开解密。
但是不足128字节的情况,比如像密码这种短数据,或者长数据也并不总是128的倍数,会留下小尾巴,这就有点尴尬。
因此我们还需要用某种方法,将不足128字节的数据拼(padding)到128字节,再进行加密;解密得到的数据,需要把padding的数据去掉,才能得原始数据。
真是让人头秃。
继续。
对于普通文本,一个简单的做法是用 ASCII 0 进行填充。
但这会带来2个问题:
1. 如果原文中包含了 ASCII 0,就无法有效识别
2. 对于相同的输入,总是能得到相同的密文。
问题 2 可能招致某些类型的攻击,例如前面引用中提到的 "frequency analysis",以一个简化的场景为例,假设每个单词是单独加密的,在英文中单词 a 出现的次数最多,通过统计密文出现的频率,可以破译对应的明文。
一个改进的方案是,使用一些随机数进行填充,这样可以保证相同明文每次加密得到不同的密文。
基于这个思路,RFC 2313 制定了RSA的加密标准 PKCS #1: RSA Encryption Version 1.5,通过在128字节中的前11个字节里加入一些随机数,保证每次加密得到的密文不同。
回到最初的问题,通过查看PHP的 openssl_public_encrypt 文档,可以发现它有一个 $padding 参数,默认值是 OPENSSL_PKCS1_PADDING 。
而银行给的Java代码是
Cipher.getInstance("RSA", new BouncyCastleProvider());
按照官方文档的说明,这里的 RSA 等于 "RSA/NONE/NoPadding"。
最后,通过在 PHP 代码中给数据手动填充前导 ASCII 0,并指定 OPENSSL_NO_PADDING,终于和Java代码兼容了。
问题圆满解决。
等等……
银行用的是NoPadding?
== THE END ==
其实关于RSA还有一些其他有趣的事情,这次就先写到这里,下次(如果我还记得的话),可以聊聊RSA和币圈的一点小八卦。
按照前几篇的套路,文末还是要贴一下招聘广告:
我在网盟广告业务线(穿山甲),由于业务持续高速发展,长期缺人、不限HC。关于字节跳动面试的详情,可参考我之前写的《程序员面试指北:面试官视角》
~ 投递链接 ~
后端开发(上海)
https://job.toutiao.com/s/sBAvKe
后端开发(北京)
https://job.toutiao.com/s/sBMyxk
广告策略研发(上海)
https://job.toutiao.com/s/sBDMAK
其他地区、职能线
https://job.toutiao.com/s/sB9Jqk