MasterKey.java

/*
 * Copyright 2020 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 androidx.security.crypto;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;

/**
 * Wrapper for a master key used in the library.
 *
 * On Android M (API 23) and above, this is class references a key that's stored in the
 * Android Keystore. On Android L (API 21, 22), there isn't a master key.
 */
public final class MasterKey {
    static final String KEYSTORE_PATH_URI = "android-keystore://";

    /**
     * The default master key alias.
     */
    public static final String DEFAULT_MASTER_KEY_ALIAS = "_androidx_security_master_key_";

    /**
     * The default and recommended size for the master key.
     */
    public static final int DEFAULT_AES_GCM_MASTER_KEY_SIZE = 256;

    private static final int DEFAULT_AUTHENTICATION_VALIDITY_DURATION_SECONDS = 5 * 60;

    @NonNull
    private final String mKeyAlias;
    @Nullable
    private final KeyGenParameterSpec mKeyGenParameterSpec;

    /**
     * Algorithm/Cipher choices used for the master key.
     */
    public enum KeyScheme {
        AES256_GCM
    }

    /**
     * The default validity period for authentication in seconds.
     */
    @SuppressLint("MethodNameUnits")
    public static int getDefaultAuthenticationValidityDurationSeconds() {
        return DEFAULT_AUTHENTICATION_VALIDITY_DURATION_SECONDS;
    }

    /* package */ MasterKey(@NonNull String keyAlias, @Nullable Object keyGenParameterSpec) {
        mKeyAlias = keyAlias;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            mKeyGenParameterSpec = (KeyGenParameterSpec) keyGenParameterSpec;
        } else {
            mKeyGenParameterSpec = null;
        }
    }

    /**
     * Checks if this key is backed by the Android Keystore.
     *
     * @return {@code true} if the key is in Android Keystore, {@code false} otherwise. This
     * method always returns false when called on Android Lollipop (API 21 and 22).
     */
    public boolean isKeyStoreBacked() {
        // Keystore is not used prior to Android M (API 23)
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return false;
        }

        try {
            KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
            keyStore.load(null);
            return keyStore.containsAlias(mKeyAlias);
        } catch (KeyStoreException | CertificateException
                | NoSuchAlgorithmException | IOException ignored) {
            return false;
        }
    }

    /**
     * Gets whether user authentication is required to use this key.
     *
     * This method always returns {@code false} on Android L (API 21 + 22).
     */
    public boolean isUserAuthenticationRequired() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return false;
        }
        return mKeyGenParameterSpec != null && mKeyGenParameterSpec.isUserAuthenticationRequired();
    }

    /**
     * Gets the duration in seconds that the key is unlocked for following user authentication.
     *
     * The value returned for this method is only meaningful on Android M+ (API 23) when
     * {@link #isUserAuthenticationRequired()} returns {@code true}.
     *
     * @return The duration the key is unlocked for in seconds.
     */
    @SuppressLint("MethodNameUnits")
    public int getUserAuthenticationValidityDurationSeconds() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return 0;
        }
        return mKeyGenParameterSpec == null ? 0 :
                mKeyGenParameterSpec.getUserAuthenticationValidityDurationSeconds();
    }

    /**
     * Gets whether the key is backed by strong box.
     */
    public boolean isStrongBoxBacked() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || mKeyGenParameterSpec == null) {
            return false;
        }
        return mKeyGenParameterSpec.isStrongBoxBacked();
    }

    @NonNull
    @Override
    public String toString() {
        return "MasterKey{keyAlias=" + mKeyAlias
                + ", isKeyStoreBacked=" + isKeyStoreBacked()
                + "}";
    }

    @NonNull/* package */ String getKeyAlias() {
        return mKeyAlias;
    }

    /**
     * Builder for generating a {@link MasterKey}.
     */
    public static final class Builder {
        @NonNull
        private final String mKeyAlias;

        @Nullable
        private KeyGenParameterSpec mKeyGenParameterSpec;
        @Nullable
        private KeyScheme mKeyScheme;

        private boolean mAuthenticationRequired;
        private int mUserAuthenticationValidityDurationSeconds;
        private boolean mRequestStrongBoxBacked;

        private final Context mContext;

        /**
         * Creates a builder for a {@link MasterKey} using the default alias of
         * {@link #DEFAULT_MASTER_KEY_ALIAS}.
         *
         * @param context The context to use with this master key.
         */
        public Builder(@NonNull Context context) {
            this(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS);
        }

        /**
         * Creates a builder for a {@link MasterKey}.
         *
         * @param context The context to use with this master key.
         */
        public Builder(@NonNull Context context, @NonNull String keyAlias) {
            mContext = context.getApplicationContext();
            mKeyAlias = keyAlias;
        }

        /**
         * Sets a {@link KeyScheme} to be used for the master key.
         * This uses a default {@link KeyGenParameterSpec} associated with the provided
         * {@code KeyScheme}.
         * NOTE: Either this method OR {@link #setKeyGenParameterSpec} should be used to set
         * the parameters to use for building the master key. Calling either function after
         * the other will throw an {@link IllegalArgumentException}.
         *
         * @param keyScheme The KeyScheme to use.
         * @return This builder.
         */
        @NonNull
        public Builder setKeyScheme(@NonNull KeyScheme keyScheme) {
            switch (keyScheme) {
                case AES256_GCM:
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        if (mKeyGenParameterSpec != null) {
                            throw new IllegalArgumentException("KeyScheme set after setting a "
                                    + "KeyGenParamSpec");
                        }
                    }
                    break;
                default:
                    throw new IllegalArgumentException("Unsupported scheme: " + keyScheme);
            }
            mKeyScheme = keyScheme;
            return this;
        }

        /**
         * When used with {@link #setKeyScheme(KeyScheme)}, sets that the built master key should
         * require the user to authenticate before it's unlocked, probably using the
         * androidx.biometric library.
         *
         * This method sets the validity duration of the key to
         * {@link #getDefaultAuthenticationValidityDurationSeconds()}.
         *
         * @param authenticationRequired Whether user authentication should be required to use
         *                               the key.
         * @return This builder.
         */
        @NonNull
        public Builder setUserAuthenticationRequired(boolean authenticationRequired) {
            return setUserAuthenticationRequired(authenticationRequired,
                    getDefaultAuthenticationValidityDurationSeconds());
        }

        /**
         * When used with {@link #setKeyScheme(KeyScheme)}, sets that the built master key should
         * require the user to authenticate before it's unlocked, probably using the
         * androidx.biometric library, and that the key should remain unlocked for the provided
         * duration.
         *
         * @param authenticationRequired                    Whether user authentication should be
         *                                                  required to use the key.
         * @param userAuthenticationValidityDurationSeconds Duration in seconds that the key
         *                                                  should remain unlocked following user
         *                                                  authentication.
         * @return This builder.
         */
        @NonNull
        public Builder setUserAuthenticationRequired(boolean authenticationRequired,
                @IntRange(from = 1) int userAuthenticationValidityDurationSeconds) {
            mAuthenticationRequired = authenticationRequired;
            mUserAuthenticationValidityDurationSeconds = userAuthenticationValidityDurationSeconds;
            return this;
        }

        /**
         * Sets whether or not to request this key is strong box backed. This setting is only
         * applicable on {@link Build.VERSION_CODES#P} and above, and only on devices that
         * support Strongbox.
         *
         * @param requestStrongBoxBacked Whether to request to use strongbox
         * @return This builder.
         */
        @NonNull
        public Builder setRequestStrongBoxBacked(boolean requestStrongBoxBacked) {
            mRequestStrongBoxBacked = requestStrongBoxBacked;
            return this;
        }

        /**
         * Sets a custom {@link KeyGenParameterSpec} to use as the basis of the master key.
         * NOTE: Either this method OR {@link #setKeyScheme(KeyScheme)} should be used to set
         * the parameters to use for building the master key. Calling either function after
         * the other will throw an {@link IllegalArgumentException}.
         *
         * @param keyGenParameterSpec The key spec to use.
         * @return This builder.
         */
        @NonNull
        @RequiresApi(Build.VERSION_CODES.M)
        public Builder setKeyGenParameterSpec(@NonNull KeyGenParameterSpec keyGenParameterSpec) {
            if (mKeyScheme != null) {
                throw new IllegalArgumentException("KeyGenParamSpec set after setting a "
                        + "KeyScheme");
            }
            if (!mKeyAlias.equals(keyGenParameterSpec.getKeystoreAlias())) {
                throw new IllegalArgumentException("KeyGenParamSpec's key alias does not match "
                        + "provided alias (" + mKeyAlias + " vs "
                        + keyGenParameterSpec.getKeystoreAlias());
            }
            mKeyGenParameterSpec = keyGenParameterSpec;
            return this;
        }

        /**
         * Builds a {@link MasterKey} from this builder.
         *
         * @return The master key.
         */
        @NonNull
        public MasterKey build() throws GeneralSecurityException, IOException {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                return buildOnM();
            } else {
                return new MasterKey(mKeyAlias, null);
            }
        }

        @SuppressWarnings("deprecation")
        @RequiresApi(Build.VERSION_CODES.M)
        private MasterKey buildOnM() throws GeneralSecurityException, IOException {
            if (mKeyScheme == null && mKeyGenParameterSpec == null) {
                throw new IllegalArgumentException("build() called before "
                        + "setKeyGenParameterSpec or setKeyScheme.");
            }

            if (mKeyScheme == KeyScheme.AES256_GCM) {
                KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
                        mKeyAlias,
                        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                        .setKeySize(DEFAULT_AES_GCM_MASTER_KEY_SIZE);

                if (mAuthenticationRequired) {
                    builder.setUserAuthenticationRequired(true)
                            .setUserAuthenticationValidityDurationSeconds(
                                    mUserAuthenticationValidityDurationSeconds);
                }

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && mRequestStrongBoxBacked) {
                    if (mContext.getPackageManager().hasSystemFeature(
                            PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
                        builder.setIsStrongBoxBacked(true);
                    }
                }

                mKeyGenParameterSpec = builder.build();
            }
            if (mKeyGenParameterSpec == null) {
                // This really should not happen.
                throw new NullPointerException("KeyGenParameterSpec was null after build() check");
            }

            @SuppressWarnings("deprecation")
            String keyAlias = MasterKeys.getOrCreate(mKeyGenParameterSpec);
            return new MasterKey(keyAlias, mKeyGenParameterSpec);
        }
    }
}