/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.pm; import android.content.Context; import android.content.pm.EphemeralApplicationInfo; import android.content.pm.PackageParser; import android.content.pm.PackageUserState; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Binder; import android.os.Environment; import android.provider.Settings; import android.util.AtomicFile; import android.util.Slog; import android.util.SparseArray; import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.ArrayUtils; import com.android.internal.util.XmlUtils; import libcore.io.IoUtils; import libcore.util.EmptyArray; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; /** * This class is a part of the package manager service that is responsible * for managing data associated with ephemeral apps such as cached uninstalled * ephemeral apps and ephemeral apps' cookies. */ class EphemeralApplicationRegistry { private static final boolean DEBUG = false; private static final boolean ENABLED = false; private static final String LOG_TAG = "EphemeralAppRegistry"; private static final long DEFAULT_UNINSTALLED_EPHEMERAL_APP_CACHE_DURATION_MILLIS = DEBUG ? 60 * 1000L /* one min */ : 30 * 24 * 60 * 60 * 1000L; /* one month */ private final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); private static final String EPHEMERAL_APPS_FOLDER = "ephemeral"; private static final String EPHEMERAL_APP_ICON_FILE = "icon.png"; private static final String EPHEMERAL_APP_COOKIE_FILE_PREFIX = "cookie_"; private static final String EPHEMERAL_APP_COOKIE_FILE_SIFFIX = ".dat"; private static final String EPHEMERAL_APP_METADATA_FILE = "metadata.xml"; private static final String TAG_PACKAGE = "package"; private static final String TAG_PERMS = "perms"; private static final String TAG_PERM = "perm"; private static final String ATTR_LABEL = "label"; private static final String ATTR_NAME = "name"; private static final String ATTR_GRANTED = "granted"; private final PackageManagerService mService; @GuardedBy("mService.mPackages") private SparseArray<List<UninstalledEphemeralAppState>> mUninstalledEphemeralApps; public EphemeralApplicationRegistry(PackageManagerService service) { mService = service; } public byte[] getEphemeralApplicationCookieLPw(String packageName, int userId) { if (!ENABLED) { return EmptyArray.BYTE; } pruneUninstalledEphemeralAppsLPw(userId); File cookieFile = peekEphemeralCookieFile(packageName, userId); if (cookieFile != null && cookieFile.exists()) { try { return IoUtils.readFileAsByteArray(cookieFile.toString()); } catch (IOException e) { Slog.w(LOG_TAG, "Error reading cookie file: " + cookieFile); } } return null; } public boolean setEphemeralApplicationCookieLPw(String packageName, byte[] cookie, int userId) { if (!ENABLED) { return false; } pruneUninstalledEphemeralAppsLPw(userId); PackageParser.Package pkg = mService.mPackages.get(packageName); if (pkg == null) { return false; } if (!isValidCookie(mService.mContext, cookie)) { return false; } File appDir = getEphemeralApplicationDir(pkg.packageName, userId); if (!appDir.exists() && !appDir.mkdirs()) { return false; } File cookieFile = computeEphemeralCookieFile(pkg, userId); if (cookieFile.exists() && !cookieFile.delete()) { return false; } try (FileOutputStream fos = new FileOutputStream(cookieFile)) { fos.write(cookie, 0, cookie.length); } catch (IOException e) { Slog.w(LOG_TAG, "Error writing cookie file: " + cookieFile); return false; } return true; } public Bitmap getEphemeralApplicationIconLPw(String packageName, int userId) { if (!ENABLED) { return null; } pruneUninstalledEphemeralAppsLPw(userId); File iconFile = new File(getEphemeralApplicationDir(packageName, userId), EPHEMERAL_APP_ICON_FILE); if (iconFile.exists()) { return BitmapFactory.decodeFile(iconFile.toString()); } return null; } public List<EphemeralApplicationInfo> getEphemeralApplicationsLPw(int userId) { if (!ENABLED) { return Collections.emptyList(); } pruneUninstalledEphemeralAppsLPw(userId); List<EphemeralApplicationInfo> result = getInstalledEphemeralApplicationsLPr(userId); result.addAll(getUninstalledEphemeralApplicationsLPr(userId)); return result; } public void onPackageInstalledLPw(PackageParser.Package pkg) { if (!ENABLED) { return; } PackageSetting ps = (PackageSetting) pkg.mExtras; if (ps == null) { return; } for (int userId : UserManagerService.getInstance().getUserIds()) { pruneUninstalledEphemeralAppsLPw(userId); // Ignore not installed apps if (mService.mPackages.get(pkg.packageName) == null || !ps.getInstalled(userId)) { continue; } // Propagate permissions before removing any state propagateEphemeralAppPermissionsIfNeeded(pkg, userId); // Remove the in-memory state if (mUninstalledEphemeralApps != null) { List<UninstalledEphemeralAppState> uninstalledAppStates = mUninstalledEphemeralApps.get(userId); if (uninstalledAppStates != null) { final int appCount = uninstalledAppStates.size(); for (int i = 0; i < appCount; i++) { UninstalledEphemeralAppState uninstalledAppState = uninstalledAppStates.get(i); if (uninstalledAppState.mEphemeralApplicationInfo .getPackageName().equals(pkg.packageName)) { uninstalledAppStates.remove(i); break; } } } } // Remove the on-disk state except the cookie File ephemeralAppDir = getEphemeralApplicationDir(pkg.packageName, userId); new File(ephemeralAppDir, EPHEMERAL_APP_METADATA_FILE).delete(); new File(ephemeralAppDir, EPHEMERAL_APP_ICON_FILE).delete(); // If app signature changed - wipe the cookie File currentCookieFile = peekEphemeralCookieFile(pkg.packageName, userId); if (currentCookieFile == null) { continue; } File expectedCookeFile = computeEphemeralCookieFile(pkg, userId); if (!currentCookieFile.equals(expectedCookeFile)) { Slog.i(LOG_TAG, "Signature for package " + pkg.packageName + " changed - dropping cookie"); currentCookieFile.delete(); } } } public void onPackageUninstalledLPw(PackageParser.Package pkg) { if (!ENABLED) { return; } if (pkg == null) { return; } PackageSetting ps = (PackageSetting) pkg.mExtras; if (ps == null) { return; } for (int userId : UserManagerService.getInstance().getUserIds()) { pruneUninstalledEphemeralAppsLPw(userId); if (mService.mPackages.get(pkg.packageName) != null && ps.getInstalled(userId)) { continue; } if (pkg.applicationInfo.isEphemeralApp()) { // Add a record for an uninstalled ephemeral app addUninstalledEphemeralAppLPw(pkg, userId); } else { // Deleting an app prunes all ephemeral state such as cookie deleteDir(getEphemeralApplicationDir(pkg.packageName, userId)); } } } public void onUserRemovedLPw(int userId) { if (!ENABLED) { return; } if (mUninstalledEphemeralApps != null) { mUninstalledEphemeralApps.remove(userId); } deleteDir(getEphemeralApplicationsDir(userId)); } private void addUninstalledEphemeralAppLPw(PackageParser.Package pkg, int userId) { EphemeralApplicationInfo uninstalledApp = createEphemeralAppInfoForPackage(pkg, userId); if (uninstalledApp == null) { return; } if (mUninstalledEphemeralApps == null) { mUninstalledEphemeralApps = new SparseArray<>(); } List<UninstalledEphemeralAppState> uninstalledAppStates = mUninstalledEphemeralApps.get(userId); if (uninstalledAppStates == null) { uninstalledAppStates = new ArrayList<>(); mUninstalledEphemeralApps.put(userId, uninstalledAppStates); } UninstalledEphemeralAppState uninstalledAppState = new UninstalledEphemeralAppState( uninstalledApp, System.currentTimeMillis()); uninstalledAppStates.add(uninstalledAppState); writeUninstalledEphemeralAppMetadata(uninstalledApp, userId); writeEphemeralApplicationIconLPw(pkg, userId); } private void writeEphemeralApplicationIconLPw(PackageParser.Package pkg, int userId) { File appDir = getEphemeralApplicationDir(pkg.packageName, userId); if (!appDir.exists()) { return; } Drawable icon = pkg.applicationInfo.loadIcon(mService.mContext.getPackageManager()); final Bitmap bitmap; if (icon instanceof BitmapDrawable) { bitmap = ((BitmapDrawable) icon).getBitmap(); } else { bitmap = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); icon.draw(canvas); } File iconFile = new File(getEphemeralApplicationDir(pkg.packageName, userId), EPHEMERAL_APP_ICON_FILE); try (FileOutputStream out = new FileOutputStream(iconFile)) { bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); } catch (Exception e) { Slog.e(LOG_TAG, "Error writing ephemeral app icon", e); } } private void pruneUninstalledEphemeralAppsLPw(int userId) { final long maxCacheDurationMillis = Settings.Global.getLong( mService.mContext.getContentResolver(), Settings.Global.UNINSTALLED_EPHEMERAL_APP_CACHE_DURATION_MILLIS, DEFAULT_UNINSTALLED_EPHEMERAL_APP_CACHE_DURATION_MILLIS); // Prune in-memory state if (mUninstalledEphemeralApps != null) { List<UninstalledEphemeralAppState> uninstalledAppStates = mUninstalledEphemeralApps.get(userId); if (uninstalledAppStates != null) { final int appCount = uninstalledAppStates.size(); for (int j = appCount - 1; j >= 0; j--) { UninstalledEphemeralAppState uninstalledAppState = uninstalledAppStates.get(j); final long elapsedCachingMillis = System.currentTimeMillis() - uninstalledAppState.mTimestamp; if (elapsedCachingMillis > maxCacheDurationMillis) { uninstalledAppStates.remove(j); } } if (uninstalledAppStates.isEmpty()) { mUninstalledEphemeralApps.remove(userId); } } } // Prune on-disk state File ephemeralAppsDir = getEphemeralApplicationsDir(userId); if (!ephemeralAppsDir.exists()) { return; } File[] files = ephemeralAppsDir.listFiles(); if (files == null) { return; } for (File ephemeralDir : files) { if (!ephemeralDir.isDirectory()) { continue; } File metadataFile = new File(ephemeralDir, EPHEMERAL_APP_METADATA_FILE); if (!metadataFile.exists()) { continue; } final long elapsedCachingMillis = System.currentTimeMillis() - metadataFile.lastModified(); if (elapsedCachingMillis > maxCacheDurationMillis) { deleteDir(ephemeralDir); } } } private List<EphemeralApplicationInfo> getInstalledEphemeralApplicationsLPr(int userId) { List<EphemeralApplicationInfo> result = null; final int packageCount = mService.mPackages.size(); for (int i = 0; i < packageCount; i++) { PackageParser.Package pkg = mService.mPackages.valueAt(i); if (!pkg.applicationInfo.isEphemeralApp()) { continue; } EphemeralApplicationInfo info = createEphemeralAppInfoForPackage(pkg, userId); if (info == null) { continue; } if (result == null) { result = new ArrayList<>(); } result.add(info); } return result; } private EphemeralApplicationInfo createEphemeralAppInfoForPackage( PackageParser.Package pkg, int userId) { PackageSetting ps = (PackageSetting) pkg.mExtras; if (ps == null) { return null; } PackageUserState userState = ps.readUserState(userId); if (userState == null || !userState.installed || userState.hidden) { return null; } String[] requestedPermissions = new String[pkg.requestedPermissions.size()]; pkg.requestedPermissions.toArray(requestedPermissions); Set<String> permissions = ps.getPermissionsState().getPermissions(userId); String[] grantedPermissions = new String[permissions.size()]; permissions.toArray(grantedPermissions); return new EphemeralApplicationInfo(pkg.applicationInfo, requestedPermissions, grantedPermissions); } private List<EphemeralApplicationInfo> getUninstalledEphemeralApplicationsLPr(int userId) { List<UninstalledEphemeralAppState> uninstalledAppStates = getUninstalledEphemeralAppStatesLPr(userId); if (uninstalledAppStates == null || uninstalledAppStates.isEmpty()) { return Collections.emptyList(); } List<EphemeralApplicationInfo> uninstalledApps = new ArrayList<>(); final int stateCount = uninstalledAppStates.size(); for (int i = 0; i < stateCount; i++) { UninstalledEphemeralAppState uninstalledAppState = uninstalledAppStates.get(i); uninstalledApps.add(uninstalledAppState.mEphemeralApplicationInfo); } return uninstalledApps; } private void propagateEphemeralAppPermissionsIfNeeded(PackageParser.Package pkg, int userId) { EphemeralApplicationInfo appInfo = getOrParseUninstalledEphemeralAppInfo(pkg.packageName, userId); if (appInfo == null) { return; } if (ArrayUtils.isEmpty(appInfo.getGrantedPermissions())) { return; } final long identity = Binder.clearCallingIdentity(); try { for (String grantedPermission : appInfo.getGrantedPermissions()) { mService.grantRuntimePermission(pkg.packageName, grantedPermission, userId); } } finally { Binder.restoreCallingIdentity(identity); } } private EphemeralApplicationInfo getOrParseUninstalledEphemeralAppInfo(String packageName, int userId) { if (mUninstalledEphemeralApps != null) { List<UninstalledEphemeralAppState> uninstalledAppStates = mUninstalledEphemeralApps.get(userId); if (uninstalledAppStates != null) { final int appCount = uninstalledAppStates.size(); for (int i = 0; i < appCount; i++) { UninstalledEphemeralAppState uninstalledAppState = uninstalledAppStates.get(i); if (uninstalledAppState.mEphemeralApplicationInfo .getPackageName().equals(packageName)) { return uninstalledAppState.mEphemeralApplicationInfo; } } } } File metadataFile = new File(getEphemeralApplicationDir(packageName, userId), EPHEMERAL_APP_METADATA_FILE); UninstalledEphemeralAppState uninstalledAppState = parseMetadataFile(metadataFile); if (uninstalledAppState == null) { return null; } return uninstalledAppState.mEphemeralApplicationInfo; } private List<UninstalledEphemeralAppState> getUninstalledEphemeralAppStatesLPr(int userId) { List<UninstalledEphemeralAppState> uninstalledAppStates = null; if (mUninstalledEphemeralApps != null) { uninstalledAppStates = mUninstalledEphemeralApps.get(userId); if (uninstalledAppStates != null) { return uninstalledAppStates; } } File ephemeralAppsDir = getEphemeralApplicationsDir(userId); if (ephemeralAppsDir.exists()) { File[] files = ephemeralAppsDir.listFiles(); if (files != null) { for (File ephemeralDir : files) { if (!ephemeralDir.isDirectory()) { continue; } File metadataFile = new File(ephemeralDir, EPHEMERAL_APP_METADATA_FILE); UninstalledEphemeralAppState uninstalledAppState = parseMetadataFile(metadataFile); if (uninstalledAppState == null) { continue; } if (uninstalledAppStates == null) { uninstalledAppStates = new ArrayList<>(); } uninstalledAppStates.add(uninstalledAppState); } } } if (uninstalledAppStates != null) { if (mUninstalledEphemeralApps == null) { mUninstalledEphemeralApps = new SparseArray<>(); } mUninstalledEphemeralApps.put(userId, uninstalledAppStates); } return uninstalledAppStates; } private static boolean isValidCookie(Context context, byte[] cookie) { if (ArrayUtils.isEmpty(cookie)) { return true; } return cookie.length <= context.getPackageManager().getEphemeralCookieMaxSizeBytes(); } private static UninstalledEphemeralAppState parseMetadataFile(File metadataFile) { if (!metadataFile.exists()) { return null; } FileInputStream in; try { in = new AtomicFile(metadataFile).openRead(); } catch (FileNotFoundException fnfe) { Slog.i(LOG_TAG, "No ephemeral metadata file"); return null; } final File ephemeralDir = metadataFile.getParentFile(); final long timestamp = metadataFile.lastModified(); final String packageName = ephemeralDir.getName(); try { XmlPullParser parser = Xml.newPullParser(); parser.setInput(in, StandardCharsets.UTF_8.name()); return new UninstalledEphemeralAppState( parseMetadata(parser, packageName), timestamp); } catch (XmlPullParserException | IOException e) { throw new IllegalStateException("Failed parsing ephemeral" + " metadata file: " + metadataFile, e); } finally { IoUtils.closeQuietly(in); } } private static File computeEphemeralCookieFile(PackageParser.Package pkg, int userId) { File appDir = getEphemeralApplicationDir(pkg.packageName, userId); String cookieFile = EPHEMERAL_APP_COOKIE_FILE_PREFIX + computePackageCertDigest(pkg) + EPHEMERAL_APP_COOKIE_FILE_SIFFIX; return new File(appDir, cookieFile); } private static File peekEphemeralCookieFile(String packageName, int userId) { File appDir = getEphemeralApplicationDir(packageName, userId); if (!appDir.exists()) { return null; } for (File file : appDir.listFiles()) { if (!file.isDirectory() && file.getName().startsWith(EPHEMERAL_APP_COOKIE_FILE_PREFIX) && file.getName().endsWith(EPHEMERAL_APP_COOKIE_FILE_SIFFIX)) { return file; } } return null; } private static EphemeralApplicationInfo parseMetadata(XmlPullParser parser, String packageName) throws IOException, XmlPullParserException { final int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (TAG_PACKAGE.equals(parser.getName())) { return parsePackage(parser, packageName); } } return null; } private static EphemeralApplicationInfo parsePackage(XmlPullParser parser, String packageName) throws IOException, XmlPullParserException { String label = parser.getAttributeValue(null, ATTR_LABEL); List<String> outRequestedPermissions = new ArrayList<>(); List<String> outGrantedPermissions = new ArrayList<>(); final int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (TAG_PERMS.equals(parser.getName())) { parsePermissions(parser, outRequestedPermissions, outGrantedPermissions); } } String[] requestedPermissions = new String[outRequestedPermissions.size()]; outRequestedPermissions.toArray(requestedPermissions); String[] grantedPermissions = new String[outGrantedPermissions.size()]; outGrantedPermissions.toArray(grantedPermissions); return new EphemeralApplicationInfo(packageName, label, requestedPermissions, grantedPermissions); } private static void parsePermissions(XmlPullParser parser, List<String> outRequestedPermissions, List<String> outGrantedPermissions) throws IOException, XmlPullParserException { final int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser,outerDepth)) { if (TAG_PERM.equals(parser.getName())) { String permission = XmlUtils.readStringAttribute(parser, ATTR_NAME); outRequestedPermissions.add(permission); if (XmlUtils.readBooleanAttribute(parser, ATTR_GRANTED)) { outGrantedPermissions.add(permission); } } } } private void writeUninstalledEphemeralAppMetadata( EphemeralApplicationInfo ephemeralApp, int userId) { File appDir = getEphemeralApplicationDir(ephemeralApp.getPackageName(), userId); if (!appDir.exists() && !appDir.mkdirs()) { return; } File metadataFile = new File(appDir, EPHEMERAL_APP_METADATA_FILE); AtomicFile destination = new AtomicFile(metadataFile); FileOutputStream out = null; try { out = destination.startWrite(); XmlSerializer serializer = Xml.newSerializer(); serializer.setOutput(out, StandardCharsets.UTF_8.name()); serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); serializer.startDocument(null, true); serializer.startTag(null, TAG_PACKAGE); serializer.attribute(null, ATTR_LABEL, ephemeralApp.loadLabel( mService.mContext.getPackageManager()).toString()); serializer.startTag(null, TAG_PERMS); for (String permission : ephemeralApp.getRequestedPermissions()) { serializer.startTag(null, TAG_PERM); serializer.attribute(null, ATTR_NAME, permission); if (ArrayUtils.contains(ephemeralApp.getGrantedPermissions(), permission)) { serializer.attribute(null, ATTR_GRANTED, String.valueOf(true)); } serializer.endTag(null, TAG_PERM); } serializer.endTag(null, TAG_PERMS); serializer.endTag(null, TAG_PACKAGE); serializer.endDocument(); destination.finishWrite(out); } catch (Throwable t) { Slog.wtf(LOG_TAG, "Failed to write ephemeral state, restoring backup", t); destination.failWrite(out); } finally { IoUtils.closeQuietly(out); } } private static String computePackageCertDigest(PackageParser.Package pkg) { MessageDigest messageDigest; try { messageDigest = MessageDigest.getInstance("SHA256"); } catch (NoSuchAlgorithmException e) { /* can't happen */ return null; } messageDigest.update(pkg.mSignatures[0].toByteArray()); final byte[] digest = messageDigest.digest(); final int digestLength = digest.length; final int charCount = 2 * digestLength; final char[] chars = new char[charCount]; for (int i = 0; i < digestLength; i++) { final int byteHex = digest[i] & 0xFF; chars[i * 2] = HEX_ARRAY[byteHex >>> 4]; chars[i * 2 + 1] = HEX_ARRAY[byteHex & 0x0F]; } return new String(chars); } private static File getEphemeralApplicationsDir(int userId) { return new File(Environment.getUserSystemDirectory(userId), EPHEMERAL_APPS_FOLDER); } private static File getEphemeralApplicationDir(String packageName, int userId) { return new File (getEphemeralApplicationsDir(userId), packageName); } private static void deleteDir(File dir) { File[] files = dir.listFiles(); if (files != null) { for (File file : dir.listFiles()) { deleteDir(file); } } dir.delete(); } private static final class UninstalledEphemeralAppState { final EphemeralApplicationInfo mEphemeralApplicationInfo; final long mTimestamp; public UninstalledEphemeralAppState(EphemeralApplicationInfo ephemeralApp, long timestamp) { mEphemeralApplicationInfo = ephemeralApp; mTimestamp = timestamp; } } }