From 28dea68567d3df2c459af3bafd6a26ac0508acc1 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Wed, 20 Feb 2019 13:37:51 -0800 Subject: [PATCH] Partition weaver slots to avoid conflicts between the host image and GSI. The device OS and an installed GSI will both attempt to write authentication data to the same weaver slots. To prevent this, we can use the /metadata partition (required for GSI support) to communicate which slots are in use between OS images. In this change, PasswordSlotManager stores a simple plain-text mapping in /metadata/password_slots/slot_map using Java Properties. Each key is an integer slot >= 0 and the value is either "host" or "gsi", where N is the index of the GSI. Currently only one GSI is allowed, so this number is 1. SyntheticPasswordManager always informs PasswordSlotManager of which slots are actually in use, to avoid saving stale slot assignments. Stale assignments won't happen from device wipes (since the GSI and /metadata would be erased), but they are possible for deleted GSIs. PasswordSlotManager is not informed of when a GSI is deleted (since the GSI could be on, for example, an SD card), so any slots it was using are not recycled until a fresh GSI is booted. This should not be a problem since using more than 2-3 slots is rare. If no /metadata partition is present, GSIs will not work, so in this case PasswordSlotManager simply skips saving to disk. Bug: 123716647 Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/locksettings Test: atest PasswordSlotManagerTests Test: PIN unlocks device after booting into GSI Change-Id: Ibcbd32d0085061fbfc0bb2ea1f3a605a104fabe3 --- .../locksettings/LockSettingsService.java | 3 +- .../locksettings/PasswordSlotManager.java | 191 ++++++++++++++++++ .../SyntheticPasswordManager.java | 9 +- .../BaseLockSettingsServiceTests.java | 6 +- .../MockSyntheticPasswordManager.java | 7 +- .../PasswordSlotManagerTestable.java | 61 ++++++ .../PasswordSlotManagerTests.java | 122 +++++++++++ .../locksettings/SyntheticPasswordTests.java | 2 +- 8 files changed, 394 insertions(+), 7 deletions(-) create mode 100644 services/core/java/com/android/server/locksettings/PasswordSlotManager.java create mode 100644 services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTestable.java create mode 100644 services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTests.java diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index d83eb08de3f50..6900a2ce7a6b0 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -404,7 +404,8 @@ public class LockSettingsService extends ILockSettings.Stub { } public SyntheticPasswordManager getSyntheticPasswordManager(LockSettingsStorage storage) { - return new SyntheticPasswordManager(getContext(), storage, getUserManager()); + return new SyntheticPasswordManager(getContext(), storage, getUserManager(), + new PasswordSlotManager()); } public boolean hasBiometrics() { diff --git a/services/core/java/com/android/server/locksettings/PasswordSlotManager.java b/services/core/java/com/android/server/locksettings/PasswordSlotManager.java new file mode 100644 index 0000000000000..686ae2bbaedea --- /dev/null +++ b/services/core/java/com/android/server/locksettings/PasswordSlotManager.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.locksettings; + +import android.os.SystemProperties; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * A class that maintains a mapping of which password slots are used by alternate OS images when + * dual-booting a device. Currently, slots can either be owned by the host OS or a live GSI. + * This mapping is stored in /metadata/password_slots/slot_map using Java Properties. + * + * If a /metadata partition does not exist, GSIs are not supported, and PasswordSlotManager will + * simply not persist the slot mapping. + */ +public class PasswordSlotManager { + private static final String TAG = "PasswordSlotManager"; + + private static final String GSI_RUNNING_PROP = "ro.gsid.image_running"; + private static final String SLOT_MAP_DIR = "/metadata/password_slots"; + + // This maps each used password slot to the OS image that created it. Password slots are + // integer keys/indices into secure storage. The OS image is recorded as a string. The factory + // image is "host" and GSIs are "gsi" where N >= 1. + private final Map mSlotMap; + + public PasswordSlotManager() { + mSlotMap = loadSlotMap(); + } + + @VisibleForTesting + protected String getSlotMapDir() { + return SLOT_MAP_DIR; + } + + @VisibleForTesting + protected int getGsiImageNumber() { + return SystemProperties.getInt(GSI_RUNNING_PROP, 0); + } + + /** + * Notify the manager of which slots are definitively in use by the current OS image. + * + * @throws RuntimeException + */ + public void refreshActiveSlots(Set activeSlots) throws RuntimeException { + // Update which slots are owned by the current image. + final HashSet slotsToDelete = new HashSet(); + for (Map.Entry entry : mSlotMap.entrySet()) { + // Delete possibly stale entries for the current image. + if (entry.getValue().equals(getMode())) { + slotsToDelete.add(entry.getKey()); + } + } + for (Integer slot : slotsToDelete) { + mSlotMap.remove(slot); + } + + // Add slots for the current image. + for (Integer slot : activeSlots) { + mSlotMap.put(slot, getMode()); + } + + saveSlotMap(); + } + + /** + * Mark the given slot as in use by the current OS image. + * + * @throws RuntimeException + */ + public void markSlotInUse(int slot) throws RuntimeException { + if (mSlotMap.containsKey(slot) && !mSlotMap.get(slot).equals(getMode())) { + throw new RuntimeException("password slot " + slot + " is not available"); + } + mSlotMap.put(slot, getMode()); + saveSlotMap(); + } + + /** + * Mark the given slot as no longer in use by the current OS image. + * + * @throws RuntimeException + */ + public void markSlotDeleted(int slot) throws RuntimeException { + if (mSlotMap.containsKey(slot) && mSlotMap.get(slot) != getMode()) { + throw new RuntimeException("password slot " + slot + " cannot be deleted"); + } + mSlotMap.remove(slot); + saveSlotMap(); + } + + /** + * Return the set of slots used across all OS images. + * + * @return Integer set of all used slots. + */ + public Set getUsedSlots() { + return Collections.unmodifiableSet(mSlotMap.keySet()); + } + + private File getSlotMapFile() { + return Paths.get(getSlotMapDir(), "slot_map").toFile(); + } + + private String getMode() { + int gsiIndex = getGsiImageNumber(); + if (gsiIndex > 0) { + return "gsi" + gsiIndex; + } + return "host"; + } + + @VisibleForTesting + protected Map loadSlotMap(InputStream stream) throws IOException { + final HashMap map = new HashMap(); + final Properties props = new Properties(); + props.load(stream); + for (String slotString : props.stringPropertyNames()) { + final int slot = Integer.parseInt(slotString); + final String owner = props.getProperty(slotString); + map.put(slot, owner); + } + return map; + } + + private Map loadSlotMap() { + // It's okay if the file doesn't exist. + final File file = getSlotMapFile(); + if (file.exists()) { + try (FileInputStream stream = new FileInputStream(file)) { + return loadSlotMap(stream); + } catch (Exception e) { + Slog.e(TAG, "Could not load slot map file", e); + } + } + return new HashMap(); + } + + @VisibleForTesting + protected void saveSlotMap(OutputStream stream) throws IOException { + final Properties props = new Properties(); + for (Map.Entry entry : mSlotMap.entrySet()) { + props.setProperty(entry.getKey().toString(), entry.getValue()); + } + props.store(stream, ""); + } + + private void saveSlotMap() { + if (!getSlotMapFile().getParentFile().exists()) { + Slog.w(TAG, "Not saving slot map, " + getSlotMapDir() + " does not exist"); + return; + } + + try (FileOutputStream fos = new FileOutputStream(getSlotMapFile())) { + saveSlotMap(fos); + } catch (IOException e) { + Slog.e(TAG, "failed to save password slot map", e); + } + } +} diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java index 0e195bcc98e06..ca1ed242b3cdd 100644 --- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java +++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java @@ -287,14 +287,16 @@ public class SyntheticPasswordManager { private LockSettingsStorage mStorage; private IWeaver mWeaver; private WeaverConfig mWeaverConfig; + private PasswordSlotManager mPasswordSlotManager; private final UserManager mUserManager; public SyntheticPasswordManager(Context context, LockSettingsStorage storage, - UserManager userManager) { + UserManager userManager, PasswordSlotManager passwordSlotManager) { mContext = context; mStorage = storage; mUserManager = userManager; + mPasswordSlotManager = passwordSlotManager; } @VisibleForTesting @@ -324,6 +326,7 @@ public class SyntheticPasswordManager { mWeaver = null; } }); + mPasswordSlotManager.refreshActiveSlots(getUsedWeaverSlots()); } } catch (RemoteException e) { Slog.e(TAG, "Failed to get weaver service", e); @@ -561,6 +564,7 @@ public class SyntheticPasswordManager { Log.i(TAG, "Destroy weaver slot " + slot + " for user " + userId); try { weaverEnroll(slot, null, null); + mPasswordSlotManager.markSlotDeleted(slot); } catch (RemoteException e) { Log.w(TAG, "Failed to destroy slot", e); } @@ -595,6 +599,7 @@ public class SyntheticPasswordManager { private int getNextAvailableWeaverSlot() { Set usedSlots = getUsedWeaverSlots(); + usedSlots.addAll(mPasswordSlotManager.getUsedSlots()); for (int i = 0; i < mWeaverConfig.slots; i++) { if (!usedSlots.contains(i)) { return i; @@ -640,6 +645,7 @@ public class SyntheticPasswordManager { return DEFAULT_HANDLE; } saveWeaverSlot(weaverSlot, handle, userId); + mPasswordSlotManager.markSlotInUse(weaverSlot); synchronizeWeaverFrpPassword(pwd, requestedQuality, userId, weaverSlot); pwd.passwordHandle = null; @@ -798,6 +804,7 @@ public class SyntheticPasswordManager { return false; } saveWeaverSlot(slot, handle, userId); + mPasswordSlotManager.markSlotInUse(slot); } saveSecdiscardable(handle, tokenData.secdiscardableOnDisk, userId); createSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_TOKEN_BASED, authToken, diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java index aadf92448a52c..3d02576c8e7f0 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java @@ -88,6 +88,7 @@ public class BaseLockSettingsServiceTests extends AndroidTestCase { IAuthSecret mAuthSecretService; WindowManagerInternal mMockWindowManager; FakeGsiService mGsiService; + PasswordSlotManagerTestable mPasswordSlotManager; protected boolean mHasSecureLockScreen; @Override @@ -103,6 +104,7 @@ public class BaseLockSettingsServiceTests extends AndroidTestCase { mDevicePolicyManagerInternal = mock(DevicePolicyManagerInternal.class); mMockWindowManager = mock(WindowManagerInternal.class); mGsiService = new FakeGsiService(); + mPasswordSlotManager = new PasswordSlotManagerTestable(); LocalServices.removeServiceForTest(LockSettingsInternal.class); LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class); @@ -135,7 +137,7 @@ public class BaseLockSettingsServiceTests extends AndroidTestCase { } }; mSpManager = new MockSyntheticPasswordManager(mContext, mStorage, mGateKeeperService, - mUserManager); + mUserManager, mPasswordSlotManager); mAuthSecretService = mock(IAuthSecret.class); mService = new LockSettingsServiceTestable(mContext, mLockPatternUtils, mStorage, mGateKeeperService, mKeyStore, setUpStorageManagerMock(), mActivityManager, @@ -223,6 +225,8 @@ public class BaseLockSettingsServiceTests extends AndroidTestCase { File storageDir = mStorage.mStorageDir; assertTrue(FileUtils.deleteContents(storageDir)); + + mPasswordSlotManager.cleanup(); } protected void assertNotEquals(long expected, long actual) { diff --git a/services/tests/servicestests/src/com/android/server/locksettings/MockSyntheticPasswordManager.java b/services/tests/servicestests/src/com/android/server/locksettings/MockSyntheticPasswordManager.java index 6f681797b88a4..afc0f0c367372 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/MockSyntheticPasswordManager.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/MockSyntheticPasswordManager.java @@ -35,10 +35,12 @@ public class MockSyntheticPasswordManager extends SyntheticPasswordManager { private FakeGateKeeperService mGateKeeper; private IWeaver mWeaverService; + private PasswordSlotManagerTestable mPasswordSlotManager; public MockSyntheticPasswordManager(Context context, LockSettingsStorage storage, - FakeGateKeeperService gatekeeper, UserManager userManager) { - super(context, storage, userManager); + FakeGateKeeperService gatekeeper, UserManager userManager, + PasswordSlotManager passwordSlotManager) { + super(context, storage, userManager, passwordSlotManager); mGateKeeper = gatekeeper; } @@ -113,5 +115,4 @@ public class MockSyntheticPasswordManager extends SyntheticPasswordManager { mWeaverService = new MockWeaverService(); initWeaverService(); } - } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTestable.java new file mode 100644 index 0000000000000..1e855a9819ac4 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTestable.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.locksettings; + +import androidx.test.InstrumentationRegistry; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class PasswordSlotManagerTestable extends PasswordSlotManager { + + private int mGsiImageNumber; + private String mSlotMapDir; + + public PasswordSlotManagerTestable() { + mGsiImageNumber = 0; + } + + @Override + protected int getGsiImageNumber() { + return mGsiImageNumber; + } + + @Override + protected String getSlotMapDir() { + if (mSlotMapDir == null) { + final File testDir = InstrumentationRegistry.getContext().getFilesDir(); + if (!testDir.exists()) { + testDir.mkdirs(); + } + + mSlotMapDir = testDir.getPath(); + } + return mSlotMapDir; + } + + void setGsiImageNumber(int gsiImageNumber) { + mGsiImageNumber = gsiImageNumber; + } + + void cleanup() { + try { + Files.delete(Paths.get(getSlotMapDir(), "slot_map")); + } catch (Exception e) { + } + } +}; diff --git a/services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTests.java b/services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTests.java new file mode 100644 index 0000000000000..1d5a99b8af04e --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTests.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.locksettings; + +import android.test.AndroidTestCase; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class PasswordSlotManagerTests extends AndroidTestCase { + + PasswordSlotManagerTestable mManager; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mManager = new PasswordSlotManagerTestable(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + + mManager.cleanup(); + } + + public void testBasicSlotUse() throws Exception { + mManager.markSlotInUse(0); + mManager.markSlotInUse(1); + + Set expected = new HashSet(); + expected.add(0); + expected.add(1); + assertEquals(expected, mManager.getUsedSlots()); + + mManager.markSlotDeleted(1); + + expected = new HashSet(); + expected.add(0); + assertEquals(expected, mManager.getUsedSlots()); + } + + public void testMergeSlots() throws Exception { + // Add some slots from a different OS image. + mManager.setGsiImageNumber(1); + mManager.markSlotInUse(4); + mManager.markSlotInUse(6); + + // Switch back to the host image. + mManager.setGsiImageNumber(0); + mManager.markSlotInUse(0); + mManager.markSlotInUse(3); + mManager.markSlotInUse(5); + + // Correct slot information for the host image. + Set actual = new HashSet(); + actual.add(1); + actual.add(3); + mManager.refreshActiveSlots(actual); + + Set expected = new HashSet(); + expected.add(1); + expected.add(3); + expected.add(4); + expected.add(6); + assertEquals(expected, mManager.getUsedSlots()); + } + + public void testSerialization() throws Exception { + mManager.markSlotInUse(0); + mManager.markSlotInUse(1); + mManager.setGsiImageNumber(1); + mManager.markSlotInUse(4); + + final ByteArrayOutputStream saved = new ByteArrayOutputStream(); + mManager.saveSlotMap(saved); + + final HashMap expected = new HashMap(); + expected.put(0, "host"); + expected.put(1, "host"); + expected.put(4, "gsi1"); + + final Map map = mManager.loadSlotMap( + new ByteArrayInputStream(saved.toByteArray())); + assertEquals(expected, map); + } + + public void testSaving() throws Exception { + mManager.markSlotInUse(0); + mManager.markSlotInUse(1); + mManager.setGsiImageNumber(1); + mManager.markSlotInUse(4); + + // Make a new one. It should load the previous map. + mManager = new PasswordSlotManagerTestable(); + + Set expected = new HashSet(); + expected.add(0); + expected.add(1); + expected.add(4); + assertEquals(expected, mManager.getUsedSlots()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java index 89e155ef0d8cd..fe5a4de5bb0f3 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java @@ -68,7 +68,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { final String PASSWORD = "user-password"; final String BADPASSWORD = "bad-password"; MockSyntheticPasswordManager manager = new MockSyntheticPasswordManager(mContext, mStorage, - mGateKeeperService, mUserManager); + mGateKeeperService, mUserManager, mPasswordSlotManager); AuthenticationToken authToken = manager.newSyntheticPasswordAndSid(mGateKeeperService, null, null, USER_ID); long handle = manager.createPasswordBasedSyntheticPassword(mGateKeeperService, PASSWORD,