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

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

Merge "Implement RecoverableKeyStoreLoader loading keys into users' keystores"

parents 793fdca6 38321c7a
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -68,4 +68,13 @@ public interface RecoverableKeyStorage {
    SecretKey loadFromAndroidKeyStore(String alias) throws KeyStoreException,
            NoSuchAlgorithmException,
            UnrecoverableEntryException;

    /**
     * Removes the entry with the given {@code alias} from AndroidKeyStore.
     *
     * @throws KeyStoreException if an error occurred deleting the key from AndroidKeyStore.
     *
     * @hide
     */
    void removeFromAndroidKeyStore(String alias) throws KeyStoreException;
}
+24 −18
Original line number Diff line number Diff line
@@ -16,44 +16,40 @@

package com.android.server.locksettings.recoverablekeystore;

import android.security.keystore.AndroidKeyStoreProvider;
import android.security.keystore.KeyProtection;

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

import javax.crypto.SecretKey;

/**
 * Implementation of {@link RecoverableKeyStorage}.
 * Implementation of {@link RecoverableKeyStorage} for a specific application.
 *
 * <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.
     * A new instance, storing recoverable keys for the given {@code userId}.
     *
     * @throws KeyStoreException if unable to load AndroidKeyStore.
     * @throws NoSuchProviderException if AndroidKeyStore is not in this version of Android. Should
     *     never occur.
     *
     * @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);
        }
    public static RecoverableKeyStorageImpl newInstance(int userId) throws KeyStoreException,
            NoSuchProviderException {
        KeyStore keyStore = AndroidKeyStoreProvider.getKeyStoreForUid(userId);
        return new RecoverableKeyStorageImpl(keyStore);
    }

@@ -75,8 +71,7 @@ public class RecoverableKeyStorageImpl implements RecoverableKeyStorage {
    }

    /**
     * Imports {@code key} into AndroidKeyStore, keyed by the application's uid and the
     * {@code alias}.
     * Imports {@code key} into the application's AndroidKeyStore, keyed by {@code alias}.
     *
     * @param alias The alias of the key.
     * @param key The key.
@@ -93,7 +88,7 @@ public class RecoverableKeyStorageImpl implements RecoverableKeyStorage {
    }

    /**
     * Loads a key handle from AndroidKeyStore.
     * Loads a key handle from the application's AndroidKeyStore.
     *
     * @param alias Alias of the key to load.
     * @return The key handle.
@@ -104,7 +99,18 @@ public class RecoverableKeyStorageImpl implements RecoverableKeyStorage {
    @Override
    public SecretKey loadFromAndroidKeyStore(String alias)
            throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableEntryException {
        return ((KeyStore.SecretKeyEntry) mKeyStore.getEntry(alias, /*protParam=*/ null))
                .getSecretKey();
        return ((SecretKey) mKeyStore.getKey(alias, /*password=*/ null));
    }

    /**
     * Removes the entry with the given {@code alias} from the application's AndroidKeyStore.
     *
     * @throws KeyStoreException if an error occurred deleting the key from AndroidKeyStore.
     *
     * @hide
     */
    @Override
    public void removeFromAndroidKeyStore(String alias) throws KeyStoreException {
        mKeyStore.deleteEntry(alias);
    }
}
+155 −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 static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;

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 java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.util.Random;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class RecoverableKeyStorageImplTest {
    private static final String KEY_ALGORITHM = "AES";
    private static final int GCM_TAG_LENGTH_BYTES = 16;
    private static final int BITS_PER_BYTE = 8;
    private static final int GCM_TAG_LENGTH_BITS = GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE;
    private static final int GCM_NONCE_LENGTH_BYTES = 12;
    private static final String TEST_KEY_ALIAS = "RecoverableKeyStorageImplTestKey";
    private static final int KEYSTORE_UID_SELF = -1;

    private RecoverableKeyStorageImpl mRecoverableKeyStorage;

    @Before
    public void setUp() throws Exception {
        mRecoverableKeyStorage = RecoverableKeyStorageImpl.newInstance(
                /*userId=*/ KEYSTORE_UID_SELF);
    }

    @After
    public void tearDown() {
        try {
            mRecoverableKeyStorage.removeFromAndroidKeyStore(TEST_KEY_ALIAS);
        } catch (KeyStoreException e) {
            // Do nothing.
        }
    }

    @Test
    public void loadFromAndroidKeyStore_loadsAKeyThatWasImported() throws Exception {
        SecretKey key = generateKey();
        mRecoverableKeyStorage.importIntoAndroidKeyStore(
                TEST_KEY_ALIAS,
                key,
                getKeyProperties());

        assertKeysAreEquivalent(
                key, mRecoverableKeyStorage.loadFromAndroidKeyStore(TEST_KEY_ALIAS));
    }

    @Test
    public void importIntoAndroidKeyStore_importsWithKeyProperties() throws Exception {
        mRecoverableKeyStorage.importIntoAndroidKeyStore(
                TEST_KEY_ALIAS,
                generateKey(),
                getKeyProperties());

        SecretKey key = mRecoverableKeyStorage.loadFromAndroidKeyStore(TEST_KEY_ALIAS);

        Mac mac = Mac.getInstance("HmacSHA256");
        try {
            // Fails because missing PURPOSE_SIGN or PURPOSE_VERIFY
            mac.init(key);
            fail("Was able to initialize Mac with an ENCRYPT/DECRYPT-only key.");
        } catch (InvalidKeyException e) {
            // expect exception
        }
    }

    @Test
    public void removeFromAndroidKeyStore_removesAnEntry() throws Exception {
        mRecoverableKeyStorage.importIntoAndroidKeyStore(
                TEST_KEY_ALIAS,
                generateKey(),
                getKeyProperties());

        mRecoverableKeyStorage.removeFromAndroidKeyStore(TEST_KEY_ALIAS);

        assertNull(mRecoverableKeyStorage.loadFromAndroidKeyStore(TEST_KEY_ALIAS));
    }

    private static KeyProtection getKeyProperties() {
        return new KeyProtection.Builder(
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .build();
    }

    /**
     * Asserts that {@code b} key can decrypt data encrypted with {@code a} key. Otherwise throws.
     */
    private static void assertKeysAreEquivalent(SecretKey a, SecretKey b) throws Exception {
        byte[] plaintext = "doge".getBytes(StandardCharsets.UTF_8);
        byte[] nonce = generateGcmNonce();

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, a, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, nonce));
        byte[] encrypted = cipher.doFinal(plaintext);

        cipher.init(Cipher.DECRYPT_MODE, b, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, nonce));
        byte[] decrypted = cipher.doFinal(encrypted);

        assertArrayEquals(decrypted, plaintext);
    }

    /**
     * Returns a new random GCM nonce.
     */
    private static byte[] generateGcmNonce() {
        Random random = new Random();
        byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTES];
        random.nextBytes(nonce);
        return nonce;
    }

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