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