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,