diff options
| author | 2021-01-14 04:42:51 +0000 | |
|---|---|---|
| committer | 2021-01-14 04:42:51 +0000 | |
| commit | d59b2a1bd7e2db49678d265cd059f354932780db (patch) | |
| tree | cf4ad60c6719a062de5083b4dcd397978e2d5180 | |
| parent | 0bb7e55f1088c6c0e5012c77a3ae2a1d75594d8b (diff) | |
| parent | 500579cd2b22b8d19a53fb5aa9692b1438375e05 (diff) | |
Merge changes Id0e18bef,Ie2b5f559 am: 5bf384cae9 am: e5d8f60389 am: 500579cd2b
Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/1531240
MUST ONLY BE SUBMITTED BY AUTOMERGER
Change-Id: I44fcae82534c876bec19f9c90d05dd363727be9b
6 files changed, 390 insertions, 92 deletions
diff --git a/services/core/java/com/android/server/locksettings/AesEncryptionUtil.java b/services/core/java/com/android/server/locksettings/AesEncryptionUtil.java new file mode 100644 index 000000000000..8e7e419a6b0e --- /dev/null +++ b/services/core/java/com/android/server/locksettings/AesEncryptionUtil.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2020 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; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Objects; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + +class AesEncryptionUtil { + /** The algorithm used for the encryption of the key blob. */ + private static final String CIPHER_ALGO = "AES/GCM/NoPadding"; + + private AesEncryptionUtil() {} + + static byte[] decrypt(SecretKey key, DataInputStream cipherStream) throws IOException { + Objects.requireNonNull(key); + Objects.requireNonNull(cipherStream); + + int ivSize = cipherStream.readInt(); + if (ivSize < 0 || ivSize > 32) { + throw new IOException("IV out of range: " + ivSize); + } + byte[] iv = new byte[ivSize]; + cipherStream.readFully(iv); + + int rawCipherTextSize = cipherStream.readInt(); + if (rawCipherTextSize < 0) { + throw new IOException("Invalid cipher text size: " + rawCipherTextSize); + } + + byte[] rawCipherText = new byte[rawCipherTextSize]; + cipherStream.readFully(rawCipherText); + + final byte[] plainText; + try { + Cipher c = Cipher.getInstance(CIPHER_ALGO); + c.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv)); + plainText = c.doFinal(rawCipherText); + } catch (NoSuchAlgorithmException | InvalidKeyException | BadPaddingException + | IllegalBlockSizeException | NoSuchPaddingException + | InvalidAlgorithmParameterException e) { + throw new IOException("Could not decrypt cipher text", e); + } + + return plainText; + } + + static byte[] decrypt(SecretKey key, byte[] cipherText) throws IOException { + Objects.requireNonNull(key); + Objects.requireNonNull(cipherText); + + DataInputStream cipherStream = new DataInputStream(new ByteArrayInputStream(cipherText)); + return decrypt(key, cipherStream); + } + + static byte[] encrypt(SecretKey key, byte[] plainText) throws IOException { + Objects.requireNonNull(key); + Objects.requireNonNull(plainText); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + final byte[] cipherText; + final byte[] iv; + try { + Cipher cipher = Cipher.getInstance(CIPHER_ALGO); + cipher.init(Cipher.ENCRYPT_MODE, key); + cipherText = cipher.doFinal(plainText); + iv = cipher.getIV(); + } catch (NoSuchAlgorithmException | BadPaddingException | IllegalBlockSizeException + | NoSuchPaddingException | InvalidKeyException e) { + throw new IOException("Could not encrypt input data", e); + } + + dos.writeInt(iv.length); + dos.write(iv); + dos.writeInt(cipherText.length); + dos.write(cipherText); + + return bos.toByteArray(); + } +} diff --git a/services/core/java/com/android/server/locksettings/RebootEscrowData.java b/services/core/java/com/android/server/locksettings/RebootEscrowData.java index 2b1907985aeb..38eeb88e63b0 100644 --- a/services/core/java/com/android/server/locksettings/RebootEscrowData.java +++ b/services/core/java/com/android/server/locksettings/RebootEscrowData.java @@ -16,22 +16,14 @@ package com.android.server.locksettings; -import com.android.internal.util.Preconditions; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; +import java.util.Objects; -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.IvParameterSpec; +import javax.crypto.SecretKey; /** * Holds the data necessary to complete a reboot escrow of the Synthetic Password. @@ -41,22 +33,17 @@ class RebootEscrowData { * This is the current version of the escrow data format. This should be incremented if the * format on disk is changed. */ - private static final int CURRENT_VERSION = 1; - - /** The algorithm used for the encryption of the key blob. */ - private static final String CIPHER_ALGO = "AES/GCM/NoPadding"; + private static final int CURRENT_VERSION = 2; - private RebootEscrowData(byte spVersion, byte[] iv, byte[] syntheticPassword, byte[] blob, + private RebootEscrowData(byte spVersion, byte[] syntheticPassword, byte[] blob, RebootEscrowKey key) { mSpVersion = spVersion; - mIv = iv; mSyntheticPassword = syntheticPassword; mBlob = blob; mKey = key; } private final byte mSpVersion; - private final byte[] mIv; private final byte[] mSyntheticPassword; private final byte[] mBlob; private final RebootEscrowKey mKey; @@ -65,10 +52,6 @@ class RebootEscrowData { return mSpVersion; } - public byte[] getIv() { - return mIv; - } - public byte[] getSyntheticPassword() { return mSyntheticPassword; } @@ -81,76 +64,43 @@ class RebootEscrowData { return mKey; } - static RebootEscrowData fromEncryptedData(RebootEscrowKey key, byte[] blob) + static RebootEscrowData fromEncryptedData(RebootEscrowKey ks, byte[] blob, SecretKey kk) throws IOException { - Preconditions.checkNotNull(key); - Preconditions.checkNotNull(blob); + Objects.requireNonNull(ks); + Objects.requireNonNull(blob); DataInputStream dis = new DataInputStream(new ByteArrayInputStream(blob)); int version = dis.readInt(); if (version != CURRENT_VERSION) { throw new IOException("Unsupported version " + version); } - byte spVersion = dis.readByte(); - int ivSize = dis.readInt(); - if (ivSize < 0 || ivSize > 32) { - throw new IOException("IV out of range: " + ivSize); - } - byte[] iv = new byte[ivSize]; - dis.readFully(iv); + // Decrypt the blob with the key from keystore first, then decrypt again with the reboot + // escrow key. + byte[] ksEncryptedBlob = AesEncryptionUtil.decrypt(kk, dis); + final byte[] syntheticPassword = AesEncryptionUtil.decrypt(ks.getKey(), ksEncryptedBlob); - int cipherTextSize = dis.readInt(); - if (cipherTextSize < 0) { - throw new IOException("Invalid cipher text size: " + cipherTextSize); - } - - byte[] cipherText = new byte[cipherTextSize]; - dis.readFully(cipherText); - - final byte[] syntheticPassword; - try { - Cipher c = Cipher.getInstance(CIPHER_ALGO); - c.init(Cipher.DECRYPT_MODE, key.getKey(), new IvParameterSpec(iv)); - syntheticPassword = c.doFinal(cipherText); - } catch (NoSuchAlgorithmException | InvalidKeyException | BadPaddingException - | IllegalBlockSizeException | NoSuchPaddingException - | InvalidAlgorithmParameterException e) { - throw new IOException("Could not decrypt ciphertext", e); - } - - return new RebootEscrowData(spVersion, iv, syntheticPassword, blob, key); + return new RebootEscrowData(spVersion, syntheticPassword, blob, ks); } - static RebootEscrowData fromSyntheticPassword(RebootEscrowKey key, byte spVersion, - byte[] syntheticPassword) + static RebootEscrowData fromSyntheticPassword(RebootEscrowKey ks, byte spVersion, + byte[] syntheticPassword, SecretKey kk) throws IOException { - Preconditions.checkNotNull(syntheticPassword); + Objects.requireNonNull(syntheticPassword); + + // Encrypt synthetic password with the escrow key first; then encrypt the blob again with + // the key from keystore. + byte[] ksEncryptedBlob = AesEncryptionUtil.encrypt(ks.getKey(), syntheticPassword); + byte[] kkEncryptedBlob = AesEncryptionUtil.encrypt(kk, ksEncryptedBlob); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - final byte[] cipherText; - final byte[] iv; - try { - Cipher cipher = Cipher.getInstance(CIPHER_ALGO); - cipher.init(Cipher.ENCRYPT_MODE, key.getKey()); - cipherText = cipher.doFinal(syntheticPassword); - iv = cipher.getIV(); - } catch (NoSuchAlgorithmException | BadPaddingException | IllegalBlockSizeException - | NoSuchPaddingException | InvalidKeyException e) { - throw new IOException("Could not encrypt reboot escrow data", e); - } - dos.writeInt(CURRENT_VERSION); dos.writeByte(spVersion); - dos.writeInt(iv.length); - dos.write(iv); - dos.writeInt(cipherText.length); - dos.write(cipherText); + dos.write(kkEncryptedBlob); - return new RebootEscrowData(spVersion, iv, syntheticPassword, bos.toByteArray(), - key); + return new RebootEscrowData(spVersion, syntheticPassword, bos.toByteArray(), ks); } } diff --git a/services/core/java/com/android/server/locksettings/RebootEscrowKeyStoreManager.java b/services/core/java/com/android/server/locksettings/RebootEscrowKeyStoreManager.java new file mode 100644 index 000000000000..bae029c79968 --- /dev/null +++ b/services/core/java/com/android/server/locksettings/RebootEscrowKeyStoreManager.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2020 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; + +import android.security.keystore.AndroidKeyStoreSpi; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.security.keystore2.AndroidKeyStoreLoadStoreParameter; +import android.security.keystore2.AndroidKeyStoreProvider; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +/** + * This class loads and generates the key used for resume on reboot from android keystore. + */ +public class RebootEscrowKeyStoreManager { + private static final String TAG = "RebootEscrowKeyStoreManager"; + + /** + * The key alias in keystore. This key is used to wrap both escrow key and escrow data. + */ + public static final String REBOOT_ESCROW_KEY_STORE_ENCRYPTION_KEY_NAME = + "reboot_escrow_key_store_encryption_key"; + + public static final int KEY_LENGTH = 256; + + /** + * Use keystore2 once it's installed. + */ + private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeystore"; + + /** + * The selinux namespace for resume_on_reboot_key + */ + private static final int KEY_STORE_NAMESPACE = 120; + + /** + * Hold this lock when getting or generating the encryption key in keystore. + */ + private final Object mKeyStoreLock = new Object(); + + @GuardedBy("mKeyStoreLock") + private SecretKey getKeyStoreEncryptionKeyLocked() { + try { + KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER); + KeyStore.LoadStoreParameter loadStoreParameter = null; + // Load from the specific namespace if keystore2 is enabled. + if (AndroidKeyStoreProvider.isInstalled()) { + loadStoreParameter = new AndroidKeyStoreLoadStoreParameter(KEY_STORE_NAMESPACE); + } + keyStore.load(loadStoreParameter); + return (SecretKey) keyStore.getKey(REBOOT_ESCROW_KEY_STORE_ENCRYPTION_KEY_NAME, + null); + } catch (IOException | GeneralSecurityException e) { + Slog.e(TAG, "Unable to get encryption key from keystore.", e); + } + return null; + } + + protected SecretKey getKeyStoreEncryptionKey() { + synchronized (mKeyStoreLock) { + return getKeyStoreEncryptionKeyLocked(); + } + } + + protected void clearKeyStoreEncryptionKey() { + synchronized (mKeyStoreLock) { + try { + KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER); + KeyStore.LoadStoreParameter loadStoreParameter = null; + // Load from the specific namespace if keystore2 is enabled. + if (AndroidKeyStoreProvider.isInstalled()) { + loadStoreParameter = new AndroidKeyStoreLoadStoreParameter(KEY_STORE_NAMESPACE); + } + keyStore.load(loadStoreParameter); + keyStore.deleteEntry(REBOOT_ESCROW_KEY_STORE_ENCRYPTION_KEY_NAME); + } catch (IOException | GeneralSecurityException e) { + Slog.e(TAG, "Unable to delete encryption key in keystore.", e); + } + } + } + + protected SecretKey generateKeyStoreEncryptionKeyIfNeeded() { + synchronized (mKeyStoreLock) { + SecretKey kk = getKeyStoreEncryptionKeyLocked(); + if (kk != null) { + return kk; + } + + try { + KeyGenerator generator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStoreSpi.NAME); + KeyGenParameterSpec.Builder parameterSpecBuilder = new KeyGenParameterSpec.Builder( + REBOOT_ESCROW_KEY_STORE_ENCRYPTION_KEY_NAME, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setKeySize(KEY_LENGTH) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE); + // Generate the key with the correct namespace if keystore2 is enabled. + if (AndroidKeyStoreProvider.isInstalled()) { + parameterSpecBuilder.setNamespace(KEY_STORE_NAMESPACE); + } + generator.init(parameterSpecBuilder.build()); + return generator.generateKey(); + } catch (GeneralSecurityException e) { + // Should never happen. + Slog.e(TAG, "Unable to generate key from keystore.", e); + } + return null; + } + } +} diff --git a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java index 8d5f553dba5c..289290bab4dc 100644 --- a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java +++ b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java @@ -40,6 +40,18 @@ import java.util.Date; import java.util.List; import java.util.Locale; +import javax.crypto.SecretKey; + +/** + * This class aims to persists the synthetic password(SP) across reboot in a secure way. In + * particular, it manages the encryption of the sp before reboot, and decryption of the sp after + * reboot. Here are the meaning of some terms. + * SP: synthetic password + * K_s: The RebootEscrowKey, i.e. AES-GCM key stored in memory + * K_k: AES-GCM key in android keystore + * RebootEscrowData: The synthetic password and its encrypted blob. We encrypt SP with K_s first, + * then with K_k, i.e. E(K_k, E(K_s, SP)) + */ class RebootEscrowManager { private static final String TAG = "RebootEscrowManager"; @@ -101,6 +113,8 @@ class RebootEscrowManager { private final Callbacks mCallbacks; + private final RebootEscrowKeyStoreManager mKeyStoreManager; + interface Callbacks { boolean isUserSecure(int userId); @@ -109,11 +123,13 @@ class RebootEscrowManager { static class Injector { protected Context mContext; - + private final RebootEscrowKeyStoreManager mKeyStoreManager; private final RebootEscrowProviderInterface mRebootEscrowProvider; Injector(Context context) { mContext = context; + mKeyStoreManager = new RebootEscrowKeyStoreManager(); + RebootEscrowProviderInterface rebootEscrowProvider = null; // TODO(xunchang) add implementation for server based ror. if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_OTA, @@ -138,6 +154,10 @@ class RebootEscrowManager { return (UserManager) mContext.getSystemService(Context.USER_SERVICE); } + public RebootEscrowKeyStoreManager getKeyStoreManager() { + return mKeyStoreManager; + } + public RebootEscrowProviderInterface getRebootEscrowProvider() { return mRebootEscrowProvider; } @@ -168,6 +188,7 @@ class RebootEscrowManager { mStorage = storage; mUserManager = injector.getUserManager(); mEventLog = injector.getEventLog(); + mKeyStoreManager = injector.getKeyStoreManager(); } void loadRebootEscrowDataIfAvailable() { @@ -183,8 +204,12 @@ class RebootEscrowManager { return; } - RebootEscrowKey escrowKey = getAndClearRebootEscrowKey(); - if (escrowKey == null) { + // Fetch the key from keystore to decrypt the escrow data & escrow key; this key is + // generated before reboot. Note that we will clear the escrow key even if the keystore key + // is null. + SecretKey kk = mKeyStoreManager.getKeyStoreEncryptionKey(); + RebootEscrowKey escrowKey = getAndClearRebootEscrowKey(kk); + if (kk == null || escrowKey == null) { Slog.w(TAG, "Had reboot escrow data for users, but no key; removing escrow storage."); for (UserInfo user : users) { mStorage.removeRebootEscrow(user.id); @@ -197,7 +222,7 @@ class RebootEscrowManager { boolean allUsersUnlocked = true; for (UserInfo user : rebootEscrowUsers) { - allUsersUnlocked &= restoreRebootEscrowForUser(user.id, escrowKey); + allUsersUnlocked &= restoreRebootEscrowForUser(user.id, escrowKey, kk); } onEscrowRestoreComplete(allUsersUnlocked); } @@ -212,7 +237,7 @@ class RebootEscrowManager { } } - private RebootEscrowKey getAndClearRebootEscrowKey() { + private RebootEscrowKey getAndClearRebootEscrowKey(SecretKey kk) { RebootEscrowProviderInterface rebootEscrowProvider = mInjector.getRebootEscrowProvider(); if (rebootEscrowProvider == null) { Slog.w(TAG, @@ -220,14 +245,16 @@ class RebootEscrowManager { return null; } - RebootEscrowKey key = rebootEscrowProvider.getAndClearRebootEscrowKey(null); + // The K_s blob maybe encrypted by K_k as well. + RebootEscrowKey key = rebootEscrowProvider.getAndClearRebootEscrowKey(kk); if (key != null) { mEventLog.addEntry(RebootEscrowEvent.RETRIEVED_STORED_KEK); } return key; } - private boolean restoreRebootEscrowForUser(@UserIdInt int userId, RebootEscrowKey key) { + private boolean restoreRebootEscrowForUser(@UserIdInt int userId, RebootEscrowKey ks, + SecretKey kk) { if (!mStorage.hasRebootEscrow(userId)) { return false; } @@ -236,7 +263,7 @@ class RebootEscrowManager { byte[] blob = mStorage.readRebootEscrow(userId); mStorage.removeRebootEscrow(userId); - RebootEscrowData escrowData = RebootEscrowData.fromEncryptedData(key, blob); + RebootEscrowData escrowData = RebootEscrowData.fromEncryptedData(ks, blob, kk); mCallbacks.onRebootEscrowRestored(escrowData.getSpVersion(), escrowData.getSyntheticPassword(), userId); @@ -246,6 +273,9 @@ class RebootEscrowManager { } catch (IOException e) { Slog.w(TAG, "Could not load reboot escrow data for user " + userId, e); return false; + } finally { + // Clear the old key in keystore. A new key will be generated by new RoR requests. + mKeyStoreManager.clearKeyStoreEncryptionKey(); } } @@ -267,11 +297,16 @@ class RebootEscrowManager { return; } + SecretKey kk = mKeyStoreManager.generateKeyStoreEncryptionKeyIfNeeded(); + if (kk == null) { + Slog.e(TAG, "Failed to generate encryption key from keystore."); + return; + } + final RebootEscrowData escrowData; try { - // TODO(xunchang) further wrap the escrowData with a key from keystore. escrowData = RebootEscrowData.fromSyntheticPassword(escrowKey, spVersion, - syntheticPassword); + syntheticPassword, kk); } catch (IOException e) { setRebootEscrowReady(false); Slog.w(TAG, "Could not escrow reboot data", e); @@ -348,7 +383,13 @@ class RebootEscrowManager { return false; } - boolean armedRebootEscrow = rebootEscrowProvider.storeRebootEscrowKey(escrowKey, null); + // We will use the same key from keystore to encrypt the escrow key and escrow data blob. + SecretKey kk = mKeyStoreManager.getKeyStoreEncryptionKey(); + if (kk == null) { + Slog.e(TAG, "Failed to get encryption key from keystore."); + return false; + } + boolean armedRebootEscrow = rebootEscrowProvider.storeRebootEscrowKey(escrowKey, kk); if (armedRebootEscrow) { mStorage.setInt(REBOOT_ESCROW_ARMED_KEY, mInjector.getBootCount(), USER_SYSTEM); mEventLog.addEntry(RebootEscrowEvent.SET_ARMED_STATUS); diff --git a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowDataTest.java b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowDataTest.java index 46f43e7af596..32445fd1a47d 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowDataTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowDataTest.java @@ -19,22 +19,44 @@ package com.android.server.locksettings; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; + import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.security.GeneralSecurityException; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + /** * atest FrameworksServicesTests:RebootEscrowDataTest */ @RunWith(AndroidJUnit4.class) public class RebootEscrowDataTest { private RebootEscrowKey mKey; + private SecretKey mKeyStoreEncryptionKey; + + private SecretKey generateNewRebootEscrowEncryptionKey() throws GeneralSecurityException { + KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES); + generator.init(new KeyGenParameterSpec.Builder( + "reboot_escrow_data_test_key", + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setKeySize(256) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build()); + return generator.generateKey(); + } @Before public void generateKey() throws Exception { mKey = RebootEscrowKey.generate(); + mKeyStoreEncryptionKey = generateNewRebootEscrowEncryptionKey(); } private static byte[] getTestSp() { @@ -47,36 +69,49 @@ public class RebootEscrowDataTest { @Test(expected = NullPointerException.class) public void fromEntries_failsOnNull() throws Exception { - RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, null); + RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, null, mKeyStoreEncryptionKey); } @Test(expected = NullPointerException.class) public void fromEncryptedData_failsOnNullData() throws Exception { byte[] testSp = getTestSp(); - RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp); + RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp, + mKeyStoreEncryptionKey); RebootEscrowKey key = RebootEscrowKey.fromKeyBytes(expected.getKey().getKeyBytes()); - RebootEscrowData.fromEncryptedData(key, null); + RebootEscrowData.fromEncryptedData(key, null, mKeyStoreEncryptionKey); } @Test(expected = NullPointerException.class) public void fromEncryptedData_failsOnNullKey() throws Exception { byte[] testSp = getTestSp(); - RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp); - RebootEscrowData.fromEncryptedData(null, expected.getBlob()); + RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp, + mKeyStoreEncryptionKey); + RebootEscrowData.fromEncryptedData(null, expected.getBlob(), mKeyStoreEncryptionKey); } @Test public void fromEntries_loopback_success() throws Exception { byte[] testSp = getTestSp(); - RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp); + RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp, + mKeyStoreEncryptionKey); RebootEscrowKey key = RebootEscrowKey.fromKeyBytes(expected.getKey().getKeyBytes()); - RebootEscrowData actual = RebootEscrowData.fromEncryptedData(key, expected.getBlob()); + RebootEscrowData actual = RebootEscrowData.fromEncryptedData(key, expected.getBlob(), + mKeyStoreEncryptionKey); assertThat(actual.getSpVersion(), is(expected.getSpVersion())); - assertThat(actual.getIv(), is(expected.getIv())); assertThat(actual.getKey().getKeyBytes(), is(expected.getKey().getKeyBytes())); assertThat(actual.getBlob(), is(expected.getBlob())); assertThat(actual.getSyntheticPassword(), is(expected.getSyntheticPassword())); } + + @Test + public void aesEncryptedBlob_loopback_success() throws Exception { + byte[] testSp = getTestSp(); + byte [] encrypted = AesEncryptionUtil.encrypt(mKeyStoreEncryptionKey, testSp); + byte [] decrypted = AesEncryptionUtil.decrypt(mKeyStoreEncryptionKey, encrypted); + + assertThat(decrypted, is(testSp)); + } + } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java index 98d64524d87a..f74e45b6e59b 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java @@ -61,6 +61,9 @@ import org.mockito.ArgumentCaptor; import java.io.File; import java.util.ArrayList; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + @SmallTest @Presubmit @RunWith(AndroidJUnit4.class) @@ -77,15 +80,25 @@ public class RebootEscrowManagerTests { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, }; + // Hex encoding of a randomly generated AES key for test. + private static final byte[] TEST_AES_KEY = new byte[] { + 0x48, 0x19, 0x12, 0x54, 0x13, 0x13, 0x52, 0x31, + 0x44, 0x74, 0x61, 0x54, 0x29, 0x74, 0x37, 0x61, + 0x70, 0x70, 0x75, 0x25, 0x27, 0x31, 0x49, 0x09, + 0x26, 0x52, 0x72, 0x63, 0x63, 0x61, 0x78, 0x23, + }; + private Context mContext; private UserManager mUserManager; private RebootEscrowManager.Callbacks mCallbacks; private IRebootEscrow mRebootEscrow; + private RebootEscrowKeyStoreManager mKeyStoreManager; LockSettingsStorageTestable mStorage; private MockableRebootEscrowInjected mInjected; private RebootEscrowManager mService; + private SecretKey mAesKey; public interface MockableRebootEscrowInjected { int getBootCount(); @@ -98,9 +111,11 @@ public class RebootEscrowManagerTests { private final RebootEscrowProviderInterface mRebootEscrowProvider; private final UserManager mUserManager; private final MockableRebootEscrowInjected mInjected; + private final RebootEscrowKeyStoreManager mKeyStoreManager; MockInjector(Context context, UserManager userManager, IRebootEscrow rebootEscrow, + RebootEscrowKeyStoreManager keyStoreManager, MockableRebootEscrowInjected injected) { super(context); mRebootEscrow = rebootEscrow; @@ -114,6 +129,7 @@ public class RebootEscrowManagerTests { }; mRebootEscrowProvider = new RebootEscrowProviderHalImpl(halInjector); mUserManager = userManager; + mKeyStoreManager = keyStoreManager; mInjected = injected; } @@ -128,6 +144,11 @@ public class RebootEscrowManagerTests { } @Override + public RebootEscrowKeyStoreManager getKeyStoreManager() { + return mKeyStoreManager; + } + + @Override public int getBootCount() { return mInjected.getBootCount(); } @@ -144,6 +165,11 @@ public class RebootEscrowManagerTests { mUserManager = mock(UserManager.class); mCallbacks = mock(RebootEscrowManager.Callbacks.class); mRebootEscrow = mock(IRebootEscrow.class); + mKeyStoreManager = mock(RebootEscrowKeyStoreManager.class); + mAesKey = new SecretKeySpec(TEST_AES_KEY, "AES"); + + when(mKeyStoreManager.getKeyStoreEncryptionKey()).thenReturn(mAesKey); + when(mKeyStoreManager.generateKeyStoreEncryptionKeyIfNeeded()).thenReturn(mAesKey); mStorage = new LockSettingsStorageTestable(mContext, new File(InstrumentationRegistry.getContext().getFilesDir(), "locksettings")); @@ -160,7 +186,7 @@ public class RebootEscrowManagerTests { when(mCallbacks.isUserSecure(SECURE_SECONDARY_USER_ID)).thenReturn(true); mInjected = mock(MockableRebootEscrowInjected.class); mService = new RebootEscrowManager(new MockInjector(mContext, mUserManager, mRebootEscrow, - mInjected), mCallbacks, mStorage); + mKeyStoreManager, mInjected), mCallbacks, mStorage); } @Test @@ -213,6 +239,7 @@ public class RebootEscrowManagerTests { assertNotNull( mStorage.getString(RebootEscrowManager.REBOOT_ESCROW_ARMED_KEY, null, USER_SYSTEM)); verify(mRebootEscrow).storeKey(any()); + verify(mKeyStoreManager).getKeyStoreEncryptionKey(); assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID)); assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID)); @@ -300,6 +327,7 @@ public class RebootEscrowManagerTests { ArgumentCaptor<byte[]> keyByteCaptor = ArgumentCaptor.forClass(byte[].class); assertTrue(mService.armRebootEscrowIfNeeded()); verify(mRebootEscrow).storeKey(keyByteCaptor.capture()); + verify(mKeyStoreManager).getKeyStoreEncryptionKey(); assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID)); assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID)); @@ -314,6 +342,7 @@ public class RebootEscrowManagerTests { mService.loadRebootEscrowDataIfAvailable(); verify(mRebootEscrow).retrieveKey(); assertTrue(metricsSuccessCaptor.getValue()); + verify(mKeyStoreManager).clearKeyStoreEncryptionKey(); } @Test |