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 
DownloadPackageTask(Context context, Callback callback)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 
addDownloadIfNecessary( String packageName, PackageDownloadInfo downloadInfo, String label)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 
run()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 
createDownloadReceiver()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