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.permission.persistence; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.ApexEnvironment; 22 import android.content.pm.PackageManager; 23 import android.os.FileUtils; 24 import android.os.UserHandle; 25 import android.util.ArrayMap; 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.server.security.FileIntegrity; 33 34 import org.xmlpull.v1.XmlPullParser; 35 import org.xmlpull.v1.XmlPullParserException; 36 import org.xmlpull.v1.XmlSerializer; 37 38 import java.io.File; 39 import java.io.FileInputStream; 40 import java.io.FileNotFoundException; 41 import java.io.FileOutputStream; 42 import java.io.IOException; 43 import java.nio.charset.StandardCharsets; 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.Map; 47 48 /** 49 * Persistence implementation for runtime permissions. 50 * 51 * TODO(b/147914847): Remove @hide when it becomes the default. 52 * @hide 53 */ 54 public class RuntimePermissionsPersistenceImpl implements RuntimePermissionsPersistence { 55 56 private static final String LOG_TAG = RuntimePermissionsPersistenceImpl.class.getSimpleName(); 57 58 private static final String APEX_MODULE_NAME = "com.android.permission"; 59 60 private static final String RUNTIME_PERMISSIONS_FILE_NAME = "runtime-permissions.xml"; 61 private static final String RUNTIME_PERMISSIONS_RESERVE_COPY_FILE_NAME = 62 RUNTIME_PERMISSIONS_FILE_NAME + ".reservecopy"; 63 64 private static final String TAG_PACKAGE = "package"; 65 private static final String TAG_PERMISSION = "permission"; 66 private static final String TAG_RUNTIME_PERMISSIONS = "runtime-permissions"; 67 private static final String TAG_SHARED_USER = "shared-user"; 68 69 private static final String ATTRIBUTE_FINGERPRINT = "fingerprint"; 70 private static final String ATTRIBUTE_FLAGS = "flags"; 71 private static final String ATTRIBUTE_GRANTED = "granted"; 72 private static final String ATTRIBUTE_NAME = "name"; 73 private static final String ATTRIBUTE_VERSION = "version"; 74 75 @VisibleForTesting 76 interface Injector { enableFsVerity(@onNull File file)77 void enableFsVerity(@NonNull File file) throws IOException; 78 } 79 80 @NonNull 81 private final Injector mInjector; 82 RuntimePermissionsPersistenceImpl()83 RuntimePermissionsPersistenceImpl() { 84 this(file -> { 85 if (SdkLevel.isAtLeastU()) { 86 FileIntegrity.setUpFsVerity(file); 87 } 88 }); 89 } 90 91 @VisibleForTesting RuntimePermissionsPersistenceImpl(@onNull Injector injector)92 RuntimePermissionsPersistenceImpl(@NonNull Injector injector) { 93 mInjector = injector; 94 } 95 96 @Nullable 97 @Override readForUser(@onNull UserHandle user)98 public RuntimePermissionsState readForUser(@NonNull UserHandle user) { 99 File file = getFile(user); 100 try (FileInputStream inputStream = new AtomicFile(file).openRead()) { 101 XmlPullParser parser = Xml.newPullParser(); 102 parser.setInput(inputStream, null); 103 return parseXml(parser); 104 } catch (FileNotFoundException e) { 105 Log.i(LOG_TAG, "runtime-permissions.xml not found"); 106 return null; 107 } catch (Exception e) { 108 File reserveFile = getReserveCopyFile(user); 109 Log.wtf(LOG_TAG, "Reading from reserve copy: " + reserveFile, e); 110 try (FileInputStream inputStream = new AtomicFile(reserveFile).openRead()) { 111 XmlPullParser parser = Xml.newPullParser(); 112 parser.setInput(inputStream, null); 113 return parseXml(parser); 114 } catch (Exception exceptionReadingReserveFile) { 115 Log.e(LOG_TAG, "Failed to read reserve copy: " + reserveFile, 116 exceptionReadingReserveFile); 117 // Reserve copy failed, rethrow the original exception wrapped as runtime. 118 throw new IllegalStateException("Failed to read runtime-permissions.xml: " + file, 119 e); 120 } 121 } 122 } 123 124 @NonNull parseXml(@onNull XmlPullParser parser)125 private static RuntimePermissionsState parseXml(@NonNull XmlPullParser parser) 126 throws IOException, XmlPullParserException { 127 int type; 128 int depth; 129 int innerDepth = parser.getDepth() + 1; 130 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 131 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { 132 if (depth > innerDepth || type != XmlPullParser.START_TAG) { 133 continue; 134 } 135 136 if (parser.getName().equals(TAG_RUNTIME_PERMISSIONS)) { 137 return parseRuntimePermissions(parser); 138 } 139 } 140 throw new IllegalStateException("Missing <" + TAG_RUNTIME_PERMISSIONS 141 + "> in runtime-permissions.xml"); 142 } 143 144 @NonNull parseRuntimePermissions(@onNull XmlPullParser parser)145 private static RuntimePermissionsState parseRuntimePermissions(@NonNull XmlPullParser parser) 146 throws IOException, XmlPullParserException { 147 String versionValue = parser.getAttributeValue(null, ATTRIBUTE_VERSION); 148 int version = versionValue != null ? Integer.parseInt(versionValue) 149 : RuntimePermissionsState.NO_VERSION; 150 String fingerprint = parser.getAttributeValue(null, ATTRIBUTE_FINGERPRINT); 151 152 Map<String, List<RuntimePermissionsState.PermissionState>> packagePermissions = 153 new ArrayMap<>(); 154 Map<String, List<RuntimePermissionsState.PermissionState>> sharedUserPermissions = 155 new ArrayMap<>(); 156 int type; 157 int depth; 158 int innerDepth = parser.getDepth() + 1; 159 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 160 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { 161 if (depth > innerDepth || type != XmlPullParser.START_TAG) { 162 continue; 163 } 164 165 switch (parser.getName()) { 166 case TAG_PACKAGE: { 167 String packageName = parser.getAttributeValue(null, ATTRIBUTE_NAME); 168 List<RuntimePermissionsState.PermissionState> permissions = parsePermissions( 169 parser); 170 packagePermissions.put(packageName, permissions); 171 break; 172 } 173 case TAG_SHARED_USER: { 174 String sharedUserName = parser.getAttributeValue(null, ATTRIBUTE_NAME); 175 List<RuntimePermissionsState.PermissionState> permissions = parsePermissions( 176 parser); 177 sharedUserPermissions.put(sharedUserName, permissions); 178 break; 179 } 180 } 181 } 182 183 return new RuntimePermissionsState(version, fingerprint, packagePermissions, 184 sharedUserPermissions); 185 } 186 187 @NonNull parsePermissions( @onNull XmlPullParser parser)188 private static List<RuntimePermissionsState.PermissionState> parsePermissions( 189 @NonNull XmlPullParser parser) throws IOException, XmlPullParserException { 190 List<RuntimePermissionsState.PermissionState> permissions = new ArrayList<>(); 191 int type; 192 int depth; 193 int innerDepth = parser.getDepth() + 1; 194 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 195 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { 196 if (depth > innerDepth || type != XmlPullParser.START_TAG) { 197 continue; 198 } 199 200 if (parser.getName().equals(TAG_PERMISSION)) { 201 String name = parser.getAttributeValue(null, ATTRIBUTE_NAME); 202 boolean granted = Boolean.parseBoolean(parser.getAttributeValue(null, 203 ATTRIBUTE_GRANTED)); 204 int flags = Integer.parseInt(parser.getAttributeValue(null, 205 ATTRIBUTE_FLAGS), 16); 206 RuntimePermissionsState.PermissionState permission = 207 new RuntimePermissionsState.PermissionState(name, granted, flags); 208 permissions.add(permission); 209 } 210 } 211 return permissions; 212 } 213 214 @Override writeForUser(@onNull RuntimePermissionsState runtimePermissions, @NonNull UserHandle user)215 public void writeForUser(@NonNull RuntimePermissionsState runtimePermissions, 216 @NonNull UserHandle user) { 217 File reserveFile = getReserveCopyFile(user); 218 reserveFile.delete(); 219 220 File file = getFile(user); 221 AtomicFile atomicFile = new AtomicFile(file); 222 FileOutputStream outputStream = null; 223 try { 224 outputStream = atomicFile.startWrite(); 225 226 XmlSerializer serializer = Xml.newSerializer(); 227 serializer.setOutput(outputStream, StandardCharsets.UTF_8.name()); 228 serializer.startDocument(null, true); 229 230 serializeRuntimePermissions(serializer, runtimePermissions); 231 232 serializer.endDocument(); 233 atomicFile.finishWrite(outputStream); 234 } catch (Exception e) { 235 Log.wtf(LOG_TAG, "Failed to write runtime-permissions.xml, restoring backup: " + file, 236 e); 237 atomicFile.failWrite(outputStream); 238 return; 239 } finally { 240 IoUtils.closeQuietly(outputStream); 241 } 242 243 try (FileInputStream in = new FileInputStream(file); 244 FileOutputStream out = new FileOutputStream(reserveFile)) { 245 FileUtils.copy(in, out); 246 out.getFD().sync(); 247 } catch (Exception e) { 248 Log.e(LOG_TAG, "Failed to write reserve copy: " + reserveFile, e); 249 } 250 251 try { 252 mInjector.enableFsVerity(file); 253 mInjector.enableFsVerity(reserveFile); 254 } catch (Exception e) { 255 Log.e(LOG_TAG, "Failed to verity-protect runtime-permissions", e); 256 } 257 } 258 serializeRuntimePermissions(@onNull XmlSerializer serializer, @NonNull RuntimePermissionsState runtimePermissions)259 private static void serializeRuntimePermissions(@NonNull XmlSerializer serializer, 260 @NonNull RuntimePermissionsState runtimePermissions) throws IOException { 261 serializer.startTag(null, TAG_RUNTIME_PERMISSIONS); 262 263 int version = runtimePermissions.getVersion(); 264 serializer.attribute(null, ATTRIBUTE_VERSION, Integer.toString(version)); 265 String fingerprint = runtimePermissions.getFingerprint(); 266 if (fingerprint != null) { 267 serializer.attribute(null, ATTRIBUTE_FINGERPRINT, fingerprint); 268 } 269 270 for (Map.Entry<String, List<RuntimePermissionsState.PermissionState>> entry 271 : runtimePermissions.getPackagePermissions().entrySet()) { 272 String packageName = entry.getKey(); 273 List<RuntimePermissionsState.PermissionState> permissions = entry.getValue(); 274 275 serializer.startTag(null, TAG_PACKAGE); 276 serializer.attribute(null, ATTRIBUTE_NAME, packageName); 277 serializePermissions(serializer, permissions); 278 serializer.endTag(null, TAG_PACKAGE); 279 } 280 281 for (Map.Entry<String, List<RuntimePermissionsState.PermissionState>> entry 282 : runtimePermissions.getSharedUserPermissions().entrySet()) { 283 String sharedUserName = entry.getKey(); 284 List<RuntimePermissionsState.PermissionState> permissions = entry.getValue(); 285 286 serializer.startTag(null, TAG_SHARED_USER); 287 serializer.attribute(null, ATTRIBUTE_NAME, sharedUserName); 288 serializePermissions(serializer, permissions); 289 serializer.endTag(null, TAG_SHARED_USER); 290 } 291 292 serializer.endTag(null, TAG_RUNTIME_PERMISSIONS); 293 } 294 serializePermissions(@onNull XmlSerializer serializer, @NonNull List<RuntimePermissionsState.PermissionState> permissions)295 private static void serializePermissions(@NonNull XmlSerializer serializer, 296 @NonNull List<RuntimePermissionsState.PermissionState> permissions) throws IOException { 297 int permissionsSize = permissions.size(); 298 for (int i = 0; i < permissionsSize; i++) { 299 RuntimePermissionsState.PermissionState permissionState = permissions.get(i); 300 301 serializer.startTag(null, TAG_PERMISSION); 302 serializer.attribute(null, ATTRIBUTE_NAME, permissionState.getName()); 303 serializer.attribute(null, ATTRIBUTE_GRANTED, Boolean.toString( 304 permissionState.isGranted() && (permissionState.getFlags() 305 & PackageManager.FLAG_PERMISSION_ONE_TIME) == 0)); 306 serializer.attribute(null, ATTRIBUTE_FLAGS, Integer.toHexString( 307 permissionState.getFlags())); 308 serializer.endTag(null, TAG_PERMISSION); 309 } 310 } 311 312 @Override deleteForUser(@onNull UserHandle user)313 public void deleteForUser(@NonNull UserHandle user) { 314 getFile(user).delete(); 315 getReserveCopyFile(user).delete(); 316 } 317 318 @VisibleForTesting 319 @NonNull getFile(@onNull UserHandle user)320 static File getFile(@NonNull UserHandle user) { 321 ApexEnvironment apexEnvironment = ApexEnvironment.getApexEnvironment(APEX_MODULE_NAME); 322 File dataDirectory = apexEnvironment.getDeviceProtectedDataDirForUser(user); 323 return new File(dataDirectory, RUNTIME_PERMISSIONS_FILE_NAME); 324 } 325 326 @NonNull getReserveCopyFile(@onNull UserHandle user)327 private static File getReserveCopyFile(@NonNull UserHandle user) { 328 ApexEnvironment apexEnvironment = ApexEnvironment.getApexEnvironment(APEX_MODULE_NAME); 329 File dataDirectory = apexEnvironment.getDeviceProtectedDataDirForUser(user); 330 return new File(dataDirectory, RUNTIME_PERMISSIONS_RESERVE_COPY_FILE_NAME); 331 } 332 } 333