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