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