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