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, "[email protected]"));
            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, "[email protected]"));
            ASN1Primitive aoo = new DERSequence(avt).toASN1Primitive();
            return (aoo != null);
        } catch (Exception e) {
            return false;
        }
    }
    //....
}
項目源碼可以到這裏下載:http://download.csdn.net/detail/suntongo/8454957
(待續)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章