/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.content.pm; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.content.Intent; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Random; /** * Describes an externally resolvable instant application. There are three states that this class * can represent:

*

* @hide */ @SystemApi public final class InstantAppResolveInfo implements Parcelable { /** Algorithm that will be used to generate the domain digest */ private static final String SHA_ALGORITHM = "SHA-256"; private static final byte[] EMPTY_DIGEST = new byte[0]; private final InstantAppDigest mDigest; private final String mPackageName; /** The filters used to match domain */ private final List mFilters; /** The version code of the app that this class resolves to */ private final long mVersionCode; /** Data about the app that should be passed along to the Instant App installer on resolve */ private final Bundle mExtras; /** * A flag that indicates that the resolver is aware that an app may match, but would prefer * that the installer get the sanitized intent to decide. */ private final boolean mShouldLetInstallerDecide; /** Constructor for intent-based InstantApp resolution results. */ public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName, @Nullable List filters, int versionCode) { this(digest, packageName, filters, (long) versionCode, null /* extras */); } /** Constructor for intent-based InstantApp resolution results with extras. */ public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName, @Nullable List filters, long versionCode, @Nullable Bundle extras) { this(digest, packageName, filters, versionCode, extras, false); } /** Constructor for intent-based InstantApp resolution results by hostname. */ public InstantAppResolveInfo(@NonNull String hostName, @Nullable String packageName, @Nullable List filters) { this(new InstantAppDigest(hostName), packageName, filters, -1 /*versionCode*/, null /* extras */); } /** * Constructor that indicates that resolution could be delegated to the installer when the * sanitized intent contains enough information to resolve completely. */ public InstantAppResolveInfo(@Nullable Bundle extras) { this(InstantAppDigest.UNDEFINED, null, null, -1, extras, true); } private InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName, @Nullable List filters, long versionCode, @Nullable Bundle extras, boolean shouldLetInstallerDecide) { // validate arguments if ((packageName == null && (filters != null && filters.size() != 0)) || (packageName != null && (filters == null || filters.size() == 0))) { throw new IllegalArgumentException(); } mDigest = digest; if (filters != null) { mFilters = new ArrayList<>(filters.size()); mFilters.addAll(filters); } else { mFilters = null; } mPackageName = packageName; mVersionCode = versionCode; mExtras = extras; mShouldLetInstallerDecide = shouldLetInstallerDecide; } InstantAppResolveInfo(Parcel in) { mShouldLetInstallerDecide = in.readBoolean(); mExtras = in.readBundle(); if (mShouldLetInstallerDecide) { mDigest = InstantAppDigest.UNDEFINED; mPackageName = null; mFilters = Collections.emptyList(); mVersionCode = -1; } else { mDigest = in.readParcelable(null /*loader*/); mPackageName = in.readString(); mFilters = new ArrayList<>(); in.readList(mFilters, null /*loader*/); mVersionCode = in.readLong(); } } /** * Returns true if the resolver is aware that an app may match, but would prefer * that the installer get the sanitized intent to decide. This should not be true for * resolutions that include a host and will be ignored in such cases. */ public boolean shouldLetInstallerDecide() { return mShouldLetInstallerDecide; } public byte[] getDigestBytes() { return mDigest.mDigestBytes.length > 0 ? mDigest.getDigestBytes()[0] : EMPTY_DIGEST; } public int getDigestPrefix() { return mDigest.getDigestPrefix()[0]; } public String getPackageName() { return mPackageName; } public List getIntentFilters() { return mFilters; } /** * @deprecated Use {@link #getLongVersionCode} instead. */ @Deprecated public int getVersionCode() { return (int) (mVersionCode & 0xffffffff); } public long getLongVersionCode() { return mVersionCode; } @Nullable public Bundle getExtras() { return mExtras; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel out, int flags) { out.writeBoolean(mShouldLetInstallerDecide); out.writeBundle(mExtras); if (mShouldLetInstallerDecide) { return; } out.writeParcelable(mDigest, flags); out.writeString(mPackageName); out.writeList(mFilters); out.writeLong(mVersionCode); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public InstantAppResolveInfo createFromParcel(Parcel in) { return new InstantAppResolveInfo(in); } public InstantAppResolveInfo[] newArray(int size) { return new InstantAppResolveInfo[size]; } }; /** * Helper class to generate and store each of the digests and prefixes * sent to the Instant App Resolver. *

* Since intent filters may want to handle multiple hosts within a * domain [eg “*.google.com”], the resolver is presented with multiple * hash prefixes. For example, "a.b.c.d.e" generates digests for * "d.e", "c.d.e", "b.c.d.e" and "a.b.c.d.e". * * @hide */ @SystemApi public static final class InstantAppDigest implements Parcelable { static final int DIGEST_MASK = 0xfffff000; /** * A special instance that represents and undefined digest used for cases that a host was * not provided or is irrelevant to the response. */ public static final InstantAppDigest UNDEFINED = new InstantAppDigest(new byte[][]{}, new int[]{}); private static Random sRandom = null; static { try { sRandom = SecureRandom.getInstance("SHA1PRNG"); } catch (NoSuchAlgorithmException e) { // oh well sRandom = new Random(); } } /** Full digest of the domain hashes */ private final byte[][] mDigestBytes; /** The first 5 bytes of the domain hashes */ private final int[] mDigestPrefix; /** The first 5 bytes of the domain hashes interspersed with random data */ private int[] mDigestPrefixSecure; public InstantAppDigest(@NonNull String hostName) { this(hostName, -1 /*maxDigests*/); } /** @hide */ public InstantAppDigest(@NonNull String hostName, int maxDigests) { if (hostName == null) { throw new IllegalArgumentException(); } mDigestBytes = generateDigest(hostName.toLowerCase(Locale.ENGLISH), maxDigests); mDigestPrefix = new int[mDigestBytes.length]; for (int i = 0; i < mDigestBytes.length; i++) { mDigestPrefix[i] = ((mDigestBytes[i][0] & 0xFF) << 24 | (mDigestBytes[i][1] & 0xFF) << 16 | (mDigestBytes[i][2] & 0xFF) << 8 | (mDigestBytes[i][3] & 0xFF) << 0) & DIGEST_MASK; } } private InstantAppDigest(byte[][] digestBytes, int[] prefix) { this.mDigestPrefix = prefix; this.mDigestBytes = digestBytes; } private static byte[][] generateDigest(String hostName, int maxDigests) { ArrayList digests = new ArrayList<>(); try { final MessageDigest digest = MessageDigest.getInstance(SHA_ALGORITHM); if (maxDigests <= 0) { final byte[] hostBytes = hostName.getBytes(); digests.add(digest.digest(hostBytes)); } else { int prevDot = hostName.lastIndexOf('.'); prevDot = hostName.lastIndexOf('.', prevDot - 1); // shortcut for short URLs if (prevDot < 0) { digests.add(digest.digest(hostName.getBytes())); } else { byte[] hostBytes = hostName.substring(prevDot + 1, hostName.length()).getBytes(); digests.add(digest.digest(hostBytes)); int digestCount = 1; while (prevDot >= 0 && digestCount < maxDigests) { prevDot = hostName.lastIndexOf('.', prevDot - 1); hostBytes = hostName.substring(prevDot + 1, hostName.length()).getBytes(); digests.add(digest.digest(hostBytes)); digestCount++; } } } } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("could not find digest algorithm"); } return digests.toArray(new byte[digests.size()][]); } InstantAppDigest(Parcel in) { final int digestCount = in.readInt(); if (digestCount == -1) { mDigestBytes = null; } else { mDigestBytes = new byte[digestCount][]; for (int i = 0; i < digestCount; i++) { mDigestBytes[i] = in.createByteArray(); } } mDigestPrefix = in.createIntArray(); mDigestPrefixSecure = in.createIntArray(); } public byte[][] getDigestBytes() { return mDigestBytes; } public int[] getDigestPrefix() { return mDigestPrefix; } /** * Returns a digest prefix with additional random prefixes interspersed. * @hide */ public int[] getDigestPrefixSecure() { if (this == InstantAppResolveInfo.InstantAppDigest.UNDEFINED) { return getDigestPrefix(); } else if (mDigestPrefixSecure == null) { // let's generate some random data to intersperse throughout the set of prefixes final int realSize = getDigestPrefix().length; final int manufacturedSize = realSize + 10 + sRandom.nextInt(10); mDigestPrefixSecure = Arrays.copyOf(getDigestPrefix(), manufacturedSize); for (int i = realSize; i < manufacturedSize; i++) { mDigestPrefixSecure[i] = sRandom.nextInt() & DIGEST_MASK; } Arrays.sort(mDigestPrefixSecure); } return mDigestPrefixSecure; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel out, int flags) { final boolean isUndefined = this == UNDEFINED; out.writeBoolean(isUndefined); if (isUndefined) { return; } if (mDigestBytes == null) { out.writeInt(-1); } else { out.writeInt(mDigestBytes.length); for (int i = 0; i < mDigestBytes.length; i++) { out.writeByteArray(mDigestBytes[i]); } } out.writeIntArray(mDigestPrefix); out.writeIntArray(mDigestPrefixSecure); } @SuppressWarnings("hiding") public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public InstantAppDigest createFromParcel(Parcel in) { if (in.readBoolean() /* is undefined */) { return UNDEFINED; } return new InstantAppDigest(in); } @Override public InstantAppDigest[] newArray(int size) { return new InstantAppDigest[size]; } }; } }