Migrate KeyWrapUtils

Bring KeyWrapUtils in from GMSCore. This class relies heavily on a set
of protobufs, so this CL includes the creation of the protobuf target
support it and the inclusion of that target in the tests.

Bug: 111386661
Test: atest BackupFrameworksServicesRoboTests
Change-Id: I89e0c68a449f784b132780410d9de32824bb674a
This commit is contained in:
Al Sutton
2019-08-20 15:43:30 +01:00
parent ad52c6bc3a
commit c949517f4d
5 changed files with 351 additions and 1 deletions

View File

@@ -17,8 +17,15 @@
android_app {
name: "BackupEncryption",
srcs: ["src/**/*.java"],
libs: ["backup-encryption-protos"],
optimize: { enabled: false },
platform_apis: true,
certificate: "platform",
privileged: true,
}
}
java_library {
name: "backup-encryption-protos",
proto: { type: "nano" },
srcs: ["proto/**/*.proto"],
}

View File

@@ -0,0 +1,52 @@
syntax = "proto2";
package android_backup_crypto;
option java_package = "com.android.server.backup.encryption.protos";
option java_outer_classname = "WrappedKeyProto";
// Metadata associated with a tertiary key.
message KeyMetadata {
// Type of Cipher algorithm the key is used for.
enum Type {
UNKNOWN = 0;
// No padding. Uses 12-byte nonce. Tag length 16 bytes.
AES_256_GCM = 1;
}
// What kind of Cipher algorithm the key is used for. We assume at the moment
// that this will always be AES_256_GCM and throw if this is not the case.
// Provided here for forwards compatibility in case at some point we need to
// change Cipher algorithm.
optional Type type = 1;
}
// An encrypted tertiary key.
message WrappedKey {
// The Cipher with which the key was encrypted.
enum WrapAlgorithm {
UNKNOWN = 0;
// No padding. Uses 16-byte nonce (see nonce field). Tag length 16 bytes.
// The nonce is 16-bytes as this is wrapped with a key in AndroidKeyStore.
// AndroidKeyStore requires that it generates the IV, and it generates a
// 16-byte IV for you. You CANNOT provide your own IV.
AES_256_GCM = 1;
}
// Cipher algorithm used to wrap the key. We assume at the moment that this
// is always AES_256_GC and throw if this is not the case. Provided here for
// forwards compatibility if at some point we need to change Cipher algorithm.
optional WrapAlgorithm wrap_algorithm = 1;
// The nonce used to initialize the Cipher in AES/256/GCM mode.
optional bytes nonce = 2;
// The encrypted bytes of the key material.
optional bytes key = 3;
// Associated key metadata.
optional KeyMetadata metadata = 4;
// Deprecated field; Do not use
reserved 5;
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright (C) 2019 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.backup.encryption.keys;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
/** Utility functions for wrapping and unwrapping tertiary keys. */
public class KeyWrapUtils {
private static final String AES_GCM_MODE = "AES/GCM/NoPadding";
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 String KEY_ALGORITHM = "AES";
/**
* Uses the secondary key to unwrap the wrapped tertiary key.
*
* @param secondaryKey The secondary key used to wrap the tertiary key.
* @param wrappedKey The wrapped tertiary key.
* @return The unwrapped tertiary key.
* @throws InvalidKeyException if the provided secondary key cannot unwrap the tertiary key.
*/
public static SecretKey unwrap(SecretKey secondaryKey, WrappedKeyProto.WrappedKey wrappedKey)
throws InvalidKeyException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException {
if (wrappedKey.wrapAlgorithm != WrappedKeyProto.WrappedKey.AES_256_GCM) {
throw new InvalidKeyException(
String.format(
Locale.US,
"Could not unwrap key wrapped with %s algorithm",
wrappedKey.wrapAlgorithm));
}
if (wrappedKey.metadata == null) {
throw new InvalidKeyException("Metadata missing from wrapped tertiary key.");
}
if (wrappedKey.metadata.type != WrappedKeyProto.KeyMetadata.AES_256_GCM) {
throw new InvalidKeyException(
String.format(
Locale.US,
"Wrapped key was unexpected %s algorithm. Only support"
+ " AES/GCM/NoPadding.",
wrappedKey.metadata.type));
}
Cipher cipher = getCipher();
cipher.init(
Cipher.UNWRAP_MODE,
secondaryKey,
new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.nonce));
return (SecretKey) cipher.unwrap(wrappedKey.key, KEY_ALGORITHM, Cipher.SECRET_KEY);
}
/**
* Wraps the tertiary key with the secondary key.
*
* @param secondaryKey The secondary key to use for wrapping.
* @param tertiaryKey The key to wrap.
* @return The wrapped key.
* @throws InvalidKeyException if the key is not good for wrapping.
* @throws IllegalBlockSizeException if there is an issue wrapping.
*/
public static WrappedKeyProto.WrappedKey wrap(SecretKey secondaryKey, SecretKey tertiaryKey)
throws InvalidKeyException, IllegalBlockSizeException, NoSuchAlgorithmException,
NoSuchPaddingException {
Cipher cipher = getCipher();
cipher.init(Cipher.WRAP_MODE, secondaryKey);
WrappedKeyProto.WrappedKey wrappedKey = new WrappedKeyProto.WrappedKey();
wrappedKey.key = cipher.wrap(tertiaryKey);
wrappedKey.nonce = cipher.getIV();
wrappedKey.wrapAlgorithm = WrappedKeyProto.WrappedKey.AES_256_GCM;
wrappedKey.metadata = new WrappedKeyProto.KeyMetadata();
wrappedKey.metadata.type = WrappedKeyProto.KeyMetadata.AES_256_GCM;
return wrappedKey;
}
/**
* Rewraps a tertiary key with a new secondary key.
*
* @param oldSecondaryKey The old secondary key, used to unwrap the tertiary key.
* @param newSecondaryKey The new secondary key, used to rewrap the tertiary key.
* @param tertiaryKey The tertiary key, wrapped by {@code oldSecondaryKey}.
* @return The tertiary key, wrapped by {@code newSecondaryKey}.
* @throws InvalidKeyException if the key is not good for wrapping or unwrapping.
* @throws IllegalBlockSizeException if there is an issue wrapping.
*/
public static WrappedKeyProto.WrappedKey rewrap(
SecretKey oldSecondaryKey,
SecretKey newSecondaryKey,
WrappedKeyProto.WrappedKey tertiaryKey)
throws InvalidKeyException, IllegalBlockSizeException,
InvalidAlgorithmParameterException, NoSuchAlgorithmException,
NoSuchPaddingException {
return wrap(newSecondaryKey, unwrap(oldSecondaryKey, tertiaryKey));
}
private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
return Cipher.getInstance(AES_GCM_MODE);
}
// Statics only
private KeyWrapUtils() {}
}

View File

@@ -20,6 +20,7 @@ android_robolectric_test {
],
java_resource_dirs: ["config"],
libs: [
"backup-encryption-protos",
"platform-test-annotations",
"testng",
],

View File

@@ -0,0 +1,158 @@
/*
* Copyright (C) 2019 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.backup.encryption.keys;
import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
import static com.google.common.truth.Truth.assertThat;
import android.platform.test.annotations.Presubmit;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.security.InvalidKeyException;
import javax.crypto.SecretKey;
/** Key wrapping tests */
@RunWith(RobolectricTestRunner.class)
@Presubmit
public class KeyWrapUtilsTest {
private static final int KEY_SIZE_BITS = 256;
private static final int BITS_PER_BYTE = 8;
private static final int GCM_NONCE_LENGTH_BYTES = 16;
private static final int GCM_TAG_LENGTH_BYTES = 16;
/** Test a wrapped key has metadata */
@Test
public void wrap_addsMetadata() throws Exception {
WrappedKeyProto.WrappedKey wrappedKey =
KeyWrapUtils.wrap(
/*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
assertThat(wrappedKey.metadata).isNotNull();
assertThat(wrappedKey.metadata.type).isEqualTo(WrappedKeyProto.KeyMetadata.AES_256_GCM);
}
/** Test a wrapped key has an algorithm specified */
@Test
public void wrap_addsWrapAlgorithm() throws Exception {
WrappedKeyProto.WrappedKey wrappedKey =
KeyWrapUtils.wrap(
/*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
assertThat(wrappedKey.wrapAlgorithm).isEqualTo(WrappedKeyProto.WrappedKey.AES_256_GCM);
}
/** Test a wrapped key haas an nonce of the right length */
@Test
public void wrap_addsNonceOfAppropriateLength() throws Exception {
WrappedKeyProto.WrappedKey wrappedKey =
KeyWrapUtils.wrap(
/*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
assertThat(wrappedKey.nonce).hasLength(GCM_NONCE_LENGTH_BYTES);
}
/** Test a wrapped key has a key of the right length */
@Test
public void wrap_addsTagOfAppropriateLength() throws Exception {
WrappedKeyProto.WrappedKey wrappedKey =
KeyWrapUtils.wrap(
/*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
assertThat(wrappedKey.key).hasLength(KEY_SIZE_BITS / BITS_PER_BYTE + GCM_TAG_LENGTH_BYTES);
}
/** Ensure a key can be wrapped and unwrapped again */
@Test
public void unwrap_unwrapsEncryptedKey() throws Exception {
SecretKey secondaryKey = generateAesKey();
SecretKey tertiaryKey = generateAesKey();
WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, tertiaryKey);
SecretKey unwrappedKey = KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
assertThat(unwrappedKey).isEqualTo(tertiaryKey);
}
/** Ensure the unwrap method rejects keys with bad algorithms */
@Test(expected = InvalidKeyException.class)
public void unwrap_throwsForBadWrapAlgorithm() throws Exception {
SecretKey secondaryKey = generateAesKey();
WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, generateAesKey());
wrappedKey.wrapAlgorithm = WrappedKeyProto.WrappedKey.UNKNOWN;
KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
}
/** Ensure the unwrap method rejects metadata indicating the encryption type is unknown */
@Test(expected = InvalidKeyException.class)
public void unwrap_throwsForBadKeyAlgorithm() throws Exception {
SecretKey secondaryKey = generateAesKey();
WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, generateAesKey());
wrappedKey.metadata.type = WrappedKeyProto.KeyMetadata.UNKNOWN;
KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
}
/** Ensure the unwrap method rejects wrapped keys missing the metadata */
@Test(expected = InvalidKeyException.class)
public void unwrap_throwsForMissingMetadata() throws Exception {
SecretKey secondaryKey = generateAesKey();
WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, generateAesKey());
wrappedKey.metadata = null;
KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
}
/** Ensure unwrap rejects invalid secondary keys */
@Test(expected = InvalidKeyException.class)
public void unwrap_throwsForBadSecondaryKey() throws Exception {
WrappedKeyProto.WrappedKey wrappedKey =
KeyWrapUtils.wrap(
/*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
KeyWrapUtils.unwrap(generateAesKey(), wrappedKey);
}
/** Ensure rewrap can rewrap keys */
@Test
public void rewrap_canBeUnwrappedWithNewSecondaryKey() throws Exception {
SecretKey tertiaryKey = generateAesKey();
SecretKey oldSecondaryKey = generateAesKey();
SecretKey newSecondaryKey = generateAesKey();
WrappedKeyProto.WrappedKey wrappedWithOld = KeyWrapUtils.wrap(oldSecondaryKey, tertiaryKey);
WrappedKeyProto.WrappedKey wrappedWithNew =
KeyWrapUtils.rewrap(oldSecondaryKey, newSecondaryKey, wrappedWithOld);
assertThat(KeyWrapUtils.unwrap(newSecondaryKey, wrappedWithNew)).isEqualTo(tertiaryKey);
}
/** Ensure rewrap doesn't create something decryptable by an old key */
@Test(expected = InvalidKeyException.class)
public void rewrap_cannotBeUnwrappedWithOldSecondaryKey() throws Exception {
SecretKey tertiaryKey = generateAesKey();
SecretKey oldSecondaryKey = generateAesKey();
SecretKey newSecondaryKey = generateAesKey();
WrappedKeyProto.WrappedKey wrappedWithOld = KeyWrapUtils.wrap(oldSecondaryKey, tertiaryKey);
WrappedKeyProto.WrappedKey wrappedWithNew =
KeyWrapUtils.rewrap(oldSecondaryKey, newSecondaryKey, wrappedWithOld);
KeyWrapUtils.unwrap(oldSecondaryKey, wrappedWithNew);
}
}