/* * Copyright 2014, 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.managedprovisioning.task; import android.app.DownloadManager; import android.app.DownloadManager.Query; import android.app.DownloadManager.Request; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; import android.database.Cursor; import android.net.Uri; import android.text.TextUtils; import com.android.managedprovisioning.NetworkMonitor; import com.android.managedprovisioning.ProvisionLogger; import com.android.managedprovisioning.common.Utils; import com.android.managedprovisioning.model.PackageDownloadInfo; import java.io.InputStream; import java.io.IOException; import java.io.File; import java.io.FileInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; /** * Downloads all packages that were added. Also verifies that the downloaded files are the ones that * are expected. */ public class DownloadPackageTask { private static final boolean DEBUG = false; // To control logging. public static final int ERROR_HASH_MISMATCH = 0; public static final int ERROR_DOWNLOAD_FAILED = 1; public static final int ERROR_OTHER = 2; private static final String SHA1_TYPE = "SHA-1"; private static final String SHA256_TYPE = "SHA-256"; private final Context mContext; private final Callback mCallback; private BroadcastReceiver mReceiver; private final DownloadManager mDlm; private final PackageManager mPm; private int mFileNumber = 0; private final Utils mUtils = new Utils(); private Set mDownloads; public DownloadPackageTask (Context context, Callback callback) { mCallback = callback; mContext = context; mDlm = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); mDlm.setAccessFilename(true); mPm = context.getPackageManager(); mDownloads = new HashSet(); } public void addDownloadIfNecessary( String packageName, PackageDownloadInfo downloadInfo, String label) { if (downloadInfo != null && mUtils.packageRequiresUpdate(packageName, downloadInfo.minVersion, mContext)) { mDownloads.add(new DownloadStatusInfo(downloadInfo, label)); } } public void run() { if (mDownloads.size() == 0) { mCallback.onSuccess(); return; } if (!mUtils.isConnectedToNetwork(mContext)) { ProvisionLogger.loge("DownloadPackageTask: not connected to the network, can't download" + " the package"); mCallback.onError(ERROR_OTHER); } mReceiver = createDownloadReceiver(); mContext.registerReceiver(mReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); DownloadManager dm = (DownloadManager) mContext .getSystemService(Context.DOWNLOAD_SERVICE); for (DownloadStatusInfo info : mDownloads) { if (DEBUG) { ProvisionLogger.logd("Starting download from " + info.mPackageDownloadInfo.location); } Request request = new Request(Uri.parse(info.mPackageDownloadInfo.location)); // All we want is to have a different file for each apk // Note that the apk may not actually be downloaded to this path. This could happen if // this file already exists. String path = mContext.getExternalFilesDir(null) + "/download_cache/managed_provisioning_downloaded_app_" + mFileNumber + ".apk"; mFileNumber++; File downloadedFile = new File(path); downloadedFile.getParentFile().mkdirs(); // If the folder doesn't exists it is created request.setDestinationUri(Uri.fromFile(downloadedFile)); if (info.mPackageDownloadInfo.cookieHeader != null) { request.addRequestHeader("Cookie", info.mPackageDownloadInfo.cookieHeader); if (DEBUG) { ProvisionLogger.logd("Downloading with http cookie header: " + info.mPackageDownloadInfo.cookieHeader); } } info.mDownloadId = dm.enqueue(request); } } private BroadcastReceiver createDownloadReceiver() { return new BroadcastReceiver() { /** * Whenever the download manager finishes a download, record the successful download for * the corresponding DownloadStatusInfo. */ @Override public void onReceive(Context context, Intent intent) { if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { Query q = new Query(); for (DownloadStatusInfo info : mDownloads) { q.setFilterById(info.mDownloadId); Cursor c = mDlm.query(q); if (c.moveToFirst()) { long downloadId = c.getLong(c.getColumnIndex(DownloadManager.COLUMN_ID)); String filePath = c.getString(c.getColumnIndex( DownloadManager.COLUMN_LOCAL_FILENAME)); int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS); if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) { c.close(); onDownloadSuccess(downloadId, filePath); } else if (DownloadManager.STATUS_FAILED == c.getInt(columnIndex)){ int reason = c.getInt( c.getColumnIndex(DownloadManager.COLUMN_REASON)); c.close(); onDownloadFail(reason); } } } } } }; } /** * For a given successful download, check that the downloaded file is the expected file. * If the package hash is provided then that is used, otherwise a signature hash is used. * Then check if this was the last file the task had to download and finish the * DownloadPackageTask if that is the case. * @param downloadId the unique download id for the completed download. * @param location the file location of the downloaded file. */ private void onDownloadSuccess(long downloadId, String filePath) { DownloadStatusInfo info = null; for (DownloadStatusInfo infoToMatch : mDownloads) { if (downloadId == infoToMatch.mDownloadId) { info = infoToMatch; } } if (info == null || info.mDoneDownloading) { // DownloadManager can send success more than once. Only act first time. return; } else { info.mDoneDownloading = true; info.mLocation = filePath; } ProvisionLogger.logd("Downloaded succesfully to: " + info.mLocation); boolean downloadedContentsCorrect = false; if (info.mPackageDownloadInfo.packageChecksum.length > 0) { downloadedContentsCorrect = doesPackageHashMatch(info); } else if (info.mPackageDownloadInfo.signatureChecksum.length > 0) { downloadedContentsCorrect = doesASignatureHashMatch(info); } if (downloadedContentsCorrect) { info.mSuccess = true; checkSuccess(); } else { mCallback.onError(ERROR_HASH_MISMATCH); } } /** * Check whether package hash of downloaded file matches the hash given in DownloadStatusInfo. * By default, SHA-256 is used to verify the file hash. * If mPackageDownloadInfo.packageChecksumSupportsSha1 == true, SHA-1 hash is also supported for * backwards compatibility. */ private boolean doesPackageHashMatch(DownloadStatusInfo info) { byte[] packageSha256Hash, packageSha1Hash = null; ProvisionLogger.logd("Checking file hash of entire apk file."); packageSha256Hash = computeHashOfFile(info.mLocation, SHA256_TYPE); if (packageSha256Hash == null) { // Error should have been reported in computeHashOfFile(). return false; } if (Arrays.equals(info.mPackageDownloadInfo.packageChecksum, packageSha256Hash)) { return true; } // Fall back to SHA-1 if (info.mPackageDownloadInfo.packageChecksumSupportsSha1) { packageSha1Hash = computeHashOfFile(info.mLocation, SHA1_TYPE); if (Arrays.equals(info.mPackageDownloadInfo.packageChecksum, packageSha1Hash)) { return true; } } ProvisionLogger.loge("Provided hash does not match file hash."); ProvisionLogger.loge("Hash provided by programmer: " + mUtils.byteArrayToString(info.mPackageDownloadInfo.packageChecksum)); ProvisionLogger.loge("SHA-256 Hash computed from file: " + mUtils.byteArrayToString( packageSha256Hash)); if (packageSha1Hash != null) { ProvisionLogger.loge("SHA-1 Hash computed from file: " + mUtils.byteArrayToString( packageSha1Hash)); } return false; } private boolean doesASignatureHashMatch(DownloadStatusInfo info) { // Check whether a signature hash of downloaded apk matches the hash given in constructor. ProvisionLogger.logd("Checking " + SHA256_TYPE + "-hashes of all signatures of downloaded package."); List sigHashes = computeHashesOfAllSignatures(info.mLocation); if (sigHashes == null) { // Error should have been reported in computeHashesOfAllSignatures(). return false; } if (sigHashes.isEmpty()) { ProvisionLogger.loge("Downloaded package does not have any signatures."); return false; } for (byte[] sigHash : sigHashes) { if (Arrays.equals(sigHash, info.mPackageDownloadInfo.signatureChecksum)) { return true; } } ProvisionLogger.loge("Provided hash does not match any signature hash."); ProvisionLogger.loge("Hash provided by programmer: " + mUtils.byteArrayToString(info.mPackageDownloadInfo.signatureChecksum)); ProvisionLogger.loge("Hashes computed from package signatures: "); for (byte[] sigHash : sigHashes) { ProvisionLogger.loge(mUtils.byteArrayToString(sigHash)); } return false; } private void checkSuccess() { for (DownloadStatusInfo info : mDownloads) { if (!info.mSuccess) { return; } } mCallback.onSuccess(); } private void onDownloadFail(int errorCode) { ProvisionLogger.loge("Downloading package failed."); ProvisionLogger.loge("COLUMN_REASON in DownloadManager response has value: " + errorCode); mCallback.onError(ERROR_DOWNLOAD_FAILED); } private byte[] computeHashOfFile(String fileLocation, String hashType) { InputStream fis = null; MessageDigest md; byte hash[] = null; try { md = MessageDigest.getInstance(hashType); } catch (NoSuchAlgorithmException e) { ProvisionLogger.loge("Hashing algorithm " + hashType + " not supported.", e); mCallback.onError(ERROR_OTHER); return null; } try { fis = new FileInputStream(fileLocation); byte[] buffer = new byte[256]; int n = 0; while (n != -1) { n = fis.read(buffer); if (n > 0) { md.update(buffer, 0, n); } } hash = md.digest(); } catch (IOException e) { ProvisionLogger.loge("IO error.", e); mCallback.onError(ERROR_OTHER); } finally { // Close input stream quietly. try { if (fis != null) { fis.close(); } } catch (IOException e) { // Ignore. } } return hash; } public String getDownloadedPackageLocation(String label) { for (DownloadStatusInfo info : mDownloads) { if (info.mLabel.equals(label)) { return info.mLocation; } } return ""; } private List computeHashesOfAllSignatures(String packageArchiveLocation) { PackageInfo info = mPm.getPackageArchiveInfo(packageArchiveLocation, PackageManager.GET_SIGNATURES); if (info == null) { ProvisionLogger.loge("Unable to get package archive info from " + packageArchiveLocation); mCallback.onError(ERROR_OTHER); return null; } List hashes = new LinkedList(); Signature signatures[] = info.signatures; try { for (Signature signature : signatures) { byte[] hash = computeHashOfByteArray(signature.toByteArray()); hashes.add(hash); } } catch (NoSuchAlgorithmException e) { ProvisionLogger.loge("Hashing algorithm " + SHA256_TYPE + " not supported.", e); mCallback.onError(ERROR_OTHER); return null; } return hashes; } private byte[] computeHashOfByteArray(byte[] bytes) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance(SHA256_TYPE); md.update(bytes, 0, bytes.length); return md.digest(); } public void cleanUp() { if (mReceiver != null) { //Unregister receiver. mContext.unregisterReceiver(mReceiver); mReceiver = null; } //Remove download. DownloadManager dm = (DownloadManager) mContext .getSystemService(Context.DOWNLOAD_SERVICE); for (DownloadStatusInfo info : mDownloads) { boolean removeSuccess = dm.remove(info.mDownloadId) == 1; if (removeSuccess) { ProvisionLogger.logd("Successfully removed installer file."); } else { ProvisionLogger.loge("Could not remove installer file."); // Ignore this error. Failing cleanup should not stop provisioning flow. } } } public abstract static class Callback { public abstract void onSuccess(); public abstract void onError(int errorCode); } private static class DownloadStatusInfo { public final PackageDownloadInfo mPackageDownloadInfo; public final String mLabel; public long mDownloadId; public String mLocation; // Location where the package is downloaded to. public boolean mDoneDownloading; public boolean mSuccess; public DownloadStatusInfo(PackageDownloadInfo packageDownloadInfo,String label) { mPackageDownloadInfo = packageDownloadInfo; mLabel = label; mDoneDownloading = false; } } }