blob: dd849a714b9aba0c63f5aca20d95b2dff611d27c [file] [log] [blame]
/*
* 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);
}
}
}