/* * Copyright (C) 2010 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.defcontainer; import com.android.internal.app.IMediaContainerService; import com.android.internal.content.NativeLibraryHelper; import com.android.internal.content.PackageHelper; import android.app.IntentService; import android.content.Intent; import android.content.pm.MacAuthenticatedInputStream; import android.content.pm.ContainerEncryptionParams; import android.content.pm.IPackageManager; import android.content.pm.LimitedLengthInputStream; import android.content.pm.PackageCleanItem; import android.content.pm.PackageInfo; import android.content.pm.PackageInfoLite; import android.content.pm.PackageManager; import android.content.pm.PackageParser; import android.content.res.ObbInfo; import android.content.res.ObbScanner; import android.net.Uri; import android.os.Environment; import android.os.Environment.UserEnvironment; import android.os.FileUtils; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.StatFs; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Slog; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.DigestException; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.Mac; import javax.crypto.NoSuchPaddingException; import libcore.io.ErrnoException; import libcore.io.IoUtils; import libcore.io.Libcore; import libcore.io.Streams; import libcore.io.StructStatFs; /* * This service copies a downloaded apk to a file passed in as * a ParcelFileDescriptor or to a newly created container specified * by parameters. The DownloadManager gives access to this process * based on its uid. This process also needs the ACCESS_DOWNLOAD_MANAGER * permission to access apks downloaded via the download manager. */ public class DefaultContainerService extends IntentService { private static final String TAG = "DefContainer"; private static final boolean localLOGV = true; private static final String LIB_DIR_NAME = "lib"; private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() { /** * Creates a new container and copies resource there. * @param paackageURI the uri of resource to be copied. Can be either * a content uri or a file uri * @param cid the id of the secure container that should * be used for creating a secure container into which the resource * will be copied. * @param key Refers to key used for encrypting the secure container * @param resFileName Name of the target resource file(relative to newly * created secure container) * @return Returns the new cache path where the resource has been copied into * */ public String copyResourceToContainer(final Uri packageURI, final String cid, final String key, final String resFileName, final String publicResFileName, boolean isExternal, boolean isForwardLocked) { if (packageURI == null || cid == null) { return null; } return copyResourceInner(packageURI, cid, key, resFileName, publicResFileName, isExternal, isForwardLocked); } /** * Copy specified resource to output stream * * @param packageURI the uri of resource to be copied. Should be a file * uri * @param encryptionParams parameters describing the encryption used for * this file * @param outStream Remote file descriptor to be used for copying * @return returns status code according to those in * {@link PackageManager} */ public int copyResource(final Uri packageURI, ContainerEncryptionParams encryptionParams, ParcelFileDescriptor outStream) { if (packageURI == null || outStream == null) { return PackageManager.INSTALL_FAILED_INVALID_URI; } ParcelFileDescriptor.AutoCloseOutputStream autoOut = new ParcelFileDescriptor.AutoCloseOutputStream(outStream); try { copyFile(packageURI, autoOut, encryptionParams); return PackageManager.INSTALL_SUCCEEDED; } catch (FileNotFoundException e) { Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " FNF: " + e.getMessage()); return PackageManager.INSTALL_FAILED_INVALID_URI; } catch (IOException e) { Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " IO: " + e.getMessage()); return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; } catch (DigestException e) { Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " Security: " + e.getMessage()); return PackageManager.INSTALL_FAILED_INVALID_APK; } } /** * Determine the recommended install location for package * specified by file uri location. * @param fileUri the uri of resource to be copied. Should be a * file uri * @return Returns PackageInfoLite object containing * the package info and recommended app location. */ public PackageInfoLite getMinimalPackageInfo(final String packagePath, int flags, long threshold) { PackageInfoLite ret = new PackageInfoLite(); if (packagePath == null) { Slog.i(TAG, "Invalid package file " + packagePath); ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK; return ret; } DisplayMetrics metrics = new DisplayMetrics(); metrics.setToDefaults(); PackageParser.PackageLite pkg = PackageParser.parsePackageLite(packagePath, 0); if (pkg == null) { Slog.w(TAG, "Failed to parse package"); final File apkFile = new File(packagePath); if (!apkFile.exists()) { ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_URI; } else { ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK; } return ret; } ret.packageName = pkg.packageName; ret.versionCode = pkg.versionCode; ret.installLocation = pkg.installLocation; ret.verifiers = pkg.verifiers; ret.recommendedInstallLocation = recommendAppInstallLocation(pkg.installLocation, packagePath, flags, threshold); return ret; } @Override public boolean checkInternalFreeStorage(Uri packageUri, boolean isForwardLocked, long threshold) throws RemoteException { final File apkFile = new File(packageUri.getPath()); try { return isUnderInternalThreshold(apkFile, isForwardLocked, threshold); } catch (IOException e) { return true; } } @Override public boolean checkExternalFreeStorage(Uri packageUri, boolean isForwardLocked) throws RemoteException { final File apkFile = new File(packageUri.getPath()); try { return isUnderExternalThreshold(apkFile, isForwardLocked); } catch (IOException e) { return true; } } public ObbInfo getObbInfo(String filename) { try { return ObbScanner.getObbInfo(filename); } catch (IOException e) { Slog.d(TAG, "Couldn't get OBB info for " + filename); return null; } } @Override public long calculateDirectorySize(String path) throws RemoteException { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); final File directory = new File(path); if (directory.exists() && directory.isDirectory()) { return MeasurementUtils.measureDirectory(path); } else { return 0L; } } @Override public long[] getFileSystemStats(String path) { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); try { final StructStatFs stat = Libcore.os.statfs(path); final long totalSize = stat.f_blocks * stat.f_bsize; final long availSize = stat.f_bavail * stat.f_bsize; return new long[] { totalSize, availSize }; } catch (ErrnoException e) { throw new IllegalStateException(e); } } @Override public void clearDirectory(String path) throws RemoteException { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); final File directory = new File(path); if (directory.exists() && directory.isDirectory()) { eraseFiles(directory); } } @Override public long calculateInstalledSize(String packagePath, boolean isForwardLocked) throws RemoteException { final File packageFile = new File(packagePath); try { return calculateContainerSize(packageFile, isForwardLocked) * 1024 * 1024; } catch (IOException e) { /* * Okay, something failed, so let's just estimate it to be 2x * the file size. Note this will be 0 if the file doesn't exist. */ return packageFile.length() * 2; } } }; public DefaultContainerService() { super("DefaultContainerService"); setIntentRedelivery(true); } @Override protected void onHandleIntent(Intent intent) { if (PackageManager.ACTION_CLEAN_EXTERNAL_STORAGE.equals(intent.getAction())) { final IPackageManager pm = IPackageManager.Stub.asInterface( ServiceManager.getService("package")); PackageCleanItem item = null; try { while ((item = pm.nextPackageToClean(item)) != null) { final UserEnvironment userEnv = new UserEnvironment(item.userId); eraseFiles(userEnv.getExternalStorageAppDataDirectory(item.packageName)); eraseFiles(userEnv.getExternalStorageAppMediaDirectory(item.packageName)); if (item.andCode) { eraseFiles(userEnv.getExternalStorageAppObbDirectory(item.packageName)); } } } catch (RemoteException e) { } } } void eraseFiles(File path) { if (path.isDirectory()) { String[] files = path.list(); if (files != null) { for (String file : files) { eraseFiles(new File(path, file)); } } } path.delete(); } public IBinder onBind(Intent intent) { return mBinder; } private String copyResourceInner(Uri packageURI, String newCid, String key, String resFileName, String publicResFileName, boolean isExternal, boolean isForwardLocked) { if (isExternal) { // Make sure the sdcard is mounted. String status = Environment.getExternalStorageState(); if (!status.equals(Environment.MEDIA_MOUNTED)) { Slog.w(TAG, "Make sure sdcard is mounted."); return null; } } // The .apk file String codePath = packageURI.getPath(); File codeFile = new File(codePath); // Calculate size of container needed to hold base APK. final int sizeMb; try { sizeMb = calculateContainerSize(codeFile, isForwardLocked); } catch (IOException e) { Slog.w(TAG, "Problem when trying to copy " + codeFile.getPath()); return null; } // Create new container final String newCachePath = PackageHelper.createSdDir(sizeMb, newCid, key, Process.myUid(), isExternal); if (newCachePath == null) { Slog.e(TAG, "Failed to create container " + newCid); return null; } if (localLOGV) { Slog.i(TAG, "Created container for " + newCid + " at path : " + newCachePath); } final File resFile = new File(newCachePath, resFileName); if (FileUtils.copyFile(new File(codePath), resFile)) { if (localLOGV) { Slog.i(TAG, "Copied " + codePath + " to " + resFile); } } else { Slog.e(TAG, "Failed to copy " + codePath + " to " + resFile); // Clean up container PackageHelper.destroySdDir(newCid); return null; } try { Libcore.os.chmod(resFile.getAbsolutePath(), 0640); } catch (ErrnoException e) { Slog.e(TAG, "Could not chown APK: " + e.getMessage()); PackageHelper.destroySdDir(newCid); return null; } if (isForwardLocked) { File publicZipFile = new File(newCachePath, publicResFileName); try { PackageHelper.extractPublicFiles(resFile.getAbsolutePath(), publicZipFile); if (localLOGV) { Slog.i(TAG, "Copied resources to " + publicZipFile); } } catch (IOException e) { Slog.e(TAG, "Could not chown public APK " + publicZipFile.getAbsolutePath() + ": " + e.getMessage()); PackageHelper.destroySdDir(newCid); return null; } try { Libcore.os.chmod(publicZipFile.getAbsolutePath(), 0644); } catch (ErrnoException e) { Slog.e(TAG, "Could not chown public resource file: " + e.getMessage()); PackageHelper.destroySdDir(newCid); return null; } } final File sharedLibraryDir = new File(newCachePath, LIB_DIR_NAME); if (sharedLibraryDir.mkdir()) { int ret = NativeLibraryHelper.copyNativeBinariesIfNeededLI(codeFile, sharedLibraryDir); if (ret != PackageManager.INSTALL_SUCCEEDED) { Slog.e(TAG, "Could not copy native libraries to " + sharedLibraryDir.getPath()); PackageHelper.destroySdDir(newCid); return null; } } else { Slog.e(TAG, "Could not create native lib directory: " + sharedLibraryDir.getPath()); PackageHelper.destroySdDir(newCid); return null; } if (!PackageHelper.finalizeSdDir(newCid)) { Slog.e(TAG, "Failed to finalize " + newCid + " at path " + newCachePath); // Clean up container PackageHelper.destroySdDir(newCid); return null; } if (localLOGV) { Slog.i(TAG, "Finalized container " + newCid); } if (PackageHelper.isContainerMounted(newCid)) { if (localLOGV) { Slog.i(TAG, "Unmounting " + newCid + " at path " + newCachePath); } // Force a gc to avoid being killed. Runtime.getRuntime().gc(); PackageHelper.unMountSdDir(newCid); } else { if (localLOGV) { Slog.i(TAG, "Container " + newCid + " not mounted"); } } return newCachePath; } private static void copyToFile(InputStream inputStream, OutputStream out) throws IOException { byte[] buffer = new byte[16384]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) >= 0) { out.write(buffer, 0, bytesRead); } } private void copyFile(Uri pPackageURI, OutputStream outStream, ContainerEncryptionParams encryptionParams) throws FileNotFoundException, IOException, DigestException { String scheme = pPackageURI.getScheme(); InputStream inStream = null; try { if (scheme == null || scheme.equals("file")) { final InputStream is = new FileInputStream(new File(pPackageURI.getPath())); inStream = new BufferedInputStream(is); } else if (scheme.equals("content")) { final ParcelFileDescriptor fd; try { fd = getContentResolver().openFileDescriptor(pPackageURI, "r"); } catch (FileNotFoundException e) { Slog.e(TAG, "Couldn't open file descriptor from download service. " + "Failed with exception " + e); throw e; } if (fd == null) { Slog.e(TAG, "Provider returned no file descriptor for " + pPackageURI.toString()); throw new FileNotFoundException("provider returned no file descriptor"); } else { if (localLOGV) { Slog.i(TAG, "Opened file descriptor from download service."); } inStream = new ParcelFileDescriptor.AutoCloseInputStream(fd); } } else { Slog.e(TAG, "Package URI is not 'file:' or 'content:' - " + pPackageURI); throw new FileNotFoundException("Package URI is not 'file:' or 'content:'"); } /* * If this resource is encrypted, get the decrypted stream version * of it. */ ApkContainer container = new ApkContainer(inStream, encryptionParams); try { /* * We copy the source package file to a temp file and then * rename it to the destination file in order to eliminate a * window where the package directory scanner notices the new * package file but it's not completely copied yet. */ copyToFile(container.getInputStream(), outStream); if (!container.isAuthenticated()) { throw new DigestException(); } } catch (GeneralSecurityException e) { throw new DigestException("A problem occured copying the file."); } } finally { IoUtils.closeQuietly(inStream); } } private static class ApkContainer { private static final int MAX_AUTHENTICATED_DATA_SIZE = 16384; private final InputStream mInStream; private MacAuthenticatedInputStream mAuthenticatedStream; private byte[] mTag; public ApkContainer(InputStream inStream, ContainerEncryptionParams encryptionParams) throws IOException { if (encryptionParams == null) { mInStream = inStream; } else { mInStream = getDecryptedStream(inStream, encryptionParams); mTag = encryptionParams.getMacTag(); } } public boolean isAuthenticated() { if (mAuthenticatedStream == null) { return true; } return mAuthenticatedStream.isTagEqual(mTag); } private Mac getMacInstance(ContainerEncryptionParams encryptionParams) throws IOException { final Mac m; try { final String macAlgo = encryptionParams.getMacAlgorithm(); if (macAlgo != null) { m = Mac.getInstance(macAlgo); m.init(encryptionParams.getMacKey(), encryptionParams.getMacSpec()); } else { m = null; } return m; } catch (NoSuchAlgorithmException e) { throw new IOException(e); } catch (InvalidKeyException e) { throw new IOException(e); } catch (InvalidAlgorithmParameterException e) { throw new IOException(e); } } public InputStream getInputStream() { return mInStream; } private InputStream getDecryptedStream(InputStream inStream, ContainerEncryptionParams encryptionParams) throws IOException { final Cipher c; try { c = Cipher.getInstance(encryptionParams.getEncryptionAlgorithm()); c.init(Cipher.DECRYPT_MODE, encryptionParams.getEncryptionKey(), encryptionParams.getEncryptionSpec()); } catch (NoSuchAlgorithmException e) { throw new IOException(e); } catch (NoSuchPaddingException e) { throw new IOException(e); } catch (InvalidKeyException e) { throw new IOException(e); } catch (InvalidAlgorithmParameterException e) { throw new IOException(e); } final long encStart = encryptionParams.getEncryptedDataStart(); final long end = encryptionParams.getDataEnd(); if (end < encStart) { throw new IOException("end <= encStart"); } final Mac mac = getMacInstance(encryptionParams); if (mac != null) { final long macStart = encryptionParams.getAuthenticatedDataStart(); if (macStart >= Integer.MAX_VALUE) { throw new IOException("macStart >= Integer.MAX_VALUE"); } final long furtherOffset; if (macStart >= 0 && encStart >= 0 && macStart < encStart) { /* * If there is authenticated data at the beginning, read * that into our MAC first. */ final long authenticatedLengthLong = encStart - macStart; if (authenticatedLengthLong > MAX_AUTHENTICATED_DATA_SIZE) { throw new IOException("authenticated data is too long"); } final int authenticatedLength = (int) authenticatedLengthLong; final byte[] authenticatedData = new byte[(int) authenticatedLength]; Streams.readFully(inStream, authenticatedData, (int) macStart, authenticatedLength); mac.update(authenticatedData, 0, authenticatedLength); furtherOffset = 0; } else { /* * No authenticated data at the beginning. Just skip the * required number of bytes to the beginning of the stream. */ if (encStart > 0) { furtherOffset = encStart; } else { furtherOffset = 0; } } /* * If there is data at the end of the stream we want to ignore, * wrap this in a LimitedLengthInputStream. */ if (furtherOffset >= 0 && end > furtherOffset) { inStream = new LimitedLengthInputStream(inStream, furtherOffset, end - encStart); } else if (furtherOffset > 0) { inStream.skip(furtherOffset); } mAuthenticatedStream = new MacAuthenticatedInputStream(inStream, mac); inStream = mAuthenticatedStream; } else { if (encStart >= 0) { if (end > encStart) { inStream = new LimitedLengthInputStream(inStream, encStart, end - encStart); } else { inStream.skip(encStart); } } } return new CipherInputStream(inStream, c); } } private static final int PREFER_INTERNAL = 1; private static final int PREFER_EXTERNAL = 2; private int recommendAppInstallLocation(int installLocation, String archiveFilePath, int flags, long threshold) { int prefer; boolean checkBoth = false; final boolean isForwardLocked = (flags & PackageManager.INSTALL_FORWARD_LOCK) != 0; check_inner : { /* * Explicit install flags should override the manifest settings. */ if ((flags & PackageManager.INSTALL_INTERNAL) != 0) { prefer = PREFER_INTERNAL; break check_inner; } else if ((flags & PackageManager.INSTALL_EXTERNAL) != 0) { prefer = PREFER_EXTERNAL; break check_inner; } /* No install flags. Check for manifest option. */ if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) { prefer = PREFER_INTERNAL; break check_inner; } else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) { prefer = PREFER_EXTERNAL; checkBoth = true; break check_inner; } else if (installLocation == PackageInfo.INSTALL_LOCATION_AUTO) { // We default to preferring internal storage. prefer = PREFER_INTERNAL; checkBoth = true; break check_inner; } // Pick user preference int installPreference = Settings.Global.getInt(getApplicationContext() .getContentResolver(), Settings.Global.DEFAULT_INSTALL_LOCATION, PackageHelper.APP_INSTALL_AUTO); if (installPreference == PackageHelper.APP_INSTALL_INTERNAL) { prefer = PREFER_INTERNAL; break check_inner; } else if (installPreference == PackageHelper.APP_INSTALL_EXTERNAL) { prefer = PREFER_EXTERNAL; break check_inner; } /* * Fall back to default policy of internal-only if nothing else is * specified. */ prefer = PREFER_INTERNAL; } final boolean emulated = Environment.isExternalStorageEmulated(); final File apkFile = new File(archiveFilePath); boolean fitsOnInternal = false; if (checkBoth || prefer == PREFER_INTERNAL) { try { fitsOnInternal = isUnderInternalThreshold(apkFile, isForwardLocked, threshold); } catch (IOException e) { return PackageHelper.RECOMMEND_FAILED_INVALID_URI; } } boolean fitsOnSd = false; if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)) { try { fitsOnSd = isUnderExternalThreshold(apkFile, isForwardLocked); } catch (IOException e) { return PackageHelper.RECOMMEND_FAILED_INVALID_URI; } } if (prefer == PREFER_INTERNAL) { if (fitsOnInternal) { return PackageHelper.RECOMMEND_INSTALL_INTERNAL; } } else if (!emulated && prefer == PREFER_EXTERNAL) { if (fitsOnSd) { return PackageHelper.RECOMMEND_INSTALL_EXTERNAL; } } if (checkBoth) { if (fitsOnInternal) { return PackageHelper.RECOMMEND_INSTALL_INTERNAL; } else if (!emulated && fitsOnSd) { return PackageHelper.RECOMMEND_INSTALL_EXTERNAL; } } /* * If they requested to be on the external media by default, return that * the media was unavailable. Otherwise, indicate there was insufficient * storage space available. */ if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL) && !Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { return PackageHelper.RECOMMEND_MEDIA_UNAVAILABLE; } else { return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE; } } /** * Measure a file to see if it fits within the free space threshold. * * @param apkFile file to check * @param threshold byte threshold to compare against * @return true if file fits under threshold * @throws FileNotFoundException when APK does not exist */ private boolean isUnderInternalThreshold(File apkFile, boolean isForwardLocked, long threshold) throws IOException { long size = apkFile.length(); if (size == 0 && !apkFile.exists()) { throw new FileNotFoundException(); } if (isForwardLocked) { size += PackageHelper.extractPublicFiles(apkFile.getAbsolutePath(), null); } final StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath()); final long availInternalSize = (long) internalStats.getAvailableBlocks() * (long) internalStats.getBlockSize(); return (availInternalSize - size) > threshold; } /** * Measure a file to see if it fits in the external free space. * * @param apkFile file to check * @return true if file fits * @throws IOException when file does not exist */ private boolean isUnderExternalThreshold(File apkFile, boolean isForwardLocked) throws IOException { if (Environment.isExternalStorageEmulated()) { return false; } final int sizeMb = calculateContainerSize(apkFile, isForwardLocked); final int availSdMb; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { final StatFs sdStats = new StatFs(Environment.getExternalStorageDirectory().getPath()); final int blocksToMb = (1 << 20) / sdStats.getBlockSize(); availSdMb = sdStats.getAvailableBlocks() * blocksToMb; } else { availSdMb = -1; } return availSdMb > sizeMb; } /** * Calculate the container size for an APK. Takes into account the * * @param apkFile file from which to calculate size * @return size in megabytes (2^20 bytes) * @throws IOException when there is a problem reading the file */ private int calculateContainerSize(File apkFile, boolean forwardLocked) throws IOException { // Calculate size of container needed to hold base APK. long sizeBytes = apkFile.length(); if (sizeBytes == 0 && !apkFile.exists()) { throw new FileNotFoundException(); } // Check all the native files that need to be copied and add that to the // container size. sizeBytes += NativeLibraryHelper.sumNativeBinariesLI(apkFile); if (forwardLocked) { sizeBytes += PackageHelper.extractPublicFiles(apkFile.getPath(), null); } int sizeMb = (int) (sizeBytes >> 20); if ((sizeBytes - (sizeMb * 1024 * 1024)) > 0) { sizeMb++; } /* * Add buffer size because we don't have a good way to determine the * real FAT size. Your FAT size varies with how many directory entries * you need, how big the whole filesystem is, and other such headaches. */ sizeMb++; return sizeMb; } }