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