• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.role.persistence;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.ApexEnvironment;
22 import android.os.FileUtils;
23 import android.os.UserHandle;
24 import android.util.ArrayMap;
25 import android.util.ArraySet;
26 import android.util.AtomicFile;
27 import android.util.Log;
28 import android.util.Xml;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.modules.utils.build.SdkLevel;
32 import com.android.permission.persistence.IoUtils;
33 import com.android.server.security.FileIntegrity;
34 
35 import org.xmlpull.v1.XmlPullParser;
36 import org.xmlpull.v1.XmlPullParserException;
37 import org.xmlpull.v1.XmlSerializer;
38 
39 import java.io.File;
40 import java.io.FileInputStream;
41 import java.io.FileNotFoundException;
42 import java.io.FileOutputStream;
43 import java.io.IOException;
44 import java.nio.charset.StandardCharsets;
45 import java.util.Map;
46 import java.util.Set;
47 
48 /**
49  * Persistence implementation for roles.
50  *
51  * TODO(b/147914847): Remove @hide when it becomes the default.
52  * @hide
53  */
54 public class RolesPersistenceImpl implements RolesPersistence {
55 
56     private static final String LOG_TAG = RolesPersistenceImpl.class.getSimpleName();
57 
58     private static final String APEX_MODULE_NAME = "com.android.permission";
59 
60     private static final String ROLES_FILE_NAME = "roles.xml";
61     private static final String ROLES_RESERVE_COPY_FILE_NAME = ROLES_FILE_NAME + ".reservecopy";
62 
63     private static final String TAG_ROLES = "roles";
64     private static final String TAG_ROLE = "role";
65     private static final String TAG_HOLDER = "holder";
66 
67     private static final String ATTRIBUTE_VERSION = "version";
68     private static final String ATTRIBUTE_NAME = "name";
69     private static final String ATTRIBUTE_FALLBACK_ENABLED = "fallbackEnabled";
70     private static final String ATTRIBUTE_ACTIVE_USER_ID = "activeUserId";
71     private static final String ATTRIBUTE_PACKAGES_HASH = "packagesHash";
72 
73     @VisibleForTesting
74     interface Injector {
enableFsVerity(@onNull File file)75         void enableFsVerity(@NonNull File file) throws IOException;
76     }
77 
78     @NonNull
79     private final Injector mInjector;
80 
RolesPersistenceImpl()81     RolesPersistenceImpl() {
82         this(file -> {
83             if (SdkLevel.isAtLeastU()) {
84                 FileIntegrity.setUpFsVerity(file);
85             }
86         });
87     }
88 
89     @VisibleForTesting
RolesPersistenceImpl(@onNull Injector injector)90     RolesPersistenceImpl(@NonNull Injector injector) {
91         mInjector = injector;
92     }
93 
94     @Nullable
95     @Override
readForUser(@onNull UserHandle user)96     public RolesState readForUser(@NonNull UserHandle user) {
97         File file = getFile(user);
98         try (FileInputStream inputStream = new AtomicFile(file).openRead()) {
99             XmlPullParser parser = Xml.newPullParser();
100             parser.setInput(inputStream, null);
101             return parseXml(parser);
102         } catch (FileNotFoundException e) {
103             Log.i(LOG_TAG, "roles.xml not found");
104             return null;
105         } catch (Exception e) {
106             File reserveFile = getReserveCopyFile(user);
107             Log.wtf(LOG_TAG, "Reading from reserve copy: " + reserveFile, e);
108             try (FileInputStream inputStream = new AtomicFile(reserveFile).openRead()) {
109                 XmlPullParser parser = Xml.newPullParser();
110                 parser.setInput(inputStream, null);
111                 return parseXml(parser);
112             } catch (Exception exceptionReadingReserveFile) {
113                 Log.e(LOG_TAG, "Failed to read reserve copy: " + reserveFile,
114                         exceptionReadingReserveFile);
115                 // Reserve copy failed, rethrow the original exception wrapped as runtime.
116                 throw new IllegalStateException("Failed to read roles.xml: " + file , e);
117             }
118         }
119     }
120 
121     @NonNull
parseXml(@onNull XmlPullParser parser)122     private static RolesState parseXml(@NonNull XmlPullParser parser)
123             throws IOException, XmlPullParserException {
124         int type;
125         int depth;
126         int innerDepth = parser.getDepth() + 1;
127         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
128                 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
129             if (depth > innerDepth || type != XmlPullParser.START_TAG) {
130                 continue;
131             }
132 
133             if (parser.getName().equals(TAG_ROLES)) {
134                 return parseRoles(parser);
135             }
136         }
137         throw new IllegalStateException("Missing <" + TAG_ROLES + "> in roles.xml");
138     }
139 
140     @NonNull
parseRoles(@onNull XmlPullParser parser)141     private static RolesState parseRoles(@NonNull XmlPullParser parser)
142             throws IOException, XmlPullParserException {
143         int version = Integer.parseInt(parser.getAttributeValue(null, ATTRIBUTE_VERSION));
144         String packagesHash = parser.getAttributeValue(null, ATTRIBUTE_PACKAGES_HASH);
145 
146         Map<String, Set<String>> roles = new ArrayMap<>();
147         Set<String> fallbackEnabledRoles = new ArraySet<>();
148         Map<String, Integer> activeUserIds = new ArrayMap<>();
149         int type;
150         int depth;
151         int innerDepth = parser.getDepth() + 1;
152         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
153                 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
154             if (depth > innerDepth || type != XmlPullParser.START_TAG) {
155                 continue;
156             }
157 
158             if (parser.getName().equals(TAG_ROLE)) {
159                 String roleName = parser.getAttributeValue(null, ATTRIBUTE_NAME);
160                 String fallbackEnabled = parser.getAttributeValue(null, ATTRIBUTE_FALLBACK_ENABLED);
161                 if (Boolean.parseBoolean(fallbackEnabled)) {
162                     fallbackEnabledRoles.add(roleName);
163                 }
164                 if (com.android.permission.flags.Flags.crossUserRoleEnabled()) {
165                     String activeUserId = parser.getAttributeValue(null, ATTRIBUTE_ACTIVE_USER_ID);
166                     if (activeUserId != null) {
167                         activeUserIds.put(roleName, Integer.parseInt(activeUserId));
168                     }
169                 }
170                 Set<String> roleHolders = parseRoleHolders(parser);
171                 roles.put(roleName, roleHolders);
172             }
173         }
174 
175         if (com.android.permission.flags.Flags.crossUserRoleEnabled()) {
176             return new RolesState(version, packagesHash, roles, fallbackEnabledRoles,
177                     activeUserIds);
178         } else {
179             return new RolesState(version, packagesHash, roles, fallbackEnabledRoles);
180         }
181     }
182 
183     @NonNull
parseRoleHolders(@onNull XmlPullParser parser)184     private static Set<String> parseRoleHolders(@NonNull XmlPullParser parser)
185             throws IOException, XmlPullParserException {
186         Set<String> roleHolders = new ArraySet<>();
187         int type;
188         int depth;
189         int innerDepth = parser.getDepth() + 1;
190         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
191                 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
192             if (depth > innerDepth || type != XmlPullParser.START_TAG) {
193                 continue;
194             }
195 
196             if (parser.getName().equals(TAG_HOLDER)) {
197                 String roleHolder = parser.getAttributeValue(null, ATTRIBUTE_NAME);
198                 roleHolders.add(roleHolder);
199             }
200         }
201         return roleHolders;
202     }
203 
204     @Override
writeForUser(@onNull RolesState roles, @NonNull UserHandle user)205     public void writeForUser(@NonNull RolesState roles, @NonNull UserHandle user) {
206         File file = getFile(user);
207         AtomicFile atomicFile = new AtomicFile(file);
208         FileOutputStream outputStream = null;
209         try {
210             outputStream = atomicFile.startWrite();
211 
212             XmlSerializer serializer = Xml.newSerializer();
213             serializer.setOutput(outputStream, StandardCharsets.UTF_8.name());
214             serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
215             serializer.startDocument(null, true);
216 
217             serializeRoles(serializer, roles);
218 
219             serializer.endDocument();
220             atomicFile.finishWrite(outputStream);
221         } catch (Exception e) {
222             Log.wtf(LOG_TAG, "Failed to write roles.xml, restoring backup: " + file,
223                     e);
224             atomicFile.failWrite(outputStream);
225             return;
226         } finally {
227             IoUtils.closeQuietly(outputStream);
228         }
229 
230         File reserveFile = getReserveCopyFile(user);
231         reserveFile.delete();
232         try (FileInputStream in = new FileInputStream(file);
233              FileOutputStream out = new FileOutputStream(reserveFile)) {
234             FileUtils.copy(in, out);
235             out.getFD().sync();
236         } catch (Exception e) {
237             Log.e(LOG_TAG, "Failed to write reserve copy: " + reserveFile, e);
238         }
239 
240         try {
241             mInjector.enableFsVerity(file);
242             mInjector.enableFsVerity(reserveFile);
243         } catch (Exception e) {
244             Log.e(LOG_TAG, "Failed to verity-protect roles", e);
245         }
246     }
247 
serializeRoles(@onNull XmlSerializer serializer, @NonNull RolesState roles)248     private static void serializeRoles(@NonNull XmlSerializer serializer,
249             @NonNull RolesState roles) throws IOException {
250         serializer.startTag(null, TAG_ROLES);
251 
252         int version = roles.getVersion();
253         serializer.attribute(null, ATTRIBUTE_VERSION, Integer.toString(version));
254         String packagesHash = roles.getPackagesHash();
255         if (packagesHash != null) {
256             serializer.attribute(null, ATTRIBUTE_PACKAGES_HASH, packagesHash);
257         }
258 
259         Set<String> fallbackEnabledRoles = roles.getFallbackEnabledRoles();
260         Map<String, Integer> activeUserIds = roles.getActiveUserIds();
261         for (Map.Entry<String, Set<String>> entry : roles.getRoles().entrySet()) {
262             String roleName = entry.getKey();
263             Set<String> roleHolders = entry.getValue();
264             boolean isFallbackEnabled = fallbackEnabledRoles.contains(roleName);
265             Integer activeUserId = com.android.permission.flags.Flags.crossUserRoleEnabled()
266                     ? activeUserIds.get(roleName) : null;
267 
268             serializer.startTag(null, TAG_ROLE);
269             serializer.attribute(null, ATTRIBUTE_NAME, roleName);
270             serializer.attribute(null, ATTRIBUTE_FALLBACK_ENABLED,
271                     Boolean.toString(isFallbackEnabled));
272             if (activeUserId != null) {
273                 serializer.attribute(
274                         null, ATTRIBUTE_ACTIVE_USER_ID, Integer.toString(activeUserId));
275             }
276             serializeRoleHolders(serializer, roleHolders);
277             serializer.endTag(null, TAG_ROLE);
278         }
279 
280         serializer.endTag(null, TAG_ROLES);
281     }
282 
serializeRoleHolders(@onNull XmlSerializer serializer, @NonNull Set<String> roleHolders)283     private static void serializeRoleHolders(@NonNull XmlSerializer serializer,
284             @NonNull Set<String> roleHolders) throws IOException {
285         for (String roleHolder : roleHolders) {
286             serializer.startTag(null, TAG_HOLDER);
287             serializer.attribute(null, ATTRIBUTE_NAME, roleHolder);
288             serializer.endTag(null, TAG_HOLDER);
289         }
290     }
291 
292     @Override
deleteForUser(@onNull UserHandle user)293     public void deleteForUser(@NonNull UserHandle user) {
294         getFile(user).delete();
295         getReserveCopyFile(user).delete();
296     }
297 
298     @VisibleForTesting
299     @NonNull
getFile(@onNull UserHandle user)300     static File getFile(@NonNull UserHandle user) {
301         ApexEnvironment apexEnvironment = ApexEnvironment.getApexEnvironment(APEX_MODULE_NAME);
302         File dataDirectory = apexEnvironment.getDeviceProtectedDataDirForUser(user);
303         return new File(dataDirectory, ROLES_FILE_NAME);
304     }
305 
306     @NonNull
getReserveCopyFile(@onNull UserHandle user)307     private static File getReserveCopyFile(@NonNull UserHandle user) {
308         ApexEnvironment apexEnvironment = ApexEnvironment.getApexEnvironment(APEX_MODULE_NAME);
309         File dataDirectory = apexEnvironment.getDeviceProtectedDataDirForUser(user);
310         return new File(dataDirectory, ROLES_RESERVE_COPY_FILE_NAME);
311     }
312 }
313