001/*
002 * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the
010 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
011 * express or implied. See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014
015package net.openid.appauth.browser;
016
017import android.content.pm.PackageInfo;
018import android.content.pm.Signature;
019import android.util.Base64;
020import androidx.annotation.NonNull;
021
022import java.security.MessageDigest;
023import java.security.NoSuchAlgorithmException;
024import java.util.HashSet;
025import java.util.Set;
026
027/**
028 * Represents a browser that may be used for an authorization flow.
029 */
030public class BrowserDescriptor {
031
032    // See: http://stackoverflow.com/a/2816747
033    private static final int PRIME_HASH_FACTOR = 92821;
034
035    private static final String DIGEST_SHA_512 = "SHA-512";
036
037    /**
038     * The package name of the browser app.
039     */
040    public final String packageName;
041
042    /**
043     * The set of {@link android.content.pm.Signature signatures} of the browser app,
044     * which have been hashed with SHA-512, and Base-64 URL-safe encoded.
045     */
046    public final Set<String> signatureHashes;
047
048    /**
049     * The version string of the browser app.
050     */
051    public final String version;
052
053    /**
054     * Whether it is intended that the browser will be used via a custom tab.
055     */
056    public final Boolean useCustomTab;
057
058    /**
059     * Creates a description of a browser from a {@link PackageInfo} object returned from the
060     * {@link android.content.pm.PackageManager}. The object is expected to include the
061     * signatures of the app, which can be retrieved with the
062     * {@link android.content.pm.PackageManager#GET_SIGNATURES GET_SIGNATURES} flag when
063     * calling {@link android.content.pm.PackageManager#getPackageInfo(String, int)}.
064     */
065    public BrowserDescriptor(@NonNull PackageInfo packageInfo, boolean useCustomTab) {
066        this(
067                packageInfo.packageName,
068                generateSignatureHashes(packageInfo.signatures),
069                packageInfo.versionName,
070                useCustomTab);
071    }
072
073    /**
074     * Creates a description of a browser from the core properties that are frequently used to
075     * decide whether a browser can be used for an authorization flow. In most cases, it is
076     * more convenient to use the other variant of the constructor that consumes a
077     * {@link PackageInfo} object provided by the package manager.
078     *
079     * @param packageName
080     *     The Android package name of the browser.
081     * @param signatureHashes
082     *     The set of SHA-512, Base64 url safe encoded signatures for the app. This can be
083     *     generated for a signature by calling {@link #generateSignatureHash(Signature)}.
084     * @param version
085     *     The version name of the browser.
086     * @param useCustomTab
087     *     Whether it is intended to use the browser as a custom tab.
088     */
089    public BrowserDescriptor(
090            @NonNull String packageName,
091            @NonNull Set<String> signatureHashes,
092            @NonNull String version,
093            boolean useCustomTab) {
094        this.packageName = packageName;
095        this.signatureHashes = signatureHashes;
096        this.version = version;
097        this.useCustomTab = useCustomTab;
098    }
099
100    /**
101     * Creates a copy of this browser descriptor, changing the intention to use it as a custom
102     * tab to the specified value.
103     */
104    @NonNull
105    public BrowserDescriptor changeUseCustomTab(boolean newUseCustomTabValue) {
106        return new BrowserDescriptor(
107                packageName,
108                signatureHashes,
109                version,
110                newUseCustomTabValue);
111    }
112
113    @Override
114    public boolean equals(Object obj) {
115        if (this == obj) {
116            return true;
117        }
118
119        if (obj == null || !(obj instanceof BrowserDescriptor)) {
120            return false;
121        }
122
123        BrowserDescriptor other = (BrowserDescriptor) obj;
124        return this.packageName.equals(other.packageName)
125                && this.version.equals(other.version)
126                && this.useCustomTab == other.useCustomTab
127                && this.signatureHashes.equals(other.signatureHashes);
128    }
129
130    @Override
131    public int hashCode() {
132        int hash = packageName.hashCode();
133
134        hash = PRIME_HASH_FACTOR * hash + version.hashCode();
135        hash = PRIME_HASH_FACTOR * hash + (useCustomTab ? 1 : 0);
136
137        for (String signatureHash : signatureHashes) {
138            hash = PRIME_HASH_FACTOR * hash + signatureHash.hashCode();
139        }
140
141        return hash;
142    }
143
144    /**
145     * Generates a SHA-512 hash, Base64 url-safe encoded, from a {@link Signature}.
146     */
147    @NonNull
148    public static String generateSignatureHash(@NonNull Signature signature) {
149        try {
150            MessageDigest digest = MessageDigest.getInstance(DIGEST_SHA_512);
151            byte[] hashBytes = digest.digest(signature.toByteArray());
152            return Base64.encodeToString(hashBytes, Base64.URL_SAFE | Base64.NO_WRAP);
153        } catch (NoSuchAlgorithmException e) {
154            throw new IllegalStateException(
155                    "Platform does not support" + DIGEST_SHA_512 + " hashing");
156        }
157    }
158
159    /**
160     * Generates a set of SHA-512, Base64 url-safe encoded signature hashes from the provided
161     * array of signatures.
162     */
163    @NonNull
164    public static Set<String> generateSignatureHashes(@NonNull Signature[] signatures) {
165        Set<String> signatureHashes = new HashSet<>();
166        for (Signature signature : signatures) {
167            signatureHashes.add(generateSignatureHash(signature));
168        }
169
170        return signatureHashes;
171    }
172}