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

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

Add KeySyncUtils

Static methods to help with the RecoverableKeyStoreLoader remote sync
flow.

Test: adb shell am instrument -w -e package com.android.server.locksettings.recoverablekeystore com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
Change-Id: Ibd5a8f6c9ee2d4d118a9e6be9b813e192205d6dc
parent 47f4e2a5
Loading
Loading
Loading
Loading
+167 −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.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

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

/**
 * Utility functions for the flow where the RecoverableKeyStoreLoader syncs keys with remote
 * storage.
 *
 * @hide
 */
public class KeySyncUtils {

    private static final String RECOVERY_KEY_ALGORITHM = "AES";
    private static final int RECOVERY_KEY_SIZE_BITS = 256;

    private static final byte[] THM_ENCRYPTED_RECOVERY_KEY_HEADER =
            "V1 THM_encrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
    private static final byte[] LOCALLY_ENCRYPTED_RECOVERY_KEY_HEADER =
            "V1 locally_encrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
    private static final byte[] ENCRYPTED_APPLICATION_KEY_HEADER =
            "V1 encrypted_application_key".getBytes(StandardCharsets.UTF_8);

    private static final byte[] THM_KF_HASH_PREFIX = "THM_KF_hash".getBytes(StandardCharsets.UTF_8);

    /**
     * Encrypts the recovery key using both the lock screen hash and the remote storage's public
     * key.
     *
     * @param publicKey The public key of the remote storage.
     * @param lockScreenHash The user's lock screen hash.
     * @param vaultParams Additional parameters to send to the remote storage.
     * @param recoveryKey The recovery key.
     * @return The encrypted bytes.
     * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
     * @throws InvalidKeyException if the public key or the lock screen could not be used to encrypt
     *     the data.
     *
     * @hide
     */
    public byte[] thmEncryptRecoveryKey(
            PublicKey publicKey,
            byte[] lockScreenHash,
            byte[] vaultParams,
            SecretKey recoveryKey
    ) throws NoSuchAlgorithmException, InvalidKeyException {
        byte[] encryptedRecoveryKey = locallyEncryptRecoveryKey(lockScreenHash, recoveryKey);
        byte[] thmKfHash = calculateThmKfHash(lockScreenHash);
        byte[] header = concat(THM_ENCRYPTED_RECOVERY_KEY_HEADER, vaultParams);
        return SecureBox.encrypt(
                /*theirPublicKey=*/ publicKey,
                /*sharedSecret=*/ thmKfHash,
                /*header=*/ header,
                /*payload=*/ encryptedRecoveryKey);
    }

    /**
     * Calculates the THM_KF hash of the lock screen hash.
     *
     * @param lockScreenHash The lock screen hash.
     * @return The hash.
     * @throws NoSuchAlgorithmException if SHA-256 is unavailable (should never happen).
     *
     * @hide
     */
    public static byte[] calculateThmKfHash(byte[] lockScreenHash)
            throws NoSuchAlgorithmException {
        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
        messageDigest.update(THM_KF_HASH_PREFIX);
        messageDigest.update(lockScreenHash);
        return messageDigest.digest();
    }

    /**
     * Encrypts the recovery key using the lock screen hash.
     *
     * @param lockScreenHash The raw lock screen hash.
     * @param recoveryKey The recovery key.
     * @return The encrypted bytes.
     * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
     * @throws InvalidKeyException if the hash cannot be used to encrypt for some reason.
     */
    private static byte[] locallyEncryptRecoveryKey(byte[] lockScreenHash, SecretKey recoveryKey)
            throws NoSuchAlgorithmException, InvalidKeyException {
        return SecureBox.encrypt(
                /*theirPublicKey=*/ null,
                /*sharedSecret=*/ lockScreenHash,
                /*header=*/ LOCALLY_ENCRYPTED_RECOVERY_KEY_HEADER,
                /*payload=*/ recoveryKey.getEncoded());
    }

    /**
     * Returns a new random 256-bit AES recovery key.
     *
     * @hide
     */
    public static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
        KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
        keyGenerator.init(RECOVERY_KEY_SIZE_BITS, SecureRandom.getInstanceStrong());
        return keyGenerator.generateKey();
    }

    /**
     * Encrypts all of the given keys with the recovery key, using SecureBox.
     *
     * @param recoveryKey The recovery key.
     * @param keys The keys, indexed by their aliases.
     * @return The encrypted key material, indexed by aliases.
     * @throws NoSuchAlgorithmException if any of the SecureBox algorithms are unavailable.
     * @throws InvalidKeyException if the recovery key is not appropriate for encrypting the keys.
     *
     * @hide
     */
    public static Map<String, byte[]> encryptKeysWithRecoveryKey(
            SecretKey recoveryKey, Map<String, SecretKey> keys)
            throws NoSuchAlgorithmException, InvalidKeyException {
        HashMap<String, byte[]> encryptedKeys = new HashMap<>();
        for (String alias : keys.keySet()) {
            SecretKey key = keys.get(alias);
            byte[] encryptedKey = SecureBox.encrypt(
                    /*theirPublicKey=*/ null,
                    /*sharedSecret=*/ recoveryKey.getEncoded(),
                    /*header=*/ ENCRYPTED_APPLICATION_KEY_HEADER,
                    /*payload=*/ key.getEncoded());
            encryptedKeys.put(alias, encryptedKey);
        }
        return encryptedKeys;
    }

    /**
     * Returns a new array, the contents of which are the concatenation of {@code a} and {@code b}.
     */
    private static byte[] concat(byte[] a, byte[] b) {
        byte[] result = new byte[a.length + b.length];
        System.arraycopy(a, 0, result, 0, a.length);
        System.arraycopy(b, 0, result, a.length, b.length);
        return result;
    }

    // Statics only
    private KeySyncUtils() {}
}
+39 −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.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;

/**
 * TODO(b/69056040) Add implementation of SecureBox. This is a placeholder so KeySyncUtils compiles.
 *
 * @hide
 */
public class SecureBox {
    /**
     * TODO(b/69056040) Add implementation of encrypt.
     *
     * @hide
     */
    public static byte[] encrypt(
            PublicKey theirPublicKey, byte[] sharedSecret, byte[] header, byte[] payload)
            throws NoSuchAlgorithmException, InvalidKeyException {
        throw new UnsupportedOperationException("Needs to be implemented.");
    }
}
+61 −0
Original line number Diff line number Diff line
@@ -16,14 +16,21 @@

package com.android.server.locksettings.recoverablekeystore;

import android.util.Log;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

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

/**
 * A {@link javax.crypto.SecretKey} wrapped with AES/GCM/NoPadding.
@@ -31,7 +38,11 @@ import javax.crypto.SecretKey;
 * @hide
 */
public class WrappedKey {
    private static final String TAG = "WrappedKey";

    private static final String KEY_WRAP_CIPHER_ALGORITHM = "AES/GCM/NoPadding";
    private static final String APPLICATION_KEY_ALGORITHM = "AES";
    private static final int GCM_TAG_LENGTH_BITS = 128;

    private final byte[] mNonce;
    private final byte[] mKeyMaterial;
@@ -112,4 +123,54 @@ public class WrappedKey {
    public byte[] getKeyMaterial() {
        return mKeyMaterial;
    }

    /**
     * Unwraps the {@code wrappedKeys} with the {@code platformKey}.
     *
     * @return The unwrapped keys, indexed by alias.
     * @throws NoSuchAlgorithmException if AES/GCM/NoPadding Cipher or AES key type is unavailable.
     *
     * @hide
     */
    public static Map<String, SecretKey> unwrapKeys(
            SecretKey platformKey,
            Map<String, WrappedKey> wrappedKeys)
            throws NoSuchAlgorithmException, NoSuchPaddingException {
        HashMap<String, SecretKey> unwrappedKeys = new HashMap<>();
        Cipher cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM);

        for (String alias : wrappedKeys.keySet()) {
            WrappedKey wrappedKey = wrappedKeys.get(alias);
            try {
                cipher.init(
                        Cipher.UNWRAP_MODE,
                        platformKey,
                        new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.getNonce()));
            } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
                Log.e(TAG,
                        String.format(
                                Locale.US,
                                "Could not init Cipher to unwrap recoverable key with alias '%s'",
                                alias),
                        e);
                continue;
            }
            SecretKey key;
            try {
                key = (SecretKey) cipher.unwrap(
                        wrappedKey.getKeyMaterial(), APPLICATION_KEY_ALGORITHM, Cipher.SECRET_KEY);
            } catch (InvalidKeyException | NoSuchAlgorithmException e) {
                Log.e(TAG,
                        String.format(
                                Locale.US,
                                "Error unwrapping recoverable key with alias '%s'",
                                alias),
                        e);
                continue;
            }
            unwrappedKeys.put(alias, key);
        }

        return unwrappedKeys;
    }
}
+82 −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.assertEquals;
import static org.junit.Assert.assertFalse;

import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;

import javax.crypto.SecretKey;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class KeySyncUtilsTest {
    private static final int RECOVERY_KEY_LENGTH_BITS = 256;
    private static final int THM_KF_HASH_SIZE = 256;
    private static final String SHA_256_ALGORITHM = "SHA-256";

    @Test
    public void calculateThmKfHash_isShaOfLockScreenHashWithPrefix() throws Exception {
        byte[] lockScreenHash = utf8Bytes("012345678910");

        byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(lockScreenHash);

        assertArrayEquals(calculateSha256(utf8Bytes("THM_KF_hash012345678910")), thmKfHash);
    }

    @Test
    public void calculateThmKfHash_is256BitsLong() throws Exception {
        byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(utf8Bytes("1234"));

        assertEquals(THM_KF_HASH_SIZE / Byte.SIZE, thmKfHash.length);
    }

    @Test
    public void generateRecoveryKey_returnsA256BitKey() throws Exception {
        SecretKey key = KeySyncUtils.generateRecoveryKey();

        assertEquals(RECOVERY_KEY_LENGTH_BITS / Byte.SIZE, key.getEncoded().length);
    }

    @Test
    public void generateRecoveryKey_generatesANewKeyEachTime() throws Exception {
        SecretKey a = KeySyncUtils.generateRecoveryKey();
        SecretKey b = KeySyncUtils.generateRecoveryKey();

        assertFalse(Arrays.equals(a.getEncoded(), b.getEncoded()));
    }

    private static byte[] utf8Bytes(String s) {
        return s.getBytes(StandardCharsets.UTF_8);
    }

    private static byte[] calculateSha256(byte[] bytes) throws Exception {
        MessageDigest messageDigest = MessageDigest.getInstance(SHA_256_ALGORITHM);
        messageDigest.update(bytes);
        return messageDigest.digest();
    }
}
+36 −1
Original line number Diff line number Diff line
@@ -16,7 +16,9 @@

package com.android.server.locksettings.recoverablekeystore;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import android.security.keystore.AndroidKeyStoreSecretKey;
import android.security.keystore.KeyGenParameterSpec;
@@ -29,6 +31,8 @@ import org.junit.Test;
import org.junit.runner.RunWith;

import java.security.KeyStore;
import java.util.HashMap;
import java.util.Map;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
@@ -70,6 +74,36 @@ public class WrappedKeyTest {
        assertEquals(rawKey, unwrappedKey);
    }

    @Test
    public void decryptWrappedKeys_decryptsWrappedKeys() throws Exception {
        String alias = "karlin";
        SecretKey platformKey = generateAndroidKeyStoreKey();
        SecretKey appKey = generateKey();
        WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey, appKey);
        HashMap<String, WrappedKey> keysByAlias = new HashMap<>();
        keysByAlias.put(alias, wrappedKey);

        Map<String, SecretKey> unwrappedKeys = WrappedKey.unwrapKeys(platformKey, keysByAlias);

        assertEquals(1, unwrappedKeys.size());
        assertTrue(unwrappedKeys.containsKey(alias));
        assertArrayEquals(appKey.getEncoded(), unwrappedKeys.get(alias).getEncoded());
    }

    @Test
    public void decryptWrappedKeys_doesNotDieIfSomeKeysAreUnwrappable() throws Exception {
        String alias = "karlin";
        SecretKey appKey = generateKey();
        WrappedKey wrappedKey = WrappedKey.fromSecretKey(generateKey(), appKey);
        HashMap<String, WrappedKey> keysByAlias = new HashMap<>();
        keysByAlias.put(alias, wrappedKey);

        Map<String, SecretKey> unwrappedKeys = WrappedKey.unwrapKeys(
                generateAndroidKeyStoreKey(), keysByAlias);

        assertEquals(0, unwrappedKeys.size());
    }

    private SecretKey generateKey() throws Exception {
        KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
        keyGenerator.init(/*keySize=*/ 256);
@@ -81,7 +115,8 @@ public class WrappedKeyTest {
                KEY_ALGORITHM,
                ANDROID_KEY_STORE_PROVIDER);
        keyGenerator.init(new KeyGenParameterSpec.Builder(
                WRAPPING_KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                WRAPPING_KEY_ALIAS,
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .build());