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 public 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 * 76 * @throws RuntimeException 77 */ refreshActiveSlots(Set<Integer> activeSlots)78 public void refreshActiveSlots(Set<Integer> activeSlots) throws RuntimeException { 79 if (mSlotMap == null) { 80 mActiveSlots = new HashSet<Integer>(activeSlots); 81 return; 82 } 83 84 // Update which slots are owned by the current image. 85 final HashSet<Integer> slotsToDelete = new HashSet<Integer>(); 86 for (Map.Entry<Integer, String> entry : mSlotMap.entrySet()) { 87 // Delete possibly stale entries for the current image. 88 if (entry.getValue().equals(getMode())) { 89 slotsToDelete.add(entry.getKey()); 90 } 91 } 92 for (Integer slot : slotsToDelete) { 93 mSlotMap.remove(slot); 94 } 95 96 // Add slots for the current image. 97 for (Integer slot : activeSlots) { 98 mSlotMap.put(slot, getMode()); 99 } 100 101 saveSlotMap(); 102 } 103 104 /** 105 * Mark the given slot as in use by the current OS image. 106 * 107 * @throws RuntimeException 108 */ markSlotInUse(int slot)109 public void markSlotInUse(int slot) throws RuntimeException { 110 ensureSlotMapLoaded(); 111 if (mSlotMap.containsKey(slot) && !mSlotMap.get(slot).equals(getMode())) { 112 throw new RuntimeException("password slot " + slot + " is not available"); 113 } 114 mSlotMap.put(slot, getMode()); 115 saveSlotMap(); 116 } 117 118 /** 119 * Mark the given slot as no longer in use by the current OS image. 120 * 121 * @throws RuntimeException 122 */ markSlotDeleted(int slot)123 public void markSlotDeleted(int slot) throws RuntimeException { 124 ensureSlotMapLoaded(); 125 if (mSlotMap.containsKey(slot) && !mSlotMap.get(slot).equals(getMode())) { 126 throw new RuntimeException("password slot " + slot + " cannot be deleted"); 127 } 128 mSlotMap.remove(slot); 129 saveSlotMap(); 130 } 131 132 /** 133 * Return the set of slots used across all OS images. 134 * 135 * @return Integer set of all used slots. 136 */ getUsedSlots()137 public Set<Integer> getUsedSlots() { 138 ensureSlotMapLoaded(); 139 return Collections.unmodifiableSet(mSlotMap.keySet()); 140 } 141 getSlotMapFile()142 private File getSlotMapFile() { 143 return Paths.get(getSlotMapDir(), "slot_map").toFile(); 144 } 145 getMode()146 private String getMode() { 147 int gsiIndex = getGsiImageNumber(); 148 if (gsiIndex > 0) { 149 return "gsi" + gsiIndex; 150 } 151 return "host"; 152 } 153 154 @VisibleForTesting loadSlotMap(InputStream stream)155 protected Map<Integer, String> loadSlotMap(InputStream stream) throws IOException { 156 final HashMap<Integer, String> map = new HashMap<Integer, String>(); 157 final Properties props = new Properties(); 158 props.load(stream); 159 for (String slotString : props.stringPropertyNames()) { 160 final int slot = Integer.parseInt(slotString); 161 final String owner = props.getProperty(slotString); 162 map.put(slot, owner); 163 } 164 return map; 165 } 166 loadSlotMap()167 private Map<Integer, String> loadSlotMap() { 168 // It's okay if the file doesn't exist. 169 final File file = getSlotMapFile(); 170 if (file.exists()) { 171 try (FileInputStream stream = new FileInputStream(file)) { 172 return loadSlotMap(stream); 173 } catch (Exception e) { 174 Slog.e(TAG, "Could not load slot map file", e); 175 } 176 } 177 return new HashMap<Integer, String>(); 178 } 179 ensureSlotMapLoaded()180 private void ensureSlotMapLoaded() { 181 if (mSlotMap == null) { 182 mSlotMap = loadSlotMap(); 183 if (mActiveSlots != null) { 184 refreshActiveSlots(mActiveSlots); 185 mActiveSlots = null; 186 } 187 } 188 } 189 190 @VisibleForTesting saveSlotMap(OutputStream stream)191 protected void saveSlotMap(OutputStream stream) throws IOException { 192 if (mSlotMap == null) { 193 return; 194 } 195 final Properties props = new Properties(); 196 for (Map.Entry<Integer, String> entry : mSlotMap.entrySet()) { 197 props.setProperty(entry.getKey().toString(), entry.getValue()); 198 } 199 props.store(stream, ""); 200 } 201 saveSlotMap()202 private void saveSlotMap() { 203 if (mSlotMap == null) { 204 return; 205 } 206 if (!getSlotMapFile().getParentFile().exists()) { 207 Slog.w(TAG, "Not saving slot map, " + getSlotMapDir() + " does not exist"); 208 return; 209 } 210 211 try (FileOutputStream fos = new FileOutputStream(getSlotMapFile())) { 212 saveSlotMap(fos); 213 } catch (IOException e) { 214 Slog.e(TAG, "failed to save password slot map", e); 215 } 216 } 217 } 218