Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a9fae14c authored by Robert Berry's avatar Robert Berry
Browse files

Add PlatformKeyManager helper for RecoverableKeyStoreLoader

Manages generating the platform key and then loading it into AndroidKeyStore
with different permissions for 'decrypt' and 'encrypt'. Encrypt should be always
available, so as to enable us to generate application keys at any time, and be
able to sync them wrapped with the platform key to disk. Decrypt should only be
available shortly after a screen unlock - i.e., so that we can unwrap the keys
persisted to disk, then rewrap them with the recovery key and sync them to the
remote storage.

Test: adb shell am instrument -w -e package com.android.server.locksettings.recoverablekeystore com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
Change-Id: I7575ea1c3c78d5544ef763324ac47dffb3993b55
parent 112d5f09
Loading
Loading
Loading
Loading
+31 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.server.locksettings.recoverablekeystore;

/**
 * Error thrown initializing {@link PlatformKeyManager} if the user is not secure (i.e., has no
 * lock screen set).
 */
public class InsecureUserException extends Exception {

    /**
     * A new instance with {@code message} error message.
     */
    public InsecureUserException(String message) {
        super(message);
    }
}
+43 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.server.locksettings.recoverablekeystore;

import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;

/**
 * Proxies {@link java.security.KeyStore}. As all of its methods are final, it cannot otherwise be
 * mocked for tests.
 *
 * @hide
 */
public interface KeyStoreProxy {

    /** @see KeyStore#containsAlias(String) */
    boolean containsAlias(String alias) throws KeyStoreException;

    /** @see KeyStore#getKey(String, char[]) */
    Key getKey(String alias, char[] password)
            throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException;

    /** @see KeyStore#setEntry(String, KeyStore.Entry, KeyStore.ProtectionParameter) */
    void setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter protParam)
            throws KeyStoreException;
}
+55 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.server.locksettings.recoverablekeystore;

import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;

/**
 * Implementation of {@link KeyStoreProxy} that delegates all method calls to the {@link KeyStore}.
 */
public class KeyStoreProxyImpl implements KeyStoreProxy {

    private final KeyStore mKeyStore;

    /**
     * A new instance, delegating to {@code keyStore}.
     */
    public KeyStoreProxyImpl(KeyStore keyStore) {
        mKeyStore = keyStore;
    }

    @Override
    public boolean containsAlias(String alias) throws KeyStoreException {
        return mKeyStore.containsAlias(alias);
    }

    @Override
    public Key getKey(String alias, char[] password)
            throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
        return mKeyStore.getKey(alias, password);
    }

    @Override
    public void setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter protParam)
            throws KeyStoreException {
        mKeyStore.setEntry(alias, entry, protParam);
    }
}
+345 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.server.locksettings.recoverablekeystore;

import android.app.KeyguardManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
import android.security.keystore.AndroidKeyStoreSecretKey;
import android.security.keystore.KeyProperties;
import android.security.keystore.KeyProtection;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;

import java.io.File;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Locale;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.security.auth.DestroyFailedException;

/**
 * Manages creating and checking the validity of the platform key.
 *
 * <p>The platform key is used to wrap the material of recoverable keys before persisting them to
 * disk. It is also used to decrypt the same keys on a screen unlock, before re-wrapping them with
 * a recovery key and syncing them with remote storage.
 *
 * <p>Each platform key has two entries in AndroidKeyStore:
 *
 * <ul>
 *     <li>Encrypt entry - this entry enables the root user to at any time encrypt.
 *     <li>Decrypt entry - this entry enables the root user to decrypt only after recent user
 *       authentication, i.e., within 15 seconds after a screen unlock.
 * </ul>
 *
 * <p>Both entries are enabled only for AES/GCM/NoPadding Cipher algorithm.
 *
 * @hide
 */
public class PlatformKeyManager {
    private static final String TAG = "PlatformKeyManager";

    private static final String KEY_ALGORITHM = "AES";
    private static final int KEY_SIZE_BITS = 256;
    private static final String SHARED_PREFS_KEY_GENERATION_ID = "generationId";
    private static final String SHARED_PREFS_PATH = "/system/recoverablekeystore/platform_keys.xml";
    private static final String KEY_ALIAS_PREFIX =
            "com.android.server.locksettings.recoverablekeystore/platform/";
    private static final String ENCRYPT_KEY_ALIAS_SUFFIX = "encrypt";
    private static final String DECRYPT_KEY_ALIAS_SUFFIX = "decrypt";
    private static final int USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS = 15;

    private final Context mContext;
    private final KeyStoreProxy mKeyStore;
    private final SharedPreferences mSharedPreferences;
    private final int mUserId;

    private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore";

    /**
     * A new instance operating on behalf of {@code userId}, storing its prefs in the location
     * defined by {@code context}.
     *
     * @param context This should be the context of the RecoverableKeyStoreLoader service.
     * @param userId The ID of the user to whose lock screen the platform key must be bound.
     * @throws KeyStoreException if failed to initialize AndroidKeyStore.
     * @throws NoSuchAlgorithmException if AES is unavailable - should never happen.
     * @throws InsecureUserException if the user does not have a lock screen set.
     * @throws SecurityException if the caller does not have permission to write to /data/system.
     *
     * @hide
     */
    public static PlatformKeyManager getInstance(Context context, int userId)
            throws KeyStoreException, NoSuchAlgorithmException, InsecureUserException {
        context = context.getApplicationContext();
        File sharedPreferencesFile = new File(
                Environment.getDataDirectory().getAbsoluteFile(), SHARED_PREFS_PATH);
        sharedPreferencesFile.mkdirs();
        PlatformKeyManager keyManager = new PlatformKeyManager(
                userId,
                context,
                new KeyStoreProxyImpl(getAndLoadAndroidKeyStore()),
                context.getSharedPreferences(sharedPreferencesFile, Context.MODE_PRIVATE));
        keyManager.init();
        return keyManager;
    }

    @VisibleForTesting
    PlatformKeyManager(
            int userId,
            Context context,
            KeyStoreProxy keyStore,
            SharedPreferences sharedPreferences) {
        mUserId = userId;
        mKeyStore = keyStore;
        mContext = context;
        mSharedPreferences = sharedPreferences;
    }

    /**
     * Returns the current generation ID of the platform key. This increments whenever a platform
     * key has to be replaced. (e.g., because the user has removed and then re-added their lock
     * screen).
     *
     * @hide
     */
    public int getGenerationId() {
        return mSharedPreferences.getInt(getGenerationIdKey(), 1);
    }

    /**
     * Returns {@code true} if the platform key is available. A platform key won't be available if
     * the user has not set up a lock screen.
     *
     * @hide
     */
    public boolean isAvailable() {
        return mContext.getSystemService(KeyguardManager.class).isDeviceSecure(mUserId);
    }

    /**
     * Generates a new key and increments the generation ID. Should be invoked if the platform key
     * is corrupted and needs to be rotated.
     *
     * @throws NoSuchAlgorithmException if AES is unavailable - should never happen.
     * @throws KeyStoreException if there is an error in AndroidKeyStore.
     *
     * @hide
     */
    public void regenerate() throws NoSuchAlgorithmException, KeyStoreException {
        int generationId = getGenerationId();
        generateAndLoadKey(generationId + 1);
        setGenerationId(generationId + 1);
    }

    /**
     * Returns the platform key used for encryption.
     *
     * @throws KeyStoreException if there was an AndroidKeyStore error.
     * @throws UnrecoverableKeyException if the key could not be recovered.
     * @throws NoSuchAlgorithmException if AES is unavailable - should never occur.
     *
     * @hide
     */
    public PlatformEncryptionKey getEncryptKey()
            throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
        int generationId = getGenerationId();
        AndroidKeyStoreSecretKey key = (AndroidKeyStoreSecretKey) mKeyStore.getKey(
                getEncryptAlias(generationId), /*password=*/ null);
        return new PlatformEncryptionKey(generationId, key);
    }

    /**
     * Returns the platform key used for decryption. Only works after a recent screen unlock.
     *
     * @throws KeyStoreException if there was an AndroidKeyStore error.
     * @throws UnrecoverableKeyException if the key could not be recovered.
     * @throws NoSuchAlgorithmException if AES is unavailable - should never occur.
     *
     * @hide
     */
    public PlatformDecryptionKey getDecryptKey()
            throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
        int generationId = getGenerationId();
        AndroidKeyStoreSecretKey key = (AndroidKeyStoreSecretKey) mKeyStore.getKey(
                getDecryptAlias(generationId), /*password=*/ null);
        return new PlatformDecryptionKey(generationId, key);
    }

    /**
     * Initializes the class. If there is no current platform key, and the user has a lock screen
     * set, will create the platform key and set the generation ID.
     *
     * @throws KeyStoreException if there was an error in AndroidKeyStore.
     * @throws NoSuchAlgorithmException if AES is unavailable - should never happen.
     *
     * @hide
     */
    public void init() throws KeyStoreException, NoSuchAlgorithmException, InsecureUserException {
        if (!isAvailable()) {
            throw new InsecureUserException(String.format(
                    Locale.US, "%d does not have a lock screen set.", mUserId));
        }

        int generationId = getGenerationId();
        if (isKeyLoaded(generationId)) {
            Log.i(TAG, String.format(
                    Locale.US, "Platform key generation %d exists already.", generationId));
            return;
        }
        if (generationId == 1) {
            Log.i(TAG, "Generating initial platform ID.");
        } else {
            Log.w(TAG, String.format(Locale.US, "Platform generation ID was %d but no "
                    + "entry was present in AndroidKeyStore. Generating fresh key.", generationId));
        }

        generateAndLoadKey(generationId);
    }

    /**
     * Returns the alias of the encryption key with the specific {@code generationId} in the
     * AndroidKeyStore.
     *
     * <p>These IDs look as follows:
     * {@code com.security.recoverablekeystore/platform/<user id>/<generation id>/encrypt}
     *
     * @param generationId The generation ID.
     * @return The alias.
     */
    private String getEncryptAlias(int generationId) {
        return KEY_ALIAS_PREFIX + mUserId + "/" + generationId + "/" + ENCRYPT_KEY_ALIAS_SUFFIX;
    }

    /**
     * Returns the alias of the decryption key with the specific {@code generationId} in the
     * AndroidKeyStore.
     *
     * <p>These IDs look as follows:
     * {@code com.security.recoverablekeystore/platform/<user id>/<generation id>/decrypt}
     *
     * @param generationId The generation ID.
     * @return The alias.
     */
    private String getDecryptAlias(int generationId) {
        return KEY_ALIAS_PREFIX + mUserId + "/" + generationId + "/" + DECRYPT_KEY_ALIAS_SUFFIX;
    }

    /**
     * Sets the current generation ID to {@code generationId}.
     */
    private void setGenerationId(int generationId) {
        mSharedPreferences.edit().putInt(getGenerationIdKey(), generationId).commit();
    }

    /**
     * Returns the current user's generation ID key in the shared preferences.
     */
    private String getGenerationIdKey() {
        return SHARED_PREFS_KEY_GENERATION_ID + "/" + mUserId;
    }

    /**
     * Returns {@code true} if a key has been loaded with the given {@code generationId} into
     * AndroidKeyStore.
     *
     * @throws KeyStoreException if there was an error checking AndroidKeyStore.
     */
    private boolean isKeyLoaded(int generationId) throws KeyStoreException {
        return mKeyStore.containsAlias(getEncryptAlias(generationId))
                && mKeyStore.containsAlias(getDecryptAlias(generationId));
    }

    /**
     * Generates a new 256-bit AES key, and loads it into AndroidKeyStore with the given
     * {@code generationId} determining its aliases.
     *
     * @throws NoSuchAlgorithmException if AES is unavailable. This should never happen, as it is
     *     available since API version 1.
     * @throws KeyStoreException if there was an issue loading the keys into AndroidKeyStore.
     */
    private void generateAndLoadKey(int generationId)
            throws NoSuchAlgorithmException, KeyStoreException {
        String encryptAlias = getEncryptAlias(generationId);
        String decryptAlias = getDecryptAlias(generationId);
        SecretKey secretKey = generateAesKey();

        mKeyStore.setEntry(
                encryptAlias,
                new KeyStore.SecretKeyEntry(secretKey),
                new KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                    .build());
        mKeyStore.setEntry(
                decryptAlias,
                new KeyStore.SecretKeyEntry(secretKey),
                new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT)
                    .setUserAuthenticationRequired(true)
                    .setUserAuthenticationValidityDurationSeconds(
                            USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS)
                    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                    .setBoundToSpecificSecureUserId(mUserId)
                    .build());

        try {
            secretKey.destroy();
        } catch (DestroyFailedException e) {
            Log.w(TAG, "Failed to destroy in-memory platform key.", e);
        }
    }

    /**
     * Generates a new 256-bit AES key, in software.
     *
     * @return The software-generated AES key.
     * @throws NoSuchAlgorithmException if AES key generation is not available. This should never
     *     happen, as AES has been supported since API level 1.
     */
    private static SecretKey generateAesKey() throws NoSuchAlgorithmException {
        KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
        keyGenerator.init(KEY_SIZE_BITS);
        return keyGenerator.generateKey();
    }

    /**
     * Returns AndroidKeyStore-provided {@link KeyStore}, having already invoked
     * {@link KeyStore#load(KeyStore.LoadStoreParameter)}.
     *
     * @throws KeyStoreException if there was a problem getting or initializing the key store.
     */
    private static KeyStore getAndLoadAndroidKeyStore() throws KeyStoreException {
        KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER);
        try {
            keyStore.load(/*param=*/ null);
        } catch (CertificateException | IOException | NoSuchAlgorithmException e) {
            // Should never happen.
            throw new KeyStoreException("Unable to load keystore.", e);
        }
        return keyStore;
    }
}
+275 −0

File added.

Preview size limit exceeded, changes collapsed.