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