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