Android 数据加密及安全网络通信杂谈(二)

(续前文)
补充前文:凡是代码中使用到 KeyChain 包 的 API,勿忘记在 AndroidManifest.xml 中加入 "android.permission.USE_CREDENTIALS" 权限。


有时候,App 可能需要查验某个证书(譬如从签名邮件或者数字信封、时间戳里获取的“签名者”)是否“可信”,可从系统凭证库的“CA”库中取出所有的“可信颁发者”证书(包括系统预装的以及用户自行安装的)对其进行检验:


//获取系统凭证库里的 CA 证书,查验证书的可信性
X509Certificate cert = ....; //待查验的证书
X509Certificate CAcert;
KeyStore mCAStore = KeyStore.getInstance("AndroidCAStore");
mCAStore.load(null, null);
Enumeration<String> als = mCAStore.aliases();
while (als.hasMoreElements()) {
    CAcert = (X509Certificate) mCAStore.getCertificate(als.nextElement());
    try {
        cert.verify(CAcert.getPublicKey());
        Log.i("CA found: ", CAcert.toString());
        break;
    } catch(Exception e) {
        CAcert = null;
    }
}
if (CAcert == null) Log.i("CA not found.", "He he");


前文提到安装证书到系统凭证库时,系统会自动根据所安装的证书是否 CA 证书来决定安装在哪里,如果待安装的用户证书不是随着密钥对一起安装的话,实际上是没有意义的,所以在安装之前,有必要判断一下待安装的是否 CA 证书:
//判断某证书是否一个 CA 证书
public boolean isCA(X509Certificate cert) {
    if (cert.getBasicConstraints()) < 0 return false;
    boolean[] kusg = cert.getKeyUsage();
    return kusg[5] && kusg[6];
}
是不是超简单?


曾经有人问过本人,系统凭证库的 API 只有安装和读取功能,有没有办法列举、删除里面的证书、私钥?答案是有的,这涉及到 Android 一个未公开的类:android.security.KeyStore(注意,这不是 JCE 的那个KeyStore)。既然是未公开的类,编译的时候还真有点小麻烦,另外不同版本的 Android 里这个类的内容也有不同,有一位大牛已经考虑到了这点,专门做了个项目方便我们编程时引用:
https://github.com/nelenkov/android-keystore 去下载下来,里面有测试代码,这里简单介绍一下你可能感兴趣的:
public byte[] get(String name),读内容,如果要读的是私钥,有些版本可能读到的数据不是你所想象的;
public boolean put(String name, byte[] value),写内容;
public boolean delete(String name),删除内容;
public String[] saw(String prefix),列出以 prefix 为开头的所有 name,在 Android 6.0 中,该方法已经更改为
public String[] list(String prefix) 了(Google 可恶吧?);
那么这个 prefix 怎么取值呢?"CACERT_"、"USRCERT_"、"USRPKEY_"、"VPN_"、"WIFI_",望文生义就不用解释了吧,如果想列出所有 name,用 "" 即可。
请花点时间读一下这篇文章,会少走些弯路:http://doridori.github.io/android-security-the%20forgetful-keystore/


3、AndroidKeyStore,从 4.2 开始,增加了这个 Service Provider,可以像 JCE 其他类型的 KeyStore 一样对其操作,当然还是有些许差别的:
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null, null);
KeyStore.Entry entry = ks.getEntry(alias, null);
从以上代码可以看出,所有关于密码的参数都是 null,因为 AndroidKeyStore 是由锁屏功能来进行保护的,所以不需要 password 了,请参阅 SDK 文档 docs/training/articles/keystore.html
从 4.3 开始,AndroidKeyStore 新增了一些方法用于生成“自签名证书”,到了 6.0,又新增了 android.security.keystore 这个包(注意,不是 android.security.KeyStore 这个一直未公开的类),本人不赞成用这方法来生成证书,理由后文再论。


小结:本文假设你在 Java、JCE、JSSE、PKI 方面有较全面的知识,所以这些方面的内容全部略去以节省篇幅。不同于网上很多侧重于源码分析的文章,本文仅针对 Android 应用开发中关于数据加密、网络通信安全范畴应掌握的知识,基本上摘录自本人以往的笔记,因而条理性差了点,还请海涵。
从本人观点来说,Android 系统的 JCE、JSSE 与传统 Java 体系的兼容度只能说勉强可用(刚从这坑拔出脚,又踩到那个雷的感觉),存放证书及密钥应尽可能采用 KeyChain 包 的 API 或者 AndroidKeyStore。
建议阅读一下这篇文章:http://blog.csdn.net/innost/article/details/44081147 以及文章后附的参考资料。


4、以下就本人以往做项目的一些东东共享出来,希望对你有所帮助:


4.1 数字证书的中文问题,Android 在解释数字证书的主题及颁发者主题时,如果其中的内容含有非英语文字(譬如中文)时,有可能出现乱码,经分析 Android 只能正确解释编码为 UTF-8 的字段,而微软 Windows Server 自带的 CA 服务以及部分权威 CA 生成的证书则采用 UTF-16BE 编码,还有极少数的 CA 采用 UCS4 编码。
在 PC 环境的 Java 中是不会出现这种问题的,究其原因是 Android 底层采用 Harmony 的代码来解释 ASN.1 格式数据,Harmony 早已停止更新了,除非 Google 什么时候想起来修正这个 Bug,否则我们将永远要面对它。
Bouncy Castle 倒是可以正确解释 UTF-16BE,但仍解释不了 UCS4,且和 Harmony 一样把 E-mail 字段转换成一串 hex,很不爽。无奈之下自己动手写了一个类:


public class x500p implements Principal {
    final byte[][] OIDs = {{0x55, 0x04, 0x06}, {0x55, 0x04, 0x08}, {0x55, 0x04, 0x07},
            {0x55, 0x04, 0x0a}, {0x55, 0x04, 0x0b}, {0x55, 0x04, 0x03}
            {0x2a, (byte)0x86, 0x48, (byte)0x86, (byte)0xf7, 0x0d, 0x01, 0x09, 0x01}};
    final String[] DNstr = {",C=", ",ST=", ",L=", ",O=", ",OU=", ",CN=", ",E="};
    private ByteArrayInputStream bis = null;
    public x500p(X500Principal xp) {
        if (xp == null) return;
        bis = new ByteArrayInputStream(xp.getEncoded());
    }
    private int preLen(int tag) {
        int itag;
        if (tag != -1) {
            itag = bis.read();
            if (itag != tag) return 0;
        }
        itag = bis.read();
        if (itag < 0x80) return itag;
        if (itag == 0x81) return bis.read();
        if (itag == 0x82) {
            itag = bis.read();
            itag <<= 8;
            return itag + bis.read();
        }
        return 0;
    }
    @Override
    public String getName() {
        if (bis == null) return null;
        byte[] oid = new byte[9];
        int oidType, valueType;
        StringBuilder sb = null;
        if (preLen(0x30) == bis.available()) { //检查其结构完整性
            sb = new StringBuilder();
            while (true) {
                if (preLen(0x31) == 0) break; //ASN.1 TAG_SETOF
                if (preLen(0x30) == 0) break; //ASN.1 TAG_SEQUENCE
                int len = preLen(0x06); //ASN.1 TAG_OID
                if (len == 0) break;
                if (len > 9) { //不能识别的 OID,跳过
                    oidType = -1;
                    bis.skip(len);
                } else {
                    bis.read(oid, 0, len);
                    for (oidType = DNstr.length -1; oidType > -1; oidType--) {
                        for (len = OIDs[oidType].length -1; len > -1; len--) {
                            if (oid[len] != OIDs[oidType][len]) break;
                        }
                        if (len < 0) break; //全等
                    }
                }
                valueType = bis.read(); //字符串编码标识
                len = preLen(-1);
                if (oidType > -1) {
                    String Chs = "UTF-16BE";
                    byte[] value;
                    if (valueType == 0x1c) { //ASN.1 TAG_UNIVERSALSTRING
                        value = new byte[len /2];
                        len = 0;
                        do {
                            bis.read();
                            bis.read();
                            bis.read(value, len, 2);
                            len++;
                            len++;
                        } while (len < value.length);
                    } else {
                        value = new byte[len];
                        bis.read(value, 0, len);
                        if (valueType != 0x1e) Chs = "UTF-8"; //ASN.1 TAG_BMPSTRING
                    }
                    sb.append(DNstr[oidType]).append(new String(value, Chs));
                } else {
                    bis.skip(len); //不能识别的 OID,跳过
                }
            }
        }
        try {
            bis.close();
        } catch (IOException ignored) {}
        return (sb == null)||(sb.length() == 0) ? null : sb.substring(1);
    }
}
//解释证书主题及颁发者主题
X509Certificate cert = ....;
String subj = (new x500p(cert.getSubjectX500Principal())).getName();
String issuer_subj = (new x500p(cert.getIssuerX500Principal())).getName();


4.2 关于 Bouncy Castle,如前所述,JCE、JSSE 所能做到的功能不一定能满足你的需求,这时你可能需要看看 Bouncy Castle 能否满足,如果能满足,你有三种选择:
A.去 Bouncy Castle 官网下载合适的 jar 包,打包到你的 apk 中,此法只适合 Android 3.0 以上。
B.去 https://github.com/rtyley/spongycastle 下载源码,编译后选取合适的 jar 包,打包到你的 apk 中,此法适用于任何 Android 版本。
C.从 Android 环境里把 BouncyCastle.dex 复制出来(有些版本是 odex 文件),再用如 dex2jar 之类的工具转换为 jar 包,仅编译时用,不要打包到 apk 中,获得 jar 包是一劳永逸的事,不妨一试,好处是 apk 的体积较小。
由于版本变迁历史的缘故,Bouncy Castle 有版本不兼容的问题,经本人分析并实测,Android 内置的 Bouncy Castle 分三个阶段:3.0 之前、3.0 至 4.1、4.2 之后,同阶段的可以兼容,如果 App 要求跨阶段会有点麻烦,下面以本人做的一个项目为例介绍一下步骤:
a.因为 apk 要求在 4.0 以上版本中运行,需要两个不同版本的 BouncyCastle.dex,于是启动 SDK 的虚拟机,用 adb 从中提取,4.1、4.2 各提取一个,分别改名为 bc41.jar 和 bc42.jar;
b.设计一个接口,本例中为 CertFunc,新建两个项目,分别命名为 app41 和 app42,把 CertFunc.java 复制到这两个项目中,将 bc41.jar 复制到 app41 中,将 bc42.jar 复制到 app42 中;
c.在这两个项目中实现 CertFunc 接口,类名分别为 CertFunc41 和 CertFunc42,其实它们的代码大部分是相同的,只有少量因 Bouncy Castle 的版本差异而不同,分别编译得到两个 jar 包,为方便起见,把这俩 jar 包合并为一个;
d.新建一个项目 appcert,把上一步编译合并得到的那个 jar 包复制过来,同时把 CertFunc.java 也复制过来,然后开始编写你的代码,在需要调用 CertFunc 里的方法时,先判断一下运行环境中的 Bouncy Castle 版本再引用合适的类即可:
import org.pkisvc.android.CertFunc;
import org.pkisvc.android41.CertFunc41;
import org.pkisvc.android42.CertFunc42;
CertFunc CrtFn;
if (CertFunc42.checkVer()) CrtFn = new CertFunc42();
else if (CertFunc41.checkVer()) CrtFn = new CertFunc41();
else { CrtFn = null; ....}
....
那么,怎么判断 Bouncy Castle 版本呢?且看类里是怎么实现的:
public class CertFunc41 implements CertFunc {
    public static boolean checkVer() {
        try {
            if (Class.forName
                    ("com.android.org.bouncycastle.asn1.DERObject") == null)
                return false;
            ASN1EncodableVector avt = new ASN1EncodableVector();
            avt.add(new GeneralName(GeneralName.rfc822Name, "1@2.go"));
            DERObject doo = new DERSequence(avt).toASN1Object();
            return (doo != null);
        } catch (Exception e) {
            return false;
        }
    }
    //....
}


public class CertFunc42 implements CertFunc {
    public static boolean checkVer() {
        try {
            if (Class.forName
                    ("com.android.org.bouncycastle.asn1.ASN1Primitive") == null)
                return false;
            ASN1EncodableVector avt = new ASN1EncodableVector();
            avt.add(new GeneralName(GeneralName.rfc822Name, "1@2.go"));
            ASN1Primitive aoo = new DERSequence(avt).toASN1Primitive();
            return (aoo != null);
        } catch (Exception e) {
            return false;
        }
    }
    //....
}
项目源码可以到这里下载:http://download.csdn.net/detail/suntongo/8454957
(待续)
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页