• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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