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