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