Android 数据加密及安全网络通信杂谈
前言:本人多年从事软件开发,发现大多数程序员(其中包括不少是资深的)、CTO、PM们对信息安全的了解几乎为零!很多时候,项目负责人在不得不面对信息安全需求时,随意指派某个程序员(通常还是入行时间最短、技术经验最少的那位)负责与信息安全有关的代码。
另外,即使是信息安全行业的专业公司,技术队伍也是良莠不齐,对信息安全的综合认识水平。。。。总的来说,在下表示不~敢~恭~维~。
随着移动设备功能的日益丰富及使用普及程度的飙升,很多软件产品从方案设计之初到上线发布都暴露出对信息安全认识不足或数字安全技术应用不当的情况,千疮百孔状况频频是普遍现象。
数据加密及安全网络通信是信息安全领域中技术含量最高的领域,尤其是其中的公共密钥基础设施(PKI)规范,内容繁杂晦涩,将其恰当运用于软/硬件产品并非易事,往往会出现产品使用极不方便而漏洞百出被轻易破解的尴尬局面。
作为移动/手持设备的两大操作系统之一的 Android,其版本的升级通常也伴随着其安全模块的变迁,其中API的变迁,大量底层细节的变更导致很多应用 App 难以适应多版本的要求。Android 的安全模块涵盖多个方面,内容较多,本文仅针对其中的加密(cryptographic)和安全网络通信(SSL)范畴说一说本人的一些看法。
1、Android 的 Java 安全框架,具体实现被称为 JCE 及 JSSE,Java 体系从面世至今已颇有年头了,其安全框架的内容一直保持得很稳定,相关书籍、文档很丰富,本文不打算讨论它,Android 选择了 Java 为主要开发语言,很自然也继承了 JCE 和 JSSE,但细看之下还是有些差异:
Service Provider:“原版”的 Java 版本中,Service Provider 多数是 Sun 和 IBM 的作品,而 Android 选用了3个开源项目的代码:Harmony、Bouncy Castle、OpenSSL。
Harmony:是一个比较复杂的 Java 项目,Android 只用了其中 Security、xnet 的一部分代码,其 Provider 仅提供 SSL/TLS 的支持,另外的部分代码用于 X.509 数字证书对象的解释等等。Harmony 的代码已多年没有更新了,所以 Android 的这部分代码也一直没有变动,4.0 之后,增加了一个“AndroidCAStore”还是挺实用的。
Bouncy Castle:是 Android 中功能最丰富的 Service Provider,没有之一(但不提供 SSL/TLS 的直接支持),而且 Android 版本的升级同时也采用当时最新的 Bouncy Castle 版本的代码。早期的 Android 版本沿用 Bouncy Castle 的包名前缀 org.bouncycastle.*,从 3.0 开始,包名前缀改为 com.android.org.bouncycastle.*,这样的改动不影响 JCE 的兼容性,却为应用软件开发者采用“原版”的
Bouncy Castle 提供了方便。
OpenSSL:是 C 语言编写的著名开源项目,Android 通过 JNI 将其封装为一个 “AndroidOpenSSL”,早期的版本仅用于对 WebView 提供 https 支持(而且是仅用于单向 SSL),后来逐渐丰富,到了 4.2,已支持大多数常用的加密算法,4.4之后则更为丰富,大有取代 Bouncy Castle 的势头。
从 4.2 开始,Android 新增了一个 Service Provider,命名为 AndroidKeyStore,此前,在 Android 中的 KeyStore 只有 bks 和 PKCS#12 两种格式,底层由 Bouncy Castle 的代码来实现,而 AndroidKeyStore 的底层则因 Android 版本的不同而差别很大,在应用层来看,AndroidKeyStore 可看作是 JCE 的 KeyStore 中的一种,但其 load 方法中所有参数都须为 null,而且没有 store
方法。
虽说 Android 各版本的 JCE 及 JSSE 保持稳定,但是其 Service Provider 的差异还是值得注意的,如果你编写的代码希望能适应多个 Android 版本,那么需要了解每个版本的细节,以下是一段罗列 Service Provider 情况的代码以供参考:
Provider[] providers = Security.getProviders();
StringBuilder sB = new StringBuilder();
for (Provider provider : providers) {
sB.setLength(0);
sB.append(provider.getName());
Set<Provider.Service> serviceSet = provider.getServices();
for (Provider.Service service : serviceSet) {
sB.append("\n Type=").append(service.getType());
sB.append("; Alg=").append(service.getAlgorithm());
}
Log.i("Security Provider: ", sB.toString());
}
在各个 Android 版本中运行这段代码,你会发现各版本的差异有多大。另外,Android 各版本对 JCE、JSSE 各种接口、类采用的默认 Provider 也不尽相同,本人就曾遇到一段关于数字签名的代码在某些版本中运行正常而在另一些版本中运行出错的情况,经分析是默认 Provider 不一致所致,最后改为在各有关类的 getInstance 方法中明确指出一致的 Provider 参数才解决了问题。
实际应用中,JCE、JSSE 并不能满足所有的数据加密、安全网络通信的相关需求,比如有些应用需用到 PKCS#7、S/MEMI 以及其他一些安全相关的技术规范,JCE 则无能为力,这些问题将在后文中讨论。
2、Android 的凭证库(Credential Store)及其 API,从版本 1.6 开始(本人使用的第一台 Android 手机),通过“设置--安全--从 SD 卡安装数字证书”操作,可以将SD卡根目录下的证书文件(*.cer, *.crt, *.p12, *.pfx)安装到系统的“凭证库”里,凭证库分为“可信CA凭证”和“个人凭证”两部分,安装时根据文件内容会自动决定安装在哪里,凭证库里证书及其私钥用于浏览器(WebView)、VPN 以及 Wi-Fi WAPI 设备的认证,如果你编写的 App
仅需要 https 通信来保障数据安全,则几乎不需要在代码上操心,只须指导用户去获取并安装证书就万事大吉了(Yeah! 让 JCE、JSSE 见鬼去吧)。
从 4.0 开始,Android 提供了一组 API 使应用程序也能访问这个库:
android.security.KeyChain,关于这个类的详细用法,请参阅 Android SDK 文档,本文仅谈谈本人实际应用中的一些经验。
首先谈一下 KeyChain.createInstallIntent(),调用这个方法后得到一个 Intent,这个 Intent 有什么用处呢?且看以下的代码:
//从 SD 卡安装证书
public void install_SD(Context context) {
Intent intent = KeyChain.createInstallIntent();
context.startActivity(intent);
}
private void install_ANY(Context context, String type, byte[] value) {
Intent intent = KeyChain.createInstallIntent();
intent.putExtra(type, value);
context.startActivity(intent);
}
//安装证书
public void install_CRT(Context context, Certificate cert) {
install_ANY(context, "CERT", cert.getEncoded());
}
//安装PKCS#12证书
public void install_P12(Context context, byte[] p12) {
install_ANY(context, "PKCS12", p12);
}
//安装证书以及密钥对
public void install_Credential(Context context, KeyPair pair, Certificate cert) {
Intent intent = KeyChain.createInstallIntent();
intent.putExtra("PKEY", pair.getPrivate().getEncoded());
intent.putExtra("KEY", pair.getPublic().getEncoded());
context.startActivity(intent);
SystemClock.sleep(2000);
install_CRT(context, cert);
}
另外,SDK 文档里有两句话:
These extras may be combined with EXTRA_NAME to provide a default alias name for credentials being installed.
When used with startActivityForResult(Intent, int), RESULT_OK will be returned if a credential was successfully installed, otherwise RESULT_CANCELED will be returned.
看起来很美,实测之后却不完全是那么回事,你就当他没说吧。
很多程序员编写的 https、SSL/TLS 相关代码,测试过程中总遇到些坑,其实90%是没安装合适的证书导致的,网上一些文章给出误人子弟的“解决办法”是忽略所有 SSL 错误,这样 SSL 握手倒是成功了,通信也似乎是“正常”了,然而这是一种外行的做法,为钓鱼网站和 MITM (中间人攻击)打开了方便之门。本人曾在某项目里采用了一个著名的 WebSocket 包,测试中间人攻击居然成功了,仔细检查其代码发现作者在 SSL 方面竟是个外行。
由于 Android 有了这个系统级的凭证库,只要安装了合适的 CA 证书(也可能包括用户证书),浏览器或者内嵌 WebView 的 App 就可以正常访问 https://...... 这类网站了,如果要编写 Socket 通信的代码,也是极简单的事:
SSLSocket sslSocket = (SSLSocket) SSLContext.getDefault().getSocketFactory().createSocket(new Socket(), "my.ip.addr", 4430, true);
也许有人说,SSLSocketFactory.getDefault()不是也可以吗?本人测试过,大部分的 Android 版本可以,但某些版本不行,所以还是用上面的办法为妥。
如果编写 SSL/TLS 服务端代码(SSLServerSocket),则不建议使用默认的 SSLContext,也不要把服务端证书安装到系统凭证库里,理由将在后文讨论。
除了前文所述的,KeyChain 包还提供读取系统凭证库的方法,应用 App 可以读出已安装的证书(包括 CA 信任链)及其匹配的私钥:
public static X509Certificate[] mCerts;
public static PrivateKey mKey;
public static String mAlias = null; //如果此变量在调用之前不是null,则与此同名的证书在UI中成为默认首选项。
private static final boolean mEnd[];
private static Activity mActivity = ....; //此变量需要初始化为当前运行的 Activity。
/**
* 调出系统证书选择界面,所选证书的别名保存在 mAlias,证书信任链保存在 mCerts,私钥保存在 mKeys。
* @param needCRT 是否需要读出证书链。
* @param needKey 是否需要读出私钥。
* @return 选择结果,系统未安装证书或使用者放弃选择则为 false。
*/
public boolean getCredential(boolean needCRT, boolean needKey) {
new Thread(new Runnable() {
@Override
public void run() {
KeyChain.choosePrivateKeyAlias(mActivity, new KeyChainAliasCallback() {
@Override
public void alias(String alias) {
if (alias != null) {
mAlias = alias;
if (needCRT) mCerts = KeyChain.getCertificateChain(mActivity, alias);
if (needKey) mKey = KeyChain.getPrivateKey(mActivity, alias);
} else mAlias = null;
synchronized (mEnd) {
mEnd.notify();
}
}
}, null, null, null, 0, mAlias);
}
}).start();
synchronized (mEnd) {
try {
mEnd.wait();
} catch (InterruptedException ignored) {}
}
return mAlias != null;
}
如读出成功,mCerts[0] 就是所要的证书,mCerts[1]....就是该证书的信任链,从证书中取出的公钥可以用于加密或验证签名,mKey 就是该证书的匹配私钥,可以用于解密或签名。
在 4.0 版本中,读出来的私钥是可以通过 getEncoded() 得到其具体内容的,这显然是个不好的表现,4.1 之后这个问题已得到修正。
KeyChain 包的内容大致就是这些了,更详细的内容请仔细阅读 SDK 文档。需要注意的是,choosePrivateKeyAlias、getCertificateChain、getPrivateKey 这三个方法是不能在程序的主线程里调用的。
读取证书或私钥用到的参数 alias 必须是 choosePrivateKeyAlias 得到的值,否则会抛出异常,这样的设计是为了阻止应用 App “悄悄地”读出凭证库里的证书或私钥用于不正当的用途(这就是信息安全界里经常提到的“显式调用原则”)。但是,如果在 Android 设备里运行 SSL/TLS 服务端使用系统凭证库的话,一有客户端发起连接,服务端就弹出个证书选择框让人选择服务端证书那就烦死了,因此本人不推荐在服务端代码使用系统凭证库,服务端代码较合适的办法就是回归 JSSE,请参阅 SSLContext、KeyManager、TrustManager
相关文档及代码范例。
(待续)