From 5d33c4652a54aea53029b16db3d4cc4e632f841a Mon Sep 17 00:00:00 2001 From: Tianjie Date: Tue, 15 Dec 2020 15:46:27 -0800 Subject: Load and generate key from keystore in reboot escrow According to the design, we need to wrap both reboot escrow key and reboot escrow data with an additional key from android keystore. The new key in keystore is created upon a new resume on reboot request, and cleared after decryption is attempted at boot time. Bug: 172780686 Test: atest FrameworksServicesTests:RebootEscrowDataTest \ FrameworksServicesTests:LockSettingsServiceTests \ FrameworksServicesTests:RecoverySystemServiceTest \ FrameworksServicesTests:RebootEscrowManagerTests Change-Id: Ie2b5f559fb5d3217337e31b0dff507b98715384d --- .../locksettings/RebootEscrowKeyStoreManager.java | 134 +++++++++++++++++++++ .../server/locksettings/RebootEscrowManager.java | 60 +++++++-- .../locksettings/RebootEscrowManagerTests.java | 31 ++++- 3 files changed, 215 insertions(+), 10 deletions(-) create mode 100644 services/core/java/com/android/server/locksettings/RebootEscrowKeyStoreManager.java 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..19a332692bdf 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); 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,6 +297,12 @@ 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. @@ -348,7 +384,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/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; } @@ -127,6 +143,11 @@ public class RebootEscrowManagerTests { return mRebootEscrowProvider; } + @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 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 -- cgit v1.2.3-59-g8ed1b From f4c1cf66670e3f4962802ed59b22691736902176 Mon Sep 17 00:00:00 2001 From: Tianjie Date: Tue, 15 Dec 2020 10:53:31 -0800 Subject: Wrap the escrow data with key from keystore Factor out a class to handle the aes encrypted blob, and further encrypt the reboot escrow data with a local key from key store. Bug: 172780686 Test: atest FrameworksServicesTests:RebootEscrowDataTest \ FrameworksServicesTests:LockSettingsServiceTests \ FrameworksServicesTests:RecoverySystemServiceTest \ FrameworksServicesTests:RebootEscrowManagerTests ; atest CtsAppSecurityHostTestCases:ResumeOnRebootHostTest Change-Id: Id0e18bef4d3b194a254fa1755cd17a43a7b6e5bc --- .../server/locksettings/AesEncryptionUtil.java | 109 +++++++++++++++++++++ .../server/locksettings/RebootEscrowData.java | 94 +++++------------- .../server/locksettings/RebootEscrowManager.java | 5 +- .../server/locksettings/RebootEscrowDataTest.java | 51 ++++++++-- 4 files changed, 176 insertions(+), 83 deletions(-) create mode 100644 services/core/java/com/android/server/locksettings/AesEncryptionUtil.java 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/RebootEscrowManager.java b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java index 19a332692bdf..289290bab4dc 100644 --- a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java +++ b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java @@ -263,7 +263,7 @@ class RebootEscrowManager { byte[] blob = mStorage.readRebootEscrow(userId); mStorage.removeRebootEscrow(userId); - RebootEscrowData escrowData = RebootEscrowData.fromEncryptedData(ks, blob); + RebootEscrowData escrowData = RebootEscrowData.fromEncryptedData(ks, blob, kk); mCallbacks.onRebootEscrowRestored(escrowData.getSpVersion(), escrowData.getSyntheticPassword(), userId); @@ -305,9 +305,8 @@ class RebootEscrowManager { 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); 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)); + } + } -- cgit v1.2.3-59-g8ed1b