1 // Copyright 2015 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.offlinepages; 6 7 import android.annotation.SuppressLint; 8 import android.content.Context; 9 import android.content.pm.PackageManager; 10 import android.content.pm.PackageManager.NameNotFoundException; 11 import android.content.pm.Signature; 12 import android.os.Process; 13 import android.text.TextUtils; 14 import android.util.Base64; 15 16 import androidx.annotation.VisibleForTesting; 17 18 import org.json.JSONArray; 19 import org.json.JSONException; 20 21 import org.chromium.chrome.browser.tab.Tab; 22 import org.chromium.chrome.browser.tab.TabAssociatedApp; 23 24 import java.security.MessageDigest; 25 import java.security.NoSuchAlgorithmException; 26 import java.util.Arrays; 27 28 /** 29 * Class encapsulating the application origin of a particular offline page request. 30 */ 31 public class OfflinePageOrigin { 32 private final String mAppName; 33 private final String[] mSignatures; 34 35 /** Creates origin based on the context and tab. */ OfflinePageOrigin(Context context, Tab tab)36 public OfflinePageOrigin(Context context, Tab tab) { 37 this(context, TabAssociatedApp.getAppId(tab)); 38 } 39 40 /** Creates origin based on the context and an app name. */ OfflinePageOrigin(Context context, String appName)41 public OfflinePageOrigin(Context context, String appName) { 42 if (TextUtils.isEmpty(appName)) { 43 mAppName = ""; 44 mSignatures = null; 45 } else { 46 mSignatures = getAppSignaturesFor(context, appName); 47 // If signatures returned null, the app probably doesn't exist. Assume Chrome. 48 if (mSignatures == null) { 49 mAppName = ""; 50 } else { 51 mAppName = appName; 52 } 53 } 54 } 55 56 /** Creates origin based on a qualified string. Assumes Chrome if invalid. */ OfflinePageOrigin(String jsonString)57 public OfflinePageOrigin(String jsonString) { 58 String name = ""; 59 String[] signatures = null; 60 try { 61 JSONArray info = new JSONArray(jsonString); 62 if (info.length() == 2) { 63 name = info.getString(0); 64 JSONArray signatureInfo = info.getJSONArray(1); 65 signatures = new String[signatureInfo.length()]; 66 for (int i = 0; i < signatures.length; i++) { 67 signatures[i] = signatureInfo.getString(i); 68 } 69 } 70 } catch (JSONException e) { 71 // JSON malformed. Set name and signature to default. 72 name = ""; 73 signatures = null; 74 } finally { 75 mAppName = name; 76 mSignatures = signatures; 77 } 78 } 79 80 /** Creates origin based on uid and context. */ OfflinePageOrigin(Context context, int uid)81 public OfflinePageOrigin(Context context, int uid) { 82 if (uid == Process.myUid()) { 83 mAppName = ""; 84 mSignatures = null; 85 return; 86 } 87 PackageManager pm = context.getPackageManager(); 88 String[] packages = pm.getPackagesForUid(uid); 89 if (packages.length != 1) { 90 mAppName = ""; 91 mSignatures = null; 92 } else { 93 mAppName = packages[0]; 94 mSignatures = getAppSignaturesFor(context, mAppName); 95 } 96 } 97 98 /** Creates a Chrome origin. */ OfflinePageOrigin()99 public OfflinePageOrigin() { 100 this("", null); 101 } 102 103 @VisibleForTesting OfflinePageOrigin(String appName, String[] signatures)104 OfflinePageOrigin(String appName, String[] signatures) { 105 mAppName = appName; 106 mSignatures = signatures; 107 } 108 109 /** 110 * Encode the origin information into a JSON string of 111 * [appName, [SHA-256 encoded signature, SHA-256 encoded signature...]] 112 * 113 * @return The JSON encoded origin information or empty string if there is 114 * no app information (ie assuming chrome). 115 */ encodeAsJsonString()116 public String encodeAsJsonString() { 117 // We default to "", implying chrome-only if inputs invalid. 118 if (isChrome()) return ""; 119 // JSONArray(Object[]) requires API 19 120 JSONArray signatureArray = new JSONArray(); 121 for (String s : mSignatures) signatureArray.put(s); 122 return new JSONArray().put(mAppName).put(signatureArray).toString(); 123 } 124 125 /** 126 * Returns whether the signature recorded in this origin matches the signature 127 * in the context. 128 * 129 * Returns true if this origin is Chrome. 130 */ doesSignatureMatch(Context context)131 public boolean doesSignatureMatch(Context context) { 132 String[] currentSignatures = getAppSignaturesFor(context, mAppName); 133 return Arrays.equals(mSignatures, currentSignatures); 134 } 135 136 /** Returns whether this origin is chrome. */ isChrome()137 public boolean isChrome() { 138 return TextUtils.isEmpty(mAppName) || mSignatures == null; 139 } 140 141 /** Returns the application package name of this origin. */ getAppName()142 public String getAppName() { 143 return mAppName; 144 } 145 146 @Override toString()147 public String toString() { 148 return encodeAsJsonString(); 149 } 150 151 @Override equals(Object other)152 public boolean equals(Object other) { 153 if (other != null && other instanceof OfflinePageOrigin) { 154 OfflinePageOrigin o = (OfflinePageOrigin) other; 155 return mAppName.equals(o.mAppName) && Arrays.equals(mSignatures, o.mSignatures); 156 } 157 return false; 158 } 159 160 @Override hashCode()161 public int hashCode() { 162 return Arrays.deepHashCode(new Object[] {mAppName, mSignatures}); 163 } 164 165 /** 166 * @param context The context to look up signatures. 167 * @param appName The name of the application to look up. 168 * @return a sorted list of strings representing the signatures of an app. 169 * Null if the app name is invalid or cannot be found. 170 */ 171 @SuppressLint("PackageManagerGetSignatures") 172 // https://stackoverflow.com/questions/39192844/android-studio-warning-when-using-packagemanager-get-signatures getAppSignaturesFor(Context context, String appName)173 private static String[] getAppSignaturesFor(Context context, String appName) { 174 if (TextUtils.isEmpty(appName)) return null; 175 try { 176 PackageManager packageManager = context.getPackageManager(); 177 Signature[] signatureList = 178 packageManager.getPackageInfo(appName, PackageManager.GET_SIGNATURES) 179 .signatures; 180 MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); 181 String[] sigStrings = new String[signatureList.length]; 182 for (int i = 0; i < sigStrings.length; i++) { 183 messageDigest.update(signatureList[i].toByteArray()); 184 185 // The digest is reset after completing the hash computation. 186 sigStrings[i] = byteArrayToString(messageDigest.digest()); 187 } 188 Arrays.sort(sigStrings); 189 return sigStrings; 190 } catch (NameNotFoundException e) { 191 return null; // Cannot find the app anymore. No signatures. 192 } catch (NoSuchAlgorithmException e) { 193 return null; // Cannot find the SHA-256 encryption algorithm. Shouldn't happen. 194 } 195 } 196 197 /** 198 * Formats bytes into a string for easier comparison. 199 * 200 * @param input Input bytes. 201 * @return A string representation of the input bytes, e.g., "0123456789abcdefg" 202 */ byteArrayToString(byte[] input)203 private static String byteArrayToString(byte[] input) { 204 if (input == null) return null; 205 206 return Base64.encodeToString(input, Base64.DEFAULT); 207 } 208 } 209