| /* |
| * 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<Void, Void, Boolean> { |
| |
| @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<Void,Void,Boolean>() { |
| @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); |
| } |
| } |
| } |