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