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