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 file = getFile(user); 218 AtomicFile atomicFile = new AtomicFile(file); 219 FileOutputStream outputStream = null; 220 try { 221 outputStream = atomicFile.startWrite(); 222 223 XmlSerializer serializer = Xml.newSerializer(); 224 serializer.setOutput(outputStream, StandardCharsets.UTF_8.name()); 225 serializer.startDocument(null, true); 226 227 serializeRuntimePermissions(serializer, runtimePermissions); 228 229 serializer.endDocument(); 230 atomicFile.finishWrite(outputStream); 231 } catch (Exception e) { 232 Log.wtf(LOG_TAG, "Failed to write runtime-permissions.xml, restoring backup: " + file, 233 e); 234 atomicFile.failWrite(outputStream); 235 return; 236 } finally { 237 IoUtils.closeQuietly(outputStream); 238 } 239 240 File reserveFile = getReserveCopyFile(user); 241 reserveFile.delete(); 242 try (FileInputStream in = new FileInputStream(file); 243 FileOutputStream out = new FileOutputStream(reserveFile)) { 244 FileUtils.copy(in, out); 245 out.getFD().sync(); 246 } catch (Exception e) { 247 Log.e(LOG_TAG, "Failed to write reserve copy: " + reserveFile, e); 248 } 249 250 try { 251 mInjector.enableFsVerity(file); 252 mInjector.enableFsVerity(reserveFile); 253 } catch (Exception e) { 254 Log.e(LOG_TAG, "Failed to verity-protect runtime-permissions", e); 255 } 256 } 257 serializeRuntimePermissions(@onNull XmlSerializer serializer, @NonNull RuntimePermissionsState runtimePermissions)258 private static void serializeRuntimePermissions(@NonNull XmlSerializer serializer, 259 @NonNull RuntimePermissionsState runtimePermissions) throws IOException { 260 serializer.startTag(null, TAG_RUNTIME_PERMISSIONS); 261 262 int version = runtimePermissions.getVersion(); 263 serializer.attribute(null, ATTRIBUTE_VERSION, Integer.toString(version)); 264 String fingerprint = runtimePermissions.getFingerprint(); 265 if (fingerprint != null) { 266 serializer.attribute(null, ATTRIBUTE_FINGERPRINT, fingerprint); 267 } 268 269 for (Map.Entry<String, List<RuntimePermissionsState.PermissionState>> entry 270 : runtimePermissions.getPackagePermissions().entrySet()) { 271 String packageName = entry.getKey(); 272 List<RuntimePermissionsState.PermissionState> permissions = entry.getValue(); 273 274 serializer.startTag(null, TAG_PACKAGE); 275 serializer.attribute(null, ATTRIBUTE_NAME, packageName); 276 serializePermissions(serializer, permissions); 277 serializer.endTag(null, TAG_PACKAGE); 278 } 279 280 for (Map.Entry<String, List<RuntimePermissionsState.PermissionState>> entry 281 : runtimePermissions.getSharedUserPermissions().entrySet()) { 282 String sharedUserName = entry.getKey(); 283 List<RuntimePermissionsState.PermissionState> permissions = entry.getValue(); 284 285 serializer.startTag(null, TAG_SHARED_USER); 286 serializer.attribute(null, ATTRIBUTE_NAME, sharedUserName); 287 serializePermissions(serializer, permissions); 288 serializer.endTag(null, TAG_SHARED_USER); 289 } 290 291 serializer.endTag(null, TAG_RUNTIME_PERMISSIONS); 292 } 293 serializePermissions(@onNull XmlSerializer serializer, @NonNull List<RuntimePermissionsState.PermissionState> permissions)294 private static void serializePermissions(@NonNull XmlSerializer serializer, 295 @NonNull List<RuntimePermissionsState.PermissionState> permissions) throws IOException { 296 int permissionsSize = permissions.size(); 297 for (int i = 0; i < permissionsSize; i++) { 298 RuntimePermissionsState.PermissionState permissionState = permissions.get(i); 299 300 serializer.startTag(null, TAG_PERMISSION); 301 serializer.attribute(null, ATTRIBUTE_NAME, permissionState.getName()); 302 serializer.attribute(null, ATTRIBUTE_GRANTED, Boolean.toString( 303 permissionState.isGranted() && (permissionState.getFlags() 304 & PackageManager.FLAG_PERMISSION_ONE_TIME) == 0)); 305 serializer.attribute(null, ATTRIBUTE_FLAGS, Integer.toHexString( 306 permissionState.getFlags())); 307 serializer.endTag(null, TAG_PERMISSION); 308 } 309 } 310 311 @Override deleteForUser(@onNull UserHandle user)312 public void deleteForUser(@NonNull UserHandle user) { 313 getFile(user).delete(); 314 getReserveCopyFile(user).delete(); 315 } 316 317 @VisibleForTesting 318 @NonNull getFile(@onNull UserHandle user)319 static File getFile(@NonNull UserHandle user) { 320 ApexEnvironment apexEnvironment = ApexEnvironment.getApexEnvironment(APEX_MODULE_NAME); 321 File dataDirectory = apexEnvironment.getDeviceProtectedDataDirForUser(user); 322 return new File(dataDirectory, RUNTIME_PERMISSIONS_FILE_NAME); 323 } 324 325 @NonNull getReserveCopyFile(@onNull UserHandle user)326 private static File getReserveCopyFile(@NonNull UserHandle user) { 327 ApexEnvironment apexEnvironment = ApexEnvironment.getApexEnvironment(APEX_MODULE_NAME); 328 File dataDirectory = apexEnvironment.getDeviceProtectedDataDirForUser(user); 329 return new File(dataDirectory, RUNTIME_PERMISSIONS_RESERVE_COPY_FILE_NAME); 330 } 331 } 332