1. 程式人生 > >Android 資料加密及安全網路通訊雜談(一)

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 相關文件及程式碼範例。
(待續)