/*
 * 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<String, byte[]> 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<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) {
                Toast.makeText(getApplicationContext(), R.string.ca_cert_is_added,
                        Toast.LENGTH_LONG).show();
                setResult(RESULT_OK);
            }
            finish();
        }
    }

    private void installOthers() {
        // 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<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);
            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;
    }
}
