/*
 * Copyright 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 au.gov.health.covidsafe.security.crypto;

import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Arrays;

import javax.crypto.KeyGenerator;

/**
 * Convenient methods to create and obtain master keys in Android Keystore.
 *
 * <p>The master keys are used to encrypt data encryption keys for encrypting files and preferences.
 */
public final class MasterKeys {
    private MasterKeys() {
    }

    private static final int KEY_SIZE = 256;

    private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
    static final String KEYSTORE_PATH_URI = "android-keystore://";
    static final String MASTER_KEY_ALIAS = "_androidx_security_master_key_";

    @NonNull
    public static final KeyGenParameterSpec AES256_GCM_SPEC =
            createAES256GCMKeyGenParameterSpec(MASTER_KEY_ALIAS);

    /**
     * Provides a safe and easy to use KenGenParameterSpec with the settings.
     * Algorithm: AES
     * Block Mode: GCM
     * Padding: No Padding
     * Key Size: 256
     *
     * @param keyAlias The alias for the master key
     * @return The spec for the master key with the specified keyAlias
     */
    @NonNull
    private static KeyGenParameterSpec createAES256GCMKeyGenParameterSpec(
            @NonNull String keyAlias) {
        KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
                keyAlias,
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .setKeySize(KEY_SIZE);
        return builder.build();
    }

    /**
     * Creates or gets the master key provided
     *
     * The encryption scheme is required fields to ensure that the type of
     * encryption used is clear to developers.
     *
     * @param keyGenParameterSpec The key encryption scheme
     * @return The key alias for the master key
     */
    @NonNull
    public static String getOrCreate(
            @NonNull KeyGenParameterSpec keyGenParameterSpec)
            throws GeneralSecurityException, IOException {
        validate(keyGenParameterSpec);
        if (!MasterKeys.keyExists(keyGenParameterSpec.getKeystoreAlias())) {
            generateKey(keyGenParameterSpec);
        }
        return keyGenParameterSpec.getKeystoreAlias();
    }

    @VisibleForTesting
    static void validate(KeyGenParameterSpec spec) {
        if (spec.getKeySize() != KEY_SIZE) {
            throw new IllegalArgumentException(
                    "invalid key size, want " + KEY_SIZE + " bits got " + spec.getKeySize()
                            + " bits");
        }
        if (!Arrays.equals(spec.getBlockModes(), new String[]{KeyProperties.BLOCK_MODE_GCM})) {
            throw new IllegalArgumentException(
                    "invalid block mode, want " + KeyProperties.BLOCK_MODE_GCM + " got "
                            + Arrays.toString(spec.getBlockModes()));
        }
        if (spec.getPurposes() != (KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)) {
            throw new IllegalArgumentException(
                    "invalid purposes mode, want PURPOSE_ENCRYPT | PURPOSE_DECRYPT got "
                            + spec.getPurposes());
        }
        if (!Arrays.equals(spec.getEncryptionPaddings(), new String[]
                {KeyProperties.ENCRYPTION_PADDING_NONE})) {
            throw new IllegalArgumentException(
                    "invalid padding mode, want " + KeyProperties.ENCRYPTION_PADDING_NONE + " got "
                            + Arrays.toString(spec.getEncryptionPaddings()));
        }
        if (spec.isUserAuthenticationRequired()
                && spec.getUserAuthenticationValidityDurationSeconds() < 1) {
            throw new IllegalArgumentException(
                    "per-operation authentication is not supported "
                            + "(UserAuthenticationValidityDurationSeconds must be >0)");
        }
    }

    private static void generateKey(@NonNull KeyGenParameterSpec keyGenParameterSpec)
            throws GeneralSecurityException {
        KeyGenerator keyGenerator = KeyGenerator.getInstance(
                KeyProperties.KEY_ALGORITHM_AES,
                ANDROID_KEYSTORE);
        keyGenerator.init(keyGenParameterSpec);
        keyGenerator.generateKey();
    }

    private static boolean keyExists(@NonNull String keyAlias)
            throws GeneralSecurityException, IOException {
        KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
        keyStore.load(null);
        return keyStore.containsAlias(keyAlias);
    }

}
