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 createCredentialHelper(Intent intent)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 onCreate(Bundle savedStates)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 onResume()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 needsKeyStoreAccess()126 private boolean needsKeyStoreAccess() { 127 return ((mCredentials.hasKeyPair() || mCredentials.hasUserCertificate()) 128 && (mKeyStore.state() != KeyStore.State.UNLOCKED)); 129 } 130 131 @Override onPause()132 protected void onPause() { 133 super.onPause(); 134 mState = STATE_PAUSED; 135 } 136 137 @Override onSaveInstanceState(Bundle outStates)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 onCreateDialog(int dialogId)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 onActivityResult(int requestCode, int resultCode, Intent data)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 doInBackground(Void... unused)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 onPostExecute(Boolean success)207 @Override protected void onPostExecute(Boolean success) { 208 if (success) { 209 setResult(RESULT_OK); 210 } 211 finish(); 212 } 213 } 214 installOthers()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 sendUnlockKeyStoreIntent()240 private void sendUnlockKeyStoreIntent() { 241 Credentials.getInstance().unlock(this); 242 } 243 nameCredential()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 saveKeyPair()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 savePkeyMap(Map<String, byte[]> map)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 getPkeyMap()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 extractPkcs12InBackground(final String password)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 onExtractionDone(boolean success)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 createPkcs12PasswordDialog()317 private Dialog createPkcs12PasswordDialog() { 318 View view = View.inflate(this, R.layout.password_dialog, null); 319 mView.setView(view); 320 if (mView.getHasEmptyError()) { 321 mView.showError(R.string.password_empty_error); 322 mView.setHasEmptyError(false); 323 } 324 325 String title = mCredentials.getName(); 326 title = TextUtils.isEmpty(title) 327 ? getString(R.string.pkcs12_password_dialog_title) 328 : getString(R.string.pkcs12_file_password_dialog_title, title); 329 Dialog d = new AlertDialog.Builder(this) 330 .setView(view) 331 .setTitle(title) 332 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 333 public void onClick(DialogInterface dialog, int id) { 334 String password = mView.getText(R.id.credential_password); 335 mNextAction = new Pkcs12ExtractAction(password); 336 mNextAction.run(CertInstaller.this); 337 } 338 }) 339 .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 340 public void onClick(DialogInterface dialog, int id) { 341 toastErrorAndFinish(R.string.cert_not_saved); 342 } 343 }) 344 .create(); 345 d.setOnCancelListener(new DialogInterface.OnCancelListener() { 346 @Override public void onCancel(DialogInterface dialog) { 347 toastErrorAndFinish(R.string.cert_not_saved); 348 } 349 }); 350 return d; 351 } 352 createNameCredentialDialog()353 private Dialog createNameCredentialDialog() { 354 ViewGroup view = (ViewGroup) View.inflate(this, R.layout.name_credential_dialog, null); 355 mView.setView(view); 356 if (mView.getHasEmptyError()) { 357 mView.showError(R.string.name_empty_error); 358 mView.setHasEmptyError(false); 359 } 360 mView.setText(R.id.credential_info, mCredentials.getDescription(this).toString()); 361 final EditText nameInput = (EditText) view.findViewById(R.id.credential_name); 362 nameInput.setText(getDefaultName()); 363 nameInput.selectAll(); 364 Dialog d = new AlertDialog.Builder(this) 365 .setView(view) 366 .setTitle(R.string.name_credential_dialog_title) 367 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 368 public void onClick(DialogInterface dialog, int id) { 369 String name = mView.getText(R.id.credential_name); 370 if (TextUtils.isEmpty(name)) { 371 mView.setHasEmptyError(true); 372 removeDialog(NAME_CREDENTIAL_DIALOG); 373 showDialog(NAME_CREDENTIAL_DIALOG); 374 } else { 375 removeDialog(NAME_CREDENTIAL_DIALOG); 376 mCredentials.setName(name); 377 378 // install everything to system keystore 379 try { 380 startActivityForResult( 381 mCredentials.createSystemInstallIntent(), 382 REQUEST_SYSTEM_INSTALL_CODE); 383 } catch (ActivityNotFoundException e) { 384 Log.w(TAG, "systemInstall(): " + e); 385 toastErrorAndFinish(R.string.cert_not_saved); 386 } 387 } 388 } 389 }) 390 .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 391 public void onClick(DialogInterface dialog, int id) { 392 toastErrorAndFinish(R.string.cert_not_saved); 393 } 394 }) 395 .create(); 396 d.setOnCancelListener(new DialogInterface.OnCancelListener() { 397 @Override public void onCancel(DialogInterface dialog) { 398 toastErrorAndFinish(R.string.cert_not_saved); 399 } 400 }); 401 return d; 402 } 403 getDefaultName()404 private String getDefaultName() { 405 String name = mCredentials.getName(); 406 if (TextUtils.isEmpty(name)) { 407 return null; 408 } else { 409 // remove the extension from the file name 410 int index = name.lastIndexOf("."); 411 if (index > 0) name = name.substring(0, index); 412 return name; 413 } 414 } 415 toastErrorAndFinish(int msgId)416 private void toastErrorAndFinish(int msgId) { 417 Toast.makeText(this, msgId, Toast.LENGTH_SHORT).show(); 418 finish(); 419 } 420 421 private static class MyMap extends LinkedHashMap<String, byte[]> 422 implements Serializable { 423 private static final long serialVersionUID = 1L; 424 425 @Override removeEldestEntry(Map.Entry eldest)426 protected boolean removeEldestEntry(Map.Entry eldest) { 427 // Note: one key takes about 1300 bytes in the keystore, so be 428 // cautious about allowing more outstanding keys in the map that 429 // may go beyond keystore's max length for one entry. 430 return (size() > 3); 431 } 432 } 433 434 private interface MyAction extends Serializable { run(CertInstaller host)435 void run(CertInstaller host); 436 } 437 438 private static class Pkcs12ExtractAction implements MyAction { 439 private final String mPassword; 440 private transient boolean hasRun; 441 Pkcs12ExtractAction(String password)442 Pkcs12ExtractAction(String password) { 443 mPassword = password; 444 } 445 run(CertInstaller host)446 public void run(CertInstaller host) { 447 if (hasRun) { 448 return; 449 } 450 hasRun = true; 451 host.extractPkcs12InBackground(mPassword); 452 } 453 } 454 455 private static class InstallOthersAction implements MyAction { run(CertInstaller host)456 public void run(CertInstaller host) { 457 host.mNextAction = null; 458 host.installOthers(); 459 } 460 } 461 462 private static class OnExtractionDoneAction implements MyAction { 463 private final boolean mSuccess; 464 OnExtractionDoneAction(boolean success)465 OnExtractionDoneAction(boolean success) { 466 mSuccess = success; 467 } 468 run(CertInstaller host)469 public void run(CertInstaller host) { 470 host.onExtractionDone(mSuccess); 471 } 472 } 473 } 474