1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.locksettings; 18 19 import android.os.SystemProperties; 20 import android.util.Slog; 21 22 import com.android.internal.annotations.VisibleForTesting; 23 24 import java.io.File; 25 import java.io.FileInputStream; 26 import java.io.FileOutputStream; 27 import java.io.IOException; 28 import java.io.InputStream; 29 import java.io.OutputStream; 30 import java.nio.file.Paths; 31 import java.util.Collections; 32 import java.util.HashMap; 33 import java.util.HashSet; 34 import java.util.Map; 35 import java.util.Properties; 36 import java.util.Set; 37 38 /** 39 * A class that maintains a mapping of which password slots are used by alternate OS images when 40 * dual-booting a device. Currently, slots can either be owned by the host OS or a live GSI. 41 * This mapping is stored in /metadata/password_slots/slot_map using Java Properties. 42 * 43 * If a /metadata partition does not exist, GSIs are not supported, and PasswordSlotManager will 44 * simply not persist the slot mapping. 45 */ 46 class PasswordSlotManager { 47 private static final String TAG = "PasswordSlotManager"; 48 49 private static final String GSI_RUNNING_PROP = "ro.gsid.image_running"; 50 private static final String SLOT_MAP_DIR = "/metadata/password_slots"; 51 52 // This maps each used password slot to the OS image that created it. Password slots are 53 // integer keys/indices into secure storage. The OS image is recorded as a string. The factory 54 // image is "host" and GSIs are "gsi<N>" where N >= 1. 55 private Map<Integer, String> mSlotMap; 56 57 // Cache the active slots until loadSlotMap() is called. 58 private Set<Integer> mActiveSlots; 59 PasswordSlotManager()60 public PasswordSlotManager() { 61 } 62 63 @VisibleForTesting getSlotMapDir()64 protected String getSlotMapDir() { 65 return SLOT_MAP_DIR; 66 } 67 68 @VisibleForTesting getGsiImageNumber()69 protected int getGsiImageNumber() { 70 return SystemProperties.getInt(GSI_RUNNING_PROP, 0); 71 } 72 73 /** 74 * Notify the manager of which slots are definitively in use by the current OS image. 75 */ refreshActiveSlots(Set<Integer> activeSlots)76 public void refreshActiveSlots(Set<Integer> activeSlots) throws RuntimeException { 77 if (mSlotMap == null) { 78 mActiveSlots = new HashSet<Integer>(activeSlots); 79 return; 80 } 81 82 // Update which slots are owned by the current image. 83 final HashSet<Integer> slotsToDelete = new HashSet<Integer>(); 84 for (Map.Entry<Integer, String> entry : mSlotMap.entrySet()) { 85 // Delete possibly stale entries for the current image. 86 if (entry.getValue().equals(getMode())) { 87 slotsToDelete.add(entry.getKey()); 88 } 89 } 90 for (Integer slot : slotsToDelete) { 91 mSlotMap.remove(slot); 92 } 93 94 // Add slots for the current image. 95 for (Integer slot : activeSlots) { 96 mSlotMap.put(slot, getMode()); 97 } 98 99 saveSlotMap(); 100 } 101 102 /** 103 * Mark the given slot as in use by the current OS image. 104 */ markSlotInUse(int slot)105 public void markSlotInUse(int slot) throws RuntimeException { 106 ensureSlotMapLoaded(); 107 if (mSlotMap.containsKey(slot) && !mSlotMap.get(slot).equals(getMode())) { 108 throw new IllegalStateException("password slot " + slot + " is not available"); 109 } 110 mSlotMap.put(slot, getMode()); 111 saveSlotMap(); 112 } 113 114 /** 115 * Mark the given slot as no longer in use by the current OS image. 116 */ markSlotDeleted(int slot)117 public void markSlotDeleted(int slot) throws RuntimeException { 118 ensureSlotMapLoaded(); 119 if (mSlotMap.containsKey(slot) && !mSlotMap.get(slot).equals(getMode())) { 120 throw new IllegalStateException("password slot " + slot + " cannot be deleted"); 121 } 122 mSlotMap.remove(slot); 123 saveSlotMap(); 124 } 125 126 /** 127 * Return the set of slots used across all OS images. 128 * 129 * @return Integer set of all used slots. 130 */ getUsedSlots()131 public Set<Integer> getUsedSlots() { 132 ensureSlotMapLoaded(); 133 return Collections.unmodifiableSet(mSlotMap.keySet()); 134 } 135 getSlotMapFile()136 private File getSlotMapFile() { 137 return Paths.get(getSlotMapDir(), "slot_map").toFile(); 138 } 139 getMode()140 private String getMode() { 141 int gsiIndex = getGsiImageNumber(); 142 if (gsiIndex > 0) { 143 return "gsi" + gsiIndex; 144 } 145 return "host"; 146 } 147 148 @VisibleForTesting loadSlotMap(InputStream stream)149 protected Map<Integer, String> loadSlotMap(InputStream stream) throws IOException { 150 final HashMap<Integer, String> map = new HashMap<Integer, String>(); 151 final Properties props = new Properties(); 152 props.load(stream); 153 for (String slotString : props.stringPropertyNames()) { 154 final int slot = Integer.parseInt(slotString); 155 final String owner = props.getProperty(slotString); 156 map.put(slot, owner); 157 } 158 return map; 159 } 160 loadSlotMap()161 private Map<Integer, String> loadSlotMap() { 162 // It's okay if the file doesn't exist. 163 final File file = getSlotMapFile(); 164 if (file.exists()) { 165 try (FileInputStream stream = new FileInputStream(file)) { 166 return loadSlotMap(stream); 167 } catch (Exception e) { 168 Slog.e(TAG, "Could not load slot map file", e); 169 } 170 } 171 return new HashMap<Integer, String>(); 172 } 173 ensureSlotMapLoaded()174 private void ensureSlotMapLoaded() { 175 if (mSlotMap == null) { 176 mSlotMap = loadSlotMap(); 177 if (mActiveSlots != null) { 178 refreshActiveSlots(mActiveSlots); 179 mActiveSlots = null; 180 } 181 } 182 } 183 184 @VisibleForTesting saveSlotMap(OutputStream stream)185 protected void saveSlotMap(OutputStream stream) throws IOException { 186 if (mSlotMap == null) { 187 return; 188 } 189 final Properties props = new Properties(); 190 for (Map.Entry<Integer, String> entry : mSlotMap.entrySet()) { 191 props.setProperty(entry.getKey().toString(), entry.getValue()); 192 } 193 props.store(stream, ""); 194 } 195 saveSlotMap()196 private void saveSlotMap() { 197 if (mSlotMap == null) { 198 return; 199 } 200 if (!getSlotMapFile().getParentFile().exists()) { 201 Slog.w(TAG, "Not saving slot map, " + getSlotMapDir() + " does not exist"); 202 return; 203 } 204 205 try (FileOutputStream fos = new FileOutputStream(getSlotMapFile())) { 206 saveSlotMap(fos); 207 } catch (IOException e) { 208 Slog.e(TAG, "failed to save password slot map", e); 209 } 210 } 211 } 212