预览:本文将着重讲解 Android KeyStore、so 库保护 app key / secret、HTTPS 原理及其防中间人攻击措施。
谈到 Android 安全性话题,Android Developers 官方网站给出了许多很好的建议和讲解,涵盖了存储数据、权限、网络、处理凭据、输入验证、处理用户数据、加密等方方面面,甚至对于动态加载代码也提供了建议,具体可以看看 training 的 security tips 章节。而今天,我想特别来讲一讲在 Android 密钥保护和 C/S 网络传输安全 这两方面的具体安全措施。
密钥的保护以及网络传输安全 应该是移动应用安全最关键的内容。
所谓的密钥,简单来说,可以认为是我们常用的 app key / secret / token 或数据加密的 key,这些 keys 就像我们宝库的钥匙,一旦泄露,就像大门被人撬开,什么安全都无从谈起。因此,密钥们的安全存储、防窃取便显得异常重要。我曾经看过许多国内外著名的应用在使用自家 API 或 SaaS 服务的时候,在 Java 代码或 SharePreferences 里明文记录着 app key / secret / token,这样的做法,就算使用了 proguard 对代码进行混淆,也是非常容易被逆向获得服务端接入密钥,非常危险。
在这一方面,Android 提供大量用来保护数据的加密算法,例如 Cipher 类中提供了 AES 和 RSA 算法,再例如安全随机数生成器 SecureRandom 给 KeyGenerator 提供了更加可靠的初始化参数,避免离线攻击等等。
而如果需要存储密钥以供重复使用,Android 提供了 KeyStore 等可以长期存储和检索加密密钥的机制,Android KeyStore 系统特别适合于存储加密密钥。”AndroidKeyStore” 是 KeyStore 的一个子集,存进 AndroidKeyStore 的 key 将受到签名保护,并且这些 key 是存在系统里的,而不是在 App 的 data 目录下,依托于硬件的 KeyChain 存储,可以做到 private key 一旦存入就无法取出,总之,每个 App 自己创建的 key,别的应用是访问不到的。
很多时候,我们会需要将用户的账号密码或 token 存储下来,以做到下次打开免登的目的。
KeyStore 提供了两个能力:
有了这两个能力,我们的密钥保护就变得很容易了,你只需要:
其中加密算法可以使用 Cipher AES 来保证安全性,不要使用自己创造的加密算法。
这就是使用 KeyStore 的一整套流程,另外 KeyStore 还可以用来做数据签名和签名验证,就像一个黑匣子一样,具体可以自行搜索了解。
KeyStore 适于存储运行时生产获取到的数据,比如运行时,用户输入的密码,或者服务端传下来的 token,但无法用于存储我们需要预设在 App 内的 API key / secret,对于这类需要预设的固定密钥,我将介绍一种十分安全、难破解的保护方式。
首先我们需要思考,这个 key 应该放到哪里才能够最大限度提升其被逆向获取的难度,放 Java 代码里?根本不安全。放文件或图片像素里?顶多续 1 小时。放 so 库里?可以,so 库能够很大程度提升逆向破解难度,但如果别人把 so 库文件拿出来,再直接调用这些 native 接口,便也可以获取到你的 key,怎么办?
我的做法是在 so 库的 C 代码里 JNI_OnLoad()
方法对 APK 签名进行验证,如果签名不对,直接 crash,这样移植出去便和砖头没什么两样。而你的应用又不得不依赖这个 so 库进行获取 API key / secret,因此它又不能直接剥离,这就保证了不能没有它,又不能移植它,换句话说就是:如果别人反编译了你的代码,发现你使用 so 进行签名验证,便直接把这个 so 文件摘掉,这样做的结果是,App 获取不到你存在 so 中的 secret 了,便无法正常工作了;而如果别人对你的应用进行修改和重新签名,或移植你的 so 库来读取内部的 secret,则会因为签名验证不通过直接自爆。
以上便是对于保护 key / secret 的一些有效举措,再总结下就是,使用 so 库存储预设 key / secret,使用 Android KeyStore 存储运行时动态获取到的私密内容。
而另一方面,对网络传输进行加密、防中间人攻击也是至关重要的,如果你的密钥保护得很好,但数据却在网络传输过程中被拦截篡改,那也是前功尽弃。
如果使用 HTTP,对于 API 请求参数会变的请求,一般都会进行参数防篡改校验,和参数加密,客户端和服务端会约定一个固定的密钥作为加密的 key,这个 key 便属于上面我们讲过的需要预设在 App 代码里的,因此比较安全的做法就是将它埋藏在 so 库里。
但更推荐、更简单的做法是,使用 HTTPS,HTTPS 是 HTTP 的安全版本,为什么这么说呢?因为HTTPS 自带加密、验签、检查数据完整性等功能,它在 HTTP 下加入了 SSL (Secure Socket Layer),SSL 位于 TCP/IP 和 HTTP 协议之间,负责加密、验签、检查数据完整性工作。
HTTPS 的握手过程中能够确立客户端与服务端双方加密传输数据的密码信息,流程大致如下:
但是,尽管有了以上机制来保证端与端之间通信的安全,仍然无法保证安全,因为防不住中间人攻击,也就是说,如果有个中间人横跨在你的客户端与服务端之间,你以为你在和服务端握手,但实际上是在与这个中间人握手,这个中间人将持有你目标服务端的公钥,以及它自己产生的密钥和公钥,然后把它自己产生的公钥交给你,你以为你拿到手的是服务端直接给你的公钥,因此这个中间人能够解密你的所有请求。
你的客户端 <-->
Packet Capture/Charles(VPN 或 Wi-Fi 或代理,它们都算中间人) <-->
你的目标服务器
关于这个话题,我回答过一个 StackOverflow 问题,提问者不明白为什么他使用了 HTTPS,结果一个叫做 Packet Capture 的抓包工具居然还能获取到他的请求内容。实际上原理就是因为这个中间人替换掉了服务端的证书。
解决办法就是客户端要对服务端传下来的证书是否正确进行验证,如果使用 OkHttp 的话,可以预设服务端证书的 pining 值,通过 OkHttp Builder 的 certificatePinner 方法进行设置,非常简单。如此设置之后,一旦证书被替换掉,便与你预设的证书 pinning 不对等,握手过程就会中止,请求数据根本不会传输出去,这样中间人也就获取不到你的请求内容了。
除了设置 certificatePinner 的方法之外,还可以把服务端的证书的拷贝 完整地放在客户端一份,等服务端传下来的时候,对本地证书进行对比,若不对等,则抛出错误。对于 OkHttp,可以使用 OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory()) 来给它传递你的证书。
这么做以后,便可以防止网络传输过程中中间人获取你的加密信息。整个应用的基础安全措施也就到此实施结束了。值得一提的是,对于 HTTPS,很多人存在误解,以为 GET 请求是不安全的,POST 才是安全的,实际上,不仅仅 GET 的参数内容会被加密,你的 header、body 等等都是会被加密,都是安全的。
最后,感谢你耐心阅读到到此,虽然本文一行代码都没有贴出,但我相信它提供了很全面详细的理论指南,至于具体的代码,都可以通过 Google 轻松获得,或者有能力的人也可以自行编写实现。另外,还可以参考文末推荐的文章链接,包括 Android Developers 网站和 OkHttp 的 wiki 相关篇章。如果你因为我的文章而受益,你需要对此文进行付费,这样我花费的时间可以得到一些回报,就像王垠说的:
我一直很高尚的样子,不愿意为此收费。然而,根据经济学的原理,这是有害社会的 😛 经济的原理是这样,有价值的事物,应该在经济上受到相应的支持,这样好的东西才能受到鼓励,发扬光大,不好的东西才可能被人忘记。
所以现在我决定,给我觉得价值比较大的文章加上大概的价格,这样喜欢文章的人可以自愿付费,当然也可以不付费。谢谢你的支持!
等价交换,谢谢你的支持!本文大概值 12 人民币,你可以扫描以下支付宝对我进行付费打赏,再次表示感谢:
相关链接:
以上都是非常好的推荐文章,如果你没有付费,希望能够再考虑下,谢谢。