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_PACKAGES_HASH = "packagesHash";
71 
72     @VisibleForTesting
73     interface Injector {
enableFsVerity(@onNull File file)74         void enableFsVerity(@NonNull File file) throws IOException;
75     }
76 
77     @NonNull
78     private final Injector mInjector;
79 
RolesPersistenceImpl()80     RolesPersistenceImpl() {
81         this(file -> {
82             if (SdkLevel.isAtLeastU()) {
83                 FileIntegrity.setUpFsVerity(file);
84             }
85         });
86     }
87 
88     @VisibleForTesting
RolesPersistenceImpl(@onNull Injector injector)89     RolesPersistenceImpl(@NonNull Injector injector) {
90         mInjector = injector;
91     }
92 
93     @Nullable
94     @Override
readForUser(@onNull UserHandle user)95     public RolesState readForUser(@NonNull UserHandle user) {
96         File file = getFile(user);
97         try (FileInputStream inputStream = new AtomicFile(file).openRead()) {
98             XmlPullParser parser = Xml.newPullParser();
99             parser.setInput(inputStream, null);
100             return parseXml(parser);
101         } catch (FileNotFoundException e) {
102             Log.i(LOG_TAG, "roles.xml not found");
103             return null;
104         } catch (Exception e) {
105             File reserveFile = getReserveCopyFile(user);
106             Log.wtf(LOG_TAG, "Reading from reserve copy: " + reserveFile, e);
107             try (FileInputStream inputStream = new AtomicFile(reserveFile).openRead()) {
108                 XmlPullParser parser = Xml.newPullParser();
109                 parser.setInput(inputStream, null);
110                 return parseXml(parser);
111             } catch (Exception exceptionReadingReserveFile) {
112                 Log.e(LOG_TAG, "Failed to read reserve copy: " + reserveFile,
113                         exceptionReadingReserveFile);
114                 // Reserve copy failed, rethrow the original exception wrapped as runtime.
115                 throw new IllegalStateException("Failed to read roles.xml: " + file , e);
116             }
117         }
118     }
119 
120     @NonNull
parseXml(@onNull XmlPullParser parser)121     private static RolesState parseXml(@NonNull XmlPullParser parser)
122             throws IOException, XmlPullParserException {
123         int type;
124         int depth;
125         int innerDepth = parser.getDepth() + 1;
126         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
127                 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
128             if (depth > innerDepth || type != XmlPullParser.START_TAG) {
129                 continue;
130             }
131 
132             if (parser.getName().equals(TAG_ROLES)) {
133                 return parseRoles(parser);
134             }
135         }
136         throw new IllegalStateException("Missing <" + TAG_ROLES + "> in roles.xml");
137     }
138 
139     @NonNull
parseRoles(@onNull XmlPullParser parser)140     private static RolesState parseRoles(@NonNull XmlPullParser parser)
141             throws IOException, XmlPullParserException {
142         int version = Integer.parseInt(parser.getAttributeValue(null, ATTRIBUTE_VERSION));
143         String packagesHash = parser.getAttributeValue(null, ATTRIBUTE_PACKAGES_HASH);
144 
145         Map<String, Set<String>> roles = new ArrayMap<>();
146         Set<String> fallbackEnabledRoles = new ArraySet<>();
147         int type;
148         int depth;
149         int innerDepth = parser.getDepth() + 1;
150         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
151                 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
152             if (depth > innerDepth || type != XmlPullParser.START_TAG) {
153                 continue;
154             }
155 
156             if (parser.getName().equals(TAG_ROLE)) {
157                 String roleName = parser.getAttributeValue(null, ATTRIBUTE_NAME);
158                 String fallbackEnabled = parser.getAttributeValue(null, ATTRIBUTE_FALLBACK_ENABLED);
159                 if (Boolean.parseBoolean(fallbackEnabled)) {
160                     fallbackEnabledRoles.add(roleName);
161                 }
162                 Set<String> roleHolders = parseRoleHolders(parser);
163                 roles.put(roleName, roleHolders);
164             }
165         }
166 
167         return new RolesState(version, packagesHash, roles, fallbackEnabledRoles);
168     }
169 
170     @NonNull
parseRoleHolders(@onNull XmlPullParser parser)171     private static Set<String> parseRoleHolders(@NonNull XmlPullParser parser)
172             throws IOException, XmlPullParserException {
173         Set<String> roleHolders = new ArraySet<>();
174         int type;
175         int depth;
176         int innerDepth = parser.getDepth() + 1;
177         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
178                 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
179             if (depth > innerDepth || type != XmlPullParser.START_TAG) {
180                 continue;
181             }
182 
183             if (parser.getName().equals(TAG_HOLDER)) {
184                 String roleHolder = parser.getAttributeValue(null, ATTRIBUTE_NAME);
185                 roleHolders.add(roleHolder);
186             }
187         }
188         return roleHolders;
189     }
190 
191     @Override
writeForUser(@onNull RolesState roles, @NonNull UserHandle user)192     public void writeForUser(@NonNull RolesState roles, @NonNull UserHandle user) {
193         File file = getFile(user);
194         AtomicFile atomicFile = new AtomicFile(file);
195         FileOutputStream outputStream = null;
196         try {
197             outputStream = atomicFile.startWrite();
198 
199             XmlSerializer serializer = Xml.newSerializer();
200             serializer.setOutput(outputStream, StandardCharsets.UTF_8.name());
201             serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
202             serializer.startDocument(null, true);
203 
204             serializeRoles(serializer, roles);
205 
206             serializer.endDocument();
207             atomicFile.finishWrite(outputStream);
208         } catch (Exception e) {
209             Log.wtf(LOG_TAG, "Failed to write roles.xml, restoring backup: " + file,
210                     e);
211             atomicFile.failWrite(outputStream);
212             return;
213         } finally {
214             IoUtils.closeQuietly(outputStream);
215         }
216 
217         File reserveFile = getReserveCopyFile(user);
218         reserveFile.delete();
219         try (FileInputStream in = new FileInputStream(file);
220              FileOutputStream out = new FileOutputStream(reserveFile)) {
221             FileUtils.copy(in, out);
222             out.getFD().sync();
223         } catch (Exception e) {
224             Log.e(LOG_TAG, "Failed to write reserve copy: " + reserveFile, e);
225         }
226 
227         try {
228             mInjector.enableFsVerity(file);
229             mInjector.enableFsVerity(reserveFile);
230         } catch (Exception e) {
231             Log.e(LOG_TAG, "Failed to verity-protect roles", e);
232         }
233     }
234 
serializeRoles(@onNull XmlSerializer serializer, @NonNull RolesState roles)235     private static void serializeRoles(@NonNull XmlSerializer serializer,
236             @NonNull RolesState roles) throws IOException {
237         serializer.startTag(null, TAG_ROLES);
238 
239         int version = roles.getVersion();
240         serializer.attribute(null, ATTRIBUTE_VERSION, Integer.toString(version));
241         String packagesHash = roles.getPackagesHash();
242         if (packagesHash != null) {
243             serializer.attribute(null, ATTRIBUTE_PACKAGES_HASH, packagesHash);
244         }
245 
246         Set<String> fallbackEnabledRoles = roles.getFallbackEnabledRoles();
247         for (Map.Entry<String, Set<String>> entry : roles.getRoles().entrySet()) {
248             String roleName = entry.getKey();
249             Set<String> roleHolders = entry.getValue();
250             boolean isFallbackEnabled = fallbackEnabledRoles.contains(roleName);
251 
252             serializer.startTag(null, TAG_ROLE);
253             serializer.attribute(null, ATTRIBUTE_NAME, roleName);
254             serializer.attribute(null, ATTRIBUTE_FALLBACK_ENABLED,
255                     Boolean.toString(isFallbackEnabled));
256             serializeRoleHolders(serializer, roleHolders);
257             serializer.endTag(null, TAG_ROLE);
258         }
259 
260         serializer.endTag(null, TAG_ROLES);
261     }
262 
serializeRoleHolders(@onNull XmlSerializer serializer, @NonNull Set<String> roleHolders)263     private static void serializeRoleHolders(@NonNull XmlSerializer serializer,
264             @NonNull Set<String> roleHolders) throws IOException {
265         for (String roleHolder : roleHolders) {
266             serializer.startTag(null, TAG_HOLDER);
267             serializer.attribute(null, ATTRIBUTE_NAME, roleHolder);
268             serializer.endTag(null, TAG_HOLDER);
269         }
270     }
271 
272     @Override
deleteForUser(@onNull UserHandle user)273     public void deleteForUser(@NonNull UserHandle user) {
274         getFile(user).delete();
275         getReserveCopyFile(user).delete();
276     }
277 
278     @VisibleForTesting
279     @NonNull
getFile(@onNull UserHandle user)280     static File getFile(@NonNull UserHandle user) {
281         ApexEnvironment apexEnvironment = ApexEnvironment.getApexEnvironment(APEX_MODULE_NAME);
282         File dataDirectory = apexEnvironment.getDeviceProtectedDataDirForUser(user);
283         return new File(dataDirectory, ROLES_FILE_NAME);
284     }
285 
286     @NonNull
getReserveCopyFile(@onNull UserHandle user)287     private static File getReserveCopyFile(@NonNull UserHandle user) {
288         ApexEnvironment apexEnvironment = ApexEnvironment.getApexEnvironment(APEX_MODULE_NAME);
289         File dataDirectory = apexEnvironment.getDeviceProtectedDataDirForUser(user);
290         return new File(dataDirectory, ROLES_RESERVE_COPY_FILE_NAME);
291     }
292 }
293