Android 數據加密及安全網絡通信雜談(一)

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 相關文檔及代碼範例。
(待續)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章