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