Loading services/core/java/com/android/server/locksettings/RebootEscrowData.java +11 −33 Original line number Diff line number Diff line Loading @@ -26,16 +26,12 @@ import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; /** * Holds the data necessary to complete a reboot escrow of the Synthetic Password. Loading @@ -47,17 +43,11 @@ class RebootEscrowData { */ private static final int CURRENT_VERSION = 1; /** The secret key will be of this format. */ private static final String KEY_ALGO = "AES"; /** The key size used for encrypting the reboot escrow data. */ private static final int KEY_SIZE_BITS = 256; /** The algorithm used for the encryption of the key blob. */ private static final String CIPHER_ALGO = "AES/GCM/NoPadding"; private RebootEscrowData(byte spVersion, byte[] iv, byte[] syntheticPassword, byte[] blob, byte[] key) { RebootEscrowKey key) { mSpVersion = spVersion; mIv = iv; mSyntheticPassword = syntheticPassword; Loading @@ -69,7 +59,7 @@ class RebootEscrowData { private final byte[] mIv; private final byte[] mSyntheticPassword; private final byte[] mBlob; private final byte[] mKey; private final RebootEscrowKey mKey; public byte getSpVersion() { return mSpVersion; Loading @@ -87,17 +77,13 @@ class RebootEscrowData { return mBlob; } public byte[] getKey() { public RebootEscrowKey getKey() { return mKey; } static SecretKeySpec fromKeyBytes(byte[] keyBytes) { return new SecretKeySpec(keyBytes, KEY_ALGO); } static RebootEscrowData fromEncryptedData(SecretKeySpec keySpec, byte[] blob) static RebootEscrowData fromEncryptedData(RebootEscrowKey key, byte[] blob) throws IOException { Preconditions.checkNotNull(keySpec); Preconditions.checkNotNull(key); Preconditions.checkNotNull(blob); DataInputStream dis = new DataInputStream(new ByteArrayInputStream(blob)); Loading Loading @@ -126,7 +112,7 @@ class RebootEscrowData { final byte[] syntheticPassword; try { Cipher c = Cipher.getInstance(CIPHER_ALGO); c.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv)); c.init(Cipher.DECRYPT_MODE, key.getKey(), new IvParameterSpec(iv)); syntheticPassword = c.doFinal(cipherText); } catch (NoSuchAlgorithmException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException Loading @@ -134,30 +120,22 @@ class RebootEscrowData { throw new IOException("Could not decrypt ciphertext", e); } return new RebootEscrowData(spVersion, iv, syntheticPassword, blob, keySpec.getEncoded()); return new RebootEscrowData(spVersion, iv, syntheticPassword, blob, key); } static RebootEscrowData fromSyntheticPassword(byte spVersion, byte[] syntheticPassword) static RebootEscrowData fromSyntheticPassword(RebootEscrowKey key, byte spVersion, byte[] syntheticPassword) throws IOException { Preconditions.checkNotNull(syntheticPassword); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); final SecretKey secretKey; try { KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGO); keyGenerator.init(KEY_SIZE_BITS, new SecureRandom()); secretKey = keyGenerator.generateKey(); } catch (NoSuchAlgorithmException e) { throw new IOException("Could not generate new secret key", e); } final byte[] cipherText; final byte[] iv; try { Cipher cipher = Cipher.getInstance(CIPHER_ALGO); cipher.init(Cipher.ENCRYPT_MODE, secretKey); cipher.init(Cipher.ENCRYPT_MODE, key.getKey()); cipherText = cipher.doFinal(syntheticPassword); iv = cipher.getIV(); } catch (NoSuchAlgorithmException | BadPaddingException | IllegalBlockSizeException Loading @@ -173,6 +151,6 @@ class RebootEscrowData { dos.write(cipherText); return new RebootEscrowData(spVersion, iv, syntheticPassword, bos.toByteArray(), secretKey.getEncoded()); key); } } services/core/java/com/android/server/locksettings/RebootEscrowKey.java 0 → 100644 +67 −0 Original line number Diff line number Diff line /* * 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.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; /** * Key used to encrypt and decrypt the {@link RebootEscrowData}. */ class RebootEscrowKey { /** The secret key will be of this format. */ private static final String KEY_ALGO = "AES"; /** The key size used for encrypting the reboot escrow data. */ private static final int KEY_SIZE_BITS = 256; private final SecretKey mKey; private RebootEscrowKey(SecretKey key) { mKey = key; } static RebootEscrowKey fromKeyBytes(byte[] keyBytes) { return new RebootEscrowKey(new SecretKeySpec(keyBytes, KEY_ALGO)); } static RebootEscrowKey generate() throws IOException { final SecretKey secretKey; try { KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGO); keyGenerator.init(KEY_SIZE_BITS, new SecureRandom()); secretKey = keyGenerator.generateKey(); } catch (NoSuchAlgorithmException e) { throw new IOException("Could not generate new secret key", e); } return new RebootEscrowKey(secretKey); } SecretKey getKey() { return mKey; } byte[] getKeyBytes() { return mKey.getEncoded(); } } services/core/java/com/android/server/locksettings/RebootEscrowManager.java +57 −20 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import android.os.UserManager; import android.util.Slog; import android.util.StatsLog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.RebootEscrowListener; Loading @@ -34,18 +35,16 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicBoolean; import javax.crypto.spec.SecretKeySpec; class RebootEscrowManager { private static final String TAG = "RebootEscrowManager"; /** * Used to track when the reboot escrow is wanted. Set to false when mRebootEscrowReady is * true. * Used to track when the reboot escrow is wanted. Should stay true once escrow is requested * unless clearRebootEscrow is called. This will allow all the active users to be unlocked * after reboot. */ private final AtomicBoolean mRebootEscrowWanted = new AtomicBoolean(false); private boolean mRebootEscrowWanted; /** Used to track when reboot escrow is ready. */ private boolean mRebootEscrowReady; Loading @@ -53,11 +52,17 @@ class RebootEscrowManager { /** Notified when mRebootEscrowReady changes. */ private RebootEscrowListener mRebootEscrowListener; /** * Hold this lock when checking or generating the reboot escrow key. */ private final Object mKeyGenerationLock = new Object(); /** * Stores the reboot escrow data between when it's supplied and when * {@link #armRebootEscrowIfNeeded()} is called. */ private RebootEscrowData mPendingRebootEscrowData; @GuardedBy("mKeyGenerationLock") private RebootEscrowKey mPendingRebootEscrowKey; private final UserManager mUserManager; Loading @@ -82,6 +87,7 @@ class RebootEscrowManager { public Context getContext() { return mContext; } public UserManager getUserManager() { return (UserManager) mContext.getSystemService(Context.USER_SERVICE); } Loading Loading @@ -123,7 +129,7 @@ class RebootEscrowManager { return; } SecretKeySpec escrowKey = getAndClearRebootEscrowKey(); RebootEscrowKey escrowKey = getAndClearRebootEscrowKey(); if (escrowKey == null) { Slog.w(TAG, "Had reboot escrow data for users, but no key; removing escrow storage."); for (UserInfo user : users) { Loading @@ -140,7 +146,7 @@ class RebootEscrowManager { StatsLog.write(StatsLog.REBOOT_ESCROW_RECOVERY_REPORTED, allUsersUnlocked); } private SecretKeySpec getAndClearRebootEscrowKey() { private RebootEscrowKey getAndClearRebootEscrowKey() { IRebootEscrow rebootEscrow = mInjector.getRebootEscrow(); if (rebootEscrow == null) { return null; Loading Loading @@ -170,14 +176,14 @@ class RebootEscrowManager { // Overwrite the existing key with the null key rebootEscrow.storeKey(new byte[32]); return RebootEscrowData.fromKeyBytes(escrowKeyBytes); return RebootEscrowKey.fromKeyBytes(escrowKeyBytes); } catch (RemoteException e) { Slog.w(TAG, "Could not retrieve escrow data"); return null; } } private boolean restoreRebootEscrowForUser(@UserIdInt int userId, SecretKeySpec escrowKey) { private boolean restoreRebootEscrowForUser(@UserIdInt int userId, RebootEscrowKey key) { if (!mStorage.hasRebootEscrow(userId)) { return false; } Loading @@ -186,7 +192,7 @@ class RebootEscrowManager { byte[] blob = mStorage.readRebootEscrow(userId); mStorage.removeRebootEscrow(userId); RebootEscrowData escrowData = RebootEscrowData.fromEncryptedData(escrowKey, blob); RebootEscrowData escrowData = RebootEscrowData.fromEncryptedData(key, blob); mCallbacks.onRebootEscrowRestored(escrowData.getSpVersion(), escrowData.getSyntheticPassword(), userId); Loading @@ -199,33 +205,60 @@ class RebootEscrowManager { void callToRebootEscrowIfNeeded(@UserIdInt int userId, byte spVersion, byte[] syntheticPassword) { if (!mRebootEscrowWanted.compareAndSet(true, false)) { if (!mRebootEscrowWanted) { return; } IRebootEscrow rebootEscrow = mInjector.getRebootEscrow(); if (rebootEscrow == null) { mRebootEscrowWanted = false; setRebootEscrowReady(false); return; } RebootEscrowKey escrowKey = generateEscrowKeyIfNeeded(); if (escrowKey == null) { Slog.e(TAG, "Could not generate escrow key"); mRebootEscrowWanted = false; setRebootEscrowReady(false); return; } final RebootEscrowData escrowData; try { escrowData = RebootEscrowData.fromSyntheticPassword(spVersion, syntheticPassword); escrowData = RebootEscrowData.fromSyntheticPassword(escrowKey, spVersion, syntheticPassword); } catch (IOException e) { setRebootEscrowReady(false); Slog.w(TAG, "Could not escrow reboot data", e); return; } mPendingRebootEscrowData = escrowData; mStorage.writeRebootEscrow(userId, escrowData.getBlob()); setRebootEscrowReady(true); } private RebootEscrowKey generateEscrowKeyIfNeeded() { synchronized (mKeyGenerationLock) { if (mPendingRebootEscrowKey != null) { return mPendingRebootEscrowKey; } RebootEscrowKey key; try { key = RebootEscrowKey.generate(); } catch (IOException e) { return null; } mPendingRebootEscrowKey = key; return key; } } private void clearRebootEscrowIfNeeded() { mRebootEscrowWanted.set(false); mRebootEscrowWanted = false; setRebootEscrowReady(false); IRebootEscrow rebootEscrow = mInjector.getRebootEscrow(); Loading Loading @@ -255,14 +288,18 @@ class RebootEscrowManager { return false; } RebootEscrowData escrowData = mPendingRebootEscrowData; if (escrowData == null) { RebootEscrowKey escrowKey; synchronized (mKeyGenerationLock) { escrowKey = mPendingRebootEscrowKey; } if (escrowKey == null) { return false; } boolean armedRebootEscrow = false; try { rebootEscrow.storeKey(escrowData.getKey()); rebootEscrow.storeKey(escrowKey.getKeyBytes()); armedRebootEscrow = true; } catch (RemoteException e) { Slog.w(TAG, "Failed escrow secret to RebootEscrow HAL", e); Loading @@ -283,7 +320,7 @@ class RebootEscrowManager { } clearRebootEscrowIfNeeded(); mRebootEscrowWanted.set(true); mRebootEscrowWanted = true; return true; } Loading services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowDataTest.java +14 −8 Original line number Diff line number Diff line Loading @@ -21,16 +21,22 @@ import static org.junit.Assert.assertThat; import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import javax.crypto.spec.SecretKeySpec; /** * atest FrameworksServicesTests:RebootEscrowDataTest */ @RunWith(AndroidJUnit4.class) public class RebootEscrowDataTest { private RebootEscrowKey mKey; @Before public void generateKey() throws Exception { mKey = RebootEscrowKey.generate(); } private static byte[] getTestSp() { byte[] testSp = new byte[10]; for (int i = 0; i < testSp.length; i++) { Loading @@ -41,30 +47,30 @@ public class RebootEscrowDataTest { @Test(expected = NullPointerException.class) public void fromEntries_failsOnNull() throws Exception { RebootEscrowData.fromSyntheticPassword((byte) 2, null); RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, null); } @Test(expected = NullPointerException.class) public void fromEncryptedData_failsOnNullData() throws Exception { byte[] testSp = getTestSp(); RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword((byte) 2, testSp); SecretKeySpec key = RebootEscrowData.fromKeyBytes(expected.getKey()); RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp); RebootEscrowKey key = RebootEscrowKey.fromKeyBytes(expected.getKey().getKeyBytes()); RebootEscrowData.fromEncryptedData(key, null); } @Test(expected = NullPointerException.class) public void fromEncryptedData_failsOnNullKey() throws Exception { byte[] testSp = getTestSp(); RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword((byte) 2, testSp); RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp); RebootEscrowData.fromEncryptedData(null, expected.getBlob()); } @Test public void fromEntries_loopback_success() throws Exception { byte[] testSp = getTestSp(); RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword((byte) 2, testSp); RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp); SecretKeySpec key = RebootEscrowData.fromKeyBytes(expected.getKey()); RebootEscrowKey key = RebootEscrowKey.fromKeyBytes(expected.getKey().getKeyBytes()); RebootEscrowData actual = RebootEscrowData.fromEncryptedData(key, expected.getBlob()); assertThat(actual.getSpVersion(), is(expected.getSpVersion())); Loading services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java +35 −3 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.server.locksettings; import static android.content.pm.UserInfo.FLAG_FULL; import static android.content.pm.UserInfo.FLAG_PRIMARY; import static android.content.pm.UserInfo.FLAG_PROFILE; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; Loading @@ -27,6 +28,7 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; Loading Loading @@ -56,7 +58,9 @@ import java.util.ArrayList; @RunWith(AndroidJUnit4.class) public class RebootEscrowManagerTests { protected static final int PRIMARY_USER_ID = 0; protected static final int NONSECURE_USER_ID = 10; protected static final int WORK_PROFILE_USER_ID = 10; protected static final int NONSECURE_SECONDARY_USER_ID = 20; protected static final int SECURE_SECONDARY_USER_ID = 21; private static final byte FAKE_SP_VERSION = 1; private static final byte[] FAKE_AUTH_TOKEN = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, Loading Loading @@ -107,10 +111,14 @@ public class RebootEscrowManagerTests { ArrayList<UserInfo> users = new ArrayList<>(); users.add(new UserInfo(PRIMARY_USER_ID, "primary", FLAG_PRIMARY)); users.add(new UserInfo(NONSECURE_USER_ID, "non-secure", FLAG_FULL)); users.add(new UserInfo(WORK_PROFILE_USER_ID, "work", FLAG_PROFILE)); users.add(new UserInfo(NONSECURE_SECONDARY_USER_ID, "non-secure", FLAG_FULL)); users.add(new UserInfo(SECURE_SECONDARY_USER_ID, "secure", FLAG_FULL)); when(mUserManager.getUsers()).thenReturn(users); when(mCallbacks.isUserSecure(PRIMARY_USER_ID)).thenReturn(true); when(mCallbacks.isUserSecure(NONSECURE_USER_ID)).thenReturn(false); when(mCallbacks.isUserSecure(WORK_PROFILE_USER_ID)).thenReturn(true); when(mCallbacks.isUserSecure(NONSECURE_SECONDARY_USER_ID)).thenReturn(false); when(mCallbacks.isUserSecure(SECURE_SECONDARY_USER_ID)).thenReturn(true); mService = new RebootEscrowManager(new MockInjector(mContext, mUserManager, mRebootEscrow), mCallbacks, mStorage); } Loading Loading @@ -154,6 +162,30 @@ public class RebootEscrowManagerTests { assertTrue(mService.armRebootEscrowIfNeeded()); verify(mRebootEscrow).storeKey(any()); assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID)); assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID)); } @Test public void armService_MultipleUsers_Success() throws Exception { RebootEscrowListener mockListener = mock(RebootEscrowListener.class); mService.setRebootEscrowListener(mockListener); mService.prepareRebootEscrow(); clearInvocations(mRebootEscrow); mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN); verify(mockListener).onPreparedForReboot(eq(true)); mService.callToRebootEscrowIfNeeded(SECURE_SECONDARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN); verify(mRebootEscrow, never()).storeKey(any()); assertTrue(mService.armRebootEscrowIfNeeded()); verify(mRebootEscrow, times(1)).storeKey(any()); assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID)); assertTrue(mStorage.hasRebootEscrow(SECURE_SECONDARY_USER_ID)); assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID)); } @Test Loading Loading
services/core/java/com/android/server/locksettings/RebootEscrowData.java +11 −33 Original line number Diff line number Diff line Loading @@ -26,16 +26,12 @@ import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; /** * Holds the data necessary to complete a reboot escrow of the Synthetic Password. Loading @@ -47,17 +43,11 @@ class RebootEscrowData { */ private static final int CURRENT_VERSION = 1; /** The secret key will be of this format. */ private static final String KEY_ALGO = "AES"; /** The key size used for encrypting the reboot escrow data. */ private static final int KEY_SIZE_BITS = 256; /** The algorithm used for the encryption of the key blob. */ private static final String CIPHER_ALGO = "AES/GCM/NoPadding"; private RebootEscrowData(byte spVersion, byte[] iv, byte[] syntheticPassword, byte[] blob, byte[] key) { RebootEscrowKey key) { mSpVersion = spVersion; mIv = iv; mSyntheticPassword = syntheticPassword; Loading @@ -69,7 +59,7 @@ class RebootEscrowData { private final byte[] mIv; private final byte[] mSyntheticPassword; private final byte[] mBlob; private final byte[] mKey; private final RebootEscrowKey mKey; public byte getSpVersion() { return mSpVersion; Loading @@ -87,17 +77,13 @@ class RebootEscrowData { return mBlob; } public byte[] getKey() { public RebootEscrowKey getKey() { return mKey; } static SecretKeySpec fromKeyBytes(byte[] keyBytes) { return new SecretKeySpec(keyBytes, KEY_ALGO); } static RebootEscrowData fromEncryptedData(SecretKeySpec keySpec, byte[] blob) static RebootEscrowData fromEncryptedData(RebootEscrowKey key, byte[] blob) throws IOException { Preconditions.checkNotNull(keySpec); Preconditions.checkNotNull(key); Preconditions.checkNotNull(blob); DataInputStream dis = new DataInputStream(new ByteArrayInputStream(blob)); Loading Loading @@ -126,7 +112,7 @@ class RebootEscrowData { final byte[] syntheticPassword; try { Cipher c = Cipher.getInstance(CIPHER_ALGO); c.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv)); c.init(Cipher.DECRYPT_MODE, key.getKey(), new IvParameterSpec(iv)); syntheticPassword = c.doFinal(cipherText); } catch (NoSuchAlgorithmException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException Loading @@ -134,30 +120,22 @@ class RebootEscrowData { throw new IOException("Could not decrypt ciphertext", e); } return new RebootEscrowData(spVersion, iv, syntheticPassword, blob, keySpec.getEncoded()); return new RebootEscrowData(spVersion, iv, syntheticPassword, blob, key); } static RebootEscrowData fromSyntheticPassword(byte spVersion, byte[] syntheticPassword) static RebootEscrowData fromSyntheticPassword(RebootEscrowKey key, byte spVersion, byte[] syntheticPassword) throws IOException { Preconditions.checkNotNull(syntheticPassword); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); final SecretKey secretKey; try { KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGO); keyGenerator.init(KEY_SIZE_BITS, new SecureRandom()); secretKey = keyGenerator.generateKey(); } catch (NoSuchAlgorithmException e) { throw new IOException("Could not generate new secret key", e); } final byte[] cipherText; final byte[] iv; try { Cipher cipher = Cipher.getInstance(CIPHER_ALGO); cipher.init(Cipher.ENCRYPT_MODE, secretKey); cipher.init(Cipher.ENCRYPT_MODE, key.getKey()); cipherText = cipher.doFinal(syntheticPassword); iv = cipher.getIV(); } catch (NoSuchAlgorithmException | BadPaddingException | IllegalBlockSizeException Loading @@ -173,6 +151,6 @@ class RebootEscrowData { dos.write(cipherText); return new RebootEscrowData(spVersion, iv, syntheticPassword, bos.toByteArray(), secretKey.getEncoded()); key); } }
services/core/java/com/android/server/locksettings/RebootEscrowKey.java 0 → 100644 +67 −0 Original line number Diff line number Diff line /* * 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.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; /** * Key used to encrypt and decrypt the {@link RebootEscrowData}. */ class RebootEscrowKey { /** The secret key will be of this format. */ private static final String KEY_ALGO = "AES"; /** The key size used for encrypting the reboot escrow data. */ private static final int KEY_SIZE_BITS = 256; private final SecretKey mKey; private RebootEscrowKey(SecretKey key) { mKey = key; } static RebootEscrowKey fromKeyBytes(byte[] keyBytes) { return new RebootEscrowKey(new SecretKeySpec(keyBytes, KEY_ALGO)); } static RebootEscrowKey generate() throws IOException { final SecretKey secretKey; try { KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGO); keyGenerator.init(KEY_SIZE_BITS, new SecureRandom()); secretKey = keyGenerator.generateKey(); } catch (NoSuchAlgorithmException e) { throw new IOException("Could not generate new secret key", e); } return new RebootEscrowKey(secretKey); } SecretKey getKey() { return mKey; } byte[] getKeyBytes() { return mKey.getEncoded(); } }
services/core/java/com/android/server/locksettings/RebootEscrowManager.java +57 −20 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import android.os.UserManager; import android.util.Slog; import android.util.StatsLog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.RebootEscrowListener; Loading @@ -34,18 +35,16 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicBoolean; import javax.crypto.spec.SecretKeySpec; class RebootEscrowManager { private static final String TAG = "RebootEscrowManager"; /** * Used to track when the reboot escrow is wanted. Set to false when mRebootEscrowReady is * true. * Used to track when the reboot escrow is wanted. Should stay true once escrow is requested * unless clearRebootEscrow is called. This will allow all the active users to be unlocked * after reboot. */ private final AtomicBoolean mRebootEscrowWanted = new AtomicBoolean(false); private boolean mRebootEscrowWanted; /** Used to track when reboot escrow is ready. */ private boolean mRebootEscrowReady; Loading @@ -53,11 +52,17 @@ class RebootEscrowManager { /** Notified when mRebootEscrowReady changes. */ private RebootEscrowListener mRebootEscrowListener; /** * Hold this lock when checking or generating the reboot escrow key. */ private final Object mKeyGenerationLock = new Object(); /** * Stores the reboot escrow data between when it's supplied and when * {@link #armRebootEscrowIfNeeded()} is called. */ private RebootEscrowData mPendingRebootEscrowData; @GuardedBy("mKeyGenerationLock") private RebootEscrowKey mPendingRebootEscrowKey; private final UserManager mUserManager; Loading @@ -82,6 +87,7 @@ class RebootEscrowManager { public Context getContext() { return mContext; } public UserManager getUserManager() { return (UserManager) mContext.getSystemService(Context.USER_SERVICE); } Loading Loading @@ -123,7 +129,7 @@ class RebootEscrowManager { return; } SecretKeySpec escrowKey = getAndClearRebootEscrowKey(); RebootEscrowKey escrowKey = getAndClearRebootEscrowKey(); if (escrowKey == null) { Slog.w(TAG, "Had reboot escrow data for users, but no key; removing escrow storage."); for (UserInfo user : users) { Loading @@ -140,7 +146,7 @@ class RebootEscrowManager { StatsLog.write(StatsLog.REBOOT_ESCROW_RECOVERY_REPORTED, allUsersUnlocked); } private SecretKeySpec getAndClearRebootEscrowKey() { private RebootEscrowKey getAndClearRebootEscrowKey() { IRebootEscrow rebootEscrow = mInjector.getRebootEscrow(); if (rebootEscrow == null) { return null; Loading Loading @@ -170,14 +176,14 @@ class RebootEscrowManager { // Overwrite the existing key with the null key rebootEscrow.storeKey(new byte[32]); return RebootEscrowData.fromKeyBytes(escrowKeyBytes); return RebootEscrowKey.fromKeyBytes(escrowKeyBytes); } catch (RemoteException e) { Slog.w(TAG, "Could not retrieve escrow data"); return null; } } private boolean restoreRebootEscrowForUser(@UserIdInt int userId, SecretKeySpec escrowKey) { private boolean restoreRebootEscrowForUser(@UserIdInt int userId, RebootEscrowKey key) { if (!mStorage.hasRebootEscrow(userId)) { return false; } Loading @@ -186,7 +192,7 @@ class RebootEscrowManager { byte[] blob = mStorage.readRebootEscrow(userId); mStorage.removeRebootEscrow(userId); RebootEscrowData escrowData = RebootEscrowData.fromEncryptedData(escrowKey, blob); RebootEscrowData escrowData = RebootEscrowData.fromEncryptedData(key, blob); mCallbacks.onRebootEscrowRestored(escrowData.getSpVersion(), escrowData.getSyntheticPassword(), userId); Loading @@ -199,33 +205,60 @@ class RebootEscrowManager { void callToRebootEscrowIfNeeded(@UserIdInt int userId, byte spVersion, byte[] syntheticPassword) { if (!mRebootEscrowWanted.compareAndSet(true, false)) { if (!mRebootEscrowWanted) { return; } IRebootEscrow rebootEscrow = mInjector.getRebootEscrow(); if (rebootEscrow == null) { mRebootEscrowWanted = false; setRebootEscrowReady(false); return; } RebootEscrowKey escrowKey = generateEscrowKeyIfNeeded(); if (escrowKey == null) { Slog.e(TAG, "Could not generate escrow key"); mRebootEscrowWanted = false; setRebootEscrowReady(false); return; } final RebootEscrowData escrowData; try { escrowData = RebootEscrowData.fromSyntheticPassword(spVersion, syntheticPassword); escrowData = RebootEscrowData.fromSyntheticPassword(escrowKey, spVersion, syntheticPassword); } catch (IOException e) { setRebootEscrowReady(false); Slog.w(TAG, "Could not escrow reboot data", e); return; } mPendingRebootEscrowData = escrowData; mStorage.writeRebootEscrow(userId, escrowData.getBlob()); setRebootEscrowReady(true); } private RebootEscrowKey generateEscrowKeyIfNeeded() { synchronized (mKeyGenerationLock) { if (mPendingRebootEscrowKey != null) { return mPendingRebootEscrowKey; } RebootEscrowKey key; try { key = RebootEscrowKey.generate(); } catch (IOException e) { return null; } mPendingRebootEscrowKey = key; return key; } } private void clearRebootEscrowIfNeeded() { mRebootEscrowWanted.set(false); mRebootEscrowWanted = false; setRebootEscrowReady(false); IRebootEscrow rebootEscrow = mInjector.getRebootEscrow(); Loading Loading @@ -255,14 +288,18 @@ class RebootEscrowManager { return false; } RebootEscrowData escrowData = mPendingRebootEscrowData; if (escrowData == null) { RebootEscrowKey escrowKey; synchronized (mKeyGenerationLock) { escrowKey = mPendingRebootEscrowKey; } if (escrowKey == null) { return false; } boolean armedRebootEscrow = false; try { rebootEscrow.storeKey(escrowData.getKey()); rebootEscrow.storeKey(escrowKey.getKeyBytes()); armedRebootEscrow = true; } catch (RemoteException e) { Slog.w(TAG, "Failed escrow secret to RebootEscrow HAL", e); Loading @@ -283,7 +320,7 @@ class RebootEscrowManager { } clearRebootEscrowIfNeeded(); mRebootEscrowWanted.set(true); mRebootEscrowWanted = true; return true; } Loading
services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowDataTest.java +14 −8 Original line number Diff line number Diff line Loading @@ -21,16 +21,22 @@ import static org.junit.Assert.assertThat; import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import javax.crypto.spec.SecretKeySpec; /** * atest FrameworksServicesTests:RebootEscrowDataTest */ @RunWith(AndroidJUnit4.class) public class RebootEscrowDataTest { private RebootEscrowKey mKey; @Before public void generateKey() throws Exception { mKey = RebootEscrowKey.generate(); } private static byte[] getTestSp() { byte[] testSp = new byte[10]; for (int i = 0; i < testSp.length; i++) { Loading @@ -41,30 +47,30 @@ public class RebootEscrowDataTest { @Test(expected = NullPointerException.class) public void fromEntries_failsOnNull() throws Exception { RebootEscrowData.fromSyntheticPassword((byte) 2, null); RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, null); } @Test(expected = NullPointerException.class) public void fromEncryptedData_failsOnNullData() throws Exception { byte[] testSp = getTestSp(); RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword((byte) 2, testSp); SecretKeySpec key = RebootEscrowData.fromKeyBytes(expected.getKey()); RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp); RebootEscrowKey key = RebootEscrowKey.fromKeyBytes(expected.getKey().getKeyBytes()); RebootEscrowData.fromEncryptedData(key, null); } @Test(expected = NullPointerException.class) public void fromEncryptedData_failsOnNullKey() throws Exception { byte[] testSp = getTestSp(); RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword((byte) 2, testSp); RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp); RebootEscrowData.fromEncryptedData(null, expected.getBlob()); } @Test public void fromEntries_loopback_success() throws Exception { byte[] testSp = getTestSp(); RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword((byte) 2, testSp); RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp); SecretKeySpec key = RebootEscrowData.fromKeyBytes(expected.getKey()); RebootEscrowKey key = RebootEscrowKey.fromKeyBytes(expected.getKey().getKeyBytes()); RebootEscrowData actual = RebootEscrowData.fromEncryptedData(key, expected.getBlob()); assertThat(actual.getSpVersion(), is(expected.getSpVersion())); Loading
services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java +35 −3 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.server.locksettings; import static android.content.pm.UserInfo.FLAG_FULL; import static android.content.pm.UserInfo.FLAG_PRIMARY; import static android.content.pm.UserInfo.FLAG_PROFILE; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; Loading @@ -27,6 +28,7 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; Loading Loading @@ -56,7 +58,9 @@ import java.util.ArrayList; @RunWith(AndroidJUnit4.class) public class RebootEscrowManagerTests { protected static final int PRIMARY_USER_ID = 0; protected static final int NONSECURE_USER_ID = 10; protected static final int WORK_PROFILE_USER_ID = 10; protected static final int NONSECURE_SECONDARY_USER_ID = 20; protected static final int SECURE_SECONDARY_USER_ID = 21; private static final byte FAKE_SP_VERSION = 1; private static final byte[] FAKE_AUTH_TOKEN = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, Loading Loading @@ -107,10 +111,14 @@ public class RebootEscrowManagerTests { ArrayList<UserInfo> users = new ArrayList<>(); users.add(new UserInfo(PRIMARY_USER_ID, "primary", FLAG_PRIMARY)); users.add(new UserInfo(NONSECURE_USER_ID, "non-secure", FLAG_FULL)); users.add(new UserInfo(WORK_PROFILE_USER_ID, "work", FLAG_PROFILE)); users.add(new UserInfo(NONSECURE_SECONDARY_USER_ID, "non-secure", FLAG_FULL)); users.add(new UserInfo(SECURE_SECONDARY_USER_ID, "secure", FLAG_FULL)); when(mUserManager.getUsers()).thenReturn(users); when(mCallbacks.isUserSecure(PRIMARY_USER_ID)).thenReturn(true); when(mCallbacks.isUserSecure(NONSECURE_USER_ID)).thenReturn(false); when(mCallbacks.isUserSecure(WORK_PROFILE_USER_ID)).thenReturn(true); when(mCallbacks.isUserSecure(NONSECURE_SECONDARY_USER_ID)).thenReturn(false); when(mCallbacks.isUserSecure(SECURE_SECONDARY_USER_ID)).thenReturn(true); mService = new RebootEscrowManager(new MockInjector(mContext, mUserManager, mRebootEscrow), mCallbacks, mStorage); } Loading Loading @@ -154,6 +162,30 @@ public class RebootEscrowManagerTests { assertTrue(mService.armRebootEscrowIfNeeded()); verify(mRebootEscrow).storeKey(any()); assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID)); assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID)); } @Test public void armService_MultipleUsers_Success() throws Exception { RebootEscrowListener mockListener = mock(RebootEscrowListener.class); mService.setRebootEscrowListener(mockListener); mService.prepareRebootEscrow(); clearInvocations(mRebootEscrow); mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN); verify(mockListener).onPreparedForReboot(eq(true)); mService.callToRebootEscrowIfNeeded(SECURE_SECONDARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN); verify(mRebootEscrow, never()).storeKey(any()); assertTrue(mService.armRebootEscrowIfNeeded()); verify(mRebootEscrow, times(1)).storeKey(any()); assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID)); assertTrue(mStorage.hasRebootEscrow(SECURE_SECONDARY_USER_ID)); assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID)); } @Test Loading