深入理解Android之Java Security第二部分(Final)
深入理解Android之Java Security(第二部分,最後)
程式碼路徑:
- Security.java:libcore/lunl/src/main/java/java/security/
- TrustedCertificateStore.java:libcore /crypto/src/main/java/org/conscrypt/
- CertInstallerMain:package/apps/CertInstaller/src/com/android/certinstaller/
- CredentialHelper:package/apps/CertInstaller/src/com/android/certinstaller/
- CertInstaller:package/apps/CertInstaller/src/com/android/certinstaller/
- CredentialStorage.java:package/apps/Settings/src/com/android/settings/
- OpenSSLRSAPrivateKey.java:libcore/crypto/src/main/java/org/conscrypt/
- OpenSSLEngine.java:libcore/crypto/src/main/java/org/conscrypt/
- OpenSSLKey.java:libcore/crypto/src/main/java/org/conscrypt/
二 Android Key/Trust Store研究
下面我們來看看Android Key/Trust Store方面的內容。這裡就會大量涉及程式碼了.....。
根據前述內容可知,Android Key/Trust Store是系統全域性的Key/Trust Store。雖然Android符合JCE/JSSE規範,但是Android平臺的實現和一般PC機上的實現有很大不同。
我們先來看KeyStore的架構,如圖16所示:
圖16 Android KeyStore架構
圖16中:
- 一個APP有兩種方式和Android Keystore互動。一種是利用JCE的KeyStore介面,並強制使用“AndroidKeyStore“作為Provider的名字。這樣,JCE就會建立AndroidKeyStore物件。當然,這個物件也就是個代理,它會建立另外一個KeyStore物件。這個KeyStore就是android.security.KeyStore。雖然名字一樣,但是包名卻不同,這個是android特有的。
- 另外一條路是使用Android提供的KeyChain API。KeyChain我覺得從“Key和CertificatesChain的意思”來理解KeyChain的命名可能會更加全面點。KeyChain會和一個叫KeyChainService的服務互動。這個KeyChainService又是執行在“keychain“程序裡的。keychain程序裡也會建立android.security.KeyStore物件。
- 再來看android.security.KeyStore(以後簡稱AS Store,而JCE裡的,我們則簡稱JSStore)。好吧,binder無處不在。AS(AndroidSecurity) Store其實也是一個代理,它會通過binder和一個native的程序“keystore“互動。而keystore又會和硬體中的SEE(Security Element Enviroment)裝置互動(ARM平臺幾乎就是Trust Zone了)。高通平中,SEE裝置被叫做QSEE。keystore程序會載入一個名叫“libQSEEComAPI.so”的檔案。
為什麼要搞這麼複雜?
- KeyChain其實簡化了使用。通過前面的例子大家可以看到,JCE其實用起來是很麻煩的,而且還要考慮各種Provider的情況。而且,通過KeyChain API能使用系統級別的KeyStore,而且還有對應的許可權管理。比如,不是某個APP都能使用特定alias的Key和Chain的。有一些需要使用者確認。
- 而更重要的功能是把硬體key managment功能融合到AS Keystore裡來了。這在普通的JCE中是沒有的。硬體級別的KM聽起來(實際上也是)應該是夠安全的了:)
關於SEE和TrustZone,圖17給了一個示意圖:
圖17 TrustZone示意圖[16]
簡單點看,ARM晶片上其實跑了兩個系統,一個是Android系統,另外一個是安全的系統。Android系統藉助指定的API才能和安全系統互動。
提示:關於TrustZone,請童鞋們參考[13],[14][15]。
言歸正傳,我們馬上來看AS KeyStore相關的程式碼。如下是分析路線:
- 匯入前面例子中反覆提到的test-keychain.p12檔案到系統裡去,看看這部分的流程。
- 在示例中使用KeyChain API獲取這個檔案中的Private Key和CA資訊。
- 刪除系統中的根CA資訊。這部分和Trust Store有關(請讀者自行研究吧!)
2.1 p12檔案匯入系統流程
2.1.1 觸發PKCS12檔案匯入
在DemoActivity.java中有一個importPKCS12函式,程式碼如下所示:
[-->DemoActivity.java::importPKCS12]
void importPKCS12(){
//根據Android的規定,匯入證書等檔案到系統的時候,直接把檔案內容通過intent發過去就行
//所以我們要先開啟”test-keychain.p12”檔案,也就是KEYCHAIN_FILE
BufferedInputStream bis =new BufferedInputStream(getAssets().open(
KEYCHAIN_FILE));
byte[] keychain = newbyte[bis.available()];
bis.read(keychain);
/*
呼叫KeyChain API中的createInstallIntent。根據KeyChain.java程式碼,該Intent
的內容是:
action名:android.credentials.INSTALL
目標class的包名:com.android.certinstaller
目標class為com.android.certinstaller.CertInstallerMain
*/
Intent installIntent = KeyChain.createInstallIntent();
//Android支援兩種證書檔案格式,一種是PKCS12,一種是X.509證書
installIntent.putExtra(KeyChain.EXTRA_PKCS12, keychain);
//指定alias名,此處的ENTRY_ALIAS值為“MyKey Chain”
installIntent.putExtra(KeyChain.EXTRA_NAME,ENTRY_ALIAS);
啟動目標Activity,其實就是CertInstallerMain
startActivityForResult(installIntent, 1);
}
當處理完畢後,DemoActivity的onActivityResult會被呼叫。那個函式裡沒有什麼特別的處理。我們先略過。
2.1.2 CertInstallerMain的處理
唉,Android裡邊除了前面講到的keychain.apk外,還有一個專門用於匯入證書的certinstaller.apk。真夠麻煩的。來看CertInstallerMain的程式碼
[-->CertInstallerMain.java::onCreate]
protected void onCreate(Bundle savedInstanceState) {
.......//啟動一個執行緒,然後執行自己的run函式
new Thread(new Runnable(){
public void run() {
......
runOnUiThread(CertInstallerMain.this);
}
}).start();
}
[-->CertInstallerMain.java::run]
public void run() {
Intent intent = getIntent();
String action = (intent ==null) ? null : intent.getAction();
//我們發出的Intent的Action就是INSTALL_ACTION
if (Credentials.INSTALL_ACTION.equals(action)
||Credentials.INSTALL_AS_USER_ACTION.equals(action)) {
Bundle bundle =intent.getExtras();
......
if (bundle == null ||bundle.isEmpty() || (bundle.size() == 1
&&(bundle.containsKey(KeyChain.EXTRA_NAME)
||bundle.containsKey(Credentials.EXTRA_INSTALL_AS_UID)))) {
//安裝方式有兩種,一種是搜尋SDCard/download目錄下面的證書檔案,凡是字尾名
//為.crt/.cet/.p12/.pfx的檔案都能被自動列出來。使用者可選擇要安裝哪個檔案
} else {
//由於我們發出的Intent已經包含了證書檔案內容,所以此處再啟動一個Activity就好
Intent newIntent =new Intent(this, CertInstaller.class);
newIntent.putExtras(intent);
startActivityForResult(newIntent,REQUEST_INSTALL_CODE);
return;
}
} else if (Intent.ACTION_VIEW.equals(action)) {
......//除了安裝,列舉系統已經安裝的證書檔案也是通過CertInstall這個apk來完成的
}
finish();
}
2.1.3 CertInstaller的處理
(1) 安裝準備
[-->CertInstaller.java::onCreate]
protected void onCreate(Bundle savedStates) {
super.onCreate(savedStates);
//CredentialHelper是一個關鍵類,很多操作都是在它那完成的
mCredentials = createCredentialHelper(getIntent());
mState = (savedStates ==null) ? STATE_INIT : STATE_RUNNING;
if (mState == STATE_INIT) {
if(!mCredentials.containsAnyRawData()) {
toastErrorAndFinish(R.string.no_cert_to_saved);
finish();
} else if (//判斷這次的安裝請求是否對應一個PKCS12檔案。顯然,本例符合這個條件
mCredentials.hasPkcs12KeyStore()) {
//PKCS12檔案一般由密碼保護,所以需要彈出一個密碼輸入框
showDialog(PKCS12_PASSWORD_DIALOG);
} else {
......//其他操作
}
......
}
整個CertInstaller.apk中,核心工作是交給CredentialHelper來完成的。createCredentialHelper就是構造了一個CredentialHelper物件,我們直接來看建構函式:
[-->CredentialHelper.java::CredentialHelper]
CredentialHelper(Intent intent) {
Bundle bundle =intent.getExtras();
......
//取出Alias名,本例是“My Key Chain”,儲存到成員mName中
String name =bundle.getString(KeyChain.EXTRA_NAME);
bundle.remove(KeyChain.EXTRA_NAME);
if (name != null) mName= name;
//我們沒有設定uid,所以mUid為-1
mUid =bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, -1);
bundle.remove(Credentials.EXTRA_INSTALL_AS_UID);
//取出bundle中其他的key-value值。除了名字,其他的key就是EXTRA_PKCS12了
for (String key :bundle.keySet()) {
byte[] bytes =bundle.getByteArray(key);
mBundle.put(key, bytes);
}
//如果bundle中包含證書內容,則解析該證書。本例沒有包含證書資訊
parseCert(getData(KeyChain.EXTRA_CERTIFICATE));
}
安裝時候,會判斷此次安裝是否為PKCS12檔案,如果是的話,需要彈出密碼輸入框來解開這個檔案。判斷的程式碼是:
[-->CredentialHelper.java::hasPkcs12KeyStore]
//判斷bundle是否包含EXTRA_PKCS12引數,本例是包含的
boolean hasPkcs12KeyStore() {
returnmBundle.containsKey(KeyChain.EXTRA_PKCS12);
}
(2) 密碼輸入對話方塊處理
來看密碼輸入框程式碼,核心在
[-->CertInstaller.java:: createPkcs12PasswordDialog]
private Dialog createPkcs12PasswordDialog() {
View view =View.inflate(this, R.layout.password_dialog, null);
mView.setView(view);
......
String title =mCredentials.getName();
......
Dialog d = newAlertDialog.Builder(this).setView(view).setTitle(title)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener(){
public voidonClick(DialogInterface dialog, int id) {
Stringpassword = mView.getText(R.id.credential_password);
//構造一個Action,把密碼傳進去。Pkcs12Extraction其實就是呼叫
//CertInstaller的extractPkcs12InBackground函式
mNextAction = new Pkcs12ExtractAction(password);
mNextAction.run(CertInstaller.this);
}})....create();
...
return d;
}
圖18所示為這個密碼框:
圖18 密碼框
注意這個對話方塊的Title,“Extracfrom”後面跟的是Alias名。按了OK鍵後,程式碼執行Pkcs12ExtractAction的run,其內部呼叫就是CertInstaller的extractPkcs12InBackground。
[-->CertInstaller.java::extractPkcs12InBackground]
voidextractPkcs12InBackground(final String password) {
// show progress bar andextract certs in a background thread
showDialog(PROGRESS_BAR_DIALOG);
newAsyncTask<Void,Void,Boolean>() {
@Override protectedBoolean doInBackground(Void... unused) {
//解碼PKCS12檔案
return mCredentials.extractPkcs12(password);
}
@Override protected voidonPostExecute(Boolean success) {
MyAction action = new OnExtractionDoneAction(success);
if ......
else action.run(CertInstaller.this);//執行CertInstaller的onExtractionDone
}
}.execute();
}
解碼PKCS12檔案的核心程式碼在CredentialHelper的extractPkcs12函式中。這個函式其實和我們前面示例提到的testKeyStore幾乎一樣。
[-->CredentialHelper.java:: extractPkcs12Internal]
private boolean extractPkcs12Internal(String password) throwsException {
//下面這段程式碼和testKeyStore示例程式碼幾乎一樣
java.security.KeyStore keystore =
java.security.KeyStore.getInstance("PKCS12");
PasswordProtection passwordProtection =
new PasswordProtection(password.toCharArray());
keystore.load(newByteArrayInputStream(getData(KeyChain.EXTRA_PKCS12)),
passwordProtection.getPassword());
Enumeration<String>aliases = keystore.aliases();
......
while(aliases.hasMoreElements()) {
String alias =aliases.nextElement();
KeyStore.Entry entry =keystore.getEntry(alias, passwordProtection);
if (entry instanceof PrivateKeyEntry) {
//test-keychain.p12包含的就是一個PrivateKeyEntry
return installFrom((PrivateKeyEntry)entry);
}
}
return true;
}
extractPkcs12Internal看來只處理PrivateKeyEntry,核心在installFrom函式中,如下:
[-->CredentialHelper.java::installFrom]
private synchronized boolean installFrom(PrivateKeyEntry entry) {
//私鑰資訊
mUserKey = entry.getPrivateKey();
//證書資訊
mUserCert = (X509Certificate) entry.getCertificate();
//獲取證書鏈,然後找到其中的根證書,也就是CA證書
Certificate[] certs = entry.getCertificateChain();
Log.d(TAG, "# certsextracted = " + certs.length);
mCaCerts = newArrayList<X509Certificate>(certs.length);
for (Certificate c : certs){
X509Certificate cert =(X509Certificate) c;
//在本例中,證書鏈就包含一個證書,所以mUserCert和certs[0]是同一個。
//下面這個isCa函式前面沒介紹過。它將解析X509Certificate資訊中的
//”Basic Constraints”是否設定“Certificate Authority”為“Yes”,如果是的話
//就說明它是CA證書
if (isCa(cert)) mCaCerts.add(cert);
}
return true;
}
總之,installFrom執行完後,我們得到一個PrivateKey,一個證書,和一個CA證書。當然,這兩個證書是同一個東西。
注意,JCE似乎沒有提供合適的API來判斷一個Certificate是不是CA證書。但是有一些實卻實現了這個函式。感興趣的童鞋可參考isCa的處理。
提取完pkcs12檔案後,在onExtractoinDone裡,系統又會彈出一個框,告訴你剛才那個檔案裡都包含什麼東西。如圖19所示:
圖19 證書改名對話方塊
這個對話方塊其實是讓我們修改別名。當然,圖裡還能讓你設定這個私鑰等是幹什麼用的:
- 一個是用於VPN,另外一個是用於APP。
- 圖中最後展示了這個證書包含的東西,有一個key,一個證書和一個CA證書。
(3) 別名修改對話方塊
[-->CertInstaller.java::createNameCredentialDialog]
private Dialog createNameCredentialDialog() {
ViewGroup view =(ViewGroup) View.inflate(this,
R.layout.name_credential_dialog,null);
mView.setView(view);
......
mView.setText(R.id.credential_info,
mCredentials.getDescription(this).toString());
final EditText nameInput =(EditText)
view.findViewById(R.id.credential_name);
if ......
else {
final Spinner usageSpinner= (Spinner)
view.findViewById(R.id.credential_usage);
usageSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public voidonItemSelected(AdapterView<?> parent, View view, int position,
long id) {
switch ((int) id) {
//圖19中的VPN和APP,對應就是設定使用範疇,預設是VPN+APP,也就是系統層面都可以用
case USAGE_TYPE_SYSTEM:
mCredentials.setInstallAsUid(KeyStore.UID_SELF);//UID_SELF為-1
break;
case USAGE_TYPE_WIFI:
mCredentials.setInstallAsUid(Process.WIFI_UID);
break;
......
} }});}
nameInput.setText(getDefaultName());//獲取預設alias名
nameInput.selectAll();
Dialog d = newAlertDialog.Builder(this)
.setView(view).setTitle(R.string.name_credential_dialog_title)
.setPositiveButton(android.R.string.ok, new
DialogInterface.OnClickListener(){
public voidonClick(DialogInterface dialog, int id) {
String name =mView.getText(R.id.credential_name);
if ......
else {//使用者在這裡有機會修改這個alias名。注意,一旦從檔案中提取出證書並安裝
//到系統中的話,以前那個pkcs12檔案的password和alias名都不在需要了!
mCredentials.setName(name);
try {
startActivityForResult(//安裝這些個key啊,證書
mCredentials.createSystemInstallIntent(),
REQUEST_SYSTEM_INSTALL_CODE);
} ...
}
}
})....create();
return d;
}
最終匯入到系統KeyStore的地方其實還不是在CertInstaller裡完成的,而是由Settings!怎麼啟動Settings呢?關鍵在createSystemInstallIntent中。
[-->CredentialHelper.java::createSystemInstallIntent]
Intent createSystemInstallIntent() {
Intent intent = newIntent("com.android.credentials.INSTALL");
intent.setClassName("com.android.settings",
"com.android.settings.CredentialStorage");
//mUid=-1
intent.putExtra(Credentials.EXTRA_INSTALL_AS_UID, mUid);
try {
if (mUserKey != null) {
intent.putExtra(Credentials.EXTRA_USER_PRIVATE_KEY_NAME,
Credentials.USER_PRIVATE_KEY + mName);
intent.putExtra(Credentials.EXTRA_USER_PRIVATE_KEY_DATA,
mUserKey.getEncoded());
}
if (mUserCert != null) {
intent.putExtra(Credentials.EXTRA_USER_CERTIFICATE_NAME,
Credentials.USER_CERTIFICATE + mName);
intent.putExtra(Credentials.EXTRA_USER_CERTIFICATE_DATA,
Credentials.convertToPem(mUserCert));
}
if (!mCaCerts.isEmpty()) {
intent.putExtra(Credentials.EXTRA_CA_CERTIFICATES_NAME,
Credentials.CA_CERTIFICATE + mName);
X509Certificate[]caCerts
=mCaCerts.toArray(new X509Certificate[mCaCerts.size()]);
intent.putExtra(Credentials.EXTRA_CA_CERTIFICATES_DATA,
Credentials.convertToPem(caCerts));//將所有CA Certs轉換為PEM格式儲存
}
return intent;//返回這個Intent,用於啟動Settings中的對應安裝步驟!
} ...
}
我們要裝三個東西到系統裡,一個PrivateKey,一個證書,一個CA證書。在createSystemInstallIntent函式中,會單獨為這三個東西設定一個特殊的名字:
- PrivateKey:用“Credentials.USER_PRIVATE_KEY+ mName”組合,本例中對應的名字是”USRPKEY_My KeyChain”
- Certifcate:用“Credentials.USER_CERTIFICATE+ mName”組合,本例中對應的名字是” USRCERT_My KeyChain”
- CACert:用“Credentials.CA_CERTIFICATE+mName”組合,本例中對應的名字是” CACERT_My KeyChain”。
如程式碼所示,最終的處理將交給Settings來完成。此時,我們已經測試和最開始的pkcs12檔案沒有關係了,比如開啟pkcs12檔案的密碼,預設的alias(使用者可以修改,本例中我們沒有改!)。
2.1.4 Settings處理安裝
Settings中相關處理程式碼位於CredentialStorage中,主要處理函式是handleUnlockOrInstall:
[-->CredentialStorage.java::handleUnlockOrInstall]
private void handleUnlockOrInstall() {
......
/*
mKeyStore就是傳說中的AS KeyStore了。它有幾個狀態,最開始是UNINITIALIZED,
只有我們為它設定了密碼,它才會變成LOCKED狀態。這個密碼和使用者的鎖屏介面解鎖有關係
系統會把解鎖時使用的密碼傳遞給AS Store以初始化(或者修改新密碼)。
LOCKED:AS Store被鎖了,需要彈出解鎖框來解鎖
UNLOCKED:已經解鎖,可直接安裝
*/
switch (mKeyStore.state()) {
case UNINITIALIZED: {//未初始化,將提醒使用者設定密碼。
ensureKeyGuard();
return;
}
case LOCKED: {//鎖了,請輸入解鎖密碼
new UnlockDialog();
return;
}
case UNLOCKED: {//已經解鎖
if(!checkKeyGuardQuality()) {
newConfigureKeyGuardDialog();
return;
}
installIfAvailable();
finish();
return;
}
}
}
installIfAvailable將進行安裝。有對UNINTIALIZED流程感興趣的童鞋不妨自己研究下相關程式碼。Anyway,使用者設定的密碼最終會呼叫AS KeyStore的password函式。回頭我們還要來看這其中的處理。
先來看installIfAvailable。
[-->CredentialStorage.java:: installIfAvailable]
private void installIfAvailable() {
if (mInstallBundle != null&& !mInstallBundle.isEmpty()) {
Bundle bundle =mInstallBundle;
mInstallBundle = null;
final int uid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, -1);
//私鑰匯入到AS KeyStore
if(bundle.containsKey(Credentials.EXTRA_USER_PRIVATE_KEY_NAME)) {
String key =bundle.getString(Credentials.EXTRA_USER_PRIVATE_KEY_NAME);
byte[] value = bundle.getByteArray(
Credentials.EXTRA_USER_PRIVATE_KEY_DATA);
int flags =KeyStore.FLAG_ENCRYPTED;//加密標誌
......
mKeyStore.importKey(key, value, uid, flags);
}
int flags = (uid ==Process.WIFI_UID) ? KeyStore.FLAG_NONE :
KeyStore.FLAG_ENCRYPTED;
//匯入證書
if(bundle.containsKey(Credentials.EXTRA_USER_CERTIFICATE_NAME)){
String certName =
bundle.getString(Credentials.EXTRA_USER_CERTIFICATE_NAME);
byte[] certData =bundle.getByteArray(
Credentials.EXTRA_USER_CERTIFICATE_DATA);
//證書匯入,呼叫AS KeyStore put函式
mKeyStore.put(certName, certData, uid, flags);
}
//匯入CA證書
if(bundle.containsKey(Credentials.EXTRA_CA_CERTIFICATES_NAME)){
String caListName = bundle.getString(
Credentials.EXTRA_CA_CERTIFICATES_NAME);
byte[] caListData = bundle.getByteArray(
Credentials.EXTRA_CA_CERTIFICATES_DATA);
mKeyStore.put(caListName, caListData, uid, flags);
}
setResult(RESULT_OK);
}}
由上述程式碼可知,Settings最終會呼叫ASStore的幾個函式把東西導進去:
- 對於Private Key來說,importKey將被呼叫,傳入的引數包括Key名,key資料,uid和flags。Key名是前面別名修改對話時提到的“USRPKEY_My Key Chain”。
- 對於Cert和CA Cert,呼叫的都是put函式。引數和importKey類似。
到此,Java層的流程幾乎全部介紹完畢。大家肯定還有一個疑問,ASKeyStore怎麼不講講呢?去看看程式碼就知道了,AS KeyStore就是通過binder和“android.security.keystore”服務打交道。
根據圖16可知,這個keystore服務應該在native的keystore程序裡。直接來看它吧!
2.1.5 Native keystore daemon
keystore是一個native的daemon程序,其程式碼位於system/security/keystore下。這個程序由init啟動。在init.rc中有如圖20所示的配置:
圖20 keystore啟動配置
keystore的程式碼幾乎都在keystore.cpp中,先來看它的main函式:
[-->keystore.cpp::main]
int main(int argc, char* argv[]) {
......
//修改本程序的工作目錄。由圖20可知,keystore設定的工作目錄將變成/data/misc/keystore
chdir(argv[1]);
Entropy entropy; //Entropy裝置:熵,和隨機數生成有關,能增加隨機數的隨機性
entropy.open();
//和硬體的keymaster裝置有關。如果沒有硬體實現,AOSP給了一個軟體實現
keymaster_device_t* dev;
keymaster_device_initialize(&dev);
//native層中的KeyStore,關鍵類
KeyStore keyStore(&entropy,dev);
//為系統新增一個“android.security.keystore”的服務
keyStore.initialize();
android::sp<android::IServiceManager> sm =
android::defaultServiceManager();
android::sp<android::KeyStoreProxy> proxy =
newandroid::KeyStoreProxy(&keyStore);
android::status_t ret =sm->addService(
android::String16("android.security.keystore"),proxy);
android::IPCThreadState::self()->joinThreadPool();
keymaster_device_release(dev);
return 1;
}
Native層也有一個KeyStore類,先來看它的建構函式:
(1) NativeKeyStore初始化
[-->keystore.cpp::KeyStore構造]
KeyStore(Entropy* entropy, keymaster_device_t* device)
: mEntropy(entropy)
, mDevice(device)
{
//mMetaData:一個結構體,內部只有一個int變數用來儲存版本號
memset(&mMetaData, '\0', sizeof(mMetaData));
}
再來看init函式:
[-->keystore.cpp::initialize]
ResponseCode initialize() {
//讀取工作目錄下的.metadata檔案,其中儲存的是版本號
readMetaData();
if (upgradeKeystore())
writeMetaData();//版本號寫到.metadata檔案裡
return ::NO_ERROR;
}
upgradeKeyStore用於升級Android平臺KeyStore的管理結構。這個主要是為多使用者支援的。來看圖21:
圖21 KeyStore目錄管理
圖21中,keystore目錄下有個:
- .metadata檔案用來控制版本。
- user_0資料夾:這是為了支援多使用者而設定的。初始使用者為user_0,後續新增新使用者則叫user_xxx之類的。
- user_0目錄中有個.masterkey檔案和匯入的PrivateKey,Cert和CA Cert檔案。注意,以1000_USRPKEY_My+PKey+PChain為例,它就是我們前面說的“USRPKEY_My KeyChain”非常像,只不過前面多了一個uid(這裡的uid是1000,和settings的uid是一個,也就是系統system_server的uid),然後把空格換成了“+P”。
也就是說,當我們匯入東西到AS KeyStore的時候,它會在/data/misc/keystore對應使用者(USER_X)目錄下生成類似1000_XXX_Alias的檔案。這個檔案的內容並不直接儲存的證書,PrivateKey等關鍵的二進位制資料,而是一個經過加密的二進位制陣列。這個二進位制陣列將做為類似於Tag一樣的東西,把它和實際的關鍵資料對應起來。而這些關鍵資料則是儲存到硬體的KeyMasterDevice裡的。我們接下來會看到這些檔案的生成步驟。
稍微看一下upgradeKeyStore函式:
[-->keystore.cpp::upgradeKeyStore]
bool upgradeKeystore() {
bool upgraded = false;
if (mMetaData.version ==0) {
//每一個使用者對應為程式碼中的一個UserState物件
UserState* userState = getUserState(0);
....
userState->initialize();//來看看它!
......
mMetaData.version = 1;
upgraded = true;
}
return upgraded;
}
[-->keystore.cpp:UserState:initialize]
bool initialize() {
//建立user_n目錄,我們是第一個使用者,所以n=0
mkdir(mUserDir, S_IRUSR | S_IWUSR | S_IXUSR);
//看看該目錄下有沒有.masterkey檔案,如果沒有,表明還沒有設定
if (access(mMasterKeyFile, R_OK) == 0)
setState(STATE_LOCKED); //設定KeyStore狀態,這也是前面Settings中得到的狀態
else setState(STATE_UNINITIALIZED);
return true;
}
(2) password處理
有上述程式碼可知,當沒有.masterkey檔案的時候,Native KeyStore為未初始化狀態。根據Settings裡的處理,我們只有設定了鎖屏介面時,Settings會呼叫password函式來設定密碼。對應到Native KeyStore就是它password函式。
[-->keystore.cpp::password]
int32_t password(const String16& password) {
uid_t callingUid =IPCThreadState::self()->getCallingUid();
const String8password8(password);
switch(mKeyStore->getState(callingUid)) {
case::STATE_UNINITIALIZED: {
return mKeyStore->initializeUser(password8, callingUid);
}
case ::STATE_NO_ERROR: {
return mKeyStore->writeMasterKey(password8, callingUid);
}
case ::STATE_LOCKED: {
return mKeyStore->readMasterKey(password8, callingUid);
}
}
return ::SYSTEM_ERROR;
}
initializeUser其實就是把我們設定的密碼和兩個從Entropy得到的隨機數(這些隨機數有個奇怪的稱呼,叫鹽值,salt。salt也需要儲存到.masterkey檔案裡的)搞到一起,生成一個簽名,然後再用密碼對這個簽名進行加密,最終儲存到檔案裡。
上述流程並不是很準確,偶也不想討論了。說一下解密的問題:解密並不是從.masterkey裡邊去提取password,而是讓使用者輸入密碼,然後按類似的方法得到一個簽名,最後和檔案裡的簽名去比較是否匹配!
(3) importKey處理
來看看importKey,當Settings匯入PrivateKey的時候,會呼叫它。importkey在nativeKeyStore中對應的是import函式,程式碼如下:
[-->keystore.cpp::import]
int32_t import(const String16& name, const uint8_t* data,size_t length,
int targetUid, int32_t flags) {
uid_t callingUid =IPCThreadState::self()->getCallingUid();
//許可權檢查,keystore對許可權檢查比較嚴格,只有system/vpn/wifi/rootuid的程序才
//可以操作它
if(!has_permission(callingUid, P_INSERT)) return ::PERMISSION_DENIED;
.....
State state =mKeyStore->getState(callingUid);
if ((flags &KEYSTORE_FLAG_ENCRYPTED) && !isKeystoreUnlocked(state)) {
return state;
}
//name是傳進來的,對應為“USRPKEY_My Key Chain”
String8 name8(name);
//getkeyNameForUidWithDir將把uid和空格替換上去,並加上父目錄的路徑
//最終”filename=user_0/1000_USRPKEY_My+PKey+PChain”
String8filename(mKeyStore->getKeyNameForUidWithDir(name8,targetUid));
return mKeyStore->importKey(data, length,filename.string(),
callingUid,flags);
}
來看importKey函式:
[-->keystore.cpp::importkey]
ResponseCode importKey(const uint8_t* key, size_t keyLen, constchar* filename,
uid_t uid, int32_t flags) {
uint8_t* data;
size_t dataLength;
int rc;
bool isFallback = false;
//將key資訊傳遞到keymaster device裡去,注意data這個引數,它是一個返回引數,
//是keymaster device返回的。具體是什麼,由硬體或驅動決定
rc = mDevice->import_keypair(mDevice, key, keyLen,&data, &dataLength);
//如果硬體沒這個功能,那麼就軟體實現,用得是openssl_import_keypair,其內部好像就是
//對key加了把密,然後把加密後的key資料儲存到data裡了
if (rc) {
if(mDevice->common.module->module_api_version <
KEYMASTER_MODULE_API_VERSION_0_2){
rc = openssl_import_keypair(mDevice, key,keyLen, &data, &dataLength);
isFallback = true;
}
//把import_keypair返回的data資訊放到一個Blob中
Blob keyBlob(data,dataLength, NULL, 0, TYPE_KEY_PAIR);
free(data);
keyBlob.setEncrypted(flags& KEYSTORE_FLAG_ENCRYPTED);
keyBlob.setFallback(isFallback);
//再把這個blob資訊寫到對應的檔案裡...
return put(filename, &keyBlob, uid);
}
put函式就是把data什麼資訊都往對應的檔案裡寫...讓人解脫的是,cert和CA cert呼叫的都是put函式,所以我們這也不用再單獨介紹put了....
2.1.6 小結
從這一大節的流程來看,匯入證書檔案其實是一件很麻煩的事情,涉及到好幾個程序,比如CertInstaller,Settings,native的KeyStore等。
無論如何,我們還是把資訊都寫到檔案裡了。這裡要特別指出:
- Android平臺中,證書等敏感資訊都儲存到KeyMaster Device裡了,也就是前面提到的SEE中。
- nativekeystore會在對應目錄下放幾個檔案,這幾個檔案儲存的是二進位制內容。而這些二進位制內容並不是敏感資訊,而是由敏感資訊通過一些轉換(比如加密)得到的東西。這也是所謂的KEK(Key Encryption Key,也就是給金鑰加密的金鑰)吧?
2.2 通過KeyChain獲取PrivateKey
資訊都匯入到系統裡去了,那是不是用JCE標準介面就能用呢?不是,至少我測試了不是。為什麼?先想想下面這些個問題:
- 誰都可以往系統裡導證書資訊,並設定alias。但是,其他程式是否都有許可權使用它們?
顯然不是。Android系統裡,要使用某個alias的證書,系統會彈出一個提示框以提醒使用者,這個提示框如圖22所示:
圖22 證書選擇對話方塊
圖22中:
- 首先會列出一些證書(當然,我們這裡只匯入了一個證書檔案,所以只有“MyKey Chain”)。
- 也可以選擇從sdcard中安裝.pfx或.p12檔案。這需要點選圖中的“install“按鈕。
- 剩下的就是選擇是否允許當前app使用所選別名的證書資訊了。
所以,一個app要使用系統中的某個證書(這裡是指privatekey和非CA的證書)資訊時,必須要先呼叫KeyChain的choosePrivateKeyAlias函式。我們的故事就從這裡開始:
2.2.1 choosePrivateKeyAlias介紹
先來看怎麼用它,如DemoActivity中的onActivityResult所示:
[-->DemoActivity.java::onActivityResult]
protected void onActivityResult(int requestCode, int resultCode,Intent data) {
KeyChain.choosePrivateKeyAlias(this,
//第二個引數是一個回撥物件,當用戶選擇了