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