/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.certinstaller; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.KeyguardManager; import android.app.ProgressDialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.os.Process; import android.security.KeyChain; import android.security.KeyChain.KeyChainConnection; import android.security.KeyStore; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.EditText; import android.widget.Spinner; import android.widget.Toast; import java.io.Serializable; /** * Installs certificates to the system keystore. */ public class CertInstaller extends Activity { private static final String TAG = "CertInstaller"; private static final int STATE_INIT = 1; private static final int STATE_RUNNING = 2; private static final int STATE_PAUSED = 3; private static final int NAME_CREDENTIAL_DIALOG = 1; private static final int PKCS12_PASSWORD_DIALOG = 2; private static final int PROGRESS_BAR_DIALOG = 3; private static final int REQUEST_SYSTEM_INSTALL_CODE = 1; private static final int REQUEST_CONFIRM_CREDENTIALS = 2; // key to states Bundle private static final String NEXT_ACTION_KEY = "na"; // Values for usage type spinner private static final int USAGE_TYPE_SYSTEM = 0; private static final int USAGE_TYPE_WIFI = 1; private final ViewHelper mView = new ViewHelper(); private int mState; private CredentialHelper mCredentials; private MyAction mNextAction; private CredentialHelper createCredentialHelper(Intent intent) { try { return new CredentialHelper(intent); } catch (Throwable t) { Log.w(TAG, "createCredentialHelper", t); toastErrorAndFinish(R.string.invalid_cert); return new CredentialHelper(); } } @Override protected void onCreate(Bundle savedStates) { super.onCreate(savedStates); getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); mCredentials = createCredentialHelper(getIntent()); mState = (savedStates == null) ? STATE_INIT : STATE_RUNNING; if (mState == STATE_INIT) { if (!mCredentials.containsAnyRawData()) { toastErrorAndFinish(R.string.no_cert_to_saved); finish(); } else { // Confirm credentials if there's _only_ a CA certificate // NOTE: This will affect WiFi CA certificates - those should not require // confirming the lock screen credentials but the code currently cannot skip the // confirmation for WiFi CA certificates because the user designates the certificate // to a UID only after this stage. if (mCredentials.hasCaCerts() && !mCredentials.hasPrivateKey() && !mCredentials.hasUserCertificate()) { KeyguardManager keyguardManager = getSystemService(KeyguardManager.class); Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(null, null); if (intent == null) { // No screenlock extractPkcs12OrInstall(); } else { startActivityForResult(intent, REQUEST_CONFIRM_CREDENTIALS); } } else { if (mCredentials.hasUserCertificate() && !mCredentials.hasPrivateKey()) { toastErrorAndFinish(R.string.action_missing_private_key); } else if (mCredentials.hasPrivateKey() && !mCredentials.hasUserCertificate()) { toastErrorAndFinish(R.string.action_missing_user_cert); } else { extractPkcs12OrInstall(); } } } } else { mCredentials.onRestoreStates(savedStates); mNextAction = (MyAction) savedStates.getSerializable(NEXT_ACTION_KEY); } } @Override protected void onResume() { super.onResume(); if (mState == STATE_INIT) { mState = STATE_RUNNING; } else { if (mNextAction != null) { mNextAction.run(this); } } } @Override protected void onPause() { super.onPause(); mState = STATE_PAUSED; } @Override protected void onSaveInstanceState(Bundle outStates) { super.onSaveInstanceState(outStates); mCredentials.onSaveStates(outStates); if (mNextAction != null) { outStates.putSerializable(NEXT_ACTION_KEY, mNextAction); } } @Override protected Dialog onCreateDialog (int dialogId) { switch (dialogId) { case PKCS12_PASSWORD_DIALOG: return createPkcs12PasswordDialog(); case NAME_CREDENTIAL_DIALOG: return createNameCredentialDialog(); case PROGRESS_BAR_DIALOG: ProgressDialog dialog = new ProgressDialog(this); dialog.setMessage(getString(R.string.extracting_pkcs12)); dialog.setIndeterminate(true); dialog.setCancelable(false); return dialog; default: return null; } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_SYSTEM_INSTALL_CODE: if (resultCode != RESULT_OK) { Log.d(TAG, "credential not saved, err: " + resultCode); toastErrorAndFinish(R.string.cert_not_saved); return; } Log.d(TAG, "credential is added: " + mCredentials.getName()); Toast.makeText(this, getString(R.string.cert_is_added, mCredentials.getName()), Toast.LENGTH_LONG).show(); if (mCredentials.includesVpnAndAppsTrustAnchors()) { // more work to do, don't finish just yet new InstallVpnAndAppsTrustAnchorsTask().execute(); return; } setResult(RESULT_OK); finish(); break; case REQUEST_CONFIRM_CREDENTIALS: if (resultCode == RESULT_OK) { extractPkcs12OrInstall(); return; } // Failed to confirm credentials, do nothing. finish(); break; default: Log.w(TAG, "unknown request code: " + requestCode); finish(); break; } } private void extractPkcs12OrInstall() { if (mCredentials.hasPkcs12KeyStore()) { if (mCredentials.hasPassword()) { showDialog(PKCS12_PASSWORD_DIALOG); } else { new Pkcs12ExtractAction("").run(this); } } else { MyAction action = new InstallOthersAction(); action.run(this); } } private class InstallVpnAndAppsTrustAnchorsTask extends AsyncTask { @Override protected Boolean doInBackground(Void... unused) { try { try (KeyChainConnection keyChainConnection = KeyChain.bind(CertInstaller.this)) { return mCredentials.installVpnAndAppsTrustAnchors(CertInstaller.this, keyChainConnection.getService()); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } @Override protected void onPostExecute(Boolean success) { if (success) { setResult(RESULT_OK); } finish(); } } private void installOthers() { // Sanity check: Check that there's either: // * A private key AND a user certificate, or // * A CA cert. boolean hasPrivateKeyAndUserCertificate = mCredentials.hasPrivateKey() && mCredentials.hasUserCertificate(); boolean hasCaCertificate = mCredentials.hasCaCerts(); Log.d(TAG, String.format( "Attempting credentials installation, has ca cert? %b, has user cert? %b", hasCaCertificate, hasPrivateKeyAndUserCertificate)); if (!(hasPrivateKeyAndUserCertificate || hasCaCertificate)) { finish(); return; } nameCredential(); } private void nameCredential() { if (!mCredentials.hasAnyForSystemInstall()) { toastErrorAndFinish(R.string.no_cert_to_saved); } else { showDialog(NAME_CREDENTIAL_DIALOG); } } private void extractPkcs12InBackground(final String password) { // show progress bar and extract certs in a background thread showDialog(PROGRESS_BAR_DIALOG); new AsyncTask() { @Override protected Boolean doInBackground(Void... unused) { return mCredentials.extractPkcs12(password); } @Override protected void onPostExecute(Boolean success) { MyAction action = new OnExtractionDoneAction(success); if (mState == STATE_PAUSED) { // activity is paused; run it in next onResume() mNextAction = action; } else { action.run(CertInstaller.this); } } }.execute(); } private void onExtractionDone(boolean success) { mNextAction = null; removeDialog(PROGRESS_BAR_DIALOG); if (success) { removeDialog(PKCS12_PASSWORD_DIALOG); nameCredential(); } else { showDialog(PKCS12_PASSWORD_DIALOG); mView.setText(R.id.credential_password, ""); mView.showError(R.string.password_error); } } private Dialog createPkcs12PasswordDialog() { View view = View.inflate(this, R.layout.password_dialog, null); mView.setView(view); if (mView.getHasEmptyError()) { mView.showError(R.string.password_empty_error); mView.setHasEmptyError(false); } String title = mCredentials.getName(); title = TextUtils.isEmpty(title) ? getString(R.string.pkcs12_password_dialog_title) : getString(R.string.pkcs12_file_password_dialog_title, title); Dialog d = new AlertDialog.Builder(this) .setView(view) .setTitle(title) .setPositiveButton(android.R.string.ok, (dialog, id) -> { String password = mView.getText(R.id.credential_password); mNextAction = new Pkcs12ExtractAction(password); mNextAction.run(CertInstaller.this); }) .setNegativeButton(android.R.string.cancel, (dialog, id) -> toastErrorAndFinish(R.string.cert_not_saved)) .create(); d.setOnCancelListener(dialog -> toastErrorAndFinish(R.string.cert_not_saved)); return d; } private Dialog createNameCredentialDialog() { ViewGroup view = (ViewGroup) View.inflate(this, R.layout.name_credential_dialog, null); mView.setView(view); if (mView.getHasEmptyError()) { mView.showError(R.string.name_empty_error); mView.setHasEmptyError(false); } mView.setText(R.id.credential_info, mCredentials.getDescription(this).toString()); final EditText nameInput = view.findViewById(R.id.credential_name); if (mCredentials.isInstallAsUidSet()) { view.findViewById(R.id.credential_usage_group).setVisibility(View.GONE); } else { final Spinner usageSpinner = view.findViewById(R.id.credential_usage); final View ca_capabilities_warning = view.findViewById(R.id.credential_capabilities_warning); usageSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { switch ((int) id) { case USAGE_TYPE_SYSTEM: ca_capabilities_warning.setVisibility( mCredentials.includesVpnAndAppsTrustAnchors() ? View.VISIBLE : View.GONE); mCredentials.setInstallAsUid(KeyStore.UID_SELF); break; case USAGE_TYPE_WIFI: ca_capabilities_warning.setVisibility(View.GONE); mCredentials.setInstallAsUid(Process.WIFI_UID); break; default: Log.w(TAG, "Unknown selection for scope: " + id); } } @Override public void onNothingSelected(AdapterView parent) { } }); } nameInput.setText(getDefaultName()); nameInput.selectAll(); final Context appContext = getApplicationContext(); Dialog d = new AlertDialog.Builder(this) .setView(view) .setTitle(R.string.name_credential_dialog_title) .setPositiveButton(android.R.string.ok, (dialog, id) -> { String name = mView.getText(R.id.credential_name); if (TextUtils.isEmpty(name)) { mView.setHasEmptyError(true); removeDialog(NAME_CREDENTIAL_DIALOG); showDialog(NAME_CREDENTIAL_DIALOG); } else { removeDialog(NAME_CREDENTIAL_DIALOG); mCredentials.setName(name); // install everything to system keystore try { startActivityForResult( mCredentials.createSystemInstallIntent(appContext), REQUEST_SYSTEM_INSTALL_CODE); } catch (ActivityNotFoundException e) { Log.w(TAG, "systemInstall(): " + e); toastErrorAndFinish(R.string.cert_not_saved); } } }) .setNegativeButton(android.R.string.cancel, (dialog, id) -> toastErrorAndFinish(R.string.cert_not_saved)) .create(); d.setOnCancelListener(dialog -> toastErrorAndFinish(R.string.cert_not_saved)); return d; } private String getDefaultName() { String name = mCredentials.getName(); if (TextUtils.isEmpty(name)) { return null; } else { // remove the extension from the file name int index = name.lastIndexOf("."); if (index > 0) name = name.substring(0, index); return name; } } private void toastErrorAndFinish(int msgId) { Toast.makeText(this, msgId, Toast.LENGTH_SHORT).show(); finish(); } private interface MyAction extends Serializable { void run(CertInstaller host); } private static class Pkcs12ExtractAction implements MyAction { private final String mPassword; private transient boolean hasRun; Pkcs12ExtractAction(String password) { mPassword = password; } public void run(CertInstaller host) { if (hasRun) { return; } hasRun = true; host.extractPkcs12InBackground(mPassword); } } private static class InstallOthersAction implements MyAction { public void run(CertInstaller host) { host.mNextAction = null; host.installOthers(); } } private static class OnExtractionDoneAction implements MyAction { private final boolean mSuccess; OnExtractionDoneAction(boolean success) { mSuccess = success; } public void run(CertInstaller host) { host.onExtractionDone(mSuccess); } } }