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

Commit 84321d87 authored by Robert Berry's avatar Robert Berry Committed by Android (Google) Code Review
Browse files

Merge "Add helpers for generating keys, wrapping them, and persisting them"

parents b8f2728a ce50cd30
Loading
Loading
Loading
Loading
+133 −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 android.security.recoverablekeystore;

import android.security.keystore.AndroidKeyStoreSecretKey;
import android.security.keystore.KeyProperties;
import android.security.keystore.KeyProtection;
import android.util.Log;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableEntryException;

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

/**
 * Generates keys and stores them both in AndroidKeyStore and on disk, in wrapped form.
 *
 * <p>Generates 256-bit AES keys, which can be used for encrypt / decrypt with AES/GCM/NoPadding.
 * They are synced to disk wrapped by a platform key. This allows them to be exported to a remote
 * service.
 *
 * @hide
 */
public class RecoverableKeyGenerator {
    private static final String TAG = "RecoverableKeyGenerator";
    private static final String KEY_GENERATOR_ALGORITHM = "AES";
    private static final int KEY_SIZE_BITS = 256;

    /**
     * A new {@link RecoverableKeyGenerator} instance.
     *
     * @param platformKey Secret key used to wrap generated keys before persisting to disk.
     * @param recoverableKeyStorage Class that manages persisting wrapped keys to disk.
     * @throws NoSuchAlgorithmException if "AES" key generation or "AES/GCM/NoPadding" cipher is
     *     unavailable. Should never happen.
     *
     * @hide
     */
    public static RecoverableKeyGenerator newInstance(
            AndroidKeyStoreSecretKey platformKey, RecoverableKeyStorage recoverableKeyStorage)
            throws NoSuchAlgorithmException {
        // NB: This cannot use AndroidKeyStore as the provider, as we need access to the raw key
        // material, so that it can be synced to disk in encrypted form.
        KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_GENERATOR_ALGORITHM);
        return new RecoverableKeyGenerator(keyGenerator, platformKey, recoverableKeyStorage);
    }

    private final KeyGenerator mKeyGenerator;
    private final RecoverableKeyStorage mRecoverableKeyStorage;
    private final AndroidKeyStoreSecretKey mPlatformKey;

    private RecoverableKeyGenerator(
            KeyGenerator keyGenerator,
            AndroidKeyStoreSecretKey platformKey,
            RecoverableKeyStorage recoverableKeyStorage) {
        mKeyGenerator = keyGenerator;
        mRecoverableKeyStorage = recoverableKeyStorage;
        mPlatformKey = platformKey;
    }

    /**
     * Generates a 256-bit AES key with the given alias.
     *
     * <p>Stores in the AndroidKeyStore, as well as persisting in wrapped form to disk. It is
     * persisted to disk so that it can be synced remotely, and then recovered on another device.
     * The generated key allows encrypt/decrypt only using AES/GCM/NoPadding.
     *
     * <p>The key handle returned to the caller is a reference to the AndroidKeyStore key,
     * meaning that the caller is never able to access the raw, unencrypted key.
     *
     * @param alias The alias by which the key will be known in AndroidKeyStore.
     * @throws InvalidKeyException if the platform key cannot be used to wrap keys.
     * @throws IOException if there was an issue writing the wrapped key to the wrapped key store.
     * @throws UnrecoverableEntryException if could not retrieve key after putting it in
     *     AndroidKeyStore. This should not happen.
     * @return A handle to the AndroidKeyStore key.
     *
     * @hide
     */
    public SecretKey generateAndStoreKey(String alias) throws KeyStoreException,
            InvalidKeyException, IOException, UnrecoverableEntryException {
        mKeyGenerator.init(KEY_SIZE_BITS);
        SecretKey key = mKeyGenerator.generateKey();

        mRecoverableKeyStorage.importIntoAndroidKeyStore(
                alias,
                key,
                new KeyProtection.Builder(
                        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                        .build());
        WrappedKey wrappedKey = WrappedKey.fromSecretKey(mPlatformKey, key);

        try {
            // Keep raw key material in memory for minimum possible time.
            key.destroy();
        } catch (DestroyFailedException e) {
            Log.w(TAG, "Could not destroy SecretKey.");
        }

        mRecoverableKeyStorage.persistToDisk(alias, wrappedKey);

        try {
            // Reload from the keystore, so that the caller is only provided with the handle of the
            // key, not the raw key material.
            return mRecoverableKeyStorage.loadFromAndroidKeyStore(alias);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(
                    "Impossible: NoSuchAlgorithmException when attempting to retrieve a key "
                            + "that has only just been stored in AndroidKeyStore.", e);
        }
    }
}
+71 −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 android.security.recoverablekeystore;

import android.security.keystore.KeyProtection;

import java.io.IOException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableEntryException;

import javax.crypto.SecretKey;

/**
 * Stores wrapped keys to disk, so they can be synced on the next screen unlock event.
 *
 * @hide
 */
public interface RecoverableKeyStorage {

    /**
     * Writes {@code wrappedKey} to disk, keyed by the application's uid and the {@code alias}.
     *
     * @throws IOException if an error occurred writing to disk.
     *
     * @hide
     */
    void persistToDisk(String alias, WrappedKey wrappedKey) throws IOException;

    /**
     * Imports {@code key} into AndroidKeyStore, keyed by the application's uid and
     * the {@code alias}.
     *
     * @param alias The alias of the key.
     * @param key The key.
     * @param keyProtection Protection params denoting what the key can be used for. (e.g., what
     *                      Cipher modes, whether for encrpyt/decrypt or signing, etc.)
     * @throws KeyStoreException if an error occurred loading the key into the AndroidKeyStore.
     *
     * @hide
     */
    void importIntoAndroidKeyStore(String alias, SecretKey key, KeyProtection keyProtection) throws
            KeyStoreException;

    /**
     * Loads a key handle from AndroidKeyStore.
     *
     * @param alias Alias of the key to load.
     * @return The key handle.
     * @throws KeyStoreException if an error occurred loading the key from AndroidKeyStore.
     *
     * @hide
     */
    SecretKey loadFromAndroidKeyStore(String alias) throws KeyStoreException,
            NoSuchAlgorithmException,
            UnrecoverableEntryException;
}
+110 −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 android.security.recoverablekeystore;

import android.security.keystore.KeyProtection;

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

import javax.crypto.SecretKey;

/**
 * Implementation of {@link RecoverableKeyStorage}.
 *
 * <p>Persists wrapped keys to disk, and loads raw keys into AndroidKeyStore.
 *
 * @hide
 */
public class RecoverableKeyStorageImpl implements RecoverableKeyStorage {
    private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore";

    private final KeyStore mKeyStore;

    /**
     * A new instance.
     *
     * @throws KeyStoreException if unable to load AndroidKeyStore.
     *
     * @hide
     */
    public static RecoverableKeyStorageImpl newInstance() 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 new RecoverableKeyStorageImpl(keyStore);
    }

    private RecoverableKeyStorageImpl(KeyStore keyStore) {
        mKeyStore = keyStore;
    }

    /**
     * Writes {@code wrappedKey} to disk, keyed by the application's uid and the {@code alias}.
     *
     * @throws IOException if an error occurred writing to disk.
     *
     * @hide
     */
    @Override
    public void persistToDisk(String alias, WrappedKey wrappedKey) throws IOException {
        // TODO(robertberry) Add implementation.
        throw new UnsupportedOperationException();
    }

    /**
     * Imports {@code key} into AndroidKeyStore, keyed by the application's uid and the
     * {@code alias}.
     *
     * @param alias The alias of the key.
     * @param key The key.
     * @param keyProtection Protection params denoting what the key can be used for. (e.g., what
     *                      Cipher modes, whether for encrpyt/decrypt or signing, etc.)
     * @throws KeyStoreException if an error occurred loading the key into the AndroidKeyStore.
     *
     * @hide
     */
    @Override
    public void importIntoAndroidKeyStore(String alias, SecretKey key, KeyProtection keyProtection)
            throws KeyStoreException {
        mKeyStore.setEntry(alias, new KeyStore.SecretKeyEntry(key), keyProtection);
    }

    /**
     * Loads a key handle from AndroidKeyStore.
     *
     * @param alias Alias of the key to load.
     * @return The key handle.
     * @throws KeyStoreException if an error occurred loading the key from AndroidKeyStore.
     *
     * @hide
     */
    @Override
    public SecretKey loadFromAndroidKeyStore(String alias)
            throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableEntryException {
        return ((KeyStore.SecretKeyEntry) mKeyStore.getEntry(alias, /*protParam=*/ null))
                .getSecretKey();
    }
}
+115 −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 android.security.recoverablekeystore;

import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

/**
 * A {@link javax.crypto.SecretKey} wrapped with AES/GCM/NoPadding.
 *
 * @hide
 */
public class WrappedKey {
    private static final String KEY_WRAP_CIPHER_ALGORITHM = "AES/GCM/NoPadding";

    private final byte[] mNonce;
    private final byte[] mKeyMaterial;

    /**
     * Returns a wrapped form of {@code key}, using {@code wrappingKey} to encrypt the key material.
     *
     * @throws InvalidKeyException if {@code wrappingKey} cannot be used to encrypt {@code key}, or
     *     if {@code key} does not expose its key material. See
     *     {@link android.security.keystore.AndroidKeyStoreKey} for an example of a key that does
     *     not expose its key material.
     */
    public static WrappedKey fromSecretKey(
            SecretKey wrappingKey, SecretKey key) throws InvalidKeyException, KeyStoreException {
        if (key.getEncoded() == null) {
            throw new InvalidKeyException(
                    "key does not expose encoded material. It cannot be wrapped.");
        }

        Cipher cipher;
        try {
            cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new RuntimeException(
                    "Android does not support AES/GCM/NoPadding. This should never happen.");
        }

        cipher.init(Cipher.WRAP_MODE, wrappingKey);
        byte[] encryptedKeyMaterial;
        try {
            encryptedKeyMaterial = cipher.wrap(key);
        } catch (IllegalBlockSizeException e) {
            Throwable cause = e.getCause();
            if (cause instanceof KeyStoreException) {
                // If AndroidKeyStore encounters any error here, it throws IllegalBlockSizeException
                // with KeyStoreException as the cause. This is due to there being no better option
                // here, as the Cipher#wrap only checked throws InvalidKeyException or
                // IllegalBlockSizeException. If this is the case, we want to propagate it to the
                // caller, so rethrow the cause.
                throw (KeyStoreException) cause;
            } else {
                throw new RuntimeException(
                        "IllegalBlockSizeException should not be thrown by AES/GCM/NoPadding mode.",
                        e);
            }
        }

        return new WrappedKey(/*mNonce=*/ cipher.getIV(), /*mKeyMaterial=*/ encryptedKeyMaterial);
    }

    /**
     * A new instance.
     *
     * @param nonce The nonce with which the key material was encrypted.
     * @param keyMaterial The encrypted bytes of the key material.
     *
     * @hide
     */
    public WrappedKey(byte[] nonce, byte[] keyMaterial) {
        mNonce = nonce;
        mKeyMaterial = keyMaterial;
    }

    /**
     * Returns the nonce with which the key material was encrypted.
     *
     * @hide
     */
    public byte[] getNonce() {
        return mNonce;
    }

    /**
     * Returns the encrypted key material.
     *
     * @hide
     */
    public byte[] getKeyMaterial() {
        return mKeyMaterial;
    }
}
+154 −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 android.security.recoverablekeystore;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.security.keystore.AndroidKeyStoreSecretKey;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.security.keystore.KeyProtection;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.security.KeyStore;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class RecoverableKeyGeneratorTest {
    private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore";
    private static final String KEY_ALGORITHM = "AES";
    private static final String TEST_ALIAS = "karlin";
    private static final String WRAPPING_KEY_ALIAS = "RecoverableKeyGeneratorTestWrappingKey";

    @Mock RecoverableKeyStorage mRecoverableKeyStorage;

    @Captor ArgumentCaptor<KeyProtection> mKeyProtectionArgumentCaptor;

    private AndroidKeyStoreSecretKey mPlatformKey;
    private SecretKey mKeyHandle;
    private RecoverableKeyGenerator mRecoverableKeyGenerator;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mPlatformKey = generateAndroidKeyStoreKey();
        mKeyHandle = generateKey();
        mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(
                mPlatformKey, mRecoverableKeyStorage);

        when(mRecoverableKeyStorage.loadFromAndroidKeyStore(any())).thenReturn(mKeyHandle);
    }

    @After
    public void tearDown() throws Exception {
        KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER);
        keyStore.load(/*param=*/ null);
        keyStore.deleteEntry(WRAPPING_KEY_ALIAS);
    }

    @Test
    public void generateAndStoreKey_setsKeyInKeyStore() throws Exception {
        mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);

        verify(mRecoverableKeyStorage, times(1))
                .importIntoAndroidKeyStore(eq(TEST_ALIAS), any(), any());
    }

    @Test
    public void generateAndStoreKey_storesKeyEnabledForEncryptDecrypt() throws Exception {
        mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);

        KeyProtection keyProtection = getKeyProtectionUsed();
        assertEquals(KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT,
                keyProtection.getPurposes());
    }

    @Test
    public void generateAndStoreKey_storesKeyEnabledForGCM() throws Exception {
        mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);

        KeyProtection keyProtection = getKeyProtectionUsed();
        assertArrayEquals(new String[] { KeyProperties.BLOCK_MODE_GCM },
                keyProtection.getBlockModes());
    }

    @Test
    public void generateAndStoreKey_storesKeyEnabledForNoPadding() throws Exception {
        mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);

        KeyProtection keyProtection = getKeyProtectionUsed();
        assertArrayEquals(new String[] { KeyProperties.ENCRYPTION_PADDING_NONE },
                keyProtection.getEncryptionPaddings());
    }

    @Test
    public void generateAndStoreKey_storesWrappedKey() throws Exception {
        mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);

        verify(mRecoverableKeyStorage, times(1)).persistToDisk(eq(TEST_ALIAS), any());
    }

    @Test
    public void generateAndStoreKey_returnsKeyHandle() throws Exception {
        SecretKey secretKey = mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);

        assertEquals(mKeyHandle, secretKey);
    }

    private KeyProtection getKeyProtectionUsed() throws Exception {
        verify(mRecoverableKeyStorage, times(1)).importIntoAndroidKeyStore(
                any(), any(), mKeyProtectionArgumentCaptor.capture());
        return mKeyProtectionArgumentCaptor.getValue();
    }

    private SecretKey generateKey() throws Exception {
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(/*keySize=*/ 256);
        return keyGenerator.generateKey();
    }

    private AndroidKeyStoreSecretKey generateAndroidKeyStoreKey() throws Exception {
        KeyGenerator keyGenerator = KeyGenerator.getInstance(
                KEY_ALGORITHM,
                ANDROID_KEY_STORE_PROVIDER);
        keyGenerator.init(new KeyGenParameterSpec.Builder(
                WRAPPING_KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                    .build());
        return (AndroidKeyStoreSecretKey) keyGenerator.generateKey();
    }
}
Loading