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

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

Merge "Add KeySyncUtils"

parents e4bdecc9 d416ed53
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());