Home | History | Annotate | Download | only in task
      1 /*
      2  * Copyright 2014, 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 package com.android.managedprovisioning.task;
     17 
     18 import android.app.DownloadManager;
     19 import android.app.DownloadManager.Query;
     20 import android.app.DownloadManager.Request;
     21 import android.content.BroadcastReceiver;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.IntentFilter;
     25 import android.content.pm.PackageInfo;
     26 import android.content.pm.PackageManager;
     27 import android.content.pm.PackageManager.NameNotFoundException;
     28 import android.content.pm.Signature;
     29 import android.database.Cursor;
     30 import android.net.Uri;
     31 import android.text.TextUtils;
     32 
     33 import com.android.managedprovisioning.NetworkMonitor;
     34 import com.android.managedprovisioning.ProvisionLogger;
     35 import com.android.managedprovisioning.common.Utils;
     36 import com.android.managedprovisioning.model.PackageDownloadInfo;
     37 
     38 import java.io.InputStream;
     39 import java.io.IOException;
     40 import java.io.File;
     41 import java.io.FileInputStream;
     42 import java.security.MessageDigest;
     43 import java.security.NoSuchAlgorithmException;
     44 import java.util.Arrays;
     45 import java.util.HashSet;
     46 import java.util.LinkedList;
     47 import java.util.List;
     48 import java.util.Set;
     49 
     50 /**
     51  * Downloads all packages that were added. Also verifies that the downloaded files are the ones that
     52  * are expected.
     53  */
     54 public class DownloadPackageTask {
     55     private static final boolean DEBUG = false; // To control logging.
     56 
     57     public static final int ERROR_HASH_MISMATCH = 0;
     58     public static final int ERROR_DOWNLOAD_FAILED = 1;
     59     public static final int ERROR_OTHER = 2;
     60 
     61     private static final String SHA1_TYPE = "SHA-1";
     62     private static final String SHA256_TYPE = "SHA-256";
     63 
     64     private final Context mContext;
     65     private final Callback mCallback;
     66     private BroadcastReceiver mReceiver;
     67     private final DownloadManager mDlm;
     68     private final PackageManager mPm;
     69     private int mFileNumber = 0;
     70 
     71     private final Utils mUtils = new Utils();
     72 
     73     private Set<DownloadStatusInfo> mDownloads;
     74 
     75     public DownloadPackageTask (Context context, Callback callback) {
     76         mCallback = callback;
     77         mContext = context;
     78         mDlm = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
     79         mDlm.setAccessFilename(true);
     80         mPm = context.getPackageManager();
     81 
     82         mDownloads = new HashSet<DownloadStatusInfo>();
     83     }
     84 
     85     public void addDownloadIfNecessary(
     86             String packageName, PackageDownloadInfo downloadInfo, String label) {
     87         if (downloadInfo != null
     88                 && mUtils.packageRequiresUpdate(packageName, downloadInfo.minVersion, mContext)) {
     89             mDownloads.add(new DownloadStatusInfo(downloadInfo, label));
     90         }
     91     }
     92 
     93     public void run() {
     94         if (mDownloads.size() == 0) {
     95             mCallback.onSuccess();
     96             return;
     97         }
     98         if (!mUtils.isConnectedToNetwork(mContext)) {
     99             ProvisionLogger.loge("DownloadPackageTask: not connected to the network, can't download"
    100                     + " the package");
    101             mCallback.onError(ERROR_OTHER);
    102         }
    103         mReceiver = createDownloadReceiver();
    104         mContext.registerReceiver(mReceiver,
    105                 new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
    106 
    107         DownloadManager dm = (DownloadManager) mContext
    108                 .getSystemService(Context.DOWNLOAD_SERVICE);
    109         for (DownloadStatusInfo info : mDownloads) {
    110             if (DEBUG) {
    111                 ProvisionLogger.logd("Starting download from " +
    112                         info.mPackageDownloadInfo.location);
    113             }
    114 
    115             Request request = new Request(Uri.parse(info.mPackageDownloadInfo.location));
    116             // All we want is to have a different file for each apk
    117             // Note that the apk may not actually be downloaded to this path. This could happen if
    118             // this file already exists.
    119             String path = mContext.getExternalFilesDir(null)
    120                     + "/download_cache/managed_provisioning_downloaded_app_" + mFileNumber + ".apk";
    121             mFileNumber++;
    122             File downloadedFile = new File(path);
    123             downloadedFile.getParentFile().mkdirs(); // If the folder doesn't exists it is created
    124             request.setDestinationUri(Uri.fromFile(downloadedFile));
    125             if (info.mPackageDownloadInfo.cookieHeader != null) {
    126                 request.addRequestHeader("Cookie", info.mPackageDownloadInfo.cookieHeader);
    127                 if (DEBUG) {
    128                     ProvisionLogger.logd("Downloading with http cookie header: "
    129                             + info.mPackageDownloadInfo.cookieHeader);
    130                 }
    131             }
    132             info.mDownloadId = dm.enqueue(request);
    133         }
    134     }
    135 
    136     private BroadcastReceiver createDownloadReceiver() {
    137         return new BroadcastReceiver() {
    138             /**
    139              * Whenever the download manager finishes a download, record the successful download for
    140              * the corresponding DownloadStatusInfo.
    141              */
    142             @Override
    143             public void onReceive(Context context, Intent intent) {
    144                 if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
    145                     Query q = new Query();
    146                     for (DownloadStatusInfo info : mDownloads) {
    147                         q.setFilterById(info.mDownloadId);
    148                         Cursor c = mDlm.query(q);
    149                         if (c.moveToFirst()) {
    150                             long downloadId =
    151                                     c.getLong(c.getColumnIndex(DownloadManager.COLUMN_ID));
    152                             String filePath = c.getString(c.getColumnIndex(
    153                                     DownloadManager.COLUMN_LOCAL_FILENAME));
    154                             int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
    155                             if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) {
    156                                 c.close();
    157                                 onDownloadSuccess(downloadId, filePath);
    158                             } else if (DownloadManager.STATUS_FAILED == c.getInt(columnIndex)){
    159                                 int reason = c.getInt(
    160                                         c.getColumnIndex(DownloadManager.COLUMN_REASON));
    161                                 c.close();
    162                                 onDownloadFail(reason);
    163                             }
    164                         }
    165                     }
    166                 }
    167             }
    168         };
    169     }
    170 
    171     /**
    172      * For a given successful download, check that the downloaded file is the expected file.
    173      * If the package hash is provided then that is used, otherwise a signature hash is used.
    174      * Then check if this was the last file the task had to download and finish the
    175      * DownloadPackageTask if that is the case.
    176      * @param downloadId the unique download id for the completed download.
    177      * @param location the file location of the downloaded file.
    178      */
    179     private void onDownloadSuccess(long downloadId, String filePath) {
    180         DownloadStatusInfo info = null;
    181         for (DownloadStatusInfo infoToMatch : mDownloads) {
    182             if (downloadId == infoToMatch.mDownloadId) {
    183                 info = infoToMatch;
    184             }
    185         }
    186         if (info == null || info.mDoneDownloading) {
    187             // DownloadManager can send success more than once. Only act first time.
    188             return;
    189         } else {
    190             info.mDoneDownloading = true;
    191             info.mLocation = filePath;
    192         }
    193         ProvisionLogger.logd("Downloaded succesfully to: " + info.mLocation);
    194 
    195         boolean downloadedContentsCorrect = false;
    196         if (info.mPackageDownloadInfo.packageChecksum.length > 0) {
    197             downloadedContentsCorrect = doesPackageHashMatch(info);
    198         } else if (info.mPackageDownloadInfo.signatureChecksum.length > 0) {
    199             downloadedContentsCorrect = doesASignatureHashMatch(info);
    200         }
    201 
    202         if (downloadedContentsCorrect) {
    203             info.mSuccess = true;
    204             checkSuccess();
    205         } else {
    206             mCallback.onError(ERROR_HASH_MISMATCH);
    207         }
    208     }
    209 
    210     /**
    211      * Check whether package hash of downloaded file matches the hash given in DownloadStatusInfo.
    212      * By default, SHA-256 is used to verify the file hash.
    213      * If mPackageDownloadInfo.packageChecksumSupportsSha1 == true, SHA-1 hash is also supported for
    214      * backwards compatibility.
    215      */
    216     private boolean doesPackageHashMatch(DownloadStatusInfo info) {
    217         byte[] packageSha256Hash, packageSha1Hash = null;
    218 
    219         ProvisionLogger.logd("Checking file hash of entire apk file.");
    220         packageSha256Hash = computeHashOfFile(info.mLocation, SHA256_TYPE);
    221         if (packageSha256Hash == null) {
    222             // Error should have been reported in computeHashOfFile().
    223             return false;
    224         }
    225 
    226         if (Arrays.equals(info.mPackageDownloadInfo.packageChecksum, packageSha256Hash)) {
    227             return true;
    228         }
    229 
    230         // Fall back to SHA-1
    231         if (info.mPackageDownloadInfo.packageChecksumSupportsSha1) {
    232             packageSha1Hash = computeHashOfFile(info.mLocation, SHA1_TYPE);
    233             if (Arrays.equals(info.mPackageDownloadInfo.packageChecksum, packageSha1Hash)) {
    234                 return true;
    235             }
    236         }
    237 
    238         ProvisionLogger.loge("Provided hash does not match file hash.");
    239         ProvisionLogger.loge("Hash provided by programmer: "
    240                 + mUtils.byteArrayToString(info.mPackageDownloadInfo.packageChecksum));
    241         ProvisionLogger.loge("SHA-256 Hash computed from file: " + mUtils.byteArrayToString(
    242                 packageSha256Hash));
    243         if (packageSha1Hash != null) {
    244             ProvisionLogger.loge("SHA-1 Hash computed from file: " + mUtils.byteArrayToString(
    245                     packageSha1Hash));
    246         }
    247         return false;
    248     }
    249 
    250     private boolean doesASignatureHashMatch(DownloadStatusInfo info) {
    251         // Check whether a signature hash of downloaded apk matches the hash given in constructor.
    252         ProvisionLogger.logd("Checking " + SHA256_TYPE
    253                 + "-hashes of all signatures of downloaded package.");
    254         List<byte[]> sigHashes = computeHashesOfAllSignatures(info.mLocation);
    255         if (sigHashes == null) {
    256             // Error should have been reported in computeHashesOfAllSignatures().
    257             return false;
    258         }
    259         if (sigHashes.isEmpty()) {
    260             ProvisionLogger.loge("Downloaded package does not have any signatures.");
    261             return false;
    262         }
    263         for (byte[] sigHash : sigHashes) {
    264             if (Arrays.equals(sigHash, info.mPackageDownloadInfo.signatureChecksum)) {
    265                 return true;
    266             }
    267         }
    268 
    269         ProvisionLogger.loge("Provided hash does not match any signature hash.");
    270         ProvisionLogger.loge("Hash provided by programmer: "
    271                 + mUtils.byteArrayToString(info.mPackageDownloadInfo.signatureChecksum));
    272         ProvisionLogger.loge("Hashes computed from package signatures: ");
    273         for (byte[] sigHash : sigHashes) {
    274             ProvisionLogger.loge(mUtils.byteArrayToString(sigHash));
    275         }
    276 
    277         return false;
    278     }
    279 
    280     private void checkSuccess() {
    281         for (DownloadStatusInfo info : mDownloads) {
    282             if (!info.mSuccess) {
    283                 return;
    284             }
    285         }
    286         mCallback.onSuccess();
    287     }
    288 
    289     private void onDownloadFail(int errorCode) {
    290         ProvisionLogger.loge("Downloading package failed.");
    291         ProvisionLogger.loge("COLUMN_REASON in DownloadManager response has value: "
    292                 + errorCode);
    293         mCallback.onError(ERROR_DOWNLOAD_FAILED);
    294     }
    295 
    296     private byte[] computeHashOfFile(String fileLocation, String hashType) {
    297         InputStream fis = null;
    298         MessageDigest md;
    299         byte hash[] = null;
    300         try {
    301             md = MessageDigest.getInstance(hashType);
    302         } catch (NoSuchAlgorithmException e) {
    303             ProvisionLogger.loge("Hashing algorithm " + hashType + " not supported.", e);
    304             mCallback.onError(ERROR_OTHER);
    305             return null;
    306         }
    307         try {
    308             fis = new FileInputStream(fileLocation);
    309 
    310             byte[] buffer = new byte[256];
    311             int n = 0;
    312             while (n != -1) {
    313                 n = fis.read(buffer);
    314                 if (n > 0) {
    315                     md.update(buffer, 0, n);
    316                 }
    317             }
    318             hash = md.digest();
    319         } catch (IOException e) {
    320             ProvisionLogger.loge("IO error.", e);
    321             mCallback.onError(ERROR_OTHER);
    322         } finally {
    323             // Close input stream quietly.
    324             try {
    325                 if (fis != null) {
    326                     fis.close();
    327                 }
    328             } catch (IOException e) {
    329                 // Ignore.
    330             }
    331         }
    332         return hash;
    333     }
    334 
    335     public String getDownloadedPackageLocation(String label) {
    336         for (DownloadStatusInfo info : mDownloads) {
    337             if (info.mLabel.equals(label)) {
    338                 return info.mLocation;
    339             }
    340         }
    341         return "";
    342     }
    343 
    344     private List<byte[]> computeHashesOfAllSignatures(String packageArchiveLocation) {
    345         PackageInfo info = mPm.getPackageArchiveInfo(packageArchiveLocation,
    346                 PackageManager.GET_SIGNATURES);
    347         if (info == null) {
    348             ProvisionLogger.loge("Unable to get package archive info from "
    349                     + packageArchiveLocation);
    350             mCallback.onError(ERROR_OTHER);
    351             return null;
    352         }
    353 
    354         List<byte[]> hashes = new LinkedList<byte[]>();
    355         Signature signatures[] = info.signatures;
    356         try {
    357             for (Signature signature : signatures) {
    358                byte[] hash = computeHashOfByteArray(signature.toByteArray());
    359                hashes.add(hash);
    360             }
    361         } catch (NoSuchAlgorithmException e) {
    362             ProvisionLogger.loge("Hashing algorithm " + SHA256_TYPE + " not supported.", e);
    363             mCallback.onError(ERROR_OTHER);
    364             return null;
    365         }
    366         return hashes;
    367     }
    368 
    369     private byte[] computeHashOfByteArray(byte[] bytes) throws NoSuchAlgorithmException {
    370         MessageDigest md = MessageDigest.getInstance(SHA256_TYPE);
    371         md.update(bytes, 0, bytes.length);
    372         return md.digest();
    373     }
    374 
    375     public void cleanUp() {
    376         if (mReceiver != null) {
    377             //Unregister receiver.
    378             mContext.unregisterReceiver(mReceiver);
    379             mReceiver = null;
    380         }
    381 
    382         //Remove download.
    383         DownloadManager dm = (DownloadManager) mContext
    384                 .getSystemService(Context.DOWNLOAD_SERVICE);
    385         for (DownloadStatusInfo info : mDownloads) {
    386             boolean removeSuccess = dm.remove(info.mDownloadId) == 1;
    387             if (removeSuccess) {
    388                 ProvisionLogger.logd("Successfully removed installer file.");
    389             } else {
    390                 ProvisionLogger.loge("Could not remove installer file.");
    391                 // Ignore this error. Failing cleanup should not stop provisioning flow.
    392             }
    393         }
    394     }
    395 
    396     public abstract static class Callback {
    397         public abstract void onSuccess();
    398         public abstract void onError(int errorCode);
    399     }
    400 
    401     private static class DownloadStatusInfo {
    402         public final PackageDownloadInfo mPackageDownloadInfo;
    403         public final String mLabel;
    404         public long mDownloadId;
    405         public String mLocation; // Location where the package is downloaded to.
    406         public boolean mDoneDownloading;
    407         public boolean mSuccess;
    408 
    409         public DownloadStatusInfo(PackageDownloadInfo packageDownloadInfo,String label) {
    410             mPackageDownloadInfo = packageDownloadInfo;
    411             mLabel = label;
    412             mDoneDownloading = false;
    413         }
    414     }
    415 }
    416