From 99d9c308e06e025718605e46b761b99be4e64e15 Mon Sep 17 00:00:00 2001 From: Fabian Hauck Date: Wed, 2 Sep 2020 16:43:04 +0200 Subject: [PATCH 1/3] implementation of the method "secureRedirection(Context, Uri)" to securely redirect from one app to another or the web if the app is not installed Signed-off-by: Fabian Hauck --- library/build.gradle | 2 + .../CertificateFingerprintEncoding.java | 74 +++++ .../java/net/openid/appauth/app2app/README.md | 5 + .../appauth/app2app/RedirectSession.java | 59 ++++ .../appauth/app2app/SecureRedirection.java | 299 ++++++++++++++++++ .../appauth/browser/BrowserDescriptor.java | 22 +- .../CertificateFingerprintEncodingTest.java | 40 +++ .../app2app/SecureRedirectionTest.java | 52 +++ 8 files changed, 547 insertions(+), 6 deletions(-) create mode 100644 library/java/net/openid/appauth/app2app/CertificateFingerprintEncoding.java create mode 100644 library/java/net/openid/appauth/app2app/README.md create mode 100644 library/java/net/openid/appauth/app2app/RedirectSession.java create mode 100644 library/java/net/openid/appauth/app2app/SecureRedirection.java create mode 100644 library/javatests/net/openid/appauth/app2app/CertificateFingerprintEncodingTest.java create mode 100644 library/javatests/net/openid/appauth/app2app/SecureRedirectionTest.java diff --git a/library/build.gradle b/library/build.gradle index 5fcdac48..1e239490 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -25,7 +25,9 @@ android.testBuildType "forTests" dependencies { api "androidx.browser:browser:${project.androidXVersions.browser}" implementation "androidx.annotation:annotation:${project.androidXVersions.annotation}" + implementation 'org.jetbrains:annotations:15.0' apply from: '../config/testdeps.gradle', to:it + implementation 'com.android.volley:volley:1.1.1' } apply from: '../config/style.gradle' diff --git a/library/java/net/openid/appauth/app2app/CertificateFingerprintEncoding.java b/library/java/net/openid/appauth/app2app/CertificateFingerprintEncoding.java new file mode 100644 index 00000000..4099230b --- /dev/null +++ b/library/java/net/openid/appauth/app2app/CertificateFingerprintEncoding.java @@ -0,0 +1,74 @@ +package net.openid.appauth.app2app; + +import android.util.Base64; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; +import org.json.JSONException; + +import java.util.HashSet; +import java.util.Set; + +final class CertificateFingerprintEncoding { + + private CertificateFingerprintEncoding() { + } + + /** + * This method takes the certificate fingerprints from the '/.well-known/assetlinks.json' file + * and decodes it in the correct way to compare the hashes with the ones found on the device. + */ + @NotNull + protected static Set certFingerprintsToDecodedString( + @NotNull JSONArray certFingerprints) { + Set hashes = new HashSet<>(); + + for (int i = 0; i < certFingerprints.length(); i++) { + try { + byte[] byteArray = hexStringToByteArray(certFingerprints.get(i).toString()); + String str = Base64.encodeToString(byteArray, 10); + hashes.add(str); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + return hashes; + } + + /** + * This method converts a hex string that is separated by colons into a ByteArray. + *

+ * Example hexString: 4F:69:88:01:... + */ + @NotNull + private static byte[] hexStringToByteArray(@NotNull String hexString) { + String[] hexValues = hexString.split(":"); + byte[] byteArray = new byte[hexValues.length]; + String str; + int b = 0; + + for (int i = 0; i < hexValues.length; ++i) { + str = hexValues[i]; + b = 0; + b = hexValue(str.charAt(0)); + b <<= 4; + b |= hexValue(str.charAt(1)); + byteArray[i] = (byte) b; + } + + return byteArray; + } + + /** + * Converts a single hex digit into its decimal value. + */ + private static int hexValue(char hexChar) { + int digit = Character.digit(hexChar, 16); + if (digit < 0) { + throw new IllegalArgumentException("Invalid hex char " + hexChar); + } else { + return digit; + } + } +} diff --git a/library/java/net/openid/appauth/app2app/README.md b/library/java/net/openid/appauth/app2app/README.md new file mode 100644 index 00000000..7711da9c --- /dev/null +++ b/library/java/net/openid/appauth/app2app/README.md @@ -0,0 +1,5 @@ +# App2App Redirection + +Further information about the ``app2app`` package +can be found [here](https://github.com/oauthstuff/app2app-evolution/blob/master/AppAuth-Integration.md) +and [here](https://github.com/oauthstuff/app2app-evolution). diff --git a/library/java/net/openid/appauth/app2app/RedirectSession.java b/library/java/net/openid/appauth/app2app/RedirectSession.java new file mode 100644 index 00000000..e6c25cc8 --- /dev/null +++ b/library/java/net/openid/appauth/app2app/RedirectSession.java @@ -0,0 +1,59 @@ +package net.openid.appauth.app2app; + +import android.content.Context; +import android.net.Uri; + +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +/** + * Class to hold all important information to perform a secure redirection. + */ +class RedirectSession { + + private Context mContext; + private Uri mUri; + private String mBasePackageName = ""; + private Set mBaseCertFingerprints; + + protected RedirectSession(@NotNull Context mContext, @NotNull Uri mUri) { + this.mContext = mContext; + this.mUri = mUri; + } + + @NotNull + protected Context getContext() { + return mContext; + } + + protected void setContext(@NotNull Context context) { + this.mContext = context; + } + + @NotNull + protected Uri getUri() { + return mUri; + } + + protected void setUri(@NotNull Uri uri) { + this.mUri = uri; + } + + @NotNull + protected String getBasePackageName() { + return mBasePackageName; + } + + protected void setBasePackageName(@NotNull String basePackageName) { + this.mBasePackageName = basePackageName; + } + + protected Set getBaseCertFingerprints() { + return mBaseCertFingerprints; + } + + protected void setBaseCertFingerprints(Set mBaseCertFingerprints) { + this.mBaseCertFingerprints = mBaseCertFingerprints; + } +} diff --git a/library/java/net/openid/appauth/app2app/SecureRedirection.java b/library/java/net/openid/appauth/app2app/SecureRedirection.java new file mode 100644 index 00000000..4a2a9dfe --- /dev/null +++ b/library/java/net/openid/appauth/app2app/SecureRedirection.java @@ -0,0 +1,299 @@ +package net.openid.appauth.app2app; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.Signature; +import android.content.pm.SigningInfo; +import android.graphics.Color; +import android.net.Uri; +import android.util.Pair; +import android.widget.Toast; + +import androidx.annotation.VisibleForTesting; +import androidx.browser.customtabs.CustomTabsIntent; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonArrayRequest; +import com.android.volley.toolbox.Volley; + +import net.openid.appauth.browser.BrowserAllowList; +import net.openid.appauth.browser.BrowserDescriptor; +import net.openid.appauth.browser.BrowserSelector; +import net.openid.appauth.browser.VersionedBrowserMatcher; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public final class SecureRedirection { + + private SecureRedirection() { + } + + /** + * This method redirects an user securely from one app to another with a given URL. For this to + * work it is required that the "/.well-known/assetlinks.json" file is correctly set up for this + * domain and that the target app has an intent-filter for this URL. + */ + public static void secureRedirection(@NotNull Context context, @NotNull Uri uri) { + getAssetLinksFile(new RedirectSession(context, uri)); + } + + /** + * This function retrieves the '/.well-known/assetlinks.json' file from the given domain. + */ + private static void getAssetLinksFile(@NotNull final RedirectSession redirectSession) { + RequestQueue volleyQueue = Volley.newRequestQueue(redirectSession.getContext()); + + String url = + redirectSession.getUri().getScheme() + + "://" + + redirectSession.getUri().getHost() + + ":" + + redirectSession.getUri().getPort() + + "/.well-known/assetlinks.json"; + + JsonArrayRequest request = + new JsonArrayRequest( + Request.Method.GET, + url, + null, + new Response.Listener() { + @Override + public void onResponse(JSONArray response) { + JSONArray baseCertFingerprints = + findInstalledApp(redirectSession, response); + + redirectSession.setBaseCertFingerprints( + CertificateFingerprintEncoding + .certFingerprintsToDecodedString( + baseCertFingerprints)); + + doRedirection(redirectSession); + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + System.err.println( + "Failed to fetch '/.well-known/assetlinks.json' from domain " + + "'${redirectSession.uri.host}'\nError: ${error}"); + } + }); + + volleyQueue.add(request); + } + + /** + * Find a suitable installed app to open the URI and return the signing certificate fingerprints + * for this app. If no such app is found, the signing certificate fingerprints array and the + * package name will be empty. + * + * @param redirectSession + * @param assetLinks + * @return + */ + @NotNull + private static JSONArray findInstalledApp( + @NotNull RedirectSession redirectSession, @NotNull JSONArray assetLinks) { + Pair, Map> basePair = + getBaseValuesFromAssetLinksFile(assetLinks); + Set foundPackageNames = getPackageNamesForIntent(redirectSession); + + // Intersect the set of installed apps with the set of apps + // defined in the '/.well-known/assetlinks.json' file. + basePair.first.retainAll(foundPackageNames); + + if (basePair.first.iterator().hasNext()) { + redirectSession.setBasePackageName(basePair.first.iterator().next()); + } else { + redirectSession.setBasePackageName(""); + } + + JSONArray returnValue = basePair.second.get(redirectSession.getBasePackageName()); + if (returnValue != null) { + return returnValue; + } + return new JSONArray(); + } + + /** + * Extract the package names and the certificate fingerprints from the + * '/.well-known/assetlinks.json' file. + * + * @param assetLinks + * @return + */ + @NotNull + private static Pair, Map> getBaseValuesFromAssetLinksFile( + @NotNull JSONArray assetLinks) { + Set basePackageNames = new HashSet<>(); + Map baseCertFingerprints = new HashMap<>(); + try { + for (int i = 0; i < assetLinks.length(); i++) { + JSONObject jsonObject = (JSONObject) assetLinks.get(i); + JSONObject target = (JSONObject) jsonObject.get("target"); + String basePackageName = target.get("package_name").toString(); + JSONArray baseCertFingerprint = (JSONArray) target.get("sha256_cert_fingerprints"); + + basePackageNames.add(basePackageName); + baseCertFingerprints.put(basePackageName, baseCertFingerprint); + } + } catch (JSONException exception) { + exception.printStackTrace(); + } + + return new Pair<>(basePackageNames, baseCertFingerprints); + } + + /** + * This method uses the Android Package Manager to find all apps that have an intent-filter for + * the given URI. + * + * @param redirectSession + * @return + */ + @NotNull + private static Set getPackageNamesForIntent(@NotNull RedirectSession redirectSession) { + /* + Source: https://stackoverflow.com/questions/11904158/can-i-disable-an-option-when-i-call-intent-action-view + */ + Intent intent = new Intent(Intent.ACTION_VIEW, redirectSession.getUri()); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.addCategory(Intent.CATEGORY_BROWSABLE); + + List infos = + redirectSession + .getContext() + .getPackageManager() + .queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER); + + Set packageNames = new HashSet<>(); + for (ResolveInfo info : infos) { + packageNames.add(info.activityInfo.packageName); + } + return packageNames; + } + + /** + * This method checks whether the legit app is installed and either redirect the user to this + * app or to the default browser. + */ + private static void doRedirection(@NotNull RedirectSession redirectSession) { + if (!redirectSession.getBasePackageName().isEmpty() && isAppLegit(redirectSession)) { + Intent redirectIntent = new Intent(Intent.ACTION_VIEW, redirectSession.getUri()); + redirectIntent.setPackage(redirectSession.getBasePackageName()); + redirectSession.getContext().startActivity(redirectIntent); + } else { + redirectToWeb(redirectSession.getContext(), redirectSession.getUri()); + } + } + + /** + * This method take a packageName and the signing certificate hash of this package to validate + * whether the correct app is installed on the device. + */ + private static boolean isAppLegit(@NotNull RedirectSession redirectSession) { + Set foundCertFingerprints = getSigningCertificates(redirectSession); + if (foundCertFingerprints != null) { + return matchHashes(redirectSession.getBaseCertFingerprints(), foundCertFingerprints); + } + return false; + } + + /** + * This method retrieves the signing certificate of an app from the Android Package Manager. If + * the app is not installed this method returns null. + */ + private static Set getSigningCertificates(@NotNull RedirectSession redirectSession) { + try { + Signature[] signatures; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + SigningInfo signingInfo = + redirectSession + .getContext() + .getPackageManager() + .getPackageInfo( + redirectSession.getBasePackageName(), + PackageManager.GET_SIGNING_CERTIFICATES) + .signingInfo; + signatures = signingInfo.getSigningCertificateHistory(); + } else { + signatures = + redirectSession + .getContext() + .getPackageManager() + .getPackageInfo( + redirectSession.getBasePackageName(), + PackageManager.GET_SIGNATURES) + .signatures; + } + return BrowserDescriptor.generateSignatureHashes( + signatures, BrowserDescriptor.DIGEST_SHA_256); + } catch (PackageManager.NameNotFoundException excepetion) { + return null; + } + } + + /** + * This function checks whether the two sets contain the same strings independent of their + * order. + */ + @VisibleForTesting + public static boolean matchHashes( + @NotNull Set certHashes0, @NotNull Set certHashes1) { + return certHashes0.containsAll(certHashes1) && certHashes0.size() == certHashes1.size(); + } + + /** + * This method uses the BrowserSelector class to find the user's default browser and validated + * the integrity of this browser. It then opens the given uri in an Android Custom Tab. + */ + public static void redirectToWeb(@NotNull Context context, @NotNull Uri uri) { + redirectToWeb(context, uri, 0, Color.WHITE); + } + + /** + * This method uses the BrowserSelector class to find the user's default browser and validated + * the integrity of this browser. It then opens the given uri in an Android Custom Tab. + */ + public static void redirectToWeb( + @NotNull Context context, @NotNull Uri uri, int additionalFlags, int toolbarColor) { + CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); + builder.setToolbarColor(toolbarColor); + CustomTabsIntent customTabsIntent = builder.build(); + + BrowserDescriptor browserDescriptor = + BrowserSelector.select( + context, + new BrowserAllowList( + VersionedBrowserMatcher.CHROME_CUSTOM_TAB, + VersionedBrowserMatcher.CHROME_BROWSER, + VersionedBrowserMatcher.FIREFOX_CUSTOM_TAB, + VersionedBrowserMatcher.FIREFOX_BROWSER, + VersionedBrowserMatcher.SAMSUNG_CUSTOM_TAB, + VersionedBrowserMatcher.SAMSUNG_BROWSER)); + + if (browserDescriptor != null) { + customTabsIntent + .intent + .setPackage(browserDescriptor.packageName) + .setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | additionalFlags); + customTabsIntent.launchUrl(context, uri); + } else { + Toast.makeText(context, "Could not find a browser", Toast.LENGTH_SHORT).show(); + } + } +} diff --git a/library/java/net/openid/appauth/browser/BrowserDescriptor.java b/library/java/net/openid/appauth/browser/BrowserDescriptor.java index d9f85f5a..fc322753 100644 --- a/library/java/net/openid/appauth/browser/BrowserDescriptor.java +++ b/library/java/net/openid/appauth/browser/BrowserDescriptor.java @@ -32,7 +32,8 @@ public class BrowserDescriptor { // See: http://stackoverflow.com/a/2816747 private static final int PRIME_HASH_FACTOR = 92821; - private static final String DIGEST_SHA_512 = "SHA-512"; + public static final String DIGEST_SHA_256 = "SHA-256"; + public static final String DIGEST_SHA_512 = "SHA-512"; /** * The package name of the browser app. @@ -142,17 +143,17 @@ public int hashCode() { } /** - * Generates a SHA-512 hash, Base64 url-safe encoded, from a {@link Signature}. + * Generates a SHA-* hash, Base64 url-safe encoded, from a {@link Signature}. */ @NonNull - public static String generateSignatureHash(@NonNull Signature signature) { + public static String generateSignatureHash(@NonNull Signature signature, @NonNull String digestSHA) { try { - MessageDigest digest = MessageDigest.getInstance(DIGEST_SHA_512); + MessageDigest digest = MessageDigest.getInstance(digestSHA); byte[] hashBytes = digest.digest(signature.toByteArray()); return Base64.encodeToString(hashBytes, Base64.URL_SAFE | Base64.NO_WRAP); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException( - "Platform does not support" + DIGEST_SHA_512 + " hashing"); + "Platform does not support" + digestSHA + " hashing"); } } @@ -162,9 +163,18 @@ public static String generateSignatureHash(@NonNull Signature signature) { */ @NonNull public static Set generateSignatureHashes(@NonNull Signature[] signatures) { + return generateSignatureHashes(signatures, DIGEST_SHA_512); + } + + /** + * Generates a set of SHA-*, Base64 url-safe encoded signature hashes from the provided + * array of signatures. + */ + @NonNull + public static Set generateSignatureHashes(@NonNull Signature[] signatures, @NonNull String digestSHA) { Set signatureHashes = new HashSet<>(); for (Signature signature : signatures) { - signatureHashes.add(generateSignatureHash(signature)); + signatureHashes.add(generateSignatureHash(signature, digestSHA)); } return signatureHashes; diff --git a/library/javatests/net/openid/appauth/app2app/CertificateFingerprintEncodingTest.java b/library/javatests/net/openid/appauth/app2app/CertificateFingerprintEncodingTest.java new file mode 100644 index 00000000..c410d5c5 --- /dev/null +++ b/library/javatests/net/openid/appauth/app2app/CertificateFingerprintEncodingTest.java @@ -0,0 +1,40 @@ +package net.openid.appauth.app2app; + +import net.openid.appauth.BuildConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.json.JSONArray; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Set; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 16) +public class CertificateFingerprintEncodingTest { + + @Test + public void testCertFingerprintsToDecodedString0() { + JSONArray jsonArray = new JSONArray(); + jsonArray.put("98:C7:E1:43:9C:A9:C9:68:27:FE:47:16:9A:C0:60:2A:61:5B:88:2F:CC:4E:AB:66:47:8E:67:E6:2A:93:F8:68"); + + Set hashes = CertificateFingerprintEncoding.certFingerprintsToDecodedString(jsonArray); + + assertThat(hashes.size()).isEqualTo(1); + assertThat(hashes.contains("mMfhQ5ypyWgn_kcWmsBgKmFbiC_MTqtmR45n5iqT-Gg=")).isTrue(); + } + + @Test + public void testCertFingerprintsToDecodedString1() { + JSONArray jsonArray = new JSONArray(); + jsonArray.put("58:27:63:4A:F5:D5:07:7C:DE:4B:94:27:60:B0:C7:CD:33:8D:93:13:02:8D:0B:E0:0F:C5:26:F4:88:39:F1:D5"); + + Set hashes = CertificateFingerprintEncoding.certFingerprintsToDecodedString(jsonArray); + + assertThat(hashes.size()).isEqualTo(1); + assertThat(hashes.contains("WCdjSvXVB3zeS5QnYLDHzTONkxMCjQvgD8Um9Ig58dU=")).isTrue(); + } +} diff --git a/library/javatests/net/openid/appauth/app2app/SecureRedirectionTest.java b/library/javatests/net/openid/appauth/app2app/SecureRedirectionTest.java new file mode 100644 index 00000000..0d190e3c --- /dev/null +++ b/library/javatests/net/openid/appauth/app2app/SecureRedirectionTest.java @@ -0,0 +1,52 @@ +package net.openid.appauth.app2app; + +import net.openid.appauth.BuildConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 16) +public class SecureRedirectionTest { + + @Test + public void testMatchHashesTrue0() { + Set set0 = Stream.of("foo", "bar", "baz", "qux", "corge").collect(Collectors.toCollection(HashSet::new)); + Set set1 = Stream.of("baz", "bar", "foo", "corge", "qux").collect(Collectors.toCollection(HashSet::new)); + + assertThat(SecureRedirection.matchHashes(set0, set1)).isTrue(); + } + + @Test + public void testMatchHashesTrue1() { + Set set0 = Stream.of("foo").collect(Collectors.toCollection(HashSet::new)); + Set set1 = Stream.of("foo").collect(Collectors.toCollection(HashSet::new)); + + assertThat(SecureRedirection.matchHashes(set0, set1)).isTrue(); + } + + @Test + public void testMatchHashesFalse0() { + Set set0 = Stream.of("foo", "bar", "baz", "qux", "corge").collect(Collectors.toCollection(HashSet::new)); + Set set1 = Stream.of("baz", "fred", "foo", "corge", "qux").collect(Collectors.toCollection(HashSet::new)); + + assertThat(SecureRedirection.matchHashes(set0, set1)).isFalse(); + } + + @Test + public void testMatchHashesFalse1() { + Set set0 = Stream.of("foo", "bar", "baz", "qux", "corge").collect(Collectors.toCollection(HashSet::new)); + Set set1 = Stream.of("baz", "foo", "corge", "qux").collect(Collectors.toCollection(HashSet::new)); + + assertThat(SecureRedirection.matchHashes(set0, set1)).isFalse(); + } +} From 4b9f318b1d3dcff244916b880eeefa6f679afaf8 Mon Sep 17 00:00:00 2001 From: Fabian Hauck Date: Tue, 8 Sep 2020 18:21:39 +0200 Subject: [PATCH 2/3] removal of additional dependencies Signed-off-by: Fabian Hauck --- library/build.gradle | 2 - library/java/net/openid/appauth/Utils.java | 2 +- .../CertificateFingerprintEncoding.java | 11 +- .../appauth/app2app/RedirectSession.java | 26 ++-- .../appauth/app2app/SecureRedirection.java | 128 ++++++++++-------- 5 files changed, 98 insertions(+), 71 deletions(-) diff --git a/library/build.gradle b/library/build.gradle index 1e239490..5fcdac48 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -25,9 +25,7 @@ android.testBuildType "forTests" dependencies { api "androidx.browser:browser:${project.androidXVersions.browser}" implementation "androidx.annotation:annotation:${project.androidXVersions.annotation}" - implementation 'org.jetbrains:annotations:15.0' apply from: '../config/testdeps.gradle', to:it - implementation 'com.android.volley:volley:1.1.1' } apply from: '../config/style.gradle' diff --git a/library/java/net/openid/appauth/Utils.java b/library/java/net/openid/appauth/Utils.java index c78ede27..6f2cdc5a 100644 --- a/library/java/net/openid/appauth/Utils.java +++ b/library/java/net/openid/appauth/Utils.java @@ -22,7 +22,7 @@ /** * Utility class for common operations. */ -class Utils { +public class Utils { private static final int INITIAL_READ_BUFFER_SIZE = 1024; private Utils() { diff --git a/library/java/net/openid/appauth/app2app/CertificateFingerprintEncoding.java b/library/java/net/openid/appauth/app2app/CertificateFingerprintEncoding.java index 4099230b..0d94acd2 100644 --- a/library/java/net/openid/appauth/app2app/CertificateFingerprintEncoding.java +++ b/library/java/net/openid/appauth/app2app/CertificateFingerprintEncoding.java @@ -2,7 +2,8 @@ import android.util.Base64; -import org.jetbrains.annotations.NotNull; +import androidx.annotation.NonNull; + import org.json.JSONArray; import org.json.JSONException; @@ -18,9 +19,9 @@ private CertificateFingerprintEncoding() { * This method takes the certificate fingerprints from the '/.well-known/assetlinks.json' file * and decodes it in the correct way to compare the hashes with the ones found on the device. */ - @NotNull + @NonNull protected static Set certFingerprintsToDecodedString( - @NotNull JSONArray certFingerprints) { + @NonNull JSONArray certFingerprints) { Set hashes = new HashSet<>(); for (int i = 0; i < certFingerprints.length(); i++) { @@ -41,8 +42,8 @@ protected static Set certFingerprintsToDecodedString( *

* Example hexString: 4F:69:88:01:... */ - @NotNull - private static byte[] hexStringToByteArray(@NotNull String hexString) { + @NonNull + private static byte[] hexStringToByteArray(@NonNull String hexString) { String[] hexValues = hexString.split(":"); byte[] byteArray = new byte[hexValues.length]; String str; diff --git a/library/java/net/openid/appauth/app2app/RedirectSession.java b/library/java/net/openid/appauth/app2app/RedirectSession.java index e6c25cc8..47927a79 100644 --- a/library/java/net/openid/appauth/app2app/RedirectSession.java +++ b/library/java/net/openid/appauth/app2app/RedirectSession.java @@ -3,9 +3,10 @@ import android.content.Context; import android.net.Uri; -import org.jetbrains.annotations.NotNull; +import androidx.annotation.NonNull; import java.util.Set; +import org.json.JSONArray; /** * Class to hold all important information to perform a secure redirection. @@ -16,36 +17,37 @@ class RedirectSession { private Uri mUri; private String mBasePackageName = ""; private Set mBaseCertFingerprints; + private JSONArray mAssetLinksFile = null; - protected RedirectSession(@NotNull Context mContext, @NotNull Uri mUri) { + protected RedirectSession(@NonNull Context mContext, @NonNull Uri mUri) { this.mContext = mContext; this.mUri = mUri; } - @NotNull + @NonNull protected Context getContext() { return mContext; } - protected void setContext(@NotNull Context context) { + protected void setContext(@NonNull Context context) { this.mContext = context; } - @NotNull + @NonNull protected Uri getUri() { return mUri; } - protected void setUri(@NotNull Uri uri) { + protected void setUri(@NonNull Uri uri) { this.mUri = uri; } - @NotNull + @NonNull protected String getBasePackageName() { return mBasePackageName; } - protected void setBasePackageName(@NotNull String basePackageName) { + protected void setBasePackageName(@NonNull String basePackageName) { this.mBasePackageName = basePackageName; } @@ -56,4 +58,12 @@ protected Set getBaseCertFingerprints() { protected void setBaseCertFingerprints(Set mBaseCertFingerprints) { this.mBaseCertFingerprints = mBaseCertFingerprints; } + + public JSONArray getAssetLinksFile() { + return mAssetLinksFile; + } + + public void setAssetLinksFile(JSONArray mAssetLinksFile) { + this.mAssetLinksFile = mAssetLinksFile; + } } diff --git a/library/java/net/openid/appauth/app2app/SecureRedirection.java b/library/java/net/openid/appauth/app2app/SecureRedirection.java index 4a2a9dfe..8c351577 100644 --- a/library/java/net/openid/appauth/app2app/SecureRedirection.java +++ b/library/java/net/openid/appauth/app2app/SecureRedirection.java @@ -8,25 +8,26 @@ import android.content.pm.SigningInfo; import android.graphics.Color; import android.net.Uri; +import android.os.AsyncTask; import android.util.Pair; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.browser.customtabs.CustomTabsIntent; -import com.android.volley.Request; -import com.android.volley.RequestQueue; -import com.android.volley.Response; -import com.android.volley.VolleyError; -import com.android.volley.toolbox.JsonArrayRequest; -import com.android.volley.toolbox.Volley; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; + +import net.openid.appauth.Utils; import net.openid.appauth.browser.BrowserAllowList; import net.openid.appauth.browser.BrowserDescriptor; import net.openid.appauth.browser.BrowserSelector; import net.openid.appauth.browser.VersionedBrowserMatcher; +import net.openid.appauth.connectivity.DefaultConnectionBuilder; -import org.jetbrains.annotations.NotNull; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -47,53 +48,70 @@ private SecureRedirection() { * work it is required that the "/.well-known/assetlinks.json" file is correctly set up for this * domain and that the target app has an intent-filter for this URL. */ - public static void secureRedirection(@NotNull Context context, @NotNull Uri uri) { + public static void secureRedirection(@NonNull Context context, @NonNull Uri uri) { getAssetLinksFile(new RedirectSession(context, uri)); } /** * This function retrieves the '/.well-known/assetlinks.json' file from the given domain. */ - private static void getAssetLinksFile(@NotNull final RedirectSession redirectSession) { - RequestQueue volleyQueue = Volley.newRequestQueue(redirectSession.getContext()); + private static void getAssetLinksFile(@NonNull final RedirectSession redirectSession) { + new DownloadAssetLinksFile().execute(redirectSession); + } + + private static class DownloadAssetLinksFile extends + AsyncTask { - String url = - redirectSession.getUri().getScheme() + @Override + protected RedirectSession doInBackground(RedirectSession... redirectSessions) { + RedirectSession redirectSession = redirectSessions[0]; + Uri uri = Uri.parse(redirectSession.getUri().getScheme() + "://" + redirectSession.getUri().getHost() + ":" + redirectSession.getUri().getPort() - + "/.well-known/assetlinks.json"; - - JsonArrayRequest request = - new JsonArrayRequest( - Request.Method.GET, - url, - null, - new Response.Listener() { - @Override - public void onResponse(JSONArray response) { - JSONArray baseCertFingerprints = - findInstalledApp(redirectSession, response); - - redirectSession.setBaseCertFingerprints( - CertificateFingerprintEncoding - .certFingerprintsToDecodedString( - baseCertFingerprints)); - - doRedirection(redirectSession); - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - System.err.println( - "Failed to fetch '/.well-known/assetlinks.json' from domain " - + "'${redirectSession.uri.host}'\nError: ${error}"); - } - }); - - volleyQueue.add(request); + + "/.well-known/assetlinks.json"); + + InputStream is = null; + try { + HttpURLConnection conn = DefaultConnectionBuilder.INSTANCE.openConnection(uri); + conn.setRequestMethod("GET"); + conn.setDoInput(true); + conn.connect(); + + is = conn.getInputStream(); + JSONArray response = new JSONArray(Utils.readInputStream(is)); + redirectSession.setAssetLinksFile(response); + + } catch (IOException e) { + redirectSession.setAssetLinksFile(null); + } catch (JSONException e) { + redirectSession.setAssetLinksFile(null); + } finally { + Utils.closeQuietly(is); + } + return redirectSession; + } + + @Override + protected void onPostExecute(RedirectSession redirectSession) { + if (redirectSession.getAssetLinksFile() != null) { + JSONArray baseCertFingerprints = + findInstalledApp(redirectSession, redirectSession.getAssetLinksFile()); + + redirectSession.setBaseCertFingerprints( + CertificateFingerprintEncoding + .certFingerprintsToDecodedString( + baseCertFingerprints)); + + doRedirection(redirectSession); + } else { + System.err.println( + "Failed to fetch '/.well-known/assetlinks.json' from domain " + + "'${redirectSession.uri.host}'\nError: ${error}"); + redirectToWeb(redirectSession.getContext(), redirectSession.getUri()); + } + } } /** @@ -105,9 +123,9 @@ public void onErrorResponse(VolleyError error) { * @param assetLinks * @return */ - @NotNull + @NonNull private static JSONArray findInstalledApp( - @NotNull RedirectSession redirectSession, @NotNull JSONArray assetLinks) { + @NonNull RedirectSession redirectSession, @NonNull JSONArray assetLinks) { Pair, Map> basePair = getBaseValuesFromAssetLinksFile(assetLinks); Set foundPackageNames = getPackageNamesForIntent(redirectSession); @@ -136,9 +154,9 @@ private static JSONArray findInstalledApp( * @param assetLinks * @return */ - @NotNull + @NonNull private static Pair, Map> getBaseValuesFromAssetLinksFile( - @NotNull JSONArray assetLinks) { + @NonNull JSONArray assetLinks) { Set basePackageNames = new HashSet<>(); Map baseCertFingerprints = new HashMap<>(); try { @@ -165,8 +183,8 @@ private static Pair, Map> getBaseValuesFromAssetL * @param redirectSession * @return */ - @NotNull - private static Set getPackageNamesForIntent(@NotNull RedirectSession redirectSession) { + @NonNull + private static Set getPackageNamesForIntent(@NonNull RedirectSession redirectSession) { /* Source: https://stackoverflow.com/questions/11904158/can-i-disable-an-option-when-i-call-intent-action-view */ @@ -191,7 +209,7 @@ private static Set getPackageNamesForIntent(@NotNull RedirectSession red * This method checks whether the legit app is installed and either redirect the user to this * app or to the default browser. */ - private static void doRedirection(@NotNull RedirectSession redirectSession) { + private static void doRedirection(@NonNull RedirectSession redirectSession) { if (!redirectSession.getBasePackageName().isEmpty() && isAppLegit(redirectSession)) { Intent redirectIntent = new Intent(Intent.ACTION_VIEW, redirectSession.getUri()); redirectIntent.setPackage(redirectSession.getBasePackageName()); @@ -205,7 +223,7 @@ private static void doRedirection(@NotNull RedirectSession redirectSession) { * This method take a packageName and the signing certificate hash of this package to validate * whether the correct app is installed on the device. */ - private static boolean isAppLegit(@NotNull RedirectSession redirectSession) { + private static boolean isAppLegit(@NonNull RedirectSession redirectSession) { Set foundCertFingerprints = getSigningCertificates(redirectSession); if (foundCertFingerprints != null) { return matchHashes(redirectSession.getBaseCertFingerprints(), foundCertFingerprints); @@ -217,7 +235,7 @@ private static boolean isAppLegit(@NotNull RedirectSession redirectSession) { * This method retrieves the signing certificate of an app from the Android Package Manager. If * the app is not installed this method returns null. */ - private static Set getSigningCertificates(@NotNull RedirectSession redirectSession) { + private static Set getSigningCertificates(@NonNull RedirectSession redirectSession) { try { Signature[] signatures; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { @@ -253,7 +271,7 @@ private static Set getSigningCertificates(@NotNull RedirectSession redir */ @VisibleForTesting public static boolean matchHashes( - @NotNull Set certHashes0, @NotNull Set certHashes1) { + @NonNull Set certHashes0, @NonNull Set certHashes1) { return certHashes0.containsAll(certHashes1) && certHashes0.size() == certHashes1.size(); } @@ -261,7 +279,7 @@ public static boolean matchHashes( * This method uses the BrowserSelector class to find the user's default browser and validated * the integrity of this browser. It then opens the given uri in an Android Custom Tab. */ - public static void redirectToWeb(@NotNull Context context, @NotNull Uri uri) { + public static void redirectToWeb(@NonNull Context context, @NonNull Uri uri) { redirectToWeb(context, uri, 0, Color.WHITE); } @@ -270,7 +288,7 @@ public static void redirectToWeb(@NotNull Context context, @NotNull Uri uri) { * the integrity of this browser. It then opens the given uri in an Android Custom Tab. */ public static void redirectToWeb( - @NotNull Context context, @NotNull Uri uri, int additionalFlags, int toolbarColor) { + @NonNull Context context, @NonNull Uri uri, int additionalFlags, int toolbarColor) { CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); builder.setToolbarColor(toolbarColor); CustomTabsIntent customTabsIntent = builder.build(); From 300a91d24c2f085889cdd336a95031d71acd1257 Mon Sep 17 00:00:00 2001 From: Fabian Hauck Date: Wed, 18 Nov 2020 21:09:02 +0100 Subject: [PATCH 3/3] fixed formatting issues Signed-off-by: Fabian Hauck --- .../CertificateFingerprintEncoding.java | 48 ++++--- .../appauth/app2app/RedirectSession.java | 36 +++-- .../appauth/app2app/SecureRedirection.java | 132 ++++++++++-------- .../openid/appauth/app2app/package-info.java | 19 +++ .../appauth/browser/BrowserDescriptor.java | 86 +++++------- 5 files changed, 179 insertions(+), 142 deletions(-) create mode 100644 library/java/net/openid/appauth/app2app/package-info.java diff --git a/library/java/net/openid/appauth/app2app/CertificateFingerprintEncoding.java b/library/java/net/openid/appauth/app2app/CertificateFingerprintEncoding.java index 0d94acd2..6b0ae4b3 100644 --- a/library/java/net/openid/appauth/app2app/CertificateFingerprintEncoding.java +++ b/library/java/net/openid/appauth/app2app/CertificateFingerprintEncoding.java @@ -1,7 +1,20 @@ +/* + * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved. + * + * 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 net.openid.appauth.app2app; import android.util.Base64; - import androidx.annotation.NonNull; import org.json.JSONArray; @@ -12,8 +25,11 @@ final class CertificateFingerprintEncoding { - private CertificateFingerprintEncoding() { - } + private static final int DECIMAL = 10; + private static final int HEXADECIMAL = 16; + private static final int HALF_BYTE = 4; + + private CertificateFingerprintEncoding() {} /** * This method takes the certificate fingerprints from the '/.well-known/assetlinks.json' file @@ -21,13 +37,13 @@ private CertificateFingerprintEncoding() { */ @NonNull protected static Set certFingerprintsToDecodedString( - @NonNull JSONArray certFingerprints) { + @NonNull JSONArray certFingerprints) { Set hashes = new HashSet<>(); for (int i = 0; i < certFingerprints.length(); i++) { try { byte[] byteArray = hexStringToByteArray(certFingerprints.get(i).toString()); - String str = Base64.encodeToString(byteArray, 10); + String str = Base64.encodeToString(byteArray, DECIMAL); hashes.add(str); } catch (JSONException e) { e.printStackTrace(); @@ -39,33 +55,31 @@ protected static Set certFingerprintsToDecodedString( /** * This method converts a hex string that is separated by colons into a ByteArray. - *

- * Example hexString: 4F:69:88:01:... + * + *

Example hexString: 4F:69:88:01:... */ @NonNull private static byte[] hexStringToByteArray(@NonNull String hexString) { String[] hexValues = hexString.split(":"); byte[] byteArray = new byte[hexValues.length]; String str; - int b = 0; + int tmp = 0; for (int i = 0; i < hexValues.length; ++i) { str = hexValues[i]; - b = 0; - b = hexValue(str.charAt(0)); - b <<= 4; - b |= hexValue(str.charAt(1)); - byteArray[i] = (byte) b; + tmp = 0; + tmp = hexValue(str.charAt(0)); + tmp <<= HALF_BYTE; + tmp |= hexValue(str.charAt(1)); + byteArray[i] = (byte) tmp; } return byteArray; } - /** - * Converts a single hex digit into its decimal value. - */ + /** Converts a single hex digit into its decimal value. */ private static int hexValue(char hexChar) { - int digit = Character.digit(hexChar, 16); + int digit = Character.digit(hexChar, HEXADECIMAL); if (digit < 0) { throw new IllegalArgumentException("Invalid hex char " + hexChar); } else { diff --git a/library/java/net/openid/appauth/app2app/RedirectSession.java b/library/java/net/openid/appauth/app2app/RedirectSession.java index 47927a79..72fcfa2b 100644 --- a/library/java/net/openid/appauth/app2app/RedirectSession.java +++ b/library/java/net/openid/appauth/app2app/RedirectSession.java @@ -1,16 +1,28 @@ +/* + * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved. + * + * 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 net.openid.appauth.app2app; import android.content.Context; import android.net.Uri; - import androidx.annotation.NonNull; -import java.util.Set; import org.json.JSONArray; -/** - * Class to hold all important information to perform a secure redirection. - */ +import java.util.Set; + +/** Class to hold all important information to perform a secure redirection. */ class RedirectSession { private Context mContext; @@ -19,9 +31,9 @@ class RedirectSession { private Set mBaseCertFingerprints; private JSONArray mAssetLinksFile = null; - protected RedirectSession(@NonNull Context mContext, @NonNull Uri mUri) { - this.mContext = mContext; - this.mUri = mUri; + protected RedirectSession(@NonNull Context context, @NonNull Uri uri) { + this.mContext = context; + this.mUri = uri; } @NonNull @@ -55,15 +67,15 @@ protected Set getBaseCertFingerprints() { return mBaseCertFingerprints; } - protected void setBaseCertFingerprints(Set mBaseCertFingerprints) { - this.mBaseCertFingerprints = mBaseCertFingerprints; + protected void setBaseCertFingerprints(Set baseCertFingerprints) { + this.mBaseCertFingerprints = baseCertFingerprints; } public JSONArray getAssetLinksFile() { return mAssetLinksFile; } - public void setAssetLinksFile(JSONArray mAssetLinksFile) { - this.mAssetLinksFile = mAssetLinksFile; + public void setAssetLinksFile(JSONArray assetLinksFile) { + this.mAssetLinksFile = assetLinksFile; } } diff --git a/library/java/net/openid/appauth/app2app/SecureRedirection.java b/library/java/net/openid/appauth/app2app/SecureRedirection.java index 8c351577..0f155879 100644 --- a/library/java/net/openid/appauth/app2app/SecureRedirection.java +++ b/library/java/net/openid/appauth/app2app/SecureRedirection.java @@ -1,3 +1,17 @@ +/* + * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved. + * + * 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 net.openid.appauth.app2app; import android.content.Context; @@ -11,27 +25,23 @@ import android.os.AsyncTask; import android.util.Pair; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.browser.customtabs.CustomTabsIntent; -import java.io.IOException; -import java.io.InputStream; - -import java.net.HttpURLConnection; - import net.openid.appauth.Utils; import net.openid.appauth.browser.BrowserAllowList; import net.openid.appauth.browser.BrowserDescriptor; import net.openid.appauth.browser.BrowserSelector; import net.openid.appauth.browser.VersionedBrowserMatcher; import net.openid.appauth.connectivity.DefaultConnectionBuilder; - import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -40,8 +50,7 @@ public final class SecureRedirection { - private SecureRedirection() { - } + private SecureRedirection() {} /** * This method redirects an user securely from one app to another with a given URL. For this to @@ -52,25 +61,25 @@ public static void secureRedirection(@NonNull Context context, @NonNull Uri uri) getAssetLinksFile(new RedirectSession(context, uri)); } - /** - * This function retrieves the '/.well-known/assetlinks.json' file from the given domain. - */ + /** This function retrieves the '/.well-known/assetlinks.json' file from the given domain. */ private static void getAssetLinksFile(@NonNull final RedirectSession redirectSession) { new DownloadAssetLinksFile().execute(redirectSession); } - private static class DownloadAssetLinksFile extends - AsyncTask { + private static class DownloadAssetLinksFile + extends AsyncTask { @Override protected RedirectSession doInBackground(RedirectSession... redirectSessions) { RedirectSession redirectSession = redirectSessions[0]; - Uri uri = Uri.parse(redirectSession.getUri().getScheme() - + "://" - + redirectSession.getUri().getHost() - + ":" - + redirectSession.getUri().getPort() - + "/.well-known/assetlinks.json"); + Uri uri = + Uri.parse( + redirectSession.getUri().getScheme() + + "://" + + redirectSession.getUri().getHost() + + ":" + + redirectSession.getUri().getPort() + + "/.well-known/assetlinks.json"); InputStream is = null; try { @@ -97,18 +106,17 @@ protected RedirectSession doInBackground(RedirectSession... redirectSessions) { protected void onPostExecute(RedirectSession redirectSession) { if (redirectSession.getAssetLinksFile() != null) { JSONArray baseCertFingerprints = - findInstalledApp(redirectSession, redirectSession.getAssetLinksFile()); + findInstalledApp(redirectSession, redirectSession.getAssetLinksFile()); redirectSession.setBaseCertFingerprints( - CertificateFingerprintEncoding - .certFingerprintsToDecodedString( - baseCertFingerprints)); + CertificateFingerprintEncoding.certFingerprintsToDecodedString( + baseCertFingerprints)); doRedirection(redirectSession); } else { System.err.println( - "Failed to fetch '/.well-known/assetlinks.json' from domain " - + "'${redirectSession.uri.host}'\nError: ${error}"); + "Failed to fetch '/.well-known/assetlinks.json' from domain " + + "'${redirectSession.uri.host}'\nError: ${error}"); redirectToWeb(redirectSession.getContext(), redirectSession.getUri()); } } @@ -125,9 +133,9 @@ protected void onPostExecute(RedirectSession redirectSession) { */ @NonNull private static JSONArray findInstalledApp( - @NonNull RedirectSession redirectSession, @NonNull JSONArray assetLinks) { + @NonNull RedirectSession redirectSession, @NonNull JSONArray assetLinks) { Pair, Map> basePair = - getBaseValuesFromAssetLinksFile(assetLinks); + getBaseValuesFromAssetLinksFile(assetLinks); Set foundPackageNames = getPackageNamesForIntent(redirectSession); // Intersect the set of installed apps with the set of apps @@ -156,7 +164,7 @@ private static JSONArray findInstalledApp( */ @NonNull private static Pair, Map> getBaseValuesFromAssetLinksFile( - @NonNull JSONArray assetLinks) { + @NonNull JSONArray assetLinks) { Set basePackageNames = new HashSet<>(); Map baseCertFingerprints = new HashMap<>(); try { @@ -193,10 +201,10 @@ private static Set getPackageNamesForIntent(@NonNull RedirectSession red intent.addCategory(Intent.CATEGORY_BROWSABLE); List infos = - redirectSession - .getContext() - .getPackageManager() - .queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER); + redirectSession + .getContext() + .getPackageManager() + .queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER); Set packageNames = new HashSet<>(); for (ResolveInfo info : infos) { @@ -240,26 +248,26 @@ private static Set getSigningCertificates(@NonNull RedirectSession redir Signature[] signatures; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { SigningInfo signingInfo = - redirectSession - .getContext() - .getPackageManager() - .getPackageInfo( - redirectSession.getBasePackageName(), - PackageManager.GET_SIGNING_CERTIFICATES) - .signingInfo; + redirectSession + .getContext() + .getPackageManager() + .getPackageInfo( + redirectSession.getBasePackageName(), + PackageManager.GET_SIGNING_CERTIFICATES) + .signingInfo; signatures = signingInfo.getSigningCertificateHistory(); } else { signatures = - redirectSession - .getContext() - .getPackageManager() - .getPackageInfo( - redirectSession.getBasePackageName(), - PackageManager.GET_SIGNATURES) - .signatures; + redirectSession + .getContext() + .getPackageManager() + .getPackageInfo( + redirectSession.getBasePackageName(), + PackageManager.GET_SIGNATURES) + .signatures; } return BrowserDescriptor.generateSignatureHashes( - signatures, BrowserDescriptor.DIGEST_SHA_256); + signatures, BrowserDescriptor.DIGEST_SHA_256); } catch (PackageManager.NameNotFoundException excepetion) { return null; } @@ -271,7 +279,7 @@ private static Set getSigningCertificates(@NonNull RedirectSession redir */ @VisibleForTesting public static boolean matchHashes( - @NonNull Set certHashes0, @NonNull Set certHashes1) { + @NonNull Set certHashes0, @NonNull Set certHashes1) { return certHashes0.containsAll(certHashes1) && certHashes0.size() == certHashes1.size(); } @@ -288,27 +296,27 @@ public static void redirectToWeb(@NonNull Context context, @NonNull Uri uri) { * the integrity of this browser. It then opens the given uri in an Android Custom Tab. */ public static void redirectToWeb( - @NonNull Context context, @NonNull Uri uri, int additionalFlags, int toolbarColor) { + @NonNull Context context, @NonNull Uri uri, int additionalFlags, int toolbarColor) { CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); builder.setToolbarColor(toolbarColor); CustomTabsIntent customTabsIntent = builder.build(); BrowserDescriptor browserDescriptor = - BrowserSelector.select( - context, - new BrowserAllowList( - VersionedBrowserMatcher.CHROME_CUSTOM_TAB, - VersionedBrowserMatcher.CHROME_BROWSER, - VersionedBrowserMatcher.FIREFOX_CUSTOM_TAB, - VersionedBrowserMatcher.FIREFOX_BROWSER, - VersionedBrowserMatcher.SAMSUNG_CUSTOM_TAB, - VersionedBrowserMatcher.SAMSUNG_BROWSER)); + BrowserSelector.select( + context, + new BrowserAllowList( + VersionedBrowserMatcher.CHROME_CUSTOM_TAB, + VersionedBrowserMatcher.CHROME_BROWSER, + VersionedBrowserMatcher.FIREFOX_CUSTOM_TAB, + VersionedBrowserMatcher.FIREFOX_BROWSER, + VersionedBrowserMatcher.SAMSUNG_CUSTOM_TAB, + VersionedBrowserMatcher.SAMSUNG_BROWSER)); if (browserDescriptor != null) { customTabsIntent - .intent - .setPackage(browserDescriptor.packageName) - .setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | additionalFlags); + .intent + .setPackage(browserDescriptor.packageName) + .setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | additionalFlags); customTabsIntent.launchUrl(context, uri); } else { Toast.makeText(context, "Could not find a browser", Toast.LENGTH_SHORT).show(); diff --git a/library/java/net/openid/appauth/app2app/package-info.java b/library/java/net/openid/appauth/app2app/package-info.java new file mode 100644 index 00000000..8390f501 --- /dev/null +++ b/library/java/net/openid/appauth/app2app/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved. + * + * 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. + */ + +/** + * This package provides methods to securely redirect a user from one app to another + * in an app2app OAuth 2.0 flow. + */ +package net.openid.appauth.app2app; diff --git a/library/java/net/openid/appauth/browser/BrowserDescriptor.java b/library/java/net/openid/appauth/browser/BrowserDescriptor.java index fc322753..e1a77985 100644 --- a/library/java/net/openid/appauth/browser/BrowserDescriptor.java +++ b/library/java/net/openid/appauth/browser/BrowserDescriptor.java @@ -24,9 +24,7 @@ import java.util.HashSet; import java.util.Set; -/** - * Represents a browser that may be used for an authorization flow. - */ +/** Represents a browser that may be used for an authorization flow. */ public class BrowserDescriptor { // See: http://stackoverflow.com/a/2816747 @@ -35,33 +33,27 @@ public class BrowserDescriptor { public static final String DIGEST_SHA_256 = "SHA-256"; public static final String DIGEST_SHA_512 = "SHA-512"; - /** - * The package name of the browser app. - */ + /** The package name of the browser app. */ public final String packageName; /** - * The set of {@link android.content.pm.Signature signatures} of the browser app, - * which have been hashed with SHA-512, and Base-64 URL-safe encoded. + * The set of {@link android.content.pm.Signature signatures} of the browser app, which have + * been hashed with SHA-512, and Base-64 URL-safe encoded. */ public final Set signatureHashes; - /** - * The version string of the browser app. - */ + /** The version string of the browser app. */ public final String version; - /** - * Whether it is intended that the browser will be used via a custom tab. - */ + /** Whether it is intended that the browser will be used via a custom tab. */ public final Boolean useCustomTab; /** - * Creates a description of a browser from a {@link PackageInfo} object returned from the - * {@link android.content.pm.PackageManager}. The object is expected to include the - * signatures of the app, which can be retrieved with the - * {@link android.content.pm.PackageManager#GET_SIGNATURES GET_SIGNATURES} flag when - * calling {@link android.content.pm.PackageManager#getPackageInfo(String, int)}. + * Creates a description of a browser from a {@link PackageInfo} object returned from the {@link + * android.content.pm.PackageManager}. The object is expected to include the signatures of the + * app, which can be retrieved with the {@link android.content.pm.PackageManager#GET_SIGNATURES + * GET_SIGNATURES} flag when calling {@link + * android.content.pm.PackageManager#getPackageInfo(String, int)}. */ public BrowserDescriptor(@NonNull PackageInfo packageInfo, boolean useCustomTab) { this( @@ -73,19 +65,16 @@ public BrowserDescriptor(@NonNull PackageInfo packageInfo, boolean useCustomTab) /** * Creates a description of a browser from the core properties that are frequently used to - * decide whether a browser can be used for an authorization flow. In most cases, it is - * more convenient to use the other variant of the constructor that consumes a - * {@link PackageInfo} object provided by the package manager. + * decide whether a browser can be used for an authorization flow. In most cases, it is more + * convenient to use the other variant of the constructor that consumes a {@link PackageInfo} + * object provided by the package manager. * - * @param packageName - * The Android package name of the browser. - * @param signatureHashes - * The set of SHA-512, Base64 url safe encoded signatures for the app. This can be - * generated for a signature by calling {@link #generateSignatureHash(Signature)}. - * @param version - * The version name of the browser. - * @param useCustomTab - * Whether it is intended to use the browser as a custom tab. + * @param packageName The Android package name of the browser. + * @param signatureHashes The set of SHA-512, Base64 url safe encoded signatures for the app. + * This can be generated for a signature by calling {@link + * #generateSignatureHash(Signature)}. + * @param version The version name of the browser. + * @param useCustomTab Whether it is intended to use the browser as a custom tab. */ public BrowserDescriptor( @NonNull String packageName, @@ -99,16 +88,12 @@ public BrowserDescriptor( } /** - * Creates a copy of this browser descriptor, changing the intention to use it as a custom - * tab to the specified value. + * Creates a copy of this browser descriptor, changing the intention to use it as a custom tab + * to the specified value. */ @NonNull public BrowserDescriptor changeUseCustomTab(boolean newUseCustomTabValue) { - return new BrowserDescriptor( - packageName, - signatureHashes, - version, - newUseCustomTabValue); + return new BrowserDescriptor(packageName, signatureHashes, version, newUseCustomTabValue); } @Override @@ -142,24 +127,22 @@ public int hashCode() { return hash; } - /** - * Generates a SHA-* hash, Base64 url-safe encoded, from a {@link Signature}. - */ + /** Generates a SHA-* hash, Base64 url-safe encoded, from a {@link Signature}. */ @NonNull - public static String generateSignatureHash(@NonNull Signature signature, @NonNull String digestSHA) { + public static String generateSignatureHash( + @NonNull Signature signature, @NonNull String digestSha) { try { - MessageDigest digest = MessageDigest.getInstance(digestSHA); + MessageDigest digest = MessageDigest.getInstance(digestSha); byte[] hashBytes = digest.digest(signature.toByteArray()); return Base64.encodeToString(hashBytes, Base64.URL_SAFE | Base64.NO_WRAP); } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException( - "Platform does not support" + digestSHA + " hashing"); + throw new IllegalStateException("Platform does not support" + digestSha + " hashing"); } } /** - * Generates a set of SHA-512, Base64 url-safe encoded signature hashes from the provided - * array of signatures. + * Generates a set of SHA-512, Base64 url-safe encoded signature hashes from the provided array + * of signatures. */ @NonNull public static Set generateSignatureHashes(@NonNull Signature[] signatures) { @@ -167,14 +150,15 @@ public static Set generateSignatureHashes(@NonNull Signature[] signature } /** - * Generates a set of SHA-*, Base64 url-safe encoded signature hashes from the provided - * array of signatures. + * Generates a set of SHA-*, Base64 url-safe encoded signature hashes from the provided array of + * signatures. */ @NonNull - public static Set generateSignatureHashes(@NonNull Signature[] signatures, @NonNull String digestSHA) { + public static Set generateSignatureHashes( + @NonNull Signature[] signatures, @NonNull String digestSha) { Set signatureHashes = new HashSet<>(); for (Signature signature : signatures) { - signatureHashes.add(generateSignatureHash(signature, digestSHA)); + signatureHashes.add(generateSignatureHash(signature, digestSha)); } return signatureHashes;