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 createCredentialHelper(Intent intent)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 onCreate(Bundle savedStates)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 onResume()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 needsKeyStoreAccess()134 private boolean needsKeyStoreAccess() { 135 return ((mCredentials.hasKeyPair() || mCredentials.hasUserCertificate()) 136 && !mKeyStore.isUnlocked()); 137 } 138 139 @Override onPause()140 protected void onPause() { 141 super.onPause(); 142 mState = STATE_PAUSED; 143 } 144 145 @Override onSaveInstanceState(Bundle outStates)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 onCreateDialog(int dialogId)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 onActivityResult(int requestCode, int resultCode, Intent data)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 && mCredentials.getInstallAsUid() == KeyStore.UID_SELF) { 185 // more work to do, don't finish just yet 186 new InstallCaCertsToKeyChainTask().execute(); 187 return; 188 } 189 setResult(RESULT_OK); 190 } else { 191 Log.d(TAG, "credential not saved, err: " + resultCode); 192 toastErrorAndFinish(R.string.cert_not_saved); 193 } 194 } else { 195 Log.w(TAG, "unknown request code: " + requestCode); 196 } 197 finish(); 198 } 199 200 private class InstallCaCertsToKeyChainTask extends AsyncTask<Void, Void, Boolean> { 201 doInBackground(Void... unused)202 @Override protected Boolean doInBackground(Void... unused) { 203 try { 204 KeyChainConnection keyChainConnection = KeyChain.bind(CertInstaller.this); 205 try { 206 return mCredentials.installCaCertsToKeyChain(keyChainConnection.getService()); 207 } finally { 208 keyChainConnection.close(); 209 } 210 } catch (InterruptedException e) { 211 Thread.currentThread().interrupt(); 212 return false; 213 } 214 } 215 onPostExecute(Boolean success)216 @Override protected void onPostExecute(Boolean success) { 217 if (success) { 218 setResult(RESULT_OK); 219 } 220 finish(); 221 } 222 } 223 installOthers()224 void installOthers() { 225 if (mCredentials.hasKeyPair()) { 226 saveKeyPair(); 227 finish(); 228 } else { 229 X509Certificate cert = mCredentials.getUserCertificate(); 230 if (cert != null) { 231 // find matched private key 232 String key = Util.toMd5(cert.getPublicKey().getEncoded()); 233 Map<String, byte[]> map = getPkeyMap(); 234 byte[] privatekey = map.get(key); 235 if (privatekey != null) { 236 Log.d(TAG, "found matched key: " + privatekey); 237 map.remove(key); 238 savePkeyMap(map); 239 240 mCredentials.setPrivateKey(privatekey); 241 } else { 242 Log.d(TAG, "didn't find matched private key: " + key); 243 } 244 } 245 nameCredential(); 246 } 247 } 248 sendUnlockKeyStoreIntent()249 private void sendUnlockKeyStoreIntent() { 250 Credentials.getInstance().unlock(this); 251 } 252 nameCredential()253 private void nameCredential() { 254 if (!mCredentials.hasAnyForSystemInstall()) { 255 toastErrorAndFinish(R.string.no_cert_to_saved); 256 } else { 257 showDialog(NAME_CREDENTIAL_DIALOG); 258 } 259 } 260 saveKeyPair()261 private void saveKeyPair() { 262 byte[] privatekey = mCredentials.getData(Credentials.EXTRA_PRIVATE_KEY); 263 String key = Util.toMd5(mCredentials.getData(Credentials.EXTRA_PUBLIC_KEY)); 264 Map<String, byte[]> map = getPkeyMap(); 265 map.put(key, privatekey); 266 savePkeyMap(map); 267 Log.d(TAG, "save privatekey: " + key + " --> #keys:" + map.size()); 268 } 269 savePkeyMap(Map<String, byte[]> map)270 private void savePkeyMap(Map<String, byte[]> map) { 271 if (map.isEmpty()) { 272 if (!mKeyStore.delete(PKEY_MAP_KEY)) { 273 Log.w(TAG, "savePkeyMap(): failed to delete pkey map"); 274 } 275 return; 276 } 277 byte[] bytes = Util.toBytes(map); 278 if (!mKeyStore.put(PKEY_MAP_KEY, bytes, KeyStore.UID_SELF, KeyStore.FLAG_ENCRYPTED)) { 279 Log.w(TAG, "savePkeyMap(): failed to write pkey map"); 280 } 281 } 282 getPkeyMap()283 private Map<String, byte[]> getPkeyMap() { 284 byte[] bytes = mKeyStore.get(PKEY_MAP_KEY); 285 if (bytes != null) { 286 Map<String, byte[]> map = 287 (Map<String, byte[]>) Util.fromBytes(bytes); 288 if (map != null) return map; 289 } 290 return new MyMap(); 291 } 292 extractPkcs12InBackground(final String password)293 void extractPkcs12InBackground(final String password) { 294 // show progress bar and extract certs in a background thread 295 showDialog(PROGRESS_BAR_DIALOG); 296 297 new AsyncTask<Void,Void,Boolean>() { 298 @Override protected Boolean doInBackground(Void... unused) { 299 return mCredentials.extractPkcs12(password); 300 } 301 @Override protected void onPostExecute(Boolean success) { 302 MyAction action = new OnExtractionDoneAction(success); 303 if (mState == STATE_PAUSED) { 304 // activity is paused; run it in next onResume() 305 mNextAction = action; 306 } else { 307 action.run(CertInstaller.this); 308 } 309 } 310 }.execute(); 311 } 312 onExtractionDone(boolean success)313 void onExtractionDone(boolean success) { 314 mNextAction = null; 315 removeDialog(PROGRESS_BAR_DIALOG); 316 if (success) { 317 removeDialog(PKCS12_PASSWORD_DIALOG); 318 nameCredential(); 319 } else { 320 mView.setText(R.id.credential_password, ""); 321 mView.showError(R.string.password_error); 322 showDialog(PKCS12_PASSWORD_DIALOG); 323 } 324 } 325 createPkcs12PasswordDialog()326 private Dialog createPkcs12PasswordDialog() { 327 View view = View.inflate(this, R.layout.password_dialog, null); 328 mView.setView(view); 329 if (mView.getHasEmptyError()) { 330 mView.showError(R.string.password_empty_error); 331 mView.setHasEmptyError(false); 332 } 333 334 String title = mCredentials.getName(); 335 title = TextUtils.isEmpty(title) 336 ? getString(R.string.pkcs12_password_dialog_title) 337 : getString(R.string.pkcs12_file_password_dialog_title, title); 338 Dialog d = new AlertDialog.Builder(this) 339 .setView(view) 340 .setTitle(title) 341 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 342 public void onClick(DialogInterface dialog, int id) { 343 String password = mView.getText(R.id.credential_password); 344 mNextAction = new Pkcs12ExtractAction(password); 345 mNextAction.run(CertInstaller.this); 346 } 347 }) 348 .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 349 public void onClick(DialogInterface dialog, int id) { 350 toastErrorAndFinish(R.string.cert_not_saved); 351 } 352 }) 353 .create(); 354 d.setOnCancelListener(new DialogInterface.OnCancelListener() { 355 @Override public void onCancel(DialogInterface dialog) { 356 toastErrorAndFinish(R.string.cert_not_saved); 357 } 358 }); 359 return d; 360 } 361 createNameCredentialDialog()362 private Dialog createNameCredentialDialog() { 363 ViewGroup view = (ViewGroup) View.inflate(this, R.layout.name_credential_dialog, null); 364 mView.setView(view); 365 if (mView.getHasEmptyError()) { 366 mView.showError(R.string.name_empty_error); 367 mView.setHasEmptyError(false); 368 } 369 mView.setText(R.id.credential_info, mCredentials.getDescription(this).toString()); 370 final EditText nameInput = (EditText) view.findViewById(R.id.credential_name); 371 if (mCredentials.isInstallAsUidSet()) { 372 view.findViewById(R.id.credential_usage_group).setVisibility(View.GONE); 373 } else { 374 final Spinner usageSpinner = (Spinner) view.findViewById(R.id.credential_usage); 375 376 usageSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { 377 @Override 378 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 379 switch ((int) id) { 380 case USAGE_TYPE_SYSTEM: 381 mCredentials.setInstallAsUid(KeyStore.UID_SELF); 382 break; 383 case USAGE_TYPE_WIFI: 384 mCredentials.setInstallAsUid(Process.WIFI_UID); 385 break; 386 default: 387 Log.w(TAG, "Unknown selection for scope: " + id); 388 } 389 } 390 391 @Override 392 public void onNothingSelected(AdapterView<?> parent) { 393 } 394 }); 395 } 396 nameInput.setText(getDefaultName()); 397 nameInput.selectAll(); 398 Dialog d = new AlertDialog.Builder(this) 399 .setView(view) 400 .setTitle(R.string.name_credential_dialog_title) 401 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 402 public void onClick(DialogInterface dialog, int id) { 403 String name = mView.getText(R.id.credential_name); 404 if (TextUtils.isEmpty(name)) { 405 mView.setHasEmptyError(true); 406 removeDialog(NAME_CREDENTIAL_DIALOG); 407 showDialog(NAME_CREDENTIAL_DIALOG); 408 } else { 409 removeDialog(NAME_CREDENTIAL_DIALOG); 410 mCredentials.setName(name); 411 412 // install everything to system keystore 413 try { 414 startActivityForResult( 415 mCredentials.createSystemInstallIntent(), 416 REQUEST_SYSTEM_INSTALL_CODE); 417 } catch (ActivityNotFoundException e) { 418 Log.w(TAG, "systemInstall(): " + e); 419 toastErrorAndFinish(R.string.cert_not_saved); 420 } 421 } 422 } 423 }) 424 .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 425 public void onClick(DialogInterface dialog, int id) { 426 toastErrorAndFinish(R.string.cert_not_saved); 427 } 428 }) 429 .create(); 430 d.setOnCancelListener(new DialogInterface.OnCancelListener() { 431 @Override public void onCancel(DialogInterface dialog) { 432 toastErrorAndFinish(R.string.cert_not_saved); 433 } 434 }); 435 return d; 436 } 437 getDefaultName()438 private String getDefaultName() { 439 String name = mCredentials.getName(); 440 if (TextUtils.isEmpty(name)) { 441 return null; 442 } else { 443 // remove the extension from the file name 444 int index = name.lastIndexOf("."); 445 if (index > 0) name = name.substring(0, index); 446 return name; 447 } 448 } 449 toastErrorAndFinish(int msgId)450 private void toastErrorAndFinish(int msgId) { 451 Toast.makeText(this, msgId, Toast.LENGTH_SHORT).show(); 452 finish(); 453 } 454 455 private static class MyMap extends LinkedHashMap<String, byte[]> 456 implements Serializable { 457 private static final long serialVersionUID = 1L; 458 459 @Override removeEldestEntry(Map.Entry eldest)460 protected boolean removeEldestEntry(Map.Entry eldest) { 461 // Note: one key takes about 1300 bytes in the keystore, so be 462 // cautious about allowing more outstanding keys in the map that 463 // may go beyond keystore's max length for one entry. 464 return (size() > 3); 465 } 466 } 467 468 private interface MyAction extends Serializable { run(CertInstaller host)469 void run(CertInstaller host); 470 } 471 472 private static class Pkcs12ExtractAction implements MyAction { 473 private final String mPassword; 474 private transient boolean hasRun; 475 Pkcs12ExtractAction(String password)476 Pkcs12ExtractAction(String password) { 477 mPassword = password; 478 } 479 run(CertInstaller host)480 public void run(CertInstaller host) { 481 if (hasRun) { 482 return; 483 } 484 hasRun = true; 485 host.extractPkcs12InBackground(mPassword); 486 } 487 } 488 489 private static class InstallOthersAction implements MyAction { run(CertInstaller host)490 public void run(CertInstaller host) { 491 host.mNextAction = null; 492 host.installOthers(); 493 } 494 } 495 496 private static class OnExtractionDoneAction implements MyAction { 497 private final boolean mSuccess; 498 OnExtractionDoneAction(boolean success)499 OnExtractionDoneAction(boolean success) { 500 mSuccess = success; 501 } 502 run(CertInstaller host)503 public void run(CertInstaller host) { 504 host.onExtractionDone(mSuccess); 505 } 506 } 507 } 508