Home | History | Annotate | Download | only in certinstaller
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.certinstaller;
     18 
     19 import android.app.Activity;
     20 import android.app.AlertDialog;
     21 import android.app.Dialog;
     22 import android.app.KeyguardManager;
     23 import android.app.ProgressDialog;
     24 import android.content.ActivityNotFoundException;
     25 import android.content.Context;
     26 import android.content.DialogInterface;
     27 import android.content.Intent;
     28 import android.os.AsyncTask;
     29 import android.os.Bundle;
     30 import android.os.Process;
     31 import android.security.Credentials;
     32 import android.security.KeyChain;
     33 import android.security.KeyChain.KeyChainConnection;
     34 import android.security.KeyStore;
     35 import android.text.TextUtils;
     36 import android.util.Log;
     37 import android.view.View;
     38 import android.view.ViewGroup;
     39 import android.widget.AdapterView;
     40 import android.widget.AdapterView.OnItemSelectedListener;
     41 import android.widget.EditText;
     42 import android.widget.Spinner;
     43 import android.widget.Toast;
     44 
     45 import java.io.Serializable;
     46 import java.security.cert.X509Certificate;
     47 import java.util.LinkedHashMap;
     48 import java.util.Map;
     49 
     50 /**
     51  * Installs certificates to the system keystore.
     52  */
     53 public class CertInstaller extends Activity {
     54     private static final String TAG = "CertInstaller";
     55 
     56     private static final int STATE_INIT = 1;
     57     private static final int STATE_RUNNING = 2;
     58     private static final int STATE_PAUSED = 3;
     59 
     60     private static final int NAME_CREDENTIAL_DIALOG = 1;
     61     private static final int PKCS12_PASSWORD_DIALOG = 2;
     62     private static final int PROGRESS_BAR_DIALOG = 3;
     63 
     64     private static final int REQUEST_SYSTEM_INSTALL_CODE = 1;
     65     private static final int REQUEST_CONFIRM_CREDENTIALS = 2;
     66 
     67     // key to states Bundle
     68     private static final String NEXT_ACTION_KEY = "na";
     69 
     70     // key to KeyStore
     71     private static final String PKEY_MAP_KEY = "PKEY_MAP";
     72 
     73     // Values for usage type spinner
     74     private static final int USAGE_TYPE_SYSTEM = 0;
     75     private static final int USAGE_TYPE_WIFI = 1;
     76 
     77     private final KeyStore mKeyStore = KeyStore.getInstance();
     78     private final ViewHelper mView = new ViewHelper();
     79 
     80     private int mState;
     81     private CredentialHelper mCredentials;
     82     private MyAction mNextAction;
     83 
     84     private CredentialHelper createCredentialHelper(Intent intent) {
     85         try {
     86             return new CredentialHelper(intent);
     87         } catch (Throwable t) {
     88             Log.w(TAG, "createCredentialHelper", t);
     89             toastErrorAndFinish(R.string.invalid_cert);
     90             return new CredentialHelper();
     91         }
     92     }
     93 
     94     @Override
     95     protected void onCreate(Bundle savedStates) {
     96         super.onCreate(savedStates);
     97 
     98         mCredentials = createCredentialHelper(getIntent());
     99 
    100         mState = (savedStates == null) ? STATE_INIT : STATE_RUNNING;
    101 
    102         if (mState == STATE_INIT) {
    103             if (!mCredentials.containsAnyRawData()) {
    104                 toastErrorAndFinish(R.string.no_cert_to_saved);
    105                 finish();
    106             } else {
    107                 if (mCredentials.hasCaCerts()) {
    108                     KeyguardManager keyguardManager = getSystemService(KeyguardManager.class);
    109                     Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(null, null);
    110                     if (intent == null) { // No screenlock
    111                         onScreenlockOk();
    112                     } else {
    113                         startActivityForResult(intent, REQUEST_CONFIRM_CREDENTIALS);
    114                     }
    115                 } else {
    116                     onScreenlockOk();
    117                 }
    118             }
    119         } else {
    120             mCredentials.onRestoreStates(savedStates);
    121             mNextAction = (MyAction)
    122                     savedStates.getSerializable(NEXT_ACTION_KEY);
    123         }
    124     }
    125 
    126     @Override
    127     protected void onResume() {
    128         super.onResume();
    129 
    130         if (mState == STATE_INIT) {
    131             mState = STATE_RUNNING;
    132         } else {
    133             if (mNextAction != null) {
    134                 mNextAction.run(this);
    135             }
    136         }
    137     }
    138 
    139     private boolean needsKeyStoreAccess() {
    140         return ((mCredentials.hasKeyPair() || mCredentials.hasUserCertificate())
    141                 && !mKeyStore.isUnlocked());
    142     }
    143 
    144     @Override
    145     protected void onPause() {
    146         super.onPause();
    147         mState = STATE_PAUSED;
    148     }
    149 
    150     @Override
    151     protected void onSaveInstanceState(Bundle outStates) {
    152         super.onSaveInstanceState(outStates);
    153         mCredentials.onSaveStates(outStates);
    154         if (mNextAction != null) {
    155             outStates.putSerializable(NEXT_ACTION_KEY, mNextAction);
    156         }
    157     }
    158 
    159     @Override
    160     protected Dialog onCreateDialog (int dialogId) {
    161         switch (dialogId) {
    162             case PKCS12_PASSWORD_DIALOG:
    163                 return createPkcs12PasswordDialog();
    164 
    165             case NAME_CREDENTIAL_DIALOG:
    166                 return createNameCredentialDialog();
    167 
    168             case PROGRESS_BAR_DIALOG:
    169                 ProgressDialog dialog = new ProgressDialog(this);
    170                 dialog.setMessage(getString(R.string.extracting_pkcs12));
    171                 dialog.setIndeterminate(true);
    172                 dialog.setCancelable(false);
    173                 return dialog;
    174 
    175             default:
    176                 return null;
    177         }
    178     }
    179 
    180     @Override
    181     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    182         if (requestCode == REQUEST_SYSTEM_INSTALL_CODE) {
    183             if (resultCode == RESULT_OK) {
    184                 Log.d(TAG, "credential is added: " + mCredentials.getName());
    185                 Toast.makeText(this, getString(R.string.cert_is_added, mCredentials.getName()),
    186                         Toast.LENGTH_LONG).show();
    187 
    188                 if (mCredentials.includesVpnAndAppsTrustAnchors()) {
    189                     // more work to do, don't finish just yet
    190                     new InstallVpnAndAppsTrustAnchorsTask().execute();
    191                     return;
    192                 }
    193                 setResult(RESULT_OK);
    194             } else {
    195                 Log.d(TAG, "credential not saved, err: " + resultCode);
    196                 toastErrorAndFinish(R.string.cert_not_saved);
    197             }
    198         } else if (requestCode == REQUEST_CONFIRM_CREDENTIALS) {
    199             if (resultCode == RESULT_OK) {
    200                 onScreenlockOk();
    201                 return;
    202             }
    203             // Fail to confirm credentials. Let it finish
    204         } else {
    205             Log.w(TAG, "unknown request code: " + requestCode);
    206         }
    207         finish();
    208     }
    209 
    210     private void onScreenlockOk() {
    211         if (mCredentials.hasPkcs12KeyStore()) {
    212             if (mCredentials.hasPassword()) {
    213                 showDialog(PKCS12_PASSWORD_DIALOG);
    214             } else {
    215                 new Pkcs12ExtractAction("").run(this);
    216             }
    217         } else {
    218             MyAction action = new InstallOthersAction();
    219             if (needsKeyStoreAccess()) {
    220                 sendUnlockKeyStoreIntent();
    221                 mNextAction = action;
    222             } else {
    223                 action.run(this);
    224             }
    225         }
    226     }
    227 
    228     private class InstallVpnAndAppsTrustAnchorsTask extends AsyncTask<Void, Void, Boolean> {
    229 
    230         @Override protected Boolean doInBackground(Void... unused) {
    231             try {
    232                 KeyChainConnection keyChainConnection = KeyChain.bind(CertInstaller.this);
    233                 try {
    234                     return mCredentials.installVpnAndAppsTrustAnchors(CertInstaller.this,
    235                             keyChainConnection.getService());
    236                 } finally {
    237                     keyChainConnection.close();
    238                 }
    239             } catch (InterruptedException e) {
    240                 Thread.currentThread().interrupt();
    241                 return false;
    242             }
    243         }
    244 
    245         @Override protected void onPostExecute(Boolean success) {
    246             if (success) {
    247                 setResult(RESULT_OK);
    248             }
    249             finish();
    250         }
    251     }
    252 
    253     void installOthers() {
    254         if (mCredentials.hasKeyPair()) {
    255             saveKeyPair();
    256             finish();
    257         } else {
    258             X509Certificate cert = mCredentials.getUserCertificate();
    259             if (cert != null) {
    260                 // find matched private key
    261                 String key = Util.toMd5(cert.getPublicKey().getEncoded());
    262                 Map<String, byte[]> map = getPkeyMap();
    263                 byte[] privatekey = map.get(key);
    264                 if (privatekey != null) {
    265                     Log.d(TAG, "found matched key: " + privatekey);
    266                     map.remove(key);
    267                     savePkeyMap(map);
    268 
    269                     mCredentials.setPrivateKey(privatekey);
    270                 } else {
    271                     Log.d(TAG, "didn't find matched private key: " + key);
    272                 }
    273             }
    274             nameCredential();
    275         }
    276     }
    277 
    278     private void sendUnlockKeyStoreIntent() {
    279         Credentials.getInstance().unlock(this);
    280     }
    281 
    282     private void nameCredential() {
    283         if (!mCredentials.hasAnyForSystemInstall()) {
    284             toastErrorAndFinish(R.string.no_cert_to_saved);
    285         } else {
    286             showDialog(NAME_CREDENTIAL_DIALOG);
    287         }
    288     }
    289 
    290     private void saveKeyPair() {
    291         byte[] privatekey = mCredentials.getData(Credentials.EXTRA_PRIVATE_KEY);
    292         String key = Util.toMd5(mCredentials.getData(Credentials.EXTRA_PUBLIC_KEY));
    293         Map<String, byte[]> map = getPkeyMap();
    294         map.put(key, privatekey);
    295         savePkeyMap(map);
    296         Log.d(TAG, "save privatekey: " + key + " --> #keys:" + map.size());
    297     }
    298 
    299     private void savePkeyMap(Map<String, byte[]> map) {
    300         if (map.isEmpty()) {
    301             if (!mKeyStore.delete(PKEY_MAP_KEY)) {
    302                 Log.w(TAG, "savePkeyMap(): failed to delete pkey map");
    303             }
    304             return;
    305         }
    306         byte[] bytes = Util.toBytes(map);
    307         if (!mKeyStore.put(PKEY_MAP_KEY, bytes, KeyStore.UID_SELF, KeyStore.FLAG_ENCRYPTED)) {
    308             Log.w(TAG, "savePkeyMap(): failed to write pkey map");
    309         }
    310     }
    311 
    312     private Map<String, byte[]> getPkeyMap() {
    313         byte[] bytes = mKeyStore.get(PKEY_MAP_KEY);
    314         if (bytes != null) {
    315             Map<String, byte[]> map =
    316                     (Map<String, byte[]>) Util.fromBytes(bytes);
    317             if (map != null) return map;
    318         }
    319         return new MyMap();
    320     }
    321 
    322     void extractPkcs12InBackground(final String password) {
    323         // show progress bar and extract certs in a background thread
    324         showDialog(PROGRESS_BAR_DIALOG);
    325 
    326         new AsyncTask<Void,Void,Boolean>() {
    327             @Override protected Boolean doInBackground(Void... unused) {
    328                 return mCredentials.extractPkcs12(password);
    329             }
    330             @Override protected void onPostExecute(Boolean success) {
    331                 MyAction action = new OnExtractionDoneAction(success);
    332                 if (mState == STATE_PAUSED) {
    333                     // activity is paused; run it in next onResume()
    334                     mNextAction = action;
    335                 } else {
    336                     action.run(CertInstaller.this);
    337                 }
    338             }
    339         }.execute();
    340     }
    341 
    342     void onExtractionDone(boolean success) {
    343         mNextAction = null;
    344         removeDialog(PROGRESS_BAR_DIALOG);
    345         if (success) {
    346             removeDialog(PKCS12_PASSWORD_DIALOG);
    347             nameCredential();
    348         } else {
    349             showDialog(PKCS12_PASSWORD_DIALOG);
    350             mView.setText(R.id.credential_password, "");
    351             mView.showError(R.string.password_error);
    352         }
    353     }
    354 
    355     private Dialog createPkcs12PasswordDialog() {
    356         View view = View.inflate(this, R.layout.password_dialog, null);
    357         mView.setView(view);
    358         if (mView.getHasEmptyError()) {
    359             mView.showError(R.string.password_empty_error);
    360             mView.setHasEmptyError(false);
    361         }
    362 
    363         String title = mCredentials.getName();
    364         title = TextUtils.isEmpty(title)
    365                 ? getString(R.string.pkcs12_password_dialog_title)
    366                 : getString(R.string.pkcs12_file_password_dialog_title, title);
    367         Dialog d = new AlertDialog.Builder(this)
    368                 .setView(view)
    369                 .setTitle(title)
    370                 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
    371                     public void onClick(DialogInterface dialog, int id) {
    372                         String password = mView.getText(R.id.credential_password);
    373                         mNextAction = new Pkcs12ExtractAction(password);
    374                         mNextAction.run(CertInstaller.this);
    375                      }
    376                 })
    377                 .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
    378                     public void onClick(DialogInterface dialog, int id) {
    379                         toastErrorAndFinish(R.string.cert_not_saved);
    380                     }
    381                 })
    382                 .create();
    383         d.setOnCancelListener(new DialogInterface.OnCancelListener() {
    384             @Override public void onCancel(DialogInterface dialog) {
    385                 toastErrorAndFinish(R.string.cert_not_saved);
    386             }
    387         });
    388         return d;
    389     }
    390 
    391     private Dialog createNameCredentialDialog() {
    392         ViewGroup view = (ViewGroup) View.inflate(this, R.layout.name_credential_dialog, null);
    393         mView.setView(view);
    394         if (mView.getHasEmptyError()) {
    395             mView.showError(R.string.name_empty_error);
    396             mView.setHasEmptyError(false);
    397         }
    398         mView.setText(R.id.credential_info, mCredentials.getDescription(this).toString());
    399         final EditText nameInput = (EditText) view.findViewById(R.id.credential_name);
    400         if (mCredentials.isInstallAsUidSet()) {
    401             view.findViewById(R.id.credential_usage_group).setVisibility(View.GONE);
    402         } else {
    403             final Spinner usageSpinner = (Spinner) view.findViewById(R.id.credential_usage);
    404             final View ca_capabilities_warning = view.findViewById(R.id.credential_capabilities_warning);
    405 
    406             usageSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
    407                 @Override
    408                 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
    409                     switch ((int) id) {
    410                         case USAGE_TYPE_SYSTEM:
    411                             ca_capabilities_warning.setVisibility(
    412                                     mCredentials.includesVpnAndAppsTrustAnchors() ?
    413                                     View.VISIBLE : View.GONE);
    414                             mCredentials.setInstallAsUid(KeyStore.UID_SELF);
    415                             break;
    416                         case USAGE_TYPE_WIFI:
    417                             ca_capabilities_warning.setVisibility(View.GONE);
    418                             mCredentials.setInstallAsUid(Process.WIFI_UID);
    419                             break;
    420                         default:
    421                             Log.w(TAG, "Unknown selection for scope: " + id);
    422                     }
    423                 }
    424 
    425                 @Override
    426                 public void onNothingSelected(AdapterView<?> parent) {
    427                 }
    428             });
    429         }
    430         nameInput.setText(getDefaultName());
    431         nameInput.selectAll();
    432         final Context appContext = getApplicationContext();
    433         Dialog d = new AlertDialog.Builder(this)
    434                 .setView(view)
    435                 .setTitle(R.string.name_credential_dialog_title)
    436                 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
    437                     public void onClick(DialogInterface dialog, int id) {
    438                         String name = mView.getText(R.id.credential_name);
    439                         if (TextUtils.isEmpty(name)) {
    440                             mView.setHasEmptyError(true);
    441                             removeDialog(NAME_CREDENTIAL_DIALOG);
    442                             showDialog(NAME_CREDENTIAL_DIALOG);
    443                         } else {
    444                             removeDialog(NAME_CREDENTIAL_DIALOG);
    445                             mCredentials.setName(name);
    446 
    447                             // install everything to system keystore
    448                             try {
    449                                 startActivityForResult(
    450                                         mCredentials.createSystemInstallIntent(appContext),
    451                                         REQUEST_SYSTEM_INSTALL_CODE);
    452                             } catch (ActivityNotFoundException e) {
    453                                 Log.w(TAG, "systemInstall(): " + e);
    454                                 toastErrorAndFinish(R.string.cert_not_saved);
    455                             }
    456                         }
    457                     }
    458                 })
    459                 .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
    460                     public void onClick(DialogInterface dialog, int id) {
    461                         toastErrorAndFinish(R.string.cert_not_saved);
    462                     }
    463                 })
    464                 .create();
    465         d.setOnCancelListener(new DialogInterface.OnCancelListener() {
    466             @Override public void onCancel(DialogInterface dialog) {
    467                 toastErrorAndFinish(R.string.cert_not_saved);
    468             }
    469         });
    470         return d;
    471     }
    472 
    473     private String getDefaultName() {
    474         String name = mCredentials.getName();
    475         if (TextUtils.isEmpty(name)) {
    476             return null;
    477         } else {
    478             // remove the extension from the file name
    479             int index = name.lastIndexOf(".");
    480             if (index > 0) name = name.substring(0, index);
    481             return name;
    482         }
    483     }
    484 
    485     private void toastErrorAndFinish(int msgId) {
    486         Toast.makeText(this, msgId, Toast.LENGTH_SHORT).show();
    487         finish();
    488     }
    489 
    490     private static class MyMap extends LinkedHashMap<String, byte[]>
    491             implements Serializable {
    492         private static final long serialVersionUID = 1L;
    493 
    494         @Override
    495         protected boolean removeEldestEntry(Map.Entry eldest) {
    496             // Note: one key takes about 1300 bytes in the keystore, so be
    497             // cautious about allowing more outstanding keys in the map that
    498             // may go beyond keystore's max length for one entry.
    499             return (size() > 3);
    500         }
    501     }
    502 
    503     private interface MyAction extends Serializable {
    504         void run(CertInstaller host);
    505     }
    506 
    507     private static class Pkcs12ExtractAction implements MyAction {
    508         private final String mPassword;
    509         private transient boolean hasRun;
    510 
    511         Pkcs12ExtractAction(String password) {
    512             mPassword = password;
    513         }
    514 
    515         public void run(CertInstaller host) {
    516             if (hasRun) {
    517                 return;
    518             }
    519             hasRun = true;
    520             host.extractPkcs12InBackground(mPassword);
    521         }
    522     }
    523 
    524     private static class InstallOthersAction implements MyAction {
    525         public void run(CertInstaller host) {
    526             host.mNextAction = null;
    527             host.installOthers();
    528         }
    529     }
    530 
    531     private static class OnExtractionDoneAction implements MyAction {
    532         private final boolean mSuccess;
    533 
    534         OnExtractionDoneAction(boolean success) {
    535             mSuccess = success;
    536         }
    537 
    538         public void run(CertInstaller host) {
    539             host.onExtractionDone(mSuccess);
    540         }
    541     }
    542 }
    543