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