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 new file mode 100644 index 00000000..6b0ae4b3 --- /dev/null +++ b/library/java/net/openid/appauth/app2app/CertificateFingerprintEncoding.java @@ -0,0 +1,89 @@ +/* + * 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; +import org.json.JSONException; + +import java.util.HashSet; +import java.util.Set; + +final class 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 + * and decodes it in the correct way to compare the hashes with the ones found on the device. + */ + @NonNull + protected static Set certFingerprintsToDecodedString( + @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, DECIMAL); + 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:... + */ + @NonNull + private static byte[] hexStringToByteArray(@NonNull String hexString) { + String[] hexValues = hexString.split(":"); + byte[] byteArray = new byte[hexValues.length]; + String str; + int tmp = 0; + + for (int i = 0; i < hexValues.length; ++i) { + str = hexValues[i]; + 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. */ + private static int hexValue(char hexChar) { + int digit = Character.digit(hexChar, HEXADECIMAL); + 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..72fcfa2b --- /dev/null +++ b/library/java/net/openid/appauth/app2app/RedirectSession.java @@ -0,0 +1,81 @@ +/* + * 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 org.json.JSONArray; + +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; + private JSONArray mAssetLinksFile = null; + + protected RedirectSession(@NonNull Context context, @NonNull Uri uri) { + this.mContext = context; + this.mUri = uri; + } + + @NonNull + protected Context getContext() { + return mContext; + } + + protected void setContext(@NonNull Context context) { + this.mContext = context; + } + + @NonNull + protected Uri getUri() { + return mUri; + } + + protected void setUri(@NonNull Uri uri) { + this.mUri = uri; + } + + @NonNull + protected String getBasePackageName() { + return mBasePackageName; + } + + protected void setBasePackageName(@NonNull String basePackageName) { + this.mBasePackageName = basePackageName; + } + + protected Set getBaseCertFingerprints() { + return mBaseCertFingerprints; + } + + protected void setBaseCertFingerprints(Set baseCertFingerprints) { + this.mBaseCertFingerprints = baseCertFingerprints; + } + + public JSONArray getAssetLinksFile() { + return 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 new file mode 100644 index 00000000..0f155879 --- /dev/null +++ b/library/java/net/openid/appauth/app2app/SecureRedirection.java @@ -0,0 +1,325 @@ +/* + * 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.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.os.AsyncTask; +import android.util.Pair; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.browser.customtabs.CustomTabsIntent; + +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; +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(@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(@NonNull final RedirectSession redirectSession) { + new DownloadAssetLinksFile().execute(redirectSession); + } + + 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"); + + 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()); + } + } + } + + /** + * 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 + */ + @NonNull + private static JSONArray findInstalledApp( + @NonNull RedirectSession redirectSession, @NonNull 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 + */ + @NonNull + private static Pair, Map> getBaseValuesFromAssetLinksFile( + @NonNull 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 + */ + @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 + */ + 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(@NonNull 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(@NonNull 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(@NonNull 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( + @NonNull Set certHashes0, @NonNull 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(@NonNull Context context, @NonNull 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( + @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)); + + 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/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 d9f85f5a..e1a77985 100644 --- a/library/java/net/openid/appauth/browser/BrowserDescriptor.java +++ b/library/java/net/openid/appauth/browser/BrowserDescriptor.java @@ -24,43 +24,36 @@ 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 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. - */ + /** 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( @@ -72,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, @@ -98,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 @@ -141,30 +127,38 @@ public int hashCode() { return hash; } - /** - * 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"); + 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) { + 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(); + } +}