手机 APP 应该选用哪个加密算法 - 兼吐槽 TEA

目录 无线和移动, 编程

很多 APP 产品都有通信加密的需求,一部分出于市场的要求,比如苹果对于“ATS”的强制性规定,一部分出于自身安全的考虑,比如对账号和密码的保护。这些需求大部分都可以用简单的 HTTP -> HTTPS 升级来搞定,而且几乎不用付出什么成本(除加解密的计算开支外),例如使用我之前文章介绍到的 Let's Encrypt 免费证书

但还有一类特殊的需求,HTTPS 解决不了,也就是防协议分析的需求。很多 APP 开发者应该知道,只要在手机里安装一个代理 CA 证书,就可以实现中间人攻击,通过代理软件抓到 HTTPS 包的明文内容。虽然这样的攻击很难在公开网络上进行,但对自己的手机进行抓包分析,作为 APP 和服务端通信的调试手段是被广泛使用的。

协议分析能做什么呢?可以猜想到一定的 APP 内部逻辑,可以对产品数据进行作弊攻击。举个例子:你的 APP 通过某个渠道进行推广,为了统计渠道安装、注册或者日活,你往往会在 APP 中埋一个点,当 APP 启动时,发送一些信息到服务器。如果这个协议被破解了,渠道商根本不需要真正进行推广,只需要构造一些假消息发送到你的服务器就行了。仅看数据你可能会以为这个渠道推广效果特别好,其实只是骗局而已。

这类情况下,就要求对敏感协议内容进行额外的数据保护。最常用的做法,就是对协议内容进行一次额外的加密,为了性能,往往选用对称加密算法。那么问题来了,手机 APP 开发时,应该选用哪个加密算法?

关于这个选型,国内互联网圈有个怪现状值得谈一下,那就是 TEA 算法。因为该算法在腾讯有着广泛的应用,因而被很多客户端开发人员推崇。典型推荐理由往往是:“TEA加密算法不但比较简单,而且有很强的抗差分分析能力,加密速度也比较快,还可以根据需求设置加密轮数来增加加密强度”。这是真的吗?算法安全性可以直接看维基百科上 TEA 算法的介绍,我的理解是不够安全。但其实大部分用户也不那么地在乎它的安全强度,那么性能呢?加密速度真的很快吗?

这就要从历史的角度去看了。作为曾经手撸过 “DES 差分密码攻击” 代码的程序员,表示 TEA 算法的确足够简单。在 QQ 诞生的那个年代,TEA 在计算上的确有着不小的优势。但 QQ 已经 18 岁了啊同学们,18 年来中国发生了多大的变化,世界发生了多大的变化啊!

2008 年,Intel 就发布了 x86 的 AES 指令集扩展,近几年的服务器 CPU 应该都支持,不相信你
grep aes /proc/cpuinfo 就能看到 ;2011 年 ARM 也在 ARMv8 架构下直接提供了 AES 和 SHA-1/SHA-256 指令 。这意味着什么?意味着服务端和客户端在硬件上直接支持 AES,意味着原来 N 条汇编指令只需要一条 AES 指令就完成了。其实也意味着,在绝大多数情况下 AES 才应该是你的首选

口说无凭,咱们可以看一下测试数据,x86 服务器 CPU 测试可以直接看 Crypto++ 的 benchmark 。可以看到 AES/CTR (128-bit key) 与 TEA/CTR (128-bit key) 的加密速度比是:4499 MB/s 比 72 MB/s,62 倍的差异!这就是硬件实现的威力。

ARM 手机 CPU 加密算法的 Benchmark,我没有找到。但为了更有说服力,我自己实现了两个测试 APP,一个 Android 版,一个 iOS 版。写技术文章多不容易啊,写博客之前先写三个晚上代码,泪目!!!代码在 https://github.com/solrex/cipher-speedAndroid 版可以直接在 Release 里扫码安装

首先介绍一下目前的旗舰 CPU,骁龙 835 (MSM8998) 的表现,测试机型是小米 6:

# Speed Test of 10MB Data Enc/Decryption #
# AES: 
* [AES/CBC/PKCS5Padding] ENC: 1146.9 KB/ms
* [AES/CBC/PKCS5Padding] DEC: 692.4 KB/ms
* [AES/CBC/NoPadding] ENC: 1118.8 KB/ms
* [AES/CBC/NoPadding] DEC: 1343.5 KB/ms
* [AES/ECB/PKCS5Padding] ENC: 990.4 KB/ms
* [AES/ECB/PKCS5Padding] DEC: 703.2 KB/ms
* [AES/ECB/NoPadding] ENC: 973.4 KB/ms
* [AES/ECB/NoPadding] DEC: 988.9 KB/ms
* [AES/GCM/NOPADDING] ENC: 13.9 KB/ms
* [AES/GCM/NOPADDING] DEC: 14.7 KB/ms
# DES: 
* [DES/CBC/PKCS5Padding] ENC: 20.1 KB/ms
* [DES/CBC/PKCS5Padding] DEC: 20.7 KB/ms
* [DES/CBC/NoPadding] ENC: 21.3 KB/ms
* [DES/CBC/NoPadding] DEC: 21.6 KB/ms
* [DES/ECB/PKCS5Padding] ENC: 26.3 KB/ms
* [DES/ECB/PKCS5Padding] DEC: 26.2 KB/ms
* [DES/ECB/NoPadding] ENC: 25.9 KB/ms
* [DES/ECB/NoPadding] DEC: 26.8 KB/ms
# 3DES: 
* [DESede/CBC/PKCS5Padding] ENC: 23.6 KB/ms
* [DESede/CBC/PKCS5Padding] DEC: 23.2 KB/ms
* [DESede/CBC/NoPadding] ENC: 23.6 KB/ms
* [DESede/CBC/NoPadding] DEC: 23.5 KB/ms
* [DESede/ECB/PKCS5Padding] ENC: 8.5 KB/ms
* [DESede/ECB/PKCS5Padding] DEC: 8.5 KB/ms
* [DESede/ECB/NoPadding] ENC: 8.5 KB/ms
* [DESede/ECB/NoPadding] DEC: 8.6 KB/ms
# TEA: 
* [TEA] ENC: 16.0 KB/ms
* [TEA] DEC: 18.1 KB/ms

可以看到,TEA:AES=16:990,这是多少倍?我都懒得算了。然后是 2 年前的中低端 CPU,联发科 Helio P10 (MT6755),测试机型是魅蓝 Note 3:

# Speed Test of 10MB Data Enc/Decryption #
# AES: 
* [AES/CBC/PKCS5Padding] ENC: 358.8 KB/ms
* [AES/CBC/PKCS5Padding] DEC: 267.9 KB/ms
* [AES/CBC/NoPadding] ENC: 438.8 KB/ms
* [AES/CBC/NoPadding] DEC: 515.0 KB/ms
* [AES/ECB/PKCS5Padding] ENC: 310.6 KB/ms
* [AES/ECB/PKCS5Padding] DEC: 222.1 KB/ms
* [AES/ECB/NoPadding] ENC: 312.4 KB/ms
* [AES/ECB/NoPadding] DEC: 319.5 KB/ms
* [AES/GCM/NOPADDING] ENC: 5.1 KB/ms
* [AES/GCM/NOPADDING] DEC: 5.7 KB/ms
# DES: 
* [DES/CBC/PKCS5Padding] ENC: 7.5 KB/ms
* [DES/CBC/PKCS5Padding] DEC: 7.7 KB/ms
* [DES/CBC/NoPadding] ENC: 7.7 KB/ms
* [DES/CBC/NoPadding] DEC: 7.8 KB/ms
* [DES/ECB/PKCS5Padding] ENC: 9.3 KB/ms
* [DES/ECB/PKCS5Padding] DEC: 9.2 KB/ms
* [DES/ECB/NoPadding] ENC: 9.3 KB/ms
* [DES/ECB/NoPadding] DEC: 9.5 KB/ms
# 3DES: 
* [DESede/CBC/PKCS5Padding] ENC: 12.5 KB/ms
* [DESede/CBC/PKCS5Padding] DEC: 12.3 KB/ms
* [DESede/CBC/NoPadding] ENC: 12.3 KB/ms
* [DESede/CBC/NoPadding] DEC: 12.5 KB/ms
* [DESede/ECB/PKCS5Padding] ENC: 3.1 KB/ms
* [DESede/ECB/PKCS5Padding] DEC: 3.1 KB/ms
* [DESede/ECB/NoPadding] ENC: 3.1 KB/ms
* [DESede/ECB/NoPadding] DEC: 3.1 KB/ms
# TEA: 
* [TEA] ENC: 6.2 KB/ms
* [TEA] DEC: 8.0 KB/ms

然后是 3 年前的旗舰 CPU,Apple A8,测试机型是 iPhone6。别问我为啥不用今年的苹果旗舰 CPU...

# Speed Test of 10MB Data Enc/Decryption #
# AES
* [AES/CBC/PKC7Padding] ENC: 76.0 KB/ms
* [AES/CBC/PKC7Padding] DEC: 111.3 KB/ms
* [AES/CBC/NoPadding] ENC: 138.2 KB/ms
* [AES/CBC/NoPadding] DEC: 450.7 KB/ms
* [AES/ECB/PKC7Padding] ENC: 305.6 KB/ms
* [AES/ECB/PKC7Padding] DEC: 735.9 KB/ms
* [AES/ECB/NoPadding] ENC: 330.0 KB/ms
* [AES/ECB/NoPadding] DEC: 673.6 KB/ms
# DES
* [DES/CBC/PKC7Padding] ENC: 23.1 KB/ms
* [DES/CBC/PKC7Padding] DEC: 24.5 KB/ms
* [DES/CBCPadding] ENC: 23.1 KB/ms
* [DES/CBCPadding] DEC: 22.8 KB/ms
* [DES/ECB/PKC7Padding] ENC: 19.4 KB/ms
* [DES/ECB/PKC7Padding] DEC: 20.8 KB/ms
* [DES/ECBPadding] ENC: 22.2 KB/ms
* [DES/ECBPadding] DEC: 22.2 KB/ms
# 3DES
* [3DES/CBC/PKC7Padding] ENC: 9.7 KB/ms
* [3DES/CBC/PKC7Padding] DEC: 9.8 KB/ms
* [3DES/CBC/NoPadding] ENC: 9.8 KB/ms
* [3DES/CBC/NoPadding] DEC: 9.8 KB/ms
* [3DES/ECB/PKC7Padding] ENC: 9.4 KB/ms
* [3DES/ECB/PKC7Padding] DEC: 9.1 KB/ms
* [3DES/ECB/NoPadding] ENC: 9.2 KB/ms
* [3DES/ECB/NoPadding] DEC: 9.4 KB/ms
# TEA
* [TEA] ENC: 10.9 KB/ms
* [TEA] DEC: 11.1 KB/ms

关于 Apple A8 的测试多说两句。我上面的 AES 性能,离 GeekBench 发布的 A8 AES Single Core 还有不少差距,不知道是不是测试方法差异导致。但总的来说,不影响结论,那就是 TEA 跟 AES 差距巨大

看到这里,可能大部分人心里已经做出选择了。即使还没做出选择的读者,我想你也可以考虑看看我的代码实现是否存在问题。不过最后还是回答一下开头提出的问题吧:

  • 如果你使用平台语言来实现对称加密,也就是 Android 上用 Java,iOS 上用 OC 或者 Swift,AES 是不二选择。这样能充分利用硬件提供的能力,安全性+性能肯定是最优,不要再想其他选项了。
  • 如果你使用 Native 语言来实现对称加密,在 Android 上使用 JNI 调用 C 编译的代码,的确不少人认为原生指令更难逆向。可能要在 ARM 架构上做个取舍,是取悦 v8 用户,还是取悦 v7 以下的用户,这可能影响到选型。不过我认为 AES 依然是一个好的选项,起码在服务器端,你肯定会节省成本。

如何实现高效的 URL 过滤算法

目录 算法

在上一篇文章《那么,屏蔽词系统到底该怎么做?》中,我简单讲了一下屏蔽词系统的实现思路。这篇就讲一讲另一个类似的话题,那就是如何实现高效的 URL 过滤算法。

通过某些字面特征,筛选出符合条件的 URL,对其执行特定的操作。虽然看起来像是很遥远专业的技术,但其实早就根植在你手机里的各类浏览器相关 app,以及你使用的各类互联网服务中了。举一个最简单的例子:你在微信里打开淘宝链接,背后就是一个 URL 过滤算法的实现。

还有,很多浏览器 APP 设置项里有一个开关,叫做“广告过滤”,其中很大一部分也是通过 URL 过滤实现的。那如何做到高效的 URL 过滤呢

如果拿这个问题来面试,大概率候选人会回答用正则表达式实现。其实这一点不令人惊讶,因为我曾经亲眼见过一个日活惊人的 APP 也是用正则表达式做的(真不敢相信自己的眼睛)。用正则表达式本身来实现 URL 匹配不是很大的问题,但在“广告过滤”这样的场合,意味着有成千上万的 URL 规则,很难有人能用这些规则写出来高效的正则表达式

关于这点,展开说一下。理论上来讲,把所有 URL 规则融合到一条正则表达式里,也不是不可能。比如:"http[s]{0,1}://..{0,1}(taobao|tmall).com/.",可以融合两条淘宝和天猫的 url 规则。但如果让你融合一千个不同的 url 规则到一条正则表达式里,我想很难有人有信心把它完全做对,更不敢保证后续维护这个规则库的人能做对。所以很多情况下,他们只是用几千个正则表达式实现了几千条 url 规则,想想这个匹配效率有多低!!

所以,真正在乎 URL 匹配效率的人,不会使用正则表达式。举个最典型的例子,Adblock Plus 的过滤规则(https://adblockplus.org/filters),是完全自定义了一套匹配规则。不过,Adblock Plus 在早期也是用正则表达式,而且完全就是我上面讲的那种用法,不过后来他们改进了,还专门发了篇博客(https://adblockplus.org/blog/investigating-filter-matching-algorithms , 注意里面也用到了我上篇文章提到到 Rabin-Karp )。

可是在我看来,Adblock Plus 的实现只是够用,却还不够高效。我上一篇文章提到的 Trie 树,更合适做这种事情,可能也更高效(未比较),至少更简洁。Adblock 最核心的地方,是 URL 匹配。从 Adblock Plus 定义的规则也能看出,URL 匹配其实比正则表达式匹配简单很多,无非是在普通字符串匹配之上加了一些通配符而已。

那以 Adblock Plus 的通配符为例,我来讲一下如何用 Trie 树来实现含通配符的字符串匹配

  • "*" 通配符匹配任意长的字符串
    • 包含匹配时模式串前后的 "*" 没有意义,可以直接丢掉;
    • 以中间的 "*" 做划分,原串 * 位置后面的部分进行递归子树匹配;
  • "|" 匹配网址开头结尾:对 url 预处理,扔掉 scheme 部分 "http://",头尾都加上 "|",这样自然就能匹配上模式串中的 "|" 了。
  • "^" 标记分隔符:这就更简单了,遇到 ^ 规则时,不是比较原串中字符是否与其相等,而是是否包含在某个符号表中即可。

在这些匹配规则的基础上,结合 Double Array Trie 数据结构,可以实现一个内存占用超级小但效率又非常高的 URL 过滤器了。而且 Trie 树的结构对规则的条数非常不敏感,耗时并不会随着过滤规则的增多而显著增加。

不过还得多说一句,算法是效率核心,但真正解决问题还得花很多心思在算法外。比如例外规则,规则库的动态更新等等,这里就不继续展开了。

以上只是本人的一点拙见,对 Adblock Plus 的评论也没有评测数据验证,只是希望对读者能够有些用处。此外,也欢迎大家用更好的算法来打脸

那么,屏蔽词系统到底该怎么做?

目录 算法

caoz 在最近一篇公众号文章《企业面试需要几轮》中提到一个面试问题:

大家都知道做互联网有很多屏蔽词要处理,那么需要对用户发布的内容,做屏蔽词过滤,先不考虑一些正则组合的情况,假设我有一个屏蔽词库,里面有几万条屏蔽词信息,然后我有个非常火爆的社区,每天用户产生海量内容,比如上百万篇文章或评论,现在我要求每篇文章都能快速通过屏蔽词库去检索,而且要求服务器可以支撑尽可能多的处理请求,毕竟这是成本啊。那么请设计一个屏蔽词过滤的算法逻辑。

碰巧我在面试中也喜欢问这个问题,可惜的是能答上来的的确不多。

单单从算法层面论,大概有 50% 的人能回答出来逐个进行 KMP 匹配(另外 50% 不知道什么是 KMP)。让他更进一步优化的话,20% 的人能想到一些粗糙的优化技巧,10% 的人能说出前缀树匹配。大部分能答出前缀树匹配的人会提到 AC 自动机,但只有一半能大概答出 AC 自动机的大概算法。到目前为止,还没有人回答过其它算法,比如 Rabin-Karp 或者 Commentz-Walter。

即使在算法层面上能回答出来 AC 自动机,在实现上候选人也只能形象地描述一下 Trie 树和失败指针,Trie 树的每一层使用一个 Set 或者哈希表数据结构。到目前为止,还没有人回答过用 Double Array 来实现 Trie,甚至连用前缀做 key 的一个大哈希表来模拟 Trie 树的优化也没有。虽然构造 Double Array Trie 的代价非常高,但内存占用非常小,检索效率也很高,特别适用于屏蔽词的场合。离线建好 Double Array Trie,推送到线上仅仅需要读入两个内存块就能完成更新,加载过程到了极简。

哪怕是回答出了高效的算法和实现,离一个真正有效的屏蔽词系统还相差很远。首先要清楚一件事,道高一尺,魔高一丈。你平时想过什么办法绕过屏蔽词系统,现在就得想办法对抗你曾经开过的脑洞。下面举几个最常见的例子:

  1. 简繁体混用,大小写混用,全半角混用。首先得做归一化,繁体转简体,大写转小写,全角转半角。
  2. 在屏蔽词中间加特殊字符。原串先过一遍,去掉特殊字符再过一遍。
  3. 字形相近,读音相似。积累相近字典,替换为常用字再过一遍,将所有的字转为拼音再过一遍。
  4. 同义词替换。加屏蔽词时考虑替换场景,或者挖掘替换词库用来替代。

上面这几种方法,说起来简单,做起来都是坑。一不小心,就把正常的内容当成包含屏蔽词干掉了。这就是为啥大家老吐槽在某些网站发帖,动不动就被屏蔽,关键就在于坏招太多,规则多了就有误伤啊!所以一个优秀的屏蔽词系统,还得尽量避免误伤情况,也有一些办法。

  • 先切词,如果命中屏蔽词的范围不在切词的边缘,应该是误伤。最简单的例子是:24口交换机。
  • 添加屏蔽词的例外规则,比如“成人牙刷”需要是“成人”的例外规则。

最近几年机器学习特别火,其实机器学习也特别适合这种场景,就是一个二分类问题嘛。只是超出了简单算法的范畴,这里就不展开了。

所以你看,就一个“屏蔽词过滤的算法逻辑”,就有那么多要探讨的东西。而且以目前国内大部分互联网产品的实现上来看,可能考虑的还没我这篇文章全面。这可真不是一个能轻松就回答完美的问题。