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