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