1 /*
2  * Copyright (C) 2017 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 
17 package android.content.pm;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.SystemApi;
22 import android.content.Intent;
23 import android.os.Bundle;
24 import android.os.Parcel;
25 import android.os.Parcelable;
26 
27 import java.security.MessageDigest;
28 import java.security.NoSuchAlgorithmException;
29 import java.security.SecureRandom;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.Collections;
33 import java.util.List;
34 import java.util.Locale;
35 import java.util.Random;
36 
37 /**
38  * Describes an externally resolvable instant application. There are three states that this class
39  * can represent: <p/>
40  * <ul>
41  *     <li>
42  *         The first, usable only for non http/s intents, implies that the resolver cannot
43  *         immediately resolve this intent and would prefer that resolution be deferred to the
44  *         instant app installer. Represent this state with {@link #InstantAppResolveInfo(Bundle)}.
45  *         If the {@link android.content.Intent} has the scheme set to http/s and a set of digest
46  *         prefixes were passed into one of the resolve methods in
47  *         {@link android.app.InstantAppResolverService}, this state cannot be used.
48  *     </li>
49  *     <li>
50  *         The second represents a partial match and is constructed with any of the other
51  *         constructors. By setting one or more of the {@link Nullable}arguments to null, you
52  *         communicate to the resolver in response to
53  *         {@link android.app.InstantAppResolverService#onGetInstantAppResolveInfo(Intent, int[],
54  *                String, InstantAppResolverService.InstantAppResolutionCallback)}
55  *         that you need a 2nd round of resolution to complete the request.
56  *     </li>
57  *     <li>
58  *         The third represents a complete match and is constructed with all @Nullable parameters
59  *         populated.
60  *     </li>
61  * </ul>
62  * @hide
63  */
64 @SystemApi
65 public final class InstantAppResolveInfo implements Parcelable {
66     /** Algorithm that will be used to generate the domain digest */
67     private static final String SHA_ALGORITHM = "SHA-256";
68 
69     private static final byte[] EMPTY_DIGEST = new byte[0];
70 
71     private final InstantAppDigest mDigest;
72     private final String mPackageName;
73     /** The filters used to match domain */
74     private final List<InstantAppIntentFilter> mFilters;
75     /** The version code of the app that this class resolves to */
76     private final long mVersionCode;
77     /** Data about the app that should be passed along to the Instant App installer on resolve */
78     private final Bundle mExtras;
79     /**
80      * A flag that indicates that the resolver is aware that an app may match, but would prefer
81      * that the installer get the sanitized intent to decide.
82      */
83     private final boolean mShouldLetInstallerDecide;
84 
85     /** Constructor for intent-based InstantApp resolution results. */
InstantAppResolveInfo(@onNull InstantAppDigest digest, @Nullable String packageName, @Nullable List<InstantAppIntentFilter> filters, int versionCode)86     public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName,
87             @Nullable List<InstantAppIntentFilter> filters, int versionCode) {
88         this(digest, packageName, filters, (long) versionCode, null /* extras */);
89     }
90 
91     /** Constructor for intent-based InstantApp resolution results with extras. */
InstantAppResolveInfo(@onNull InstantAppDigest digest, @Nullable String packageName, @Nullable List<InstantAppIntentFilter> filters, long versionCode, @Nullable Bundle extras)92     public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName,
93             @Nullable List<InstantAppIntentFilter> filters, long versionCode,
94             @Nullable Bundle extras) {
95         this(digest, packageName, filters, versionCode, extras, false);
96     }
97 
98     /** Constructor for intent-based InstantApp resolution results by hostname. */
InstantAppResolveInfo(@onNull String hostName, @Nullable String packageName, @Nullable List<InstantAppIntentFilter> filters)99     public InstantAppResolveInfo(@NonNull String hostName, @Nullable String packageName,
100             @Nullable List<InstantAppIntentFilter> filters) {
101         this(new InstantAppDigest(hostName), packageName, filters, -1 /*versionCode*/,
102                 null /* extras */);
103     }
104 
105     /**
106      * Constructor that indicates that resolution could be delegated to the installer when the
107      * sanitized intent contains enough information to resolve completely.
108      */
InstantAppResolveInfo(@ullable Bundle extras)109     public InstantAppResolveInfo(@Nullable Bundle extras) {
110         this(InstantAppDigest.UNDEFINED, null, null, -1, extras, true);
111     }
112 
InstantAppResolveInfo(@onNull InstantAppDigest digest, @Nullable String packageName, @Nullable List<InstantAppIntentFilter> filters, long versionCode, @Nullable Bundle extras, boolean shouldLetInstallerDecide)113     private InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName,
114             @Nullable List<InstantAppIntentFilter> filters, long versionCode,
115             @Nullable Bundle extras, boolean shouldLetInstallerDecide) {
116         // validate arguments
117         if ((packageName == null && (filters != null && filters.size() != 0))
118                 || (packageName != null && (filters == null || filters.size() == 0))) {
119             throw new IllegalArgumentException();
120         }
121         mDigest = digest;
122         if (filters != null) {
123             mFilters = new ArrayList<>(filters.size());
124             mFilters.addAll(filters);
125         } else {
126             mFilters = null;
127         }
128         mPackageName = packageName;
129         mVersionCode = versionCode;
130         mExtras = extras;
131         mShouldLetInstallerDecide = shouldLetInstallerDecide;
132     }
133 
InstantAppResolveInfo(Parcel in)134     InstantAppResolveInfo(Parcel in) {
135         mShouldLetInstallerDecide = in.readBoolean();
136         mExtras = in.readBundle();
137         if (mShouldLetInstallerDecide) {
138             mDigest = InstantAppDigest.UNDEFINED;
139             mPackageName = null;
140             mFilters = Collections.emptyList();
141             mVersionCode = -1;
142         } else {
143             mDigest = in.readParcelable(null /*loader*/);
144             mPackageName = in.readString();
145             mFilters = new ArrayList<>();
146             in.readList(mFilters, null /*loader*/);
147             mVersionCode = in.readLong();
148         }
149     }
150 
151     /**
152      * Returns true if the resolver is aware that an app may match, but would prefer
153      * that the installer get the sanitized intent to decide. This should not be true for
154      * resolutions that include a host and will be ignored in such cases.
155      */
shouldLetInstallerDecide()156     public boolean shouldLetInstallerDecide() {
157         return mShouldLetInstallerDecide;
158     }
159 
getDigestBytes()160     public byte[] getDigestBytes() {
161         return mDigest.mDigestBytes.length > 0 ? mDigest.getDigestBytes()[0] : EMPTY_DIGEST;
162     }
163 
getDigestPrefix()164     public int getDigestPrefix() {
165         return mDigest.getDigestPrefix()[0];
166     }
167 
getPackageName()168     public String getPackageName() {
169         return mPackageName;
170     }
171 
getIntentFilters()172     public List<InstantAppIntentFilter> getIntentFilters() {
173         return mFilters;
174     }
175 
176     /**
177      * @deprecated Use {@link #getLongVersionCode} instead.
178      */
179     @Deprecated
getVersionCode()180     public int getVersionCode() {
181         return (int) (mVersionCode & 0xffffffff);
182     }
183 
getLongVersionCode()184     public long getLongVersionCode() {
185         return mVersionCode;
186     }
187 
188     @Nullable
getExtras()189     public Bundle getExtras() {
190         return mExtras;
191     }
192 
193     @Override
describeContents()194     public int describeContents() {
195         return 0;
196     }
197 
198     @Override
writeToParcel(Parcel out, int flags)199     public void writeToParcel(Parcel out, int flags) {
200         out.writeBoolean(mShouldLetInstallerDecide);
201         out.writeBundle(mExtras);
202         if (mShouldLetInstallerDecide) {
203             return;
204         }
205         out.writeParcelable(mDigest, flags);
206         out.writeString(mPackageName);
207         out.writeList(mFilters);
208         out.writeLong(mVersionCode);
209     }
210 
211     public static final @android.annotation.NonNull Parcelable.Creator<InstantAppResolveInfo> CREATOR
212             = new Parcelable.Creator<InstantAppResolveInfo>() {
213         public InstantAppResolveInfo createFromParcel(Parcel in) {
214             return new InstantAppResolveInfo(in);
215         }
216 
217         public InstantAppResolveInfo[] newArray(int size) {
218             return new InstantAppResolveInfo[size];
219         }
220     };
221 
222     /**
223      * Helper class to generate and store each of the digests and prefixes
224      * sent to the Instant App Resolver.
225      * <p>
226      * Since intent filters may want to handle multiple hosts within a
227      * domain [eg “*.google.com”], the resolver is presented with multiple
228      * hash prefixes. For example, "a.b.c.d.e" generates digests for
229      * "d.e", "c.d.e", "b.c.d.e" and "a.b.c.d.e".
230      *
231      * @hide
232      */
233     @SystemApi
234     public static final class InstantAppDigest implements Parcelable {
235         static final int DIGEST_MASK = 0xfffff000;
236 
237         /**
238          * A special instance that represents and undefined digest used for cases that a host was
239          * not provided or is irrelevant to the response.
240          */
241         public static final InstantAppDigest UNDEFINED =
242                 new InstantAppDigest(new byte[][]{}, new int[]{});
243 
244         private static Random sRandom = null;
245         static {
246             try {
247                 sRandom = SecureRandom.getInstance("SHA1PRNG");
248             } catch (NoSuchAlgorithmException e) {
249                 // oh well
250                 sRandom = new Random();
251             }
252         }
253         /** Full digest of the domain hashes */
254         private final byte[][] mDigestBytes;
255         /** The first 5 bytes of the domain hashes */
256         private final int[] mDigestPrefix;
257         /** The first 5 bytes of the domain hashes interspersed with random data */
258         private int[] mDigestPrefixSecure;
259 
InstantAppDigest(@onNull String hostName)260         public InstantAppDigest(@NonNull String hostName) {
261             this(hostName, -1 /*maxDigests*/);
262         }
263 
264         /** @hide */
InstantAppDigest(@onNull String hostName, int maxDigests)265         public InstantAppDigest(@NonNull String hostName, int maxDigests) {
266             if (hostName == null) {
267                 throw new IllegalArgumentException();
268             }
269             mDigestBytes = generateDigest(hostName.toLowerCase(Locale.ENGLISH), maxDigests);
270             mDigestPrefix = new int[mDigestBytes.length];
271             for (int i = 0; i < mDigestBytes.length; i++) {
272                 mDigestPrefix[i] =
273                         ((mDigestBytes[i][0] & 0xFF) << 24
274                                 | (mDigestBytes[i][1] & 0xFF) << 16
275                                 | (mDigestBytes[i][2] & 0xFF) << 8
276                                 | (mDigestBytes[i][3] & 0xFF) << 0)
277                         & DIGEST_MASK;
278             }
279         }
280 
InstantAppDigest(byte[][] digestBytes, int[] prefix)281         private InstantAppDigest(byte[][] digestBytes, int[] prefix) {
282             this.mDigestPrefix = prefix;
283             this.mDigestBytes = digestBytes;
284         }
285 
generateDigest(String hostName, int maxDigests)286         private static byte[][] generateDigest(String hostName, int maxDigests) {
287             ArrayList<byte[]> digests = new ArrayList<>();
288             try {
289                 final MessageDigest digest = MessageDigest.getInstance(SHA_ALGORITHM);
290                 if (maxDigests <= 0) {
291                     final byte[] hostBytes = hostName.getBytes();
292                     digests.add(digest.digest(hostBytes));
293                 } else {
294                     int prevDot = hostName.lastIndexOf('.');
295                     prevDot = hostName.lastIndexOf('.', prevDot - 1);
296                     // shortcut for short URLs
297                     if (prevDot < 0) {
298                         digests.add(digest.digest(hostName.getBytes()));
299                     } else {
300                         byte[] hostBytes =
301                                 hostName.substring(prevDot + 1, hostName.length()).getBytes();
302                         digests.add(digest.digest(hostBytes));
303                         int digestCount = 1;
304                         while (prevDot >= 0 && digestCount < maxDigests) {
305                             prevDot = hostName.lastIndexOf('.', prevDot - 1);
306                             hostBytes =
307                                     hostName.substring(prevDot + 1, hostName.length()).getBytes();
308                             digests.add(digest.digest(hostBytes));
309                             digestCount++;
310                         }
311                     }
312                 }
313             } catch (NoSuchAlgorithmException e) {
314                 throw new IllegalStateException("could not find digest algorithm");
315             }
316             return digests.toArray(new byte[digests.size()][]);
317         }
318 
InstantAppDigest(Parcel in)319         InstantAppDigest(Parcel in) {
320             final int digestCount = in.readInt();
321             if (digestCount == -1) {
322                 mDigestBytes = null;
323             } else {
324                 mDigestBytes = new byte[digestCount][];
325                 for (int i = 0; i < digestCount; i++) {
326                     mDigestBytes[i] = in.createByteArray();
327                 }
328             }
329             mDigestPrefix = in.createIntArray();
330             mDigestPrefixSecure = in.createIntArray();
331         }
332 
getDigestBytes()333         public byte[][] getDigestBytes() {
334             return mDigestBytes;
335         }
336 
getDigestPrefix()337         public int[] getDigestPrefix() {
338             return mDigestPrefix;
339         }
340 
341         /**
342          * Returns a digest prefix with additional random prefixes interspersed.
343          * @hide
344          */
getDigestPrefixSecure()345         public int[] getDigestPrefixSecure() {
346             if (this == InstantAppResolveInfo.InstantAppDigest.UNDEFINED) {
347                 return getDigestPrefix();
348             } else if (mDigestPrefixSecure == null) {
349                 // let's generate some random data to intersperse throughout the set of prefixes
350                 final int realSize = getDigestPrefix().length;
351                 final int manufacturedSize = realSize + 10 + sRandom.nextInt(10);
352                 mDigestPrefixSecure = Arrays.copyOf(getDigestPrefix(), manufacturedSize);
353                 for (int i = realSize; i < manufacturedSize; i++) {
354                     mDigestPrefixSecure[i] = sRandom.nextInt() & DIGEST_MASK;
355                 }
356                 Arrays.sort(mDigestPrefixSecure);
357             }
358             return mDigestPrefixSecure;
359         }
360 
361         @Override
describeContents()362         public int describeContents() {
363             return 0;
364         }
365 
366         @Override
writeToParcel(Parcel out, int flags)367         public void writeToParcel(Parcel out, int flags) {
368             final boolean isUndefined = this == UNDEFINED;
369             out.writeBoolean(isUndefined);
370             if (isUndefined) {
371                 return;
372             }
373             if (mDigestBytes == null) {
374                 out.writeInt(-1);
375             } else {
376                 out.writeInt(mDigestBytes.length);
377                 for (int i = 0; i < mDigestBytes.length; i++) {
378                     out.writeByteArray(mDigestBytes[i]);
379                 }
380             }
381             out.writeIntArray(mDigestPrefix);
382             out.writeIntArray(mDigestPrefixSecure);
383         }
384 
385         @SuppressWarnings("hiding")
386         public static final @android.annotation.NonNull Parcelable.Creator<InstantAppDigest> CREATOR =
387                 new Parcelable.Creator<InstantAppDigest>() {
388             @Override
389             public InstantAppDigest createFromParcel(Parcel in) {
390                 if (in.readBoolean() /* is undefined */) {
391                     return UNDEFINED;
392                 }
393                 return new InstantAppDigest(in);
394             }
395             @Override
396             public InstantAppDigest[] newArray(int size) {
397                 return new InstantAppDigest[size];
398             }
399         };
400     }
401 }
402