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