/*
 * Copyright (C) 2019 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.cts.verifier.security;

import android.Manifest;
import android.app.KeyguardManager;
import android.content.pm.PackageManager;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricManager.Authenticators;
import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback;
import android.hardware.biometrics.BiometricPrompt.AuthenticationResult;
import android.hardware.biometrics.BiometricPrompt.CryptoObject;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.security.identity.AccessControlProfile;
import android.security.identity.AccessControlProfileId;
import android.security.identity.IdentityCredential;
import android.security.identity.IdentityCredentialStore;
import android.security.identity.PersonalizationData;
import android.security.identity.ResultData;
import android.security.identity.WritableIdentityCredential;
import android.widget.Button;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.cts.verifier.PassFailButtons;
import com.android.cts.verifier.R;

import java.io.ByteArrayOutputStream;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;

import co.nstant.in.cbor.CborBuilder;
import co.nstant.in.cbor.CborEncoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.builder.MapBuilder;

public class IdentityCredentialAuthentication extends PassFailButtons.Activity {
    private static final boolean DEBUG = false;
    private static final String TAG = "IdentityCredentialAuthentication";

    private static final int BIOMETRIC_REQUEST_PERMISSION_CODE = 0;

    private BiometricManager mBiometricManager;
    private KeyguardManager mKeyguardManager;

    protected int getTitleRes() {
        return R.string.sec_identity_credential_authentication_test;
    }

    private int getDescriptionRes() {
        return R.string.sec_identity_credential_authentication_test_info;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.sec_screen_lock_keys_main);
        setPassFailButtonClickListeners();
        setInfoResources(getTitleRes(), getDescriptionRes(), -1);
        getPassButton().setEnabled(false);
        requestPermissions(new String[]{Manifest.permission.USE_BIOMETRIC},
                BIOMETRIC_REQUEST_PERMISSION_CODE);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) {
        if (requestCode == BIOMETRIC_REQUEST_PERMISSION_CODE
                && state[0] == PackageManager.PERMISSION_GRANTED) {
            mBiometricManager = getSystemService(BiometricManager.class);
            mKeyguardManager = getSystemService(KeyguardManager.class);
            Button startTestButton = findViewById(R.id.sec_start_test_button);

            if (!mKeyguardManager.isKeyguardSecure()) {
                // Show a message that the user hasn't set up a lock screen.
                showToast( "Secure lock screen hasn't been set up.\n Go to "
                                + "'Settings -> Security -> Screen lock' to set up a lock screen");
                startTestButton.setEnabled(false);
                return;
            }

            startTestButton.setOnClickListener(v -> startTest());
        }
    }

    protected void showToast(String message) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
    }

    private void provisionFoo(IdentityCredentialStore store) throws Exception {
        store.deleteCredentialByName("test");
        WritableIdentityCredential wc = store.createCredential("test",
                "org.iso.18013-5.2019.mdl");

        // 'Bar' encoded as CBOR tstr
        byte[] barCbor = {0x63, 0x42, 0x61, 0x72};

        AccessControlProfile acp = new AccessControlProfile.Builder(new AccessControlProfileId(0))
                .setUserAuthenticationRequired(true)
                .setUserAuthenticationTimeout(0)
                .build();
        LinkedList<AccessControlProfileId> idsProfile0 = new LinkedList<AccessControlProfileId>();
        idsProfile0.add(new AccessControlProfileId(0));
        PersonalizationData pd = new PersonalizationData.Builder()
                                 .addAccessControlProfile(acp)
                                 .putEntry("org.iso.18013-5.2019", "Foo", idsProfile0, barCbor)
                                 .build();
        byte[] proofOfProvisioningSignature = wc.personalize(pd);

        // Create authentication keys.
        IdentityCredential credential = store.getCredentialByName("test",
                IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
        credential.setAvailableAuthenticationKeys(1, 10);
        Collection<X509Certificate> dynAuthKeyCerts = credential.getAuthKeysNeedingCertification();
        credential.storeStaticAuthenticationData(dynAuthKeyCerts.iterator().next(), new byte[0]);
    }

    private boolean getFooAndCheckNotRetrievable(IdentityCredential credential) throws Exception {
        Map<String, Collection<String>> entriesToRequest = new LinkedHashMap<>();
        entriesToRequest.put("org.iso.18013-5.2019", Arrays.asList("Foo"));

        ResultData rd = credential.getEntries(
            createItemsRequest(entriesToRequest, null),
            entriesToRequest,
            null,  // sessionTranscript
            null); // readerSignature
        if (rd.getStatus("org.iso.18013-5.2019", "Foo")
                != ResultData.STATUS_USER_AUTHENTICATION_FAILED) {
            return false;
        }
        return true;
    }

    private boolean getFooAndCheckRetrievable(IdentityCredential credential) throws Exception {
        Map<String, Collection<String>> entriesToRequest = new LinkedHashMap<>();
        entriesToRequest.put("org.iso.18013-5.2019", Arrays.asList("Foo"));

        ResultData rd = credential.getEntries(
            createItemsRequest(entriesToRequest, null),
            entriesToRequest,
            null,  // sessionTranscript
            null); // readerSignature
        if (rd.getStatus("org.iso.18013-5.2019", "Foo") != ResultData.STATUS_OK) {
            return false;
        }
        return true;
    }

    protected void startTest() {
        IdentityCredentialStore store = IdentityCredentialStore.getInstance(this);
        if (store == null) {
            showToast("No Identity Credential support, test passed.");
            getPassButton().setEnabled(true);
            return;
        }

        final int result = mBiometricManager.canAuthenticate(Authenticators.BIOMETRIC_STRONG);
        switch (result) {
            case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
                showToast("No strong biometrics (Class 3) enrolled.\n"
                        + "Go to 'Settings -> Security' to enroll");
                Button startTestButton = findViewById(R.id.sec_start_test_button);
                startTestButton.setEnabled(false);
                return;
            case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
                showToast("No strong biometrics (Class 3), test passed.");
                showToast("No Identity Credential support, test passed.");
                getPassButton().setEnabled(true);
                return;
        }

        try {
            provisionFoo(store);

            // First, check that Foo cannot be retrieved without authentication.
            //
            IdentityCredential credentialWithoutAuth = store.getCredentialByName("test",
                    IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
            if (!getFooAndCheckNotRetrievable(credentialWithoutAuth)) {
                showToast("Failed while checking that data element cannot be retrieved without"
                        + " authentication");
                return;
            }

            // Try one more time, this time with a CryptoObject that we'll use with
            // BiometricPrompt. This should work.
            //
            final IdentityCredential credentialWithAuth = store.getCredentialByName("test",
                    IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
            CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(credentialWithAuth);
            BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
            builder.setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG);
            builder.setTitle("Identity Credential");
            builder.setDescription("Authenticate to unlock credential.");
            builder.setNegativeButton("Cancel",
                    getMainExecutor(),
                    (dialogInterface, i) -> showToast("Canceled biometric prompt."));
            final BiometricPrompt prompt = builder.build();
            final AuthenticationCallback callback = new AuthenticationCallback() {
                @Override
                public void onAuthenticationSucceeded(AuthenticationResult authResult) {
                    try {
                        // Check that Foo can be retrieved because we used
                        // the CryptoObject to auth with.
                        if (!getFooAndCheckRetrievable(credentialWithAuth)) {
                            showToast("Failed while checking that data element can be"
                                    + " retrieved with authentication");
                            return;
                        }

                        // Finally, check that Foo cannot be retrieved again.
                        IdentityCredential credentialWithoutAuth2 = store.getCredentialByName(
                                "test",
                                IdentityCredentialStore
                                        .CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
                        if (!getFooAndCheckNotRetrievable(credentialWithoutAuth2)) {
                            showToast("Failed while checking that data element cannot be"
                                    + " retrieved without authentication");
                            return;
                        }

                        showToast("Test passed.");
                        getPassButton().setEnabled(true);

                    } catch (Exception e) {
                        showToast("Unexpection exception " + e);
                    }
                }
            };

            prompt.authenticate(cryptoObject, new CancellationSignal(), getMainExecutor(),
                    callback);
        } catch (Exception e) {
            showToast("Unexpection exception " + e);
        }
    }


    /*
     * Helper function to create a CBOR data for requesting data items. The IntentToRetain
     * value will be set to false for all elements.
     *
     * <p>The returned CBOR data conforms to the following CDDL schema:</p>
     *
     * <pre>
     *   ItemsRequest = {
     *     ? "docType" : DocType,
     *     "nameSpaces" : NameSpaces,
     *     ? "RequestInfo" : {* tstr => any} ; Additional info the reader wants to provide
     *   }
     *
     *   NameSpaces = {
     *     + NameSpace => DataElements     ; Requested data elements for each NameSpace
     *   }
     *
     *   DataElements = {
     *     + DataElement => IntentToRetain
     *   }
     *
     *   DocType = tstr
     *
     *   DataElement = tstr
     *   IntentToRetain = bool
     *   NameSpace = tstr
     * </pre>
     *
     * @param entriesToRequest       The entries to request, organized as a map of namespace
     *                               names with each value being a collection of data elements
     *                               in the given namespace.
     * @param docType                  The document type or {@code null} if there is no document
     *                                 type.
     * @return CBOR data conforming to the CDDL mentioned above.
     */
    private static @NonNull byte[] createItemsRequest(
            @NonNull Map<String, Collection<String>> entriesToRequest,
            @Nullable String docType) {
        CborBuilder builder = new CborBuilder();
        MapBuilder<CborBuilder> mapBuilder = builder.addMap();
        if (docType != null) {
            mapBuilder.put("docType", docType);
        }

        MapBuilder<MapBuilder<CborBuilder>> nsMapBuilder = mapBuilder.putMap("nameSpaces");
        for (String namespaceName : entriesToRequest.keySet()) {
            Collection<String> entryNames = entriesToRequest.get(namespaceName);
            MapBuilder<MapBuilder<MapBuilder<CborBuilder>>> entryNameMapBuilder =
                    nsMapBuilder.putMap(namespaceName);
            for (String entryName : entryNames) {
                entryNameMapBuilder.put(entryName, false);
            }
        }

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        CborEncoder encoder = new CborEncoder(baos);
        try {
            encoder.encode(builder.build());
        } catch (CborException e) {
            throw new RuntimeException("Error encoding CBOR", e);
        }
        return baos.toByteArray();
    }



}
