/* * 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.ProgressDialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.os.Bundle; import android.os.Process; import android.security.Credentials; import android.security.KeyChain; import android.security.KeyChain.KeyChainConnection; import android.text.TextUtils; import android.util.Log; import android.util.Slog; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.RadioGroup; import android.widget.Toast; import com.android.internal.annotations.VisibleForTesting; import java.io.Serializable; import java.util.HashMap; import java.util.Map; /** * 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 REDIRECT_CA_CERTIFICATE_DIALOG = 4; private static final int SELECT_CERTIFICATE_USAGE_DIALOG = 5; private static final int INVALID_CERTIFICATE_DIALOG = 6; private static final int REQUEST_SYSTEM_INSTALL_CODE = 1; // key to states Bundle private static final String NEXT_ACTION_KEY = "na"; private final ViewHelper mView = new ViewHelper(); private int mState; private CredentialHelper mCredentials; private MyAction mNextAction; private CredentialHelper createCredentialHelper(Intent intent) { try { Bundle bundle = intent.getExtras(); if (bundle == null) { return new CredentialHelper(); } else { int size = bundle.size(); Log.d(TAG, "# extras: " + size); String name = bundle.getString(KeyChain.EXTRA_NAME); bundle.remove(KeyChain.EXTRA_NAME); String referrer = bundle.getString(Intent.EXTRA_REFERRER); bundle.remove(Intent.EXTRA_REFERRER); String certUsageSelected = bundle.getString(Credentials.EXTRA_CERTIFICATE_USAGE); bundle.remove(Credentials.EXTRA_CERTIFICATE_USAGE); int uid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, Process.INVALID_UID); bundle.remove(Credentials.EXTRA_INSTALL_AS_UID); Map byteMap = new HashMap<>(); for (String key : bundle.keySet()) { byte[] bytes = bundle.getByteArray(key); byteMap.put(key, bytes); } return new CredentialHelper(byteMap, name, referrer, certUsageSelected, uid); } } 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 { if (installingCaCertificate()) { extractPkcs12OrInstall(); } 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); } } private boolean installingCaCertificate() { return mCredentials.hasCaCerts() && !mCredentials.hasPrivateKey() && !mCredentials.hasUserCertificate(); } @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 createNameCertificateDialog(); case PROGRESS_BAR_DIALOG: ProgressDialog dialog = new ProgressDialog(this); dialog.setMessage(getString(R.string.extracting_pkcs12)); dialog.setIndeterminate(true); dialog.setCancelable(false); return dialog; case REDIRECT_CA_CERTIFICATE_DIALOG: return createRedirectCaCertificateDialog(); case SELECT_CERTIFICATE_USAGE_DIALOG: return createSelectCertificateUsageDialog(); case INVALID_CERTIFICATE_DIALOG: return createInvalidCertificateDialog(); 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()); if (mCredentials.getCertUsageSelected().equals(Credentials.CERTIFICATE_USAGE_WIFI)) { Toast.makeText(this, R.string.wifi_cert_is_added, Toast.LENGTH_LONG).show(); } else { Toast.makeText(this, R.string.user_cert_is_added, Toast.LENGTH_LONG).show(); } setResult(RESULT_OK); 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 { if (mCredentials.calledBySettings()) { MyAction action = new InstallOthersAction(); action.run(this); } else { createRedirectOrSelectUsageDialog(); } } } 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) { Toast.makeText(getApplicationContext(), R.string.ca_cert_is_added, Toast.LENGTH_LONG).show(); 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; } if (validCertificateSelected()) { installCertificateOrShowNameDialog(); } else { showDialog(INVALID_CERTIFICATE_DIALOG); } } private boolean validCertificateSelected() { switch (mCredentials.getCertUsageSelected()) { case Credentials.CERTIFICATE_USAGE_CA: return mCredentials.hasOnlyVpnAndAppsTrustAnchors(); case Credentials.CERTIFICATE_USAGE_USER: return mCredentials.hasUserCertificate() && !mCredentials.hasOnlyVpnAndAppsTrustAnchors(); case Credentials.CERTIFICATE_USAGE_WIFI: return true; default: return false; } } private void installCertificateOrShowNameDialog() { if (!mCredentials.hasAnyForSystemInstall()) { toastErrorAndFinish(R.string.no_cert_to_saved); } else if (mCredentials.hasOnlyVpnAndAppsTrustAnchors()) { // If there's only a CA certificate to install, then it's going to be used // as a trust anchor. Install it and skip importing to Keystore. // more work to do, don't finish just yet new InstallVpnAndAppsTrustAnchorsTask().execute(); } else { // Name is required if installing User certificate 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); if (mCredentials.calledBySettings()) { if (validCertificateSelected()) { installCertificateOrShowNameDialog(); } else { showDialog(INVALID_CERTIFICATE_DIALOG); } } else { createRedirectOrSelectUsageDialog(); } } else { showDialog(PKCS12_PASSWORD_DIALOG); mView.setText(R.id.credential_password, ""); mView.showError(R.string.password_error); } } private void createRedirectOrSelectUsageDialog() { if (mCredentials.hasOnlyVpnAndAppsTrustAnchors()) { showDialog(REDIRECT_CA_CERTIFICATE_DIALOG); } else { showDialog(SELECT_CERTIFICATE_USAGE_DIALOG); } } public CharSequence getCallingAppLabel() { final String callingPkg = mCredentials.getReferrer(); if (callingPkg == null) { Log.e(TAG, "Cannot get calling calling AppPackage"); return null; } final PackageManager pm = getPackageManager(); final ApplicationInfo appInfo; try { appInfo = pm.getApplicationInfo(callingPkg, PackageManager.MATCH_DISABLED_COMPONENTS); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Unable to find info for package: " + callingPkg); return null; } return appInfo.loadLabel(pm); } private Dialog createRedirectCaCertificateDialog() { final String message = getString( R.string.redirect_ca_certificate_with_app_info_message, getCallingAppLabel()); Dialog d = new AlertDialog.Builder(this) .setTitle(R.string.redirect_ca_certificate_title) .setMessage(message) .setPositiveButton(R.string.redirect_ca_certificate_close_button, (dialog, id) -> toastErrorAndFinish(R.string.cert_not_saved)) .create(); d.setOnCancelListener(dialog -> toastErrorAndFinish(R.string.cert_not_saved)); return d; } private Dialog createSelectCertificateUsageDialog() { ViewGroup view = (ViewGroup) View.inflate(this, R.layout.select_certificate_usage_dialog, null); mView.setView(view); RadioGroup radioGroup = view.findViewById(R.id.certificate_usage); radioGroup.setOnCheckedChangeListener((group, checkedId) -> { switch (checkedId) { case R.id.user_certificate: mCredentials.setCertUsageSelectedAndUid(Credentials.CERTIFICATE_USAGE_USER); break; case R.id.wifi_certificate: mCredentials.setCertUsageSelectedAndUid(Credentials.CERTIFICATE_USAGE_WIFI); default: Slog.i(TAG, "Unknown selection for scope"); } }); final Context appContext = getApplicationContext(); Dialog d = new AlertDialog.Builder(this) .setView(view) .setPositiveButton(android.R.string.ok, (dialog, id) -> { showDialog(NAME_CREDENTIAL_DIALOG); }) .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 createInvalidCertificateDialog() { Dialog d = new AlertDialog.Builder(this) .setTitle(R.string.invalid_certificate_title) .setMessage(getString(R.string.invalid_certificate_message, getCertificateUsageName())) .setPositiveButton(R.string.invalid_certificate_close_button, (dialog, id) -> toastErrorAndFinish(R.string.cert_not_saved)) .create(); d.setOnCancelListener(dialog -> finish()); return d; } String getCertificateUsageName() { switch (mCredentials.getCertUsageSelected()) { case Credentials.CERTIFICATE_USAGE_CA: return getString(R.string.ca_certificate); case Credentials.CERTIFICATE_USAGE_USER: return getString(R.string.user_certificate); case Credentials.CERTIFICATE_USAGE_WIFI: return getString(R.string.wifi_certificate); default: return getString(R.string.certificate); } } 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 createNameCertificateDialog() { ViewGroup view = (ViewGroup) View.inflate(this, R.layout.name_certificate_dialog, null); mView.setView(view); if (mView.getHasEmptyError()) { mView.showError(R.string.name_empty_error); mView.setHasEmptyError(false); } final EditText nameInput = view.findViewById(R.id.certificate_name); 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.certificate_name); if (TextUtils.isEmpty(name)) { mView.setHasEmptyError(true); removeDialog(NAME_CREDENTIAL_DIALOG); showDialog(NAME_CREDENTIAL_DIALOG); } else { removeDialog(NAME_CREDENTIAL_DIALOG); mCredentials.setName(name); installCertificateToKeystore(appContext); } }) .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 void installCertificateToKeystore(Context context) { try { startActivityForResult( mCredentials.createSystemInstallIntent(context), REQUEST_SYSTEM_INSTALL_CODE); } catch (ActivityNotFoundException e) { Log.w(TAG, "installCertificateToKeystore(): ", e); toastErrorAndFinish(R.string.cert_not_saved); } } 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); } } @VisibleForTesting public CredentialHelper getCredentials() { return mCredentials; } }