1 /* 2 * Copyright 2022 Google LLC 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.google.android.libraries.mobiledatadownload.internal.downloader; 17 18 import android.net.Uri; 19 import com.google.android.libraries.mobiledatadownload.DownloadException; 20 import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode; 21 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 22 import com.google.android.libraries.mobiledatadownload.file.openers.ReadStreamOpener; 23 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; 24 import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupUtil; 25 import com.google.mobiledatadownload.internal.MetadataProto.DataFile; 26 import java.io.IOException; 27 import java.io.InputStream; 28 import java.security.MessageDigest; 29 import java.security.NoSuchAlgorithmException; 30 import javax.annotation.Nullable; 31 32 /** Util class that validate the downloaded file. */ 33 public final class FileValidator { 34 private static final String TAG = "FileValidator"; 35 36 private static final char[] HEX_LOWERCASE = { 37 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' 38 }; 39 40 // <internal> FileValidator()41 private FileValidator() {} 42 43 /** 44 * Returns if the file checksum verification passes. 45 * 46 * @param fileUri - the uri of the file to calculate a sha1 hash for. 47 * @param fileChecksum - the expected file checksum 48 */ verifyChecksum( SynchronousFileStorage fileStorage, Uri fileUri, String fileChecksum)49 public static boolean verifyChecksum( 50 SynchronousFileStorage fileStorage, Uri fileUri, String fileChecksum) { 51 String digest = FileValidator.computeSha1Digest(fileStorage, fileUri); 52 return digest.equals(fileChecksum); 53 } 54 55 /** 56 * Returns sha1 hash of file, empty string if unable to read file. 57 * 58 * @param uri - the uri of the file to calculate a sha1 hash for. 59 */ 60 // TODO(b/139472295): convert this to a MobStore Opener. computeSha1Digest(SynchronousFileStorage fileStorage, Uri uri)61 public static String computeSha1Digest(SynchronousFileStorage fileStorage, Uri uri) { 62 try (InputStream inputStream = fileStorage.open(uri, ReadStreamOpener.create())) { 63 return computeDigest(inputStream, "SHA1"); 64 } catch (IOException e) { 65 // TODO(b/118137672): reconsider on the swallowed exception. 66 LogUtil.e("%s: Failed to open file, uri = %s", TAG, uri); 67 return ""; 68 } 69 } 70 71 /** Compute the SHA1 of the input string. */ computeSha1Digest(String input)72 public static String computeSha1Digest(String input) { 73 MessageDigest messageDigest = getMessageDigest("SHA1"); 74 if (messageDigest == null) { 75 return ""; 76 } 77 78 byte[] bytes = input.getBytes(); 79 messageDigest.update(bytes, 0, bytes.length); 80 return bytesToStringLowercase(messageDigest.digest()); 81 } 82 83 /** 84 * Returns sha256 hash of file, empty string if unable to read file. 85 * 86 * @param uri - the uri of the file to calculate a sha256 hash for. 87 */ computeSha256Digest(SynchronousFileStorage fileStorage, Uri uri)88 public static String computeSha256Digest(SynchronousFileStorage fileStorage, Uri uri) { 89 try (InputStream inputStream = fileStorage.open(uri, ReadStreamOpener.create())) { 90 return computeDigest(inputStream, "SHA-256"); 91 } catch (IOException e) { 92 // TODO(b/118137672): reconsider on the swallowed exception. 93 LogUtil.e("%s: Failed to open file, uri = %s", TAG, uri); 94 return ""; 95 } 96 } 97 98 // Caller is responsible for opening and closing stream. computeDigest(InputStream inputStream, String algorithm)99 private static String computeDigest(InputStream inputStream, String algorithm) 100 throws IOException { 101 MessageDigest messageDigest = getMessageDigest(algorithm); 102 if (messageDigest == null) { 103 return ""; 104 } 105 106 byte[] bytes = new byte[8192]; 107 108 int byteCount = inputStream.read(bytes); 109 while (byteCount != -1) { 110 messageDigest.update(bytes, 0, byteCount); 111 byteCount = inputStream.read(bytes); 112 } 113 return bytesToStringLowercase(messageDigest.digest()); 114 } 115 116 /** 117 * Throws {@link DownloadException} if the downloaded file doesn't exist, or the SHA1 hash of the 118 * file checksum doesn't match. 119 */ validateDownloadedFile( SynchronousFileStorage fileStorage, DataFile dataFile, Uri fileUri, String checksum)120 public static void validateDownloadedFile( 121 SynchronousFileStorage fileStorage, DataFile dataFile, Uri fileUri, String checksum) 122 throws DownloadException { 123 try { 124 if (!fileStorage.exists(fileUri)) { 125 LogUtil.e( 126 "%s: Downloaded file %s is not present at %s", 127 TAG, FileGroupUtil.getFileChecksum(dataFile), fileUri); 128 throw DownloadException.builder() 129 .setDownloadResultCode(DownloadResultCode.DOWNLOADED_FILE_NOT_FOUND_ERROR) 130 .build(); 131 } 132 if (dataFile.getChecksumType() == DataFile.ChecksumType.NONE) { 133 return; 134 } 135 if (!verifyChecksum(fileStorage, fileUri, checksum)) { 136 LogUtil.e( 137 "%s: Downloaded file at uri = %s, checksum = %s verification failed", 138 TAG, fileUri, checksum); 139 throw DownloadException.builder() 140 .setDownloadResultCode(DownloadResultCode.DOWNLOADED_FILE_CHECKSUM_MISMATCH_ERROR) 141 .build(); 142 } 143 } catch (IOException e) { 144 LogUtil.e( 145 e, 146 "%s: Failed to validate download file %s", 147 TAG, 148 FileGroupUtil.getFileChecksum(dataFile)); 149 throw DownloadException.builder() 150 .setDownloadResultCode(DownloadResultCode.UNABLE_TO_VALIDATE_DOWNLOAD_FILE_ERROR) 151 .setCause(e) 152 .build(); 153 } 154 } 155 156 @Nullable getMessageDigest(String hashAlgorithm)157 private static MessageDigest getMessageDigest(String hashAlgorithm) { 158 try { 159 MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm); 160 if (messageDigest != null) { 161 return messageDigest; 162 } 163 } catch (NoSuchAlgorithmException e) { 164 // Do nothing. 165 } 166 return null; 167 } 168 bytesToStringLowercase(byte[] bytes)169 private static String bytesToStringLowercase(byte[] bytes) { 170 char[] hexChars = new char[bytes.length * 2]; 171 int j = 0; 172 for (int i = 0; i < bytes.length; i++) { 173 int v = bytes[i] & 0xFF; 174 hexChars[j++] = HEX_LOWERCASE[v >>> 4]; 175 hexChars[j++] = HEX_LOWERCASE[v & 0x0F]; 176 } 177 return new String(hexChars); 178 } 179 } 180