/*
 * Copyright (C) 2015 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.security.Credentials;
import android.security.KeyStore;
import android.security.keymaster.KeyCharacteristics;
import android.security.keymaster.KeymasterDefs;

import java.security.InvalidKeyException;
import java.security.ProviderException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactorySpi;
import javax.crypto.spec.SecretKeySpec;

/**
 * {@link SecretKeyFactorySpi} backed by Android Keystore.
 *
 * @hide
 */
public class AndroidKeyStoreSecretKeyFactorySpi extends SecretKeyFactorySpi {

    private final KeyStore mKeyStore = KeyStore.getInstance();

    @Override
    protected KeySpec engineGetKeySpec(SecretKey key,
            @SuppressWarnings("rawtypes") Class keySpecClass) throws InvalidKeySpecException {
        if (keySpecClass == null) {
            throw new InvalidKeySpecException("keySpecClass == null");
        }
        if (!(key instanceof AndroidKeyStoreSecretKey)) {
            throw new InvalidKeySpecException("Only Android KeyStore secret keys supported: " +
                    ((key != null) ? key.getClass().getName() : "null"));
        }
        if (SecretKeySpec.class.isAssignableFrom(keySpecClass)) {
            throw new InvalidKeySpecException(
                    "Key material export of Android KeyStore keys is not supported");
        }
        if (!KeyInfo.class.equals(keySpecClass)) {
            throw new InvalidKeySpecException("Unsupported key spec: " + keySpecClass.getName());
        }
        String keyAliasInKeystore = ((AndroidKeyStoreKey) key).getAlias();
        String entryAlias;
        if (keyAliasInKeystore.startsWith(Credentials.USER_SECRET_KEY)) {
            entryAlias = keyAliasInKeystore.substring(Credentials.USER_SECRET_KEY.length());
        } else {
            throw new InvalidKeySpecException("Invalid key alias: " + keyAliasInKeystore);
        }

        return getKeyInfo(mKeyStore, entryAlias, keyAliasInKeystore);
    }

    static KeyInfo getKeyInfo(KeyStore keyStore, String entryAlias, String keyAliasInKeystore) {
        KeyCharacteristics keyCharacteristics = new KeyCharacteristics();
        int errorCode =
                keyStore.getKeyCharacteristics(keyAliasInKeystore, null, null, keyCharacteristics);
        if (errorCode != KeyStore.NO_ERROR) {
            throw new ProviderException("Failed to obtain information about key."
                    + " Keystore error: " + errorCode);
        }

        boolean insideSecureHardware;
        @KeyProperties.OriginEnum int origin;
        int keySize;
        @KeyProperties.PurposeEnum int purposes;
        String[] encryptionPaddings;
        String[] signaturePaddings;
        @KeyProperties.DigestEnum String[] digests;
        @KeyProperties.BlockModeEnum String[] blockModes;
        int keymasterSwEnforcedUserAuthenticators;
        int keymasterHwEnforcedUserAuthenticators;
        try {
            if (keyCharacteristics.hwEnforced.containsTag(KeymasterDefs.KM_TAG_ORIGIN)) {
                insideSecureHardware = true;
                origin = KeyProperties.Origin.fromKeymaster(
                        keyCharacteristics.hwEnforced.getEnum(KeymasterDefs.KM_TAG_ORIGIN, -1));
            } else if (keyCharacteristics.swEnforced.containsTag(KeymasterDefs.KM_TAG_ORIGIN)) {
                insideSecureHardware = false;
                origin = KeyProperties.Origin.fromKeymaster(
                        keyCharacteristics.swEnforced.getEnum(KeymasterDefs.KM_TAG_ORIGIN, -1));
            } else {
                throw new ProviderException("Key origin not available");
            }
            long keySizeUnsigned =
                    keyCharacteristics.getUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, -1);
            if (keySizeUnsigned == -1) {
                throw new ProviderException("Key size not available");
            } else if (keySizeUnsigned > Integer.MAX_VALUE) {
                throw new ProviderException("Key too large: " + keySizeUnsigned + " bits");
            }
            keySize = (int) keySizeUnsigned;
            purposes = KeyProperties.Purpose.allFromKeymaster(
                    keyCharacteristics.getEnums(KeymasterDefs.KM_TAG_PURPOSE));

            List<String> encryptionPaddingsList = new ArrayList<String>();
            List<String> signaturePaddingsList = new ArrayList<String>();
            // Keymaster stores both types of paddings in the same array -- we split it into two.
            for (int keymasterPadding : keyCharacteristics.getEnums(KeymasterDefs.KM_TAG_PADDING)) {
                try {
                    @KeyProperties.EncryptionPaddingEnum String jcaPadding =
                            KeyProperties.EncryptionPadding.fromKeymaster(keymasterPadding);
                    encryptionPaddingsList.add(jcaPadding);
                } catch (IllegalArgumentException e) {
                    try {
                        @KeyProperties.SignaturePaddingEnum String padding =
                                KeyProperties.SignaturePadding.fromKeymaster(keymasterPadding);
                        signaturePaddingsList.add(padding);
                    } catch (IllegalArgumentException e2) {
                        throw new ProviderException(
                                "Unsupported encryption padding: " + keymasterPadding);
                    }
                }

            }
            encryptionPaddings =
                    encryptionPaddingsList.toArray(new String[encryptionPaddingsList.size()]);
            signaturePaddings =
                    signaturePaddingsList.toArray(new String[signaturePaddingsList.size()]);

            digests = KeyProperties.Digest.allFromKeymaster(
                    keyCharacteristics.getEnums(KeymasterDefs.KM_TAG_DIGEST));
            blockModes = KeyProperties.BlockMode.allFromKeymaster(
                    keyCharacteristics.getEnums(KeymasterDefs.KM_TAG_BLOCK_MODE));
            keymasterSwEnforcedUserAuthenticators =
                    keyCharacteristics.swEnforced.getEnum(KeymasterDefs.KM_TAG_USER_AUTH_TYPE, 0);
            keymasterHwEnforcedUserAuthenticators =
                    keyCharacteristics.hwEnforced.getEnum(KeymasterDefs.KM_TAG_USER_AUTH_TYPE, 0);
        } catch (IllegalArgumentException e) {
            throw new ProviderException("Unsupported key characteristic", e);
        }

        Date keyValidityStart = keyCharacteristics.getDate(KeymasterDefs.KM_TAG_ACTIVE_DATETIME);
        Date keyValidityForOriginationEnd =
                keyCharacteristics.getDate(KeymasterDefs.KM_TAG_ORIGINATION_EXPIRE_DATETIME);
        Date keyValidityForConsumptionEnd =
                keyCharacteristics.getDate(KeymasterDefs.KM_TAG_USAGE_EXPIRE_DATETIME);
        boolean userAuthenticationRequired =
                !keyCharacteristics.getBoolean(KeymasterDefs.KM_TAG_NO_AUTH_REQUIRED);
        long userAuthenticationValidityDurationSeconds =
                keyCharacteristics.getUnsignedInt(KeymasterDefs.KM_TAG_AUTH_TIMEOUT, -1);
        if (userAuthenticationValidityDurationSeconds > Integer.MAX_VALUE) {
            throw new ProviderException("User authentication timeout validity too long: "
                    + userAuthenticationValidityDurationSeconds + " seconds");
        }
        boolean userAuthenticationRequirementEnforcedBySecureHardware = (userAuthenticationRequired)
                && (keymasterHwEnforcedUserAuthenticators != 0)
                && (keymasterSwEnforcedUserAuthenticators == 0);

        return new KeyInfo(entryAlias,
                insideSecureHardware,
                origin,
                keySize,
                keyValidityStart,
                keyValidityForOriginationEnd,
                keyValidityForConsumptionEnd,
                purposes,
                encryptionPaddings,
                signaturePaddings,
                digests,
                blockModes,
                userAuthenticationRequired,
                (int) userAuthenticationValidityDurationSeconds,
                userAuthenticationRequirementEnforcedBySecureHardware);
    }

    @Override
    protected SecretKey engineGenerateSecret(KeySpec keySpec) throws InvalidKeySpecException {
        throw new InvalidKeySpecException(
                "To generate secret key in Android Keystore, use KeyGenerator initialized with "
                        + KeyGenParameterSpec.class.getName());
    }

    @Override
    protected SecretKey engineTranslateKey(SecretKey key) throws InvalidKeyException {
        if (key == null) {
            throw new InvalidKeyException("key == null");
        } else if (!(key instanceof AndroidKeyStoreSecretKey)) {
            throw new InvalidKeyException(
                    "To import a secret key into Android Keystore, use KeyStore.setEntry");
        }

        return key;
    }
}
