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