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

手机上的 AI - 在 Android/iOS 上运行 Caffe 深度学习框架

目录 开源

目前在云端基于各种深度学习框架的 AI 服务已经非常成熟,但最近的一些案例展示了在移动设备上直接运行深度神经网络模型能够带来很大的处理速度优势。比如 Facebook 在官方博客上发布的可在移动设备上进行实时视频风格转换的应用案例 “Delivering real-time AI in the palm of your hand”。其中提到 Caffe2go 框架加上优化后的网络模型,可以在 iPhone6S 上支持 20FPS 的视频风格转换。Google Tensorflow 也提供了 iOS 和 Android 的 example

Caffe 是一个知名的开源深度学习框架,在图像处理上有着非常广泛的应用。Caffe 本身基于 C++ 实现,有着非常简洁的代码结构,所以也有着很好的可移植性。早年也已经有了几个 github 项目实现了 Caffe 到 iOS/Android 平台的移植。但从我的角度来看,这些项目的编译依赖和编译过程都过于复杂,代码也不再更新,而且最终产出的产品包过大。caffe-compact 最接近我的思路,但是在两年前未完工就已经不更新了。

从我个人在 APP 产品上的经验来看,移植深度学习框架到 APP 中,不仅仅是能不能跑,跑不跑得快,还有个很重要的因素是包大小问题。因为一般用深度学习模型只是实现一个产品功能,不是整个产品。一个产品功能如果对 APP 包大小影响太大,很多 APP 产品都无法集成进去。我希望依赖库能尽量地精简,这样打包进 APP 的内容能尽量地少。所以我在春节期间在 github 上启动了一个 Caffe-Mobile 项目,将 Caffe 移植到 Android/iOS 上,并实现了以下目标:

NO_BACKWARD:手机的电量和计算能力都不允许进行模型训练,所以不如干脆移除所有的后向传播依赖代码,这样生成的库会更小,编译也更快。

最小的依赖。原始的 Caffe 依赖很多第三方库:protobuf, cblas, cuda, cudnn, gflags, glog, lmdb, leveldb, boost, opencv 等。但事实上很多依赖都是没必要的:cuda/cudnn 仅支持 GPU, gflags 仅为了支持命令行工具,lmdb/leveldb 是为了在训练时更高效地读写数据,opencv 是为了处理输入图片,很多 boost 库都可以用 c++0x 库来替换。经过精简和修改部分代码,Caffe-Mobile 的第三方库依赖缩减到两个:protobuf 和 cblas。其中在 iOS 平台上,cblas 用 Accelerate Framework 中的 vecLib 实现;在 Android 平台上, cblas 用交叉编译的 OpenBLAS 实现。

相同的代码基,相同的编译方式。两个平台都采取先用 cmake 编译 Caffe 库(.a or .so),然后再用对应平台的 IDE 集成到 app 中。编译脚本使用同一个 CMakeList.txt,无需将库的编译也放到复杂的 IDE 环境中去完成。

可随 Caffe 代码更新。为了保证开发者能追随最新 Caffe 代码更新,我在修改代码时使用了预编译宏进行分支控制。这样进行 diff/patch 时,如果 Caffe 源码改动较大,merge 时开发者可以清楚地看到哪些地方被修改,是如何改的,更方便 merge 最新更新。

除了 Caffe 库外,在 Caffe-Mobile 项目中还提供了 Android/iOS 两个平台上的最简单的 APP 实现示例 CaffeSimple,展示了在手机上使用 Caffe example 里的 MNIST 示例(深度学习领域的 Hello World)训练出来的 LeNet 模型预测一个手写字符 “8” 图片的过程和结果。 Caffe-Mobile 项目的地址在:https://github.com/solrex/caffe-mobile 欢迎体验,感兴趣的同学们也可以帮忙 Star 下 :)

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 问题下做了回答