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<N>",
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
This commit is contained in:
David Anderson
2019-02-20 13:37:51 -08:00
parent 09446796e7
commit 28dea68567
8 changed files with 394 additions and 7 deletions

View File

@@ -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() {

View File

@@ -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<N>" where N >= 1.
private final Map<Integer, String> 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<Integer> activeSlots) throws RuntimeException {
// Update which slots are owned by the current image.
final HashSet<Integer> slotsToDelete = new HashSet<Integer>();
for (Map.Entry<Integer, String> 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<Integer> 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<Integer, String> loadSlotMap(InputStream stream) throws IOException {
final HashMap<Integer, String> map = new HashMap<Integer, String>();
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<Integer, String> 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<Integer, String>();
}
@VisibleForTesting
protected void saveSlotMap(OutputStream stream) throws IOException {
final Properties props = new Properties();
for (Map.Entry<Integer, String> 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);
}
}
}

View File

@@ -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<Integer> 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,

View File

@@ -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) {

View File

@@ -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();
}
}

View File

@@ -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) {
}
}
};

View File

@@ -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<Integer> expected = new HashSet<Integer>();
expected.add(0);
expected.add(1);
assertEquals(expected, mManager.getUsedSlots());
mManager.markSlotDeleted(1);
expected = new HashSet<Integer>();
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<Integer> actual = new HashSet<Integer>();
actual.add(1);
actual.add(3);
mManager.refreshActiveSlots(actual);
Set<Integer> expected = new HashSet<Integer>();
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<Integer, String> expected = new HashMap<Integer, String>();
expected.put(0, "host");
expected.put(1, "host");
expected.put(4, "gsi1");
final Map<Integer, String> 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<Integer> expected = new HashSet<Integer>();
expected.add(0);
expected.add(1);
expected.add(4);
assertEquals(expected, mManager.getUsedSlots());
}
}

View File

@@ -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,