手机 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 依然是一个好的选项,起码在服务器端,你肯定会节省成本。

Android HTTPUrlConnection EOFException 历史 BUG

目录 Linux, 开源, 无线和移动

这是一个影响 Android 4.1-4.3 版本的 HTTPUrlConnection 库 BUG,但只会在特定条件下触发。

我们有一个 Android App,通过多个并发 POST 连接上传数据到服务器,没有加入单个请求重试机制。在某些 Android 机型上发现一个诡异的 bug,在使用中频繁出现上传失败的情况,但是在其它机型上并不能复现。

经过较长时间的排查,我们找到了上传失败出现的规律,并认为它跟 HTTP Keepalive 持久化连接机制有关。具体的规律是:当 App 上传一轮数据后,等待超过服务端 Nginx keepalive_timeout 时间后,再次尝试上传数据,就会出现上传失败,抛出 EOFException 异常。

更准确的特征可以通过连上手机的 adb shell 观察 netstat:当 App 上传一轮数据后,可以观察到有 N 个到服务器的连接处于 ESTABLISHED 状态;当等待超过服务端 Nginx keepalive_timeout 时间后,可以观察到这 N 个到服务器的连接处于 CLOSE_WAIT 状态;当上传失败后,发现部分 CLOSE_WAIT 状态的连接消失。

Java 的 HTTP Keepalive 机制,一直是由底层实现的,理论上来讲,不需要应用层关心。但从上面的 BUG 来看,对于 stale connection 的复用,在部分 Android 机型上是有问题的。为此我稍微研究了一下 Android 的源代码,的确发现了一些问题。

在 2011年12月15日,Android 开发者提交了这样一个 Commit,Commit Message 这样写到:

Change the way we cope with stale pooled connections.

Previously we'd attempt to detect if a connection was stale by
probing it. This was expensive (it relied on catching a 1-millisecond
read timing out with a SocketTimeoutException), and racy. If the
recycled connection was stale, the application would have to catch
the failure and retry.

The new approach is to try the stale connection directly and to recover
if that connection fails. This is simpler and avoids the isStale
heuristics.

This fixes some flakiness in URLConnectionTest tests like
testServerShutdownOutput and testServerClosesOutput.

Bug: http://b/2974888
Change-Id: I1f1711c0a6855f99e6ff9c348790740117c7ffb9

简单来说,这次 commit 做了一件事:在修改前,是在 TCP 连接池获取连接时,做 connection isStale 的探测。Android 开发者认为这样会在获取每个 connection 时都有 1ms 的 overhead,所以改成了在应用层发生异常时,再重试请求。 但是这个重试有个前提,就是用户的请求不能是 ChunkedStreamingMode,不能是 FixedLengthStreamingMode,这两种模式下,底层无法重试。很不幸地是,我们正好使用到了 FixedLengthStreamingMode 带来的特性。

// Code snippet of: libcore.net.http.HttpURLConnectionImpl.java
while (true) {
  try {
    httpEngine.sendRequest();
    httpEngine.readResponse();
  } catch (IOException e) {
    /*
     * If the connection was recycled, its staleness may have caused
     * the failure. Silently retry with a different connection.
     */
    OutputStream requestBody = httpEngine.getRequestBody();
    // NOTE: FixedLengthOutputStream 和 ChunkedOutputStream
    // 不是 instance of RetryableOutputStream
    if (httpEngine.hasRecycledConnection()
        && (requestBody == null || requestBody instanceof RetryableOutputStream)) {
      httpEngine.release(false);
      httpEngine = newHttpEngine(method, rawRequestHeaders, null,
          (RetryableOutputStream) requestBody);
      continue;
    }
    httpEngineFailure = e;
    throw e;
}

由于 BUG 的根源在 Android 的核心库 libcore 中。这次改动影响了从 4.1 到 4.3 的所有 Android 版本, Android 4.4 网络库的 HTTP/HTTPS 从 libcore 切换到 okhttp,所以4.4以后的 Android 版本不受影响。

既然底层不重试,那么只有在应用层重试,所以我们在应用层增加了最多『http.maxConnections+1』次重试机制,以修复此问题。在重试的时候,尝试使用一个 stale connection 会导致 EOFException,底层也会自动关闭这个连接。『http.maxConnections+1』次重试保证即使连接池中全都是 stale connection,我们也能获得一个可用的连接。

网上应该也有人遇到过这个 BUG,所以我也在这个 StackOverflow 问题下做了回答

MIUI的贴心功能

目录 应用软件, 无线和移动

预先声明:这不是一篇软文,我真的是出于对 MIUI 系统的喜爱才写的。

我的手机型号是华为 U8800,联通定制机,自带的 ROM 里太多乱七八糟的软件,后来就刷了华为官版的海外 ROM。这个 ROM 是比较干净的版本,但不知道为何老是黑屏重启,基本上每天会重启了一两次。毕竟追求的是性价比,这点儿缺陷我就忍了。

不过基于安卓用户的刷机习惯,后来我还是忍不住刷了修改的 MIUI ROM——因为我的手机型号不在 MIUI 的支持列表里。第一次刷的是 2011 光棍节版,前两天又更新到 2.3.2 版。MIUI 很多贴心改进让我爱不释手,下面我会列举一下。

  1. 菜单键+音量- 执行屏幕截图,下面的图片有些是用这个功能截的。

  2. 拨号键盘使用 T9 拼音直接搜索联系人,支持模糊查询。

    拨号键盘

  3. 通知栏内置很多有用的功能快捷开关。

    通知栏

  4. 系统自带防打扰过滤器功能,可以按照规则过滤垃圾短信和电话。

    防打扰

  5. 可在联系人列表中直接查看到手机号码的归属地,这样当某些朋友有多个手机号码时,容易判断该打哪一个。

    联系人
  6. 陌生人的未接电话可以显示号码归属地和响铃次数,而且第一声响铃静音。这对判断来电是否是垃圾电话非常有帮助。

    陌生人来电
  7. 内置流量监控和防火墙功能,可以为每个应用程序设置网络访问权限。

    流量监控和防火墙
  8. 内置音乐播放器与百度合作,提供在线音乐功能。

    音乐播放器
  9. 内置手电筒功能,且在锁屏时可以长按 Home 键打开手电筒。这个功能实在是太有用了!

    手电筒
  10. 电话功能设置很多贴心小功能:自动录音、接通挂断时振动、来电挂断时提供短信回复、通话记录点击显示联系人信息等等。

    电话设置
  11. 最后值得一提的是,虽然不算个功能,但刷了 MIUI 之后,每天重启的现象居然没了!我完全没想到第三方修改的第三方 ROM 居然比官版 ROM 还稳定,这个出乎了我的意料。