深入理解Android之Java Security第二部分(Final)

版权声明:From 阿拉神农 https://blog.csdn.net/Innost/article/details/44199503

深入理解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文件的passwordalias名都不在需要了

       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,

    //第二个参数是一个回调对象,当用户选择了哪一个alias的时候,会通过这个回调对象传回来

    new KeyChainAliasCallback() {

      @Override

      public voidalias(String alias) {

             ......

        }

        });

     }

    },

     new String[] {},//设置要使用的Key Type,比如RSA或DSA,本例不设置这个

       null, //设置证书的issuer,即指定目标证书的发行者,一般也不设置。除非事先约定好

     "localhost", //好像和SSLServer有关,用于告诉用户想用在什么ip地址或端口号上

     -1,

     ENTRY_ALIAS);//ENTRY_ALIAS的值是“My Key Chain“

    super.onActivityResult(requestCode,resultCode, data);

  }

直接来看KeyChain的实现代码:

[-->KeyChain::choosePrivateKeyAlias]

  public static voidchoosePrivateKeyAlias(Activity activity,

           KeyChainAliasCallback response,

           String[] keyTypes, Principal[] issuers,

           String host, intport,String alias) {

        ......

        //Action的值为“com.android.keychain.CHOOSER“

        Intent intent = newIntent(ACTION_CHOOSER);

       intent.putExtra(EXTRA_RESPONSE, new AliasResponse(response));

       intent.putExtra(EXTRA_HOST, host);

       intent.putExtra(EXTRA_PORT, port);

       intent.putExtra(EXTRA_ALIAS, alias);

       intent.putExtra(EXTRA_SENDER, PendingIntent.getActivity(activity, 0,

                         new Intent(), 0));

       activity.startActivity(intent);

}

“com.android.keychain.CHOOSER“这个Intent将好多年来一直默默无名但是又重要无比的keychain这个apk来响应。

(1)  KeyChainActivity

直接来看图22中那个框是咋处理的吧。

[-->KeyChainActivity.java:: showCertChooserDialog]

private void showCertChooserDialog() {

     new AliasLoader().execute();

}

[-->KeyChainActivity.java::AliasLoader]

private class AliasLoader extendsAsyncTask<Void, Void, CertificateAdapter> {

  @Override protectedCertificateAdapter doInBackground(Void...params) {

  //借助Binder和Native KeyStore交互,获取alias列表

    String[] aliasArray = mKeyStore.saw(Credentials.USER_PRIVATE_KEY);

    List<String> aliasList = ((aliasArray== null)

               ?Collections.<String>emptyList(): Arrays.asList(aliasArray));

     Collections.sort(aliasList);

     return new CertificateAdapter(aliasList);

 }

 @Override protected voidonPostExecute(CertificateAdapter adapter) {

   displayCertChooserDialog(adapter);//显示图22所示的对话框!

   }

 }

[-->KeyChainActivity.java::displayCertChooserDialog]

private voiddisplayCertChooserDialog(final CertificateAdapter adapter) {

   AlertDialog.Builder builder = new AlertDialog.Builder(this);

   ......

   if......

   else {

    .....

     builder.setPositiveButton(R.string.allow_button,

                   new DialogInterface.OnClickListener(){

       @Override public void onClick(DialogInterface dialog, int id) {

         int listViewPosition = lv.getCheckedItemPosition();

         int adapterPosition = listViewPosition-1;

         String alias = ((adapterPosition >= 0)

                  ?adapter.getItem(adapterPosition) : null);

         finish(alias);

       }

     });

   }

   ...

    final Dialog dialog =builder.create();

    ......

}

关键函数是这个finish,注意它是带参数的,非常容易和Activity的finish()混淆!

[-->KeyChainActivity.java::finish]

private void finish(String alias) {

    ......

   IKeyChainAliasCallback keyChainAliasResponse

       = IKeyChainAliasCallback.Stub.asInterface(

           getIntent().getIBinderExtra(KeyChain.EXTRA_RESPONSE));

   if (keyChainAliasResponse != null) {

     new ResponseSender(keyChainAliasResponse,alias).execute();

     return;

   }

   finish();

 }

[-->KeyChainActivity.java:ResponseSender]

private class ResponseSender extends AsyncTask<Void,Void, Void> {

   private IKeyChainAliasCallback mKeyChainAliasResponse;

    private String mAlias;

   .......

    @Override protected VoiddoInBackground(Void... unused) {

     try {

       if (mAlias != null) {

         KeyChain.KeyChainConnection connection =

                       KeyChain.bind(KeyChainActivity.this);

          try {

           //这里的Service不是Native的KeyStore,而是keychain里的KeyChainService

            connection.getService().setGrant(mSenderUid,mAlias, true);

         }  ......

       }

       mKeyChainAliasResponse.alias(mAlias);//调用我们的回调函数

(2) KeyChainService

KeyChainService也在keychain.apk中,这玩意开机就会启动,因为keychain有一个BroadcastReceiver会接收BOOT_COMPLETE消息,这个BR然后启动KeyChainService。

KeyChainService功能超级简单,就是管理了一个名叫grants的数据库。这个数据库为每一个alias和调用choosePrivateKeyAlias的进程的uid维护了一个关系,也就是所谓的权限管理。即只有在这个数据库里某个alias有对应的uid时,那个uid所在进程才能访问这个alias所代表的证书信息。

图23所示为该数据库的示例:


图23  grants.db示例

(3) getPrivateKey

设置好权限后,下一步就是从对应alias中获取PrivateKey或者是证书信息了。我们这里仅以PrivateKey为例。相应的函数是KeyChain的getPrivateKey。

[-->KeyChain.java::getPrivateKey]

public static PrivateKeygetPrivateKey(Context context, String alias)

            throws KeyChainException,InterruptedException {

  ......

  KeyChainConnectionkeyChainConnection = bind(context);

  try{

      final IKeyChainServicekeyChainService =

                 keyChainConnection.getService();

      //从KeyChainService那获得一个id,然后把这个id传到OpenSSL相关API里

     final String keyId = keyChainService.requestPrivateKey(alias);

      .....

    final OpenSSLEngine engine =OpenSSLEngine.getInstance("keystore");

    return engine.getPrivateKeyById(keyId);

  } ......

 }

由前面代码可知,PrivateKey信息之前是导入到硬件里去的,留下来的在/data/misc/keystore/user_0文件夹下是一些保存了和PrivateKey有关系的数据(想从这些数据里还原PrivateKey显然是不可能的,它有点像MD5码,和硬件里的PrivateKey有着一一对应的关系)。那么,getPrivateKey函数会把硬件里的信息弄出来吗?

显然,如果弄出来的话,安全性就没有了。所以,在Android平台中,PrivateKey永远都是保存在硬件里的,外面拿到的都是一个标志,也就是上面代码里的KeyId。那JCE怎么用这个keyid呢?

  • 前面讲了。JCE只是一个框架,具体的工作是由不同引擎来完成的。在Android上,Key相关引擎由OpenSSLEngine构造并经过google修改,其中的很多函数都会和native 的keystore交互。比如加密解决也是把数据传递到硬件来完成的,因为要严格恪守PrivateKey不外传的原则!

JCE一些接口以及Android上的OpenSSLEngine等引擎比较复杂。这里也不会一一涉及,读者有个大概了解就可以了。

先来看KeyChainService的requestPrivateKey函数:

[-->KeyChainService.java::requestPrivateKey]

public String requestPrivateKey(Stringalias) {

    //检查数据库里是不是给调用进程设置了权限

   checkArgs(alias);

   final String keystoreAlias =Credentials.USER_PRIVATE_KEY + alias;

    final int uid =Binder.getCallingUid();

   //Native KeyStore也要维护一个类似的uid-alias权限。这里要设置这个权限

   if (!mKeyStore.grant(keystoreAlias, uid)) {

         return null;

   }

   //构造keyid很简单,就是返回”1000_USRPKEY_My Key Chain”

   final StringBuilder sb = new StringBuilder();

   sb.append(Process.SYSTEM_UID);

   sb.append('_');

   sb.append(keystoreAlias);

 

   return sb.toString();

 }

简直太简单了,keyid原来就是用system的uid加上对应的alias。但是,还得为它设置权限,否则你有keyid,Native keystore也无法让你访问。

好了。到此时,我们已经成功拿到了一个PrivateKey。这个privateKey是一个接口,其具体实现应该是OpenSSLRSAPrivateKey(假设我们这个PKey是RSA类似的)。

接着,我们如果继续把玩这个PKey,比如想调用它的getEncoded获取二进制表达式,很可悲得的是它会null。是的,返回null呢!!这个....从情理上似乎说得过去,因为我们不能让私有信息外流。但从法理上我们又不爽,因为我们前面的实例中,一直是可以取出二进制信息的,到底是什么东西导致信息导入到系统后,反而取不了二进制信息了呢?

这部分内容比较繁琐,我这里简单和各位一起把代码撸一遍好了!

先从OpenSSLEngine的getInstance看起。

2.2.2  为什么PrivateKey.getEncoded返回null?

由于KeyChain getPrivateKey创建的是OpenSSLEngine,所以先来看它:

[-->OpenSSLEngine.java::getInstance]

public static OpenSSLEngine getInstance(String engine)

            throws IllegalArgumentException {

  final long engineCtx;

   synchronized(mLoadingLock) {

    //engine的值是“keystore“,这里的意思好像是从Engine库里去找对应的实现

    engineCtx = NativeCrypto.ENGINE_by_id(engine);

    ......

     NativeCrypto.ENGINE_add(engineCtx);

   }

   //返回一个OpenSSLEngine对象

   return new OpenSSLEngine(engineCtx);

  }

打破脑壳你也想不到谁注册了”keystore”到NativeCrypto来。答案在keystore-engine里,这是一个动态库,代码在system/security/keystore-engine中。来看一小段:

[-->eng_keystore.cpp:: keystore_engine_setup]

static int keystore_engine_setup(ENGINE*e) {

   ALOGV("keystore_engine_setup");

    //这个kKeystoreEngineId是一个字符串,值就是”keystore”

   //设置一些函数指针,注意红色的地方

   if (!ENGINE_set_id(e, kKeystoreEngineId)

            || !ENGINE_set_name(e, kKeystoreEngineDesc)

           ||!ENGINE_set_load_privkey_function(e, keystore_loadkey)

            ||!ENGINE_set_load_pubkey_function(e, keystore_loadkey)

            || !ENGINE_set_flags(e, 0)

            || !ENGINE_set_cmd_defns(e,keystore_cmd_defns)) {

       ALOGE("Could not set up keystore engine");

       return 0;

   }

   pthread_once(&key_handle_control, init_key_handle);

     //注册DSA,RSA和ECDSA算法所使用的函数,这也是使用系统证书信息的限制,它只支持这三种

    //算法

   if (!dsa_register(e)) {....

    } else if (!ecdsa_register(e)) {....

   } else if (!rsa_register(e)) {....

   }

   return 1;

}

然后KeyChain getPrivateKey会调用OpenSSLEngine的getPrivateKeyById:

[-->OpenSSLEngine.java::getPrivateKeyById]

public PrivateKeygetPrivateKeyById(String id) throws InvalidKeyException {

    ......

     //传递进来的id是“1000_USRPKEY_My Key Chain“

     //根据上面的代码,这里的ENGINE_load_private_key应该会调用keystore_engine的

    //keystore_loadkey函数

   final long keyRef = NativeCrypto.ENGINE_load_private_key(ctx, id);

   OpenSSLKey pkey = new OpenSSLKey(keyRef,this, id);

   try {

     return pkey.getPrivateKey();

   } ......

 }

注意这两段代码红色的地方。Java层对应的ENGINE_load_private_key最终会走到keystore-engine的keystore_loadkey函数里。代码如下所示:

[-->eng_keystore.cpp::keystore_loadkey]

static EVP_PKEY* keystore_loadkey(ENGINE* e, const char* key_id,

           UI_METHOD* ui_method,void*callback_data) {

   //说得不错吧,它果然要和native keystore通过binder交互

 sp<IServiceManager> sm = defaultServiceManager();

 sp<IBinder> binder =sm->getService(String16("android.security.keystore"));

 sp<IKeystoreService> service =interface_cast<IKeystoreService>(binder);

 uint8_t *pubkey = NULL;

  size_t pubkeyLen;

  int32_tret = service->get_pubkey(String16(key_id),&pubkey, &pubkeyLen);

 ......

   const unsigned char* tmp = reinterpret_cast<const unsignedchar*>(pubkey);

   Unique_EVP_PKEY pkey(d2i_PUBKEY(NULL, &tmp, pubkeyLen));

 

   switch (EVP_PKEY_type(pkey->type)) {

     ......

   case EVP_PKEY_RSA:{//这里的rsa_pkey_setup还会和native keystore交互,此处略过!

       rsa_pkey_setup(e, pkey.get(),key_id);

       break;

   }

    .....

   return pkey.release();

}

你看,果然我们的DemoActivity应用会和native keystore进程交互。而且很明显,我们调用的是它的get_pubkey函数,即使我们这样想用privatekey信息。真是步步为营啊,绝不泄露PrivateKey信息。最后,我们的PrivateKey将从OpenSSLKey的getPrivateKey得到:

[-->OpenSSLKey.java::getPrivateKey]

public PrivateKey getPrivateKey() throwsNoSuchAlgorithmException {

       switch (NativeCrypto.EVP_PKEY_type(ctx)) {

            case NativeCrypto.EVP_PKEY_RSA:

                return new OpenSSLRSAPrivateKey(this);

            case NativeCrypto.EVP_PKEY_DSA:

                return newOpenSSLDSAPrivateKey(this);

            case NativeCrypto.EVP_PKEY_EC:

                return newOpenSSLECPrivateKey(this);

            default:

                throw newNoSuchAlgorithmException("unknown PKEY type");

       }

    }

好了,当我们对OpenSSLRSAPrivateKey调用getEncoded的时候,会发生什么问题呢?

[-->OpenSSLRSAPrivateKey.java::getEncoded]

public final byte[] getEncoded() {

 if (key.isEngineBased()) {//我们是有Engine支持的,所以返回空

     return null;

   }

  return NativeCrypto.i2d_PKCS8_PRIV_KEY_INFO(key.getPkeyContext());

}

 

三  需要继续走的路

到此,本文基本就结束了。在这篇文章里:

  • 我们首先对JCE的基础知识进行了一些介绍。这些知识,是理解Android平台中Java Security的重要一部分,属于最基本的知识,需要大家理解。
  • 然后我们用代码对Android平台上特有的KeyChain,KeyStore,CertInstaller介绍了一番。这个...需要各位结合代码撸几把,掌握大概意思就好。

那么,剩下还有什么呢?

  • 有没有谁把SSLServerFactory和我们导入系统的KeyStore联合起来?我内心觉得这事情应该是能办成的,但是我确实没找到对应的接口函数。
  • 另外,实际上,每一个应用程序也可以借助JCE,并指明使用“AndroidKeyStore”。在这种情况下,它只能自己先把信息导进去,然后也只能和它同一个uid的进程才能用这些信息。(其实我们刚才是把信息通过uid=1000的settings导入到系统的,这个是系统全局的证书信息。应用程序可以往native keystore中导入自己uid的信息。)
  • 另外,TrustStore的问题,大家也跟着代码看看吧

...

对上述这些感兴趣的童鞋,只能请你们自己看代码玩耍了....

参考文献

Java Security

[1]  Java Security, 2nd Edition:http://shop.oreilly.com/product/9780596001575.do  中文名为《Java安全第二版》,此书是关于Java Security最好的参考书。

http://docs.oracle.com/javase/6/docs/technotes/guides/security/crypto/CryptoSpec.html

关于证书和证书文件格式

[2] http://www.360doc.com/content/13/0417/10/11791971_278827661.shtml

[3] http://www.blogjava.net/lihao336/archive/2011/08/18/356763.html

[4] http://en.wikipedia.org/wiki/PKCS

[5] http://en.wikipedia.org/wiki/X.509

X.509和PKCS介绍

[6]  http://bbs.csdn.net/topics/190044123

关于X.509和PCKS规范之间的关系的讨论

Key管理

[7]  《Java加密与解密的艺术》,作者梁栋,国人关于JavaSecurity的一本好书。

[8]  http://developer.android.com/training/articles/keystore.html

[9]   http://en.wikipedia.org/wiki/Public-key_cryptography

JSSE

[10]  http://en.wikipedia.org/wiki/Transport_Layer_Security

TLS和SSL的历史。

[11] https://developer.android.com/training/articles/security-ssl.html

Android开发文档中关于SSL方面的知识。

Cipher资料

[12]  http://www.javamex.com/tutorials/cryptography/index.shtml

 

SEE和TrustZone

[13] http://research.microsoft.com/en-us/um/people/alecw/asplos-2014.pdf

[14] http://www.ti.com.cn/cn/lit/wp/spry228/spry228.pdf

TrustZone资料

[15] http://cache.freescale.com/files/32bit/doc/white_paper/QORIQSECBOOTWP.pdf

安全系统的资料

Android Security Internals:

[16]  Android Security Internals

美国亚马逊上有电子版,可在浏览器上看。很好的一本系统讲述Android安全方面的书籍。
阅读更多
换一批

没有更多推荐了,返回首页