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 408 usageSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { 409 @Override 410 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 411 switch ((int) id) { 412 case USAGE_TYPE_SYSTEM: 413 mCredentials.setInstallAsUid(KeyStore.UID_SELF); 414 break; 415 case USAGE_TYPE_WIFI: 416 mCredentials.setInstallAsUid(Process.WIFI_UID); 417 break; 418 default: 419 Log.w(TAG, "Unknown selection for scope: " + id); 420 } 421 } 422 423 @Override 424 public void onNothingSelected(AdapterView<?> parent) { 425 } 426 }); 427 } 428 nameInput.setText(getDefaultName()); 429 nameInput.selectAll(); 430 final Context appContext = getApplicationContext(); 431 Dialog d = new AlertDialog.Builder(this) 432 .setView(view) 433 .setTitle(R.string.name_credential_dialog_title) 434 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 435 public void onClick(DialogInterface dialog, int id) { 436 String name = mView.getText(R.id.credential_name); 437 if (TextUtils.isEmpty(name)) { 438 mView.setHasEmptyError(true); 439 removeDialog(NAME_CREDENTIAL_DIALOG); 440 showDialog(NAME_CREDENTIAL_DIALOG); 441 } else { 442 removeDialog(NAME_CREDENTIAL_DIALOG); 443 mCredentials.setName(name); 444 445 // install everything to system keystore 446 try { 447 startActivityForResult( 448 mCredentials.createSystemInstallIntent(appContext), 449 REQUEST_SYSTEM_INSTALL_CODE); 450 } catch (ActivityNotFoundException e) { 451 Log.w(TAG, "systemInstall(): " + e); 452 toastErrorAndFinish(R.string.cert_not_saved); 453 } 454 } 455 } 456 }) 457 .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 458 public void onClick(DialogInterface dialog, int id) { 459 toastErrorAndFinish(R.string.cert_not_saved); 460 } 461 }) 462 .create(); 463 d.setOnCancelListener(new DialogInterface.OnCancelListener() { 464 @Override public void onCancel(DialogInterface dialog) { 465 toastErrorAndFinish(R.string.cert_not_saved); 466 } 467 }); 468 return d; 469 } 470 getDefaultName()471 private String getDefaultName() { 472 String name = mCredentials.getName(); 473 if (TextUtils.isEmpty(name)) { 474 return null; 475 } else { 476 // remove the extension from the file name 477 int index = name.lastIndexOf("."); 478 if (index > 0) name = name.substring(0, index); 479 return name; 480 } 481 } 482 toastErrorAndFinish(int msgId)483 private void toastErrorAndFinish(int msgId) { 484 Toast.makeText(this, msgId, Toast.LENGTH_SHORT).show(); 485 finish(); 486 } 487 488 private static class MyMap extends LinkedHashMap<String, byte[]> 489 implements Serializable { 490 private static final long serialVersionUID = 1L; 491 492 @Override removeEldestEntry(Map.Entry eldest)493 protected boolean removeEldestEntry(Map.Entry eldest) { 494 // Note: one key takes about 1300 bytes in the keystore, so be 495 // cautious about allowing more outstanding keys in the map that 496 // may go beyond keystore's max length for one entry. 497 return (size() > 3); 498 } 499 } 500 501 private interface MyAction extends Serializable { run(CertInstaller host)502 void run(CertInstaller host); 503 } 504 505 private static class Pkcs12ExtractAction implements MyAction { 506 private final String mPassword; 507 private transient boolean hasRun; 508 Pkcs12ExtractAction(String password)509 Pkcs12ExtractAction(String password) { 510 mPassword = password; 511 } 512 run(CertInstaller host)513 public void run(CertInstaller host) { 514 if (hasRun) { 515 return; 516 } 517 hasRun = true; 518 host.extractPkcs12InBackground(mPassword); 519 } 520 } 521 522 private static class InstallOthersAction implements MyAction { run(CertInstaller host)523 public void run(CertInstaller host) { 524 host.mNextAction = null; 525 host.installOthers(); 526 } 527 } 528 529 private static class OnExtractionDoneAction implements MyAction { 530 private final boolean mSuccess; 531 OnExtractionDoneAction(boolean success)532 OnExtractionDoneAction(boolean success) { 533 mSuccess = success; 534 } 535 run(CertInstaller host)536 public void run(CertInstaller host) { 537 host.onExtractionDone(mSuccess); 538 } 539 } 540 } 541