/*
 * Copyright (C) 2012 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 android.security.keystore;

import android.annotation.NonNull;
import android.annotation.UnsupportedAppUsage;
import android.security.KeyStore;
import android.security.keymaster.ExportResult;
import android.security.keymaster.KeyCharacteristics;
import android.security.keymaster.KeymasterDefs;

import java.io.IOException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Provider;
import java.security.ProviderException;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.interfaces.ECKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.List;

import javax.crypto.Cipher;
import javax.crypto.Mac;

/**
 * A provider focused on providing JCA interfaces for the Android KeyStore.
 *
 * @hide
 */
public class AndroidKeyStoreProvider extends Provider {
    public static final String PROVIDER_NAME = "AndroidKeyStore";

    // IMPLEMENTATION NOTE: Class names are hard-coded in this provider to avoid loading these
    // classes when this provider is instantiated and installed early on during each app's
    // initialization process.
    //
    // Crypto operations operating on the AndroidKeyStore keys must not be offered by this provider.
    // Instead, they need to be offered by AndroidKeyStoreBCWorkaroundProvider. See its Javadoc
    // for details.

    private static final String PACKAGE_NAME = "android.security.keystore";

    private static final String DESEDE_SYSTEM_PROPERTY =
            "ro.hardware.keystore_desede";

    public AndroidKeyStoreProvider() {
        super(PROVIDER_NAME, 1.0, "Android KeyStore security provider");

        boolean supports3DES = "true".equals(android.os.SystemProperties.get(DESEDE_SYSTEM_PROPERTY));

        // java.security.KeyStore
        put("KeyStore.AndroidKeyStore", PACKAGE_NAME + ".AndroidKeyStoreSpi");

        // java.security.KeyPairGenerator
        put("KeyPairGenerator.EC", PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$EC");
        put("KeyPairGenerator.RSA", PACKAGE_NAME +  ".AndroidKeyStoreKeyPairGeneratorSpi$RSA");

        // java.security.KeyFactory
        putKeyFactoryImpl("EC");
        putKeyFactoryImpl("RSA");

        // javax.crypto.KeyGenerator
        put("KeyGenerator.AES", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$AES");
        put("KeyGenerator.HmacSHA1", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA1");
        put("KeyGenerator.HmacSHA224", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA224");
        put("KeyGenerator.HmacSHA256", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA256");
        put("KeyGenerator.HmacSHA384", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA384");
        put("KeyGenerator.HmacSHA512", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA512");

        if (supports3DES) {
            put("KeyGenerator.DESede", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$DESede");
        }

        // java.security.SecretKeyFactory
        putSecretKeyFactoryImpl("AES");
        if (supports3DES) {
            putSecretKeyFactoryImpl("DESede");
        }
        putSecretKeyFactoryImpl("HmacSHA1");
        putSecretKeyFactoryImpl("HmacSHA224");
        putSecretKeyFactoryImpl("HmacSHA256");
        putSecretKeyFactoryImpl("HmacSHA384");
        putSecretKeyFactoryImpl("HmacSHA512");
    }

    /**
     * Installs a new instance of this provider (and the
     * {@link AndroidKeyStoreBCWorkaroundProvider}).
     */
    public static void install() {
        Provider[] providers = Security.getProviders();
        int bcProviderIndex = -1;
        for (int i = 0; i < providers.length; i++) {
            Provider provider = providers[i];
            if ("BC".equals(provider.getName())) {
                bcProviderIndex = i;
                break;
            }
        }

        Security.addProvider(new AndroidKeyStoreProvider());
        Provider workaroundProvider = new AndroidKeyStoreBCWorkaroundProvider();
        if (bcProviderIndex != -1) {
            // Bouncy Castle provider found -- install the workaround provider above it.
            // insertProviderAt uses 1-based positions.
            Security.insertProviderAt(workaroundProvider, bcProviderIndex + 1);
        } else {
            // Bouncy Castle provider not found -- install the workaround provider at lowest
            // priority.
            Security.addProvider(workaroundProvider);
        }
    }

    private void putSecretKeyFactoryImpl(String algorithm) {
        put("SecretKeyFactory." + algorithm, PACKAGE_NAME + ".AndroidKeyStoreSecretKeyFactorySpi");
    }

    private void putKeyFactoryImpl(String algorithm) {
        put("KeyFactory." + algorithm, PACKAGE_NAME + ".AndroidKeyStoreKeyFactorySpi");
    }

    /**
     * Gets the {@link KeyStore} operation handle corresponding to the provided JCA crypto
     * primitive.
     *
     * <p>The following primitives are supported: {@link Cipher} and {@link Mac}.
     *
     * @return KeyStore operation handle or {@code 0} if the provided primitive's KeyStore operation
     *         is not in progress.
     *
     * @throws IllegalArgumentException if the provided primitive is not supported or is not backed
     *         by AndroidKeyStore provider.
     * @throws IllegalStateException if the provided primitive is not initialized.
     */
    @UnsupportedAppUsage
    public static long getKeyStoreOperationHandle(Object cryptoPrimitive) {
        if (cryptoPrimitive == null) {
            throw new NullPointerException();
        }
        Object spi;
        if (cryptoPrimitive instanceof Signature) {
            spi = ((Signature) cryptoPrimitive).getCurrentSpi();
        } else if (cryptoPrimitive instanceof Mac) {
            spi = ((Mac) cryptoPrimitive).getCurrentSpi();
        } else if (cryptoPrimitive instanceof Cipher) {
            spi = ((Cipher) cryptoPrimitive).getCurrentSpi();
        } else {
            throw new IllegalArgumentException("Unsupported crypto primitive: " + cryptoPrimitive
                    + ". Supported: Signature, Mac, Cipher");
        }
        if (spi == null) {
            throw new IllegalStateException("Crypto primitive not initialized");
        } else if (!(spi instanceof KeyStoreCryptoOperation)) {
            throw new IllegalArgumentException(
                    "Crypto primitive not backed by AndroidKeyStore provider: " + cryptoPrimitive
                    + ", spi: " + spi);
        }
        return ((KeyStoreCryptoOperation) spi).getOperationHandle();
    }

    @NonNull
    public static AndroidKeyStorePublicKey getAndroidKeyStorePublicKey(
            @NonNull String alias,
            int uid,
            @NonNull @KeyProperties.KeyAlgorithmEnum String keyAlgorithm,
            @NonNull byte[] x509EncodedForm) {
        PublicKey publicKey;
        try {
            KeyFactory keyFactory = KeyFactory.getInstance(keyAlgorithm);
            publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(x509EncodedForm));
        } catch (NoSuchAlgorithmException e) {
            throw new ProviderException(
                    "Failed to obtain " + keyAlgorithm + " KeyFactory", e);
        } catch (InvalidKeySpecException e) {
            throw new ProviderException("Invalid X.509 encoding of public key", e);
        }
        if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(keyAlgorithm)) {
            return new AndroidKeyStoreECPublicKey(alias, uid, (ECPublicKey) publicKey);
        } else if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(keyAlgorithm)) {
            return new AndroidKeyStoreRSAPublicKey(alias, uid, (RSAPublicKey) publicKey);
        } else {
            throw new ProviderException("Unsupported Android Keystore public key algorithm: "
                    + keyAlgorithm);
        }
    }

    @NonNull
    private static AndroidKeyStorePrivateKey getAndroidKeyStorePrivateKey(
            @NonNull AndroidKeyStorePublicKey publicKey) {
        String keyAlgorithm = publicKey.getAlgorithm();
        if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(keyAlgorithm)) {
            return new AndroidKeyStoreECPrivateKey(
                    publicKey.getAlias(), publicKey.getUid(), ((ECKey) publicKey).getParams());
        } else if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(keyAlgorithm)) {
            return new AndroidKeyStoreRSAPrivateKey(
                    publicKey.getAlias(), publicKey.getUid(), ((RSAKey) publicKey).getModulus());
        } else {
            throw new ProviderException("Unsupported Android Keystore public key algorithm: "
                    + keyAlgorithm);
        }
    }

    @NonNull
    private static KeyCharacteristics getKeyCharacteristics(@NonNull KeyStore keyStore,
            @NonNull String alias, int uid)
            throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
        KeyCharacteristics keyCharacteristics = new KeyCharacteristics();
        int errorCode = keyStore.getKeyCharacteristics(
                alias, null, null, uid, keyCharacteristics);
        if (errorCode == KeyStore.KEY_PERMANENTLY_INVALIDATED) {
            throw (KeyPermanentlyInvalidatedException)
                new KeyPermanentlyInvalidatedException(
                            "User changed or deleted their auth credentials",
                            KeyStore.getKeyStoreException(errorCode));
        }
        if (errorCode != KeyStore.NO_ERROR) {
            throw (UnrecoverableKeyException)
                    new UnrecoverableKeyException("Failed to obtain information about key")
                            .initCause(KeyStore.getKeyStoreException(errorCode));
        }
        return keyCharacteristics;
    }

    @NonNull
    private static AndroidKeyStorePublicKey loadAndroidKeyStorePublicKeyFromKeystore(
            @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid,
            KeyCharacteristics keyCharacteristics)
            throws UnrecoverableKeyException {
        ExportResult exportResult = keyStore.exportKey(
                privateKeyAlias, KeymasterDefs.KM_KEY_FORMAT_X509, null, null, uid);
        if (exportResult.resultCode != KeyStore.NO_ERROR) {
            throw (UnrecoverableKeyException)
                    new UnrecoverableKeyException("Failed to obtain X.509 form of public key")
                    .initCause(KeyStore.getKeyStoreException(exportResult.resultCode));
        }
        final byte[] x509EncodedPublicKey = exportResult.exportData;

        Integer keymasterAlgorithm = keyCharacteristics.getEnum(KeymasterDefs.KM_TAG_ALGORITHM);
        if (keymasterAlgorithm == null) {
            throw new UnrecoverableKeyException("Key algorithm unknown");
        }

        String jcaKeyAlgorithm;
        try {
            jcaKeyAlgorithm = KeyProperties.KeyAlgorithm.fromKeymasterAsymmetricKeyAlgorithm(
                    keymasterAlgorithm);
        } catch (IllegalArgumentException e) {
            throw (UnrecoverableKeyException)
                    new UnrecoverableKeyException("Failed to load private key")
                    .initCause(e);
        }

        return AndroidKeyStoreProvider.getAndroidKeyStorePublicKey(
                privateKeyAlias, uid, jcaKeyAlgorithm, x509EncodedPublicKey);
    }

    @NonNull
    public static AndroidKeyStorePublicKey loadAndroidKeyStorePublicKeyFromKeystore(
            @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid)
            throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
        return loadAndroidKeyStorePublicKeyFromKeystore(keyStore, privateKeyAlias, uid,
                getKeyCharacteristics(keyStore, privateKeyAlias, uid));
    }

    @NonNull
    private static KeyPair loadAndroidKeyStoreKeyPairFromKeystore(
            @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid,
            @NonNull KeyCharacteristics keyCharacteristics)
            throws UnrecoverableKeyException {
        AndroidKeyStorePublicKey publicKey =
                loadAndroidKeyStorePublicKeyFromKeystore(keyStore, privateKeyAlias, uid,
                        keyCharacteristics);
        AndroidKeyStorePrivateKey privateKey =
                AndroidKeyStoreProvider.getAndroidKeyStorePrivateKey(publicKey);
        return new KeyPair(publicKey, privateKey);
    }

    @NonNull
    public static KeyPair loadAndroidKeyStoreKeyPairFromKeystore(
            @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid)
            throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
        return loadAndroidKeyStoreKeyPairFromKeystore(keyStore, privateKeyAlias, uid,
                getKeyCharacteristics(keyStore, privateKeyAlias, uid));
    }

    @NonNull
    private static AndroidKeyStorePrivateKey loadAndroidKeyStorePrivateKeyFromKeystore(
            @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid,
            @NonNull KeyCharacteristics keyCharacteristics)
            throws UnrecoverableKeyException {
        KeyPair keyPair = loadAndroidKeyStoreKeyPairFromKeystore(keyStore, privateKeyAlias, uid,
                keyCharacteristics);
        return (AndroidKeyStorePrivateKey) keyPair.getPrivate();
    }

    @NonNull
    public static AndroidKeyStorePrivateKey loadAndroidKeyStorePrivateKeyFromKeystore(
            @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid)
            throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
        return loadAndroidKeyStorePrivateKeyFromKeystore(keyStore, privateKeyAlias, uid,
                getKeyCharacteristics(keyStore, privateKeyAlias, uid));
    }

    @NonNull
    private static AndroidKeyStoreSecretKey loadAndroidKeyStoreSecretKeyFromKeystore(
            @NonNull String secretKeyAlias, int uid, @NonNull KeyCharacteristics keyCharacteristics)
            throws UnrecoverableKeyException {
        Integer keymasterAlgorithm = keyCharacteristics.getEnum(KeymasterDefs.KM_TAG_ALGORITHM);
        if (keymasterAlgorithm == null) {
            throw new UnrecoverableKeyException("Key algorithm unknown");
        }

        List<Integer> keymasterDigests = keyCharacteristics.getEnums(KeymasterDefs.KM_TAG_DIGEST);
        int keymasterDigest;
        if (keymasterDigests.isEmpty()) {
            keymasterDigest = -1;
        } else {
            // More than one digest can be permitted for this key. Use the first one to form the
            // JCA key algorithm name.
            keymasterDigest = keymasterDigests.get(0);
        }

        @KeyProperties.KeyAlgorithmEnum String keyAlgorithmString;
        try {
            keyAlgorithmString = KeyProperties.KeyAlgorithm.fromKeymasterSecretKeyAlgorithm(
                    keymasterAlgorithm, keymasterDigest);
        } catch (IllegalArgumentException e) {
            throw (UnrecoverableKeyException)
                    new UnrecoverableKeyException("Unsupported secret key type").initCause(e);
        }

        return new AndroidKeyStoreSecretKey(secretKeyAlias, uid, keyAlgorithmString);
    }

    @NonNull
    public static AndroidKeyStoreKey loadAndroidKeyStoreKeyFromKeystore(
            @NonNull KeyStore keyStore, @NonNull String userKeyAlias, int uid)
            throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException  {
        KeyCharacteristics keyCharacteristics = getKeyCharacteristics(keyStore, userKeyAlias, uid);

        Integer keymasterAlgorithm = keyCharacteristics.getEnum(KeymasterDefs.KM_TAG_ALGORITHM);
        if (keymasterAlgorithm == null) {
            throw new UnrecoverableKeyException("Key algorithm unknown");
        }

        if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC ||
                keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_AES ||
                keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_3DES) {
            return loadAndroidKeyStoreSecretKeyFromKeystore(userKeyAlias, uid,
                    keyCharacteristics);
        } else if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_RSA ||
                keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_EC) {
            return loadAndroidKeyStorePrivateKeyFromKeystore(keyStore, userKeyAlias, uid,
                    keyCharacteristics);
        } else {
            throw new UnrecoverableKeyException("Key algorithm unknown");
        }
    }

    /**
     * Returns an {@code AndroidKeyStore} {@link java.security.KeyStore}} of the specified UID.
     * The {@code KeyStore} contains keys and certificates owned by that UID. Such cross-UID
     * access is permitted to a few system UIDs and only to a few other UIDs (e.g., Wi-Fi, VPN)
     * all of which are system.
     *
     * <p>Note: the returned {@code KeyStore} is already initialized/loaded. Thus, there is
     * no need to invoke {@code load} on it.
     */
    @NonNull
    public static java.security.KeyStore getKeyStoreForUid(int uid)
            throws KeyStoreException, NoSuchProviderException {
        java.security.KeyStore result =
                java.security.KeyStore.getInstance("AndroidKeyStore", PROVIDER_NAME);
        try {
            result.load(new AndroidKeyStoreLoadStoreParameter(uid));
        } catch (NoSuchAlgorithmException | CertificateException | IOException e) {
            throw new KeyStoreException(
                    "Failed to load AndroidKeyStore KeyStore for UID " + uid, e);
        }
        return result;
    }
}
