From cf98935daac0995d721bf7ef7f35cbd71f0eb351 Mon Sep 17 00:00:00 2001 From: Rosalyn Tan Date: Fri, 1 Apr 2022 14:53:22 -0700 Subject: [PATCH 1/8] Initial App Check <> Play Integrity commit (#3607) * Initial commit of App Check Play Integrity files. * Added skeleton code for PlayIntegrityAppCheckProviderFactory and PlayIntegrityAppCheckProvider. * Set version in gradle.properties to 16.0.0-beta01 for now. * Updated wrong gradle.properties file in last commit. --- .../firebase-appcheck-playintegrity.gradle | 51 +++++++++++++++++++ .../gradle.properties | 1 + .../src/main/AndroidManifest.xml | 20 ++++++++ .../PlayIntegrityAppCheckProviderFactory.java | 46 +++++++++++++++++ .../PlayIntegrityAppCheckProvider.java | 34 +++++++++++++ subprojects.cfg | 1 + 6 files changed, 153 insertions(+) create mode 100644 appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle create mode 100644 appcheck/firebase-appcheck-playintegrity/gradle.properties create mode 100644 appcheck/firebase-appcheck-playintegrity/src/main/AndroidManifest.xml create mode 100644 appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java create mode 100644 appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java diff --git a/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle b/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle new file mode 100644 index 00000000000..4e02b8282fa --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle @@ -0,0 +1,51 @@ +// Copyright 2022 Google LLC +// +// 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. + +plugins { + id 'firebase-library' +} + +firebaseLibrary { + publishSources = true +} + +android { + adbOptions { + timeOutInMs 60 * 1000 + } + + compileSdkVersion project.targetSdkVersion + defaultConfig { + targetSdkVersion project.targetSdkVersion + minSdkVersion project.minSdkVersion + versionName version + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + testOptions.unitTests.includeAndroidResources = false +} + +dependencies { + implementation project(':firebase-common') + implementation project(':firebase-components') + implementation project(':appcheck:firebase-appcheck') + implementation 'com.google.android.gms:play-services-base:18.0.1' + implementation 'com.google.android.gms:play-services-tasks:18.0.1' + + testImplementation 'junit:junit:4.13-beta-2' +} diff --git a/appcheck/firebase-appcheck-playintegrity/gradle.properties b/appcheck/firebase-appcheck-playintegrity/gradle.properties new file mode 100644 index 00000000000..29ae9e151c9 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/gradle.properties @@ -0,0 +1 @@ +version=16.0.0-beta01 diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/AndroidManifest.xml b/appcheck/firebase-appcheck-playintegrity/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..3599f8e6755 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java new file mode 100644 index 00000000000..4c08c783039 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java @@ -0,0 +1,46 @@ +// Copyright 2022 Google LLC +// +// 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 com.google.firebase.appcheck.playintegrity; + +import androidx.annotation.NonNull; +import com.google.firebase.FirebaseApp; +import com.google.firebase.appcheck.AppCheckProvider; +import com.google.firebase.appcheck.AppCheckProviderFactory; +import com.google.firebase.appcheck.playintegrity.internal.PlayIntegrityAppCheckProvider; + +/** + * Implementation of an {@link AppCheckProviderFactory} that builds {@link + * PlayIntegrityAppCheckProvider}s. This is the default implementation. + */ +public class PlayIntegrityAppCheckProviderFactory implements AppCheckProviderFactory { + + private static final PlayIntegrityAppCheckProviderFactory instance = + new PlayIntegrityAppCheckProviderFactory(); + + /** + * Gets an instance of this class for installation into a {@link + * com.google.firebase.appcheck.FirebaseAppCheck} instance. + */ + @NonNull + public static PlayIntegrityAppCheckProviderFactory getInstance() { + return instance; + } + + @NonNull + @Override + public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) { + return new PlayIntegrityAppCheckProvider(); + } +} diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java new file mode 100644 index 00000000000..22d77a846cf --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java @@ -0,0 +1,34 @@ +// Copyright 2022 Google LLC +// +// 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 com.google.firebase.appcheck.playintegrity.internal; + +import androidx.annotation.NonNull; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.firebase.FirebaseException; +import com.google.firebase.appcheck.AppCheckProvider; +import com.google.firebase.appcheck.AppCheckToken; + +public class PlayIntegrityAppCheckProvider implements AppCheckProvider { + + public PlayIntegrityAppCheckProvider() {} + + @NonNull + @Override + public Task getToken() { + // TODO(rosalyntan): Implement this. + return Tasks.forException(new FirebaseException("Unimplemented")); + } +} diff --git a/subprojects.cfg b/subprojects.cfg index f3d61498230..b8e1dd646c1 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -2,6 +2,7 @@ appcheck appcheck:firebase-appcheck-debug-testing appcheck:firebase-appcheck-debug appcheck:firebase-appcheck-interop +appcheck:firebase-appcheck-playintegrity appcheck:firebase-appcheck-safetynet appcheck:firebase-appcheck appcheck:firebase-appcheck:test-app From 295c8f58d3426d9be65c2b484c1d7c1e62bfee08 Mon Sep 17 00:00:00 2001 From: Rosalyn Tan Date: Mon, 4 Apr 2022 11:42:03 -0700 Subject: [PATCH 2/8] Implement Play Integrity token exchange. (#3613) * Implement Play Integrity exchange. * Update and add unit tests. * Add @NonNull annotation. --- .../firebase-appcheck-playintegrity.gradle | 7 +- .../PlayIntegrityAppCheckProviderFactory.java | 2 +- .../ExchangePlayIntegrityTokenRequest.java | 43 +++++++ .../PlayIntegrityAppCheckProvider.java | 55 ++++++++- ...yIntegrityAppCheckProviderFactoryTest.java | 37 ++++++ ...ExchangePlayIntegrityTokenRequestTest.java | 42 +++++++ .../PlayIntegrityAppCheckProviderTest.java | 107 ++++++++++++++++++ .../appcheck/internal/NetworkClient.java | 7 +- .../appcheck/internal/NetworkClientTest.java | 61 +++++++++- 9 files changed, 350 insertions(+), 11 deletions(-) create mode 100644 appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequest.java create mode 100644 appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactoryTest.java create mode 100644 appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequestTest.java create mode 100644 appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java diff --git a/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle b/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle index 4e02b8282fa..0733ab44ace 100644 --- a/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle +++ b/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle @@ -46,6 +46,11 @@ dependencies { implementation project(':appcheck:firebase-appcheck') implementation 'com.google.android.gms:play-services-base:18.0.1' implementation 'com.google.android.gms:play-services-tasks:18.0.1' + implementation 'com.google.android.play:integrity:1.0.1' - testImplementation 'junit:junit:4.13-beta-2' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:3.4.6' + testImplementation "com.google.truth:truth:$googleTruthVersion" + testImplementation "org.robolectric:robolectric:$robolectricVersion" + testImplementation 'androidx.test:core:1.4.0' } diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java index 4c08c783039..743092d2892 100644 --- a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java @@ -41,6 +41,6 @@ public static PlayIntegrityAppCheckProviderFactory getInstance() { @NonNull @Override public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) { - return new PlayIntegrityAppCheckProvider(); + return new PlayIntegrityAppCheckProvider(firebaseApp); } } diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequest.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequest.java new file mode 100644 index 00000000000..7f3b38cf60c --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequest.java @@ -0,0 +1,43 @@ +// Copyright 2022 Google LLC +// +// 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 com.google.firebase.appcheck.playintegrity.internal; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Client-side model of the ExchangePlayIntegrityTokenRequest payload from the Firebase App Check + * Token Exchange API. + */ +public class ExchangePlayIntegrityTokenRequest { + + @VisibleForTesting static final String PLAY_INTEGRITY_TOKEN_KEY = "playIntegrityToken"; + + private final String playIntegrityToken; + + public ExchangePlayIntegrityTokenRequest(@NonNull String playIntegrityToken) { + this.playIntegrityToken = playIntegrityToken; + } + + @NonNull + public String toJsonString() throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(PLAY_INTEGRITY_TOKEN_KEY, playIntegrityToken); + + return jsonObject.toString(); + } +} diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java index 22d77a846cf..5f99d5e1228 100644 --- a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java @@ -15,20 +15,67 @@ package com.google.firebase.appcheck.playintegrity.internal; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; -import com.google.firebase.FirebaseException; +import com.google.firebase.FirebaseApp; import com.google.firebase.appcheck.AppCheckProvider; import com.google.firebase.appcheck.AppCheckToken; +import com.google.firebase.appcheck.internal.AppCheckTokenResponse; +import com.google.firebase.appcheck.internal.DefaultAppCheckToken; +import com.google.firebase.appcheck.internal.NetworkClient; +import com.google.firebase.appcheck.internal.RetryManager; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class PlayIntegrityAppCheckProvider implements AppCheckProvider { - public PlayIntegrityAppCheckProvider() {} + private static final String UTF_8 = "UTF-8"; + + private final NetworkClient networkClient; + private final ExecutorService backgroundExecutor; + private final RetryManager retryManager; + + public PlayIntegrityAppCheckProvider(@NonNull FirebaseApp firebaseApp) { + this(new NetworkClient(firebaseApp), Executors.newCachedThreadPool(), new RetryManager()); + } + + @VisibleForTesting + PlayIntegrityAppCheckProvider( + @NonNull NetworkClient networkClient, + @NonNull ExecutorService backgroundExecutor, + @NonNull RetryManager retryManager) { + this.networkClient = networkClient; + this.backgroundExecutor = backgroundExecutor; + this.retryManager = retryManager; + } @NonNull @Override public Task getToken() { - // TODO(rosalyntan): Implement this. - return Tasks.forException(new FirebaseException("Unimplemented")); + // TODO(rosalyntan): Obtain the Play Integrity challenge nonce. + ExchangePlayIntegrityTokenRequest request = + new ExchangePlayIntegrityTokenRequest("placeholder"); + Task networkTask = + Tasks.call( + backgroundExecutor, + () -> + networkClient.exchangeAttestationForAppCheckToken( + request.toJsonString().getBytes(UTF_8), + NetworkClient.PLAY_INTEGRITY, + retryManager)); + return networkTask.continueWithTask( + new Continuation>() { + @Override + public Task then(@NonNull Task task) { + if (task.isSuccessful()) { + return Tasks.forResult( + DefaultAppCheckToken.constructFromAppCheckTokenResponse(task.getResult())); + } + // TODO: Surface more error details. + return Tasks.forException(task.getException()); + } + }); } } diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactoryTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactoryTest.java new file mode 100644 index 00000000000..ed1ebddda96 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactoryTest.java @@ -0,0 +1,37 @@ +// Copyright 2022 Google LLC +// +// 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 com.google.firebase.appcheck.playintegrity; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for {@link PlayIntegrityAppCheckProviderFactory}. */ +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class PlayIntegrityAppCheckProviderFactoryTest { + + @Test + public void testGetInstance_callTwice_sameInstance() { + PlayIntegrityAppCheckProviderFactory firstInstance = + PlayIntegrityAppCheckProviderFactory.getInstance(); + PlayIntegrityAppCheckProviderFactory secondInstance = + PlayIntegrityAppCheckProviderFactory.getInstance(); + assertThat(firstInstance).isEqualTo(secondInstance); + } +} diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequestTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequestTest.java new file mode 100644 index 00000000000..8c01f97f887 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/ExchangePlayIntegrityTokenRequestTest.java @@ -0,0 +1,42 @@ +// Copyright 2022 Google LLC +// +// 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 com.google.firebase.appcheck.playintegrity.internal; + +import static com.google.common.truth.Truth.assertThat; + +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for {@link ExchangePlayIntegrityTokenRequest}. */ +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class ExchangePlayIntegrityTokenRequestTest { + private static final String PLAY_INTEGRITY_TOKEN = "playIntegrityToken"; + + @Test + public void toJsonString_expectSerialized() throws Exception { + ExchangePlayIntegrityTokenRequest exchangePlayIntegrityTokenRequest = + new ExchangePlayIntegrityTokenRequest(PLAY_INTEGRITY_TOKEN); + + String jsonString = exchangePlayIntegrityTokenRequest.toJsonString(); + JSONObject jsonObject = new JSONObject(jsonString); + + assertThat(jsonObject.getString(ExchangePlayIntegrityTokenRequest.PLAY_INTEGRITY_TOKEN_KEY)) + .isEqualTo(PLAY_INTEGRITY_TOKEN); + } +} diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java new file mode 100644 index 00000000000..faa5c6ee702 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java @@ -0,0 +1,107 @@ +// Copyright 2022 Google LLC +// +// 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 com.google.firebase.appcheck.playintegrity.internal; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.android.gms.tasks.Task; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.appcheck.AppCheckToken; +import com.google.firebase.appcheck.internal.AppCheckTokenResponse; +import com.google.firebase.appcheck.internal.DefaultAppCheckToken; +import com.google.firebase.appcheck.internal.NetworkClient; +import com.google.firebase.appcheck.internal.RetryManager; +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for {@link PlayIntegrityAppCheckProvider}. */ +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class PlayIntegrityAppCheckProviderTest { + + private static final String ATTESTATION_TOKEN = "token"; + private static final String TIME_TO_LIVE = "3600s"; + + private ExecutorService backgroundExecutor = MoreExecutors.newDirectExecutorService(); + @Mock private NetworkClient mockNetworkClient; + @Mock private RetryManager mockRetryManager; + @Mock private AppCheckTokenResponse mockAppCheckTokenResponse; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testPublicConstructor_nullFirebaseApp_expectThrows() { + assertThrows( + NullPointerException.class, + () -> { + new PlayIntegrityAppCheckProvider(null); + }); + } + + @Test + public void getToken_onSuccess_setsTaskResult() throws Exception { + when(mockNetworkClient.exchangeAttestationForAppCheckToken( + any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager))) + .thenReturn(mockAppCheckTokenResponse); + when(mockAppCheckTokenResponse.getAttestationToken()).thenReturn(ATTESTATION_TOKEN); + when(mockAppCheckTokenResponse.getTimeToLive()).thenReturn(TIME_TO_LIVE); + + PlayIntegrityAppCheckProvider provider = + new PlayIntegrityAppCheckProvider(mockNetworkClient, backgroundExecutor, mockRetryManager); + Task task = provider.getToken(); + + verify(mockNetworkClient) + .exchangeAttestationForAppCheckToken( + any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager)); + + AppCheckToken token = task.getResult(); + assertThat(token).isInstanceOf(DefaultAppCheckToken.class); + assertThat(token.getToken()).isEqualTo(ATTESTATION_TOKEN); + } + + @Test + public void getToken_onFailure_setsTaskException() throws Exception { + when(mockNetworkClient.exchangeAttestationForAppCheckToken( + any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager))) + .thenThrow(new IOException()); + + PlayIntegrityAppCheckProvider provider = + new PlayIntegrityAppCheckProvider(mockNetworkClient, backgroundExecutor, mockRetryManager); + Task task = provider.getToken(); + + verify(mockNetworkClient) + .exchangeAttestationForAppCheckToken( + any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager)); + + assertThat(task.isSuccessful()).isFalse(); + Exception exception = task.getException(); + assertThat(exception).isInstanceOf(IOException.class); + } +} diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java index 4cf4de5c602..81c8d7a0b94 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java @@ -55,6 +55,8 @@ public class NetworkClient { "https://firebaseappcheck.googleapis.com/v1beta/projects/%s/apps/%s:exchangeSafetyNetToken?key=%s"; private static final String DEBUG_EXCHANGE_URL_TEMPLATE = "https://firebaseappcheck.googleapis.com/v1beta/projects/%s/apps/%s:exchangeDebugToken?key=%s"; + private static final String PLAY_INTEGRITY_EXCHANGE_URL_TEMPLATE = + "https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:exchangePlayIntegrityToken?key=%s"; private static final String CONTENT_TYPE = "Content-Type"; private static final String APPLICATION_JSON = "application/json"; private static final String UTF_8 = "UTF-8"; @@ -69,12 +71,13 @@ public class NetworkClient { private final Provider heartBeatControllerProvider; @Retention(RetentionPolicy.SOURCE) - @IntDef({UNKNOWN, SAFETY_NET, DEBUG}) + @IntDef({UNKNOWN, SAFETY_NET, DEBUG, PLAY_INTEGRITY}) public @interface AttestationTokenType {} public static final int UNKNOWN = 0; public static final int SAFETY_NET = 1; public static final int DEBUG = 2; + public static final int PLAY_INTEGRITY = 3; public NetworkClient(@NonNull FirebaseApp firebaseApp) { this( @@ -203,6 +206,8 @@ private static String getUrlTemplate(@AttestationTokenType int tokenType) { return SAFETY_NET_EXCHANGE_URL_TEMPLATE; case DEBUG: return DEBUG_EXCHANGE_URL_TEMPLATE; + case PLAY_INTEGRITY: + return PLAY_INTEGRITY_EXCHANGE_URL_TEMPLATE; default: throw new IllegalArgumentException("Unknown token type."); } diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java index 3d9c5965480..a105dbf7042 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java @@ -69,6 +69,8 @@ public class NetworkClientTest { "https://firebaseappcheck.googleapis.com/v1beta/projects/projectId/apps/appId:exchangeSafetyNetToken?key=apiKey"; private static final String DEBUG_EXPECTED_URL = "https://firebaseappcheck.googleapis.com/v1beta/projects/projectId/apps/appId:exchangeDebugToken?key=apiKey"; + private static final String PLAY_INTEGRITY_EXPECTED_URL = + "https://firebaseappcheck.googleapis.com/v1/projects/projectId/apps/appId:exchangePlayIntegrityToken?key=apiKey"; private static final String JSON_REQUEST = "jsonRequest"; private static final int SUCCESS_CODE = 200; private static final int ERROR_CODE = 404; @@ -211,17 +213,53 @@ public void exchangeDebugToken_errorResponse_throwsException() throws Exception } @Test - public void exchangeAttestation_heartbeatNone_doesNotAttachHeader() throws Exception { + public void exchangePlayIntegrityToken_successResponse_returnsAppCheckTokenResponse() + throws Exception { JSONObject responseBodyJson = createAttestationResponse(); when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream); when(mockHttpUrlConnection.getInputStream()) .thenReturn(new ByteArrayInputStream(responseBodyJson.toString().getBytes())); when(mockHttpUrlConnection.getResponseCode()).thenReturn(SUCCESS_CODE); - // The heartbeat request header should not be attached when the heartbeat is HeartBeat.NONE. - networkClient.exchangeAttestationForAppCheckToken( - JSON_REQUEST.getBytes(), NetworkClient.SAFETY_NET, mockRetryManager); + AppCheckTokenResponse tokenResponse = + networkClient.exchangeAttestationForAppCheckToken( + JSON_REQUEST.getBytes(), NetworkClient.PLAY_INTEGRITY, mockRetryManager); + assertThat(tokenResponse.getAttestationToken()).isEqualTo(ATTESTATION_TOKEN); + assertThat(tokenResponse.getTimeToLive()).isEqualTo(TIME_TO_LIVE); + + URL expectedUrl = new URL(PLAY_INTEGRITY_EXPECTED_URL); + verify(networkClient).createHttpUrlConnection(expectedUrl); + verify(mockOutputStream) + .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length); + verify(mockRetryManager, never()).updateBackoffOnFailure(anyInt()); + verify(mockRetryManager).resetBackoffOnSuccess(); + verifyRequestHeaders(); + } + + @Test + public void exchangePlayIntegrityToken_errorResponse_throwsException() throws Exception { + JSONObject responseBodyJson = createHttpErrorResponse(); + + when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream); + when(mockHttpUrlConnection.getErrorStream()) + .thenReturn(new ByteArrayInputStream(responseBodyJson.toString().getBytes())); + when(mockHttpUrlConnection.getResponseCode()).thenReturn(ERROR_CODE); + + FirebaseException exception = + assertThrows( + FirebaseException.class, + () -> + networkClient.exchangeAttestationForAppCheckToken( + JSON_REQUEST.getBytes(), NetworkClient.PLAY_INTEGRITY, mockRetryManager)); + + assertThat(exception.getMessage()).contains(ERROR_MESSAGE); + URL expectedUrl = new URL(PLAY_INTEGRITY_EXPECTED_URL); + verify(networkClient).createHttpUrlConnection(expectedUrl); + verify(mockOutputStream) + .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length); + verify(mockRetryManager).updateBackoffOnFailure(ERROR_CODE); + verify(mockRetryManager, never()).resetBackoffOnSuccess(); verifyRequestHeaders(); } @@ -237,6 +275,21 @@ public void exchangeUnknownAttestation_throwsException() { verify(mockRetryManager, never()).resetBackoffOnSuccess(); } + @Test + public void exchangeAttestation_heartbeatNone_doesNotAttachHeader() throws Exception { + JSONObject responseBodyJson = createAttestationResponse(); + + when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream); + when(mockHttpUrlConnection.getInputStream()) + .thenReturn(new ByteArrayInputStream(responseBodyJson.toString().getBytes())); + when(mockHttpUrlConnection.getResponseCode()).thenReturn(SUCCESS_CODE); + // The heartbeat request header should not be attached when the heartbeat is HeartBeat.NONE. + networkClient.exchangeAttestationForAppCheckToken( + JSON_REQUEST.getBytes(), NetworkClient.SAFETY_NET, mockRetryManager); + + verifyRequestHeaders(); + } + @Test public void exchangeAttestation_cannotRetry_throwsException() { when(mockRetryManager.canRetry()).thenReturn(false); From c32c180f1aa39c8e098d5d724b270c331906e33b Mon Sep 17 00:00:00 2001 From: Rosalyn Tan Date: Thu, 7 Apr 2022 10:52:09 -0700 Subject: [PATCH 3/8] Implement Play Integrity attestation flow. (#3618) * Add method to call the `GeneratePlayIntegrityChallenge` endpoint to `NetworkClient`. * Implement Play Integrity attestation flow. * Add project number to IntegrityTokenRequest. * Fix `PlayIntegrityAppCheckProviderTest`s. * Add unit tests. * Address review comments. --- ...GeneratePlayIntegrityChallengeRequest.java | 35 +++++ ...eneratePlayIntegrityChallengeResponse.java | 62 ++++++++ .../PlayIntegrityAppCheckProvider.java | 80 ++++++++-- ...ratePlayIntegrityChallengeRequestTest.java | 37 +++++ ...atePlayIntegrityChallengeResponseTest.java | 64 ++++++++ .../PlayIntegrityAppCheckProviderTest.java | 147 ++++++++++++++++-- .../appcheck/internal/NetworkClient.java | 27 +++- .../appcheck/internal/NetworkClientTest.java | 71 ++++++++- 8 files changed, 491 insertions(+), 32 deletions(-) create mode 100644 appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequest.java create mode 100644 appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponse.java create mode 100644 appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequestTest.java create mode 100644 appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponseTest.java diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequest.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequest.java new file mode 100644 index 00000000000..533ecb347e2 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequest.java @@ -0,0 +1,35 @@ +// Copyright 2022 Google LLC +// +// 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 com.google.firebase.appcheck.playintegrity.internal; + +import androidx.annotation.NonNull; +import org.json.JSONObject; + +/** + * Client-side model of the GeneratePlayIntegrityChallengeRequest payload from the Firebase App + * Check Token Exchange API. + */ +public class GeneratePlayIntegrityChallengeRequest { + + public GeneratePlayIntegrityChallengeRequest() {} + + @NonNull + public String toJsonString() { + JSONObject jsonObject = new JSONObject(); + + // GeneratePlayIntegrityChallenge takes an empty POST body since the app ID is in the URL. + return jsonObject.toString(); + } +} diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponse.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponse.java new file mode 100644 index 00000000000..882e2523210 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponse.java @@ -0,0 +1,62 @@ +// Copyright 2022 Google LLC +// +// 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 com.google.firebase.appcheck.playintegrity.internal; + +import static com.google.android.gms.common.internal.Preconditions.checkNotNull; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Client-side model of the GeneratePlayIntegrityChallengeResponse payload from the Firebase App + * Check Token Exchange API. + */ +public class GeneratePlayIntegrityChallengeResponse { + + @VisibleForTesting static final String CHALLENGE_KEY = "challenge"; + @VisibleForTesting static final String TIME_TO_LIVE_KEY = "ttl"; + + private String challenge; + private String timeToLive; + + @NonNull + public static GeneratePlayIntegrityChallengeResponse fromJsonString(@NonNull String jsonString) + throws JSONException { + JSONObject jsonObject = new JSONObject(jsonString); + String challenge = jsonObject.optString(CHALLENGE_KEY, null); + String timeToLive = jsonObject.optString(TIME_TO_LIVE_KEY, null); + return new GeneratePlayIntegrityChallengeResponse(challenge, timeToLive); + } + + private GeneratePlayIntegrityChallengeResponse( + @NonNull String challenge, @NonNull String timeToLive) { + checkNotNull(challenge); + checkNotNull(timeToLive); + this.challenge = challenge; + this.timeToLive = timeToLive; + } + + @NonNull + public String getChallenge() { + return challenge; + } + + @NonNull + public String getTimeToLive() { + return timeToLive; + } +} diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java index 5f99d5e1228..2f3cfee4f7f 100644 --- a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java @@ -19,6 +19,10 @@ import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; +import com.google.android.play.core.integrity.IntegrityManager; +import com.google.android.play.core.integrity.IntegrityManagerFactory; +import com.google.android.play.core.integrity.IntegrityTokenRequest; +import com.google.android.play.core.integrity.IntegrityTokenResponse; import com.google.firebase.FirebaseApp; import com.google.firebase.appcheck.AppCheckProvider; import com.google.firebase.appcheck.AppCheckToken; @@ -33,19 +37,30 @@ public class PlayIntegrityAppCheckProvider implements AppCheckProvider { private static final String UTF_8 = "UTF-8"; + private final String projectNumber; + private final IntegrityManager integrityManager; private final NetworkClient networkClient; private final ExecutorService backgroundExecutor; private final RetryManager retryManager; public PlayIntegrityAppCheckProvider(@NonNull FirebaseApp firebaseApp) { - this(new NetworkClient(firebaseApp), Executors.newCachedThreadPool(), new RetryManager()); + this( + firebaseApp.getOptions().getGcmSenderId(), + IntegrityManagerFactory.create(firebaseApp.getApplicationContext()), + new NetworkClient(firebaseApp), + Executors.newCachedThreadPool(), + new RetryManager()); } @VisibleForTesting PlayIntegrityAppCheckProvider( + @NonNull String projectNumber, + @NonNull IntegrityManager integrityManager, @NonNull NetworkClient networkClient, @NonNull ExecutorService backgroundExecutor, @NonNull RetryManager retryManager) { + this.projectNumber = projectNumber; + this.integrityManager = integrityManager; this.networkClient = networkClient; this.backgroundExecutor = backgroundExecutor; this.retryManager = retryManager; @@ -54,24 +69,61 @@ public PlayIntegrityAppCheckProvider(@NonNull FirebaseApp firebaseApp) { @NonNull @Override public Task getToken() { - // TODO(rosalyntan): Obtain the Play Integrity challenge nonce. - ExchangePlayIntegrityTokenRequest request = - new ExchangePlayIntegrityTokenRequest("placeholder"); - Task networkTask = + return getPlayIntegrityAttestation() + .continueWithTask( + new Continuation>() { + @Override + public Task then(@NonNull Task task) { + if (task.isSuccessful()) { + ExchangePlayIntegrityTokenRequest request = + new ExchangePlayIntegrityTokenRequest(task.getResult().token()); + return Tasks.call( + backgroundExecutor, + () -> + networkClient.exchangeAttestationForAppCheckToken( + request.toJsonString().getBytes(UTF_8), + NetworkClient.PLAY_INTEGRITY, + retryManager)); + } + return Tasks.forException(task.getException()); + } + }) + .continueWithTask( + new Continuation>() { + @Override + public Task then(@NonNull Task task) { + if (task.isSuccessful()) { + return Tasks.forResult( + DefaultAppCheckToken.constructFromAppCheckTokenResponse(task.getResult())); + } + // TODO: Surface more error details. + return Tasks.forException(task.getException()); + } + }); + } + + @NonNull + private Task getPlayIntegrityAttestation() { + GeneratePlayIntegrityChallengeRequest generateChallengeRequest = + new GeneratePlayIntegrityChallengeRequest(); + Task generateChallengeTask = Tasks.call( backgroundExecutor, () -> - networkClient.exchangeAttestationForAppCheckToken( - request.toJsonString().getBytes(UTF_8), - NetworkClient.PLAY_INTEGRITY, - retryManager)); - return networkTask.continueWithTask( - new Continuation>() { + GeneratePlayIntegrityChallengeResponse.fromJsonString( + networkClient.generatePlayIntegrityChallenge( + generateChallengeRequest.toJsonString().getBytes(UTF_8), retryManager))); + return generateChallengeTask.continueWithTask( + new Continuation>() { @Override - public Task then(@NonNull Task task) { + public Task then( + @NonNull Task task) { if (task.isSuccessful()) { - return Tasks.forResult( - DefaultAppCheckToken.constructFromAppCheckTokenResponse(task.getResult())); + return integrityManager.requestIntegrityToken( + IntegrityTokenRequest.builder() + .setCloudProjectNumber(Long.parseLong(projectNumber)) + .setNonce(task.getResult().getChallenge()) + .build()); } // TODO: Surface more error details. return Tasks.forException(task.getException()); diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequestTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequestTest.java new file mode 100644 index 00000000000..abd86ae8258 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeRequestTest.java @@ -0,0 +1,37 @@ +// Copyright 2022 Google LLC +// +// 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 com.google.firebase.appcheck.playintegrity.internal; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for {@link GeneratePlayIntegrityChallengeRequest}. */ +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class GeneratePlayIntegrityChallengeRequestTest { + private static final String EMPTY_JSON = "{}"; + + @Test + public void toJsonString_expectSerialized() throws Exception { + GeneratePlayIntegrityChallengeRequest generatePlayIntegrityChallengeRequest = + new GeneratePlayIntegrityChallengeRequest(); + + assertThat(generatePlayIntegrityChallengeRequest.toJsonString()).isEqualTo(EMPTY_JSON); + } +} diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponseTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponseTest.java new file mode 100644 index 00000000000..018b1df06e7 --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/GeneratePlayIntegrityChallengeResponseTest.java @@ -0,0 +1,64 @@ +// Copyright 2022 Google LLC +// +// 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 com.google.firebase.appcheck.playintegrity.internal; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for {@link GeneratePlayIntegrityChallengeResponse}. */ +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class GeneratePlayIntegrityChallengeResponseTest { + private static final String CHALLENGE = "testChallenge"; + private static final String TIME_TO_LIVE = "3600s"; + + @Test + public void fromJsonString_expectDeserialized() throws Exception { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(GeneratePlayIntegrityChallengeResponse.CHALLENGE_KEY, CHALLENGE); + jsonObject.put(GeneratePlayIntegrityChallengeResponse.TIME_TO_LIVE_KEY, TIME_TO_LIVE); + + GeneratePlayIntegrityChallengeResponse generatePlayIntegrityChallengeResponse = + GeneratePlayIntegrityChallengeResponse.fromJsonString(jsonObject.toString()); + assertThat(generatePlayIntegrityChallengeResponse.getChallenge()).isEqualTo(CHALLENGE); + assertThat(generatePlayIntegrityChallengeResponse.getTimeToLive()).isEqualTo(TIME_TO_LIVE); + } + + @Test + public void fromJsonString_nullChallenge_throwsException() throws Exception { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(GeneratePlayIntegrityChallengeResponse.TIME_TO_LIVE_KEY, TIME_TO_LIVE); + + assertThrows( + NullPointerException.class, + () -> GeneratePlayIntegrityChallengeResponse.fromJsonString(jsonObject.toString())); + } + + @Test + public void fromJsonString_nullTimeToLive_throwsException() throws Exception { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(GeneratePlayIntegrityChallengeResponse.CHALLENGE_KEY, CHALLENGE); + + assertThrows( + NullPointerException.class, + () -> GeneratePlayIntegrityChallengeResponse.fromJsonString(jsonObject.toString())); + } +} diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java index faa5c6ee702..8b3c2bc85b6 100644 --- a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java +++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java @@ -17,11 +17,17 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.android.play.core.integrity.IntegrityManager; +import com.google.android.play.core.integrity.IntegrityTokenRequest; +import com.google.android.play.core.integrity.IntegrityTokenResponse; import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.appcheck.AppCheckToken; import com.google.firebase.appcheck.internal.AppCheckTokenResponse; @@ -30,9 +36,13 @@ import com.google.firebase.appcheck.internal.RetryManager; import java.io.IOException; import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeoutException; +import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @@ -43,17 +53,29 @@ @Config(manifest = Config.NONE) public class PlayIntegrityAppCheckProviderTest { + private static final String PROJECT_NUMBER = "123456"; private static final String ATTESTATION_TOKEN = "token"; private static final String TIME_TO_LIVE = "3600s"; + private static final String CHALLENGE = "testChallenge"; + private static final String INTEGRITY_TOKEN = "integrityToken"; - private ExecutorService backgroundExecutor = MoreExecutors.newDirectExecutorService(); + @Mock private IntegrityManager mockIntegrityManager; @Mock private NetworkClient mockNetworkClient; @Mock private RetryManager mockRetryManager; + @Mock private IntegrityTokenResponse mockIntegrityTokenResponse; @Mock private AppCheckTokenResponse mockAppCheckTokenResponse; + @Captor private ArgumentCaptor integrityTokenRequestCaptor; + @Captor private ArgumentCaptor exchangePlayIntegrityTokenRequestCaptor; + + private ExecutorService backgroundExecutor = MoreExecutors.newDirectExecutorService(); + @Before public void setup() { MockitoAnnotations.initMocks(this); + when(mockIntegrityTokenResponse.token()).thenReturn(INTEGRITY_TOKEN); + when(mockAppCheckTokenResponse.getAttestationToken()).thenReturn(ATTESTATION_TOKEN); + when(mockAppCheckTokenResponse.getTimeToLive()).thenReturn(TIME_TO_LIVE); } @Test @@ -67,41 +89,138 @@ public void testPublicConstructor_nullFirebaseApp_expectThrows() { @Test public void getToken_onSuccess_setsTaskResult() throws Exception { + when(mockNetworkClient.generatePlayIntegrityChallenge(any(), eq(mockRetryManager))) + .thenReturn(createGeneratePlayIntegrityChallengeResponse()); + when(mockIntegrityManager.requestIntegrityToken(any())) + .thenReturn(Tasks.forResult(mockIntegrityTokenResponse)); when(mockNetworkClient.exchangeAttestationForAppCheckToken( any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager))) .thenReturn(mockAppCheckTokenResponse); - when(mockAppCheckTokenResponse.getAttestationToken()).thenReturn(ATTESTATION_TOKEN); - when(mockAppCheckTokenResponse.getTimeToLive()).thenReturn(TIME_TO_LIVE); PlayIntegrityAppCheckProvider provider = - new PlayIntegrityAppCheckProvider(mockNetworkClient, backgroundExecutor, mockRetryManager); + new PlayIntegrityAppCheckProvider( + PROJECT_NUMBER, + mockIntegrityManager, + mockNetworkClient, + backgroundExecutor, + mockRetryManager); Task task = provider.getToken(); - verify(mockNetworkClient) - .exchangeAttestationForAppCheckToken( - any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager)); - AppCheckToken token = task.getResult(); assertThat(token).isInstanceOf(DefaultAppCheckToken.class); assertThat(token.getToken()).isEqualTo(ATTESTATION_TOKEN); + + verify(mockNetworkClient).generatePlayIntegrityChallenge(any(), eq(mockRetryManager)); + + verify(mockIntegrityManager).requestIntegrityToken(integrityTokenRequestCaptor.capture()); + assertThat(integrityTokenRequestCaptor.getValue().cloudProjectNumber()) + .isEqualTo(Long.parseLong(PROJECT_NUMBER)); + assertThat(integrityTokenRequestCaptor.getValue().nonce()).isEqualTo(CHALLENGE); + + verify(mockNetworkClient) + .exchangeAttestationForAppCheckToken( + exchangePlayIntegrityTokenRequestCaptor.capture(), + eq(NetworkClient.PLAY_INTEGRITY), + eq(mockRetryManager)); + String exchangePlayIntegrityTokenRequestJsonString = + new String(exchangePlayIntegrityTokenRequestCaptor.getValue()); + assertThat(exchangePlayIntegrityTokenRequestJsonString).contains(INTEGRITY_TOKEN); } @Test - public void getToken_onFailure_setsTaskException() throws Exception { + public void getToken_generateChallengeFails_setsTaskException() throws Exception { + when(mockNetworkClient.generatePlayIntegrityChallenge(any(), eq(mockRetryManager))) + .thenThrow(new IOException()); + + PlayIntegrityAppCheckProvider provider = + new PlayIntegrityAppCheckProvider( + PROJECT_NUMBER, + mockIntegrityManager, + mockNetworkClient, + backgroundExecutor, + mockRetryManager); + Task task = provider.getToken(); + + assertThat(task.isSuccessful()).isFalse(); + assertThat(task.getException()).isInstanceOf(IOException.class); + + verify(mockNetworkClient).generatePlayIntegrityChallenge(any(), eq(mockRetryManager)); + verify(mockNetworkClient, never()).exchangeAttestationForAppCheckToken(any(), anyInt(), any()); + verify(mockIntegrityManager, never()).requestIntegrityToken(any()); + } + + @Test + public void getToken_requestIntegrityTokenFails_setsTaskException() throws Exception { + when(mockNetworkClient.generatePlayIntegrityChallenge(any(), eq(mockRetryManager))) + .thenReturn(createGeneratePlayIntegrityChallengeResponse()); + when(mockIntegrityManager.requestIntegrityToken(any())) + .thenReturn(Tasks.forException(new TimeoutException())); + + PlayIntegrityAppCheckProvider provider = + new PlayIntegrityAppCheckProvider( + PROJECT_NUMBER, + mockIntegrityManager, + mockNetworkClient, + backgroundExecutor, + mockRetryManager); + Task task = provider.getToken(); + + assertThat(task.isSuccessful()).isFalse(); + assertThat(task.getException()).isInstanceOf(TimeoutException.class); + + verify(mockNetworkClient).generatePlayIntegrityChallenge(any(), eq(mockRetryManager)); + verify(mockNetworkClient, never()).exchangeAttestationForAppCheckToken(any(), anyInt(), any()); + + verify(mockIntegrityManager).requestIntegrityToken(integrityTokenRequestCaptor.capture()); + assertThat(integrityTokenRequestCaptor.getValue().cloudProjectNumber()) + .isEqualTo(Long.parseLong(PROJECT_NUMBER)); + assertThat(integrityTokenRequestCaptor.getValue().nonce()).isEqualTo(CHALLENGE); + } + + @Test + public void getToken_tokenExchangeFails_setsTaskException() throws Exception { + when(mockNetworkClient.generatePlayIntegrityChallenge(any(), eq(mockRetryManager))) + .thenReturn(createGeneratePlayIntegrityChallengeResponse()); + when(mockIntegrityManager.requestIntegrityToken(any())) + .thenReturn(Tasks.forResult(mockIntegrityTokenResponse)); when(mockNetworkClient.exchangeAttestationForAppCheckToken( any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager))) .thenThrow(new IOException()); PlayIntegrityAppCheckProvider provider = - new PlayIntegrityAppCheckProvider(mockNetworkClient, backgroundExecutor, mockRetryManager); + new PlayIntegrityAppCheckProvider( + PROJECT_NUMBER, + mockIntegrityManager, + mockNetworkClient, + backgroundExecutor, + mockRetryManager); Task task = provider.getToken(); + assertThat(task.isSuccessful()).isFalse(); + assertThat(task.getException()).isInstanceOf(IOException.class); + + verify(mockNetworkClient).generatePlayIntegrityChallenge(any(), eq(mockRetryManager)); + + verify(mockIntegrityManager).requestIntegrityToken(integrityTokenRequestCaptor.capture()); + assertThat(integrityTokenRequestCaptor.getValue().cloudProjectNumber()) + .isEqualTo(Long.parseLong(PROJECT_NUMBER)); + assertThat(integrityTokenRequestCaptor.getValue().nonce()).isEqualTo(CHALLENGE); + verify(mockNetworkClient) .exchangeAttestationForAppCheckToken( - any(), eq(NetworkClient.PLAY_INTEGRITY), eq(mockRetryManager)); + exchangePlayIntegrityTokenRequestCaptor.capture(), + eq(NetworkClient.PLAY_INTEGRITY), + eq(mockRetryManager)); + String exchangePlayIntegrityTokenRequestJsonString = + new String(exchangePlayIntegrityTokenRequestCaptor.getValue()); + assertThat(exchangePlayIntegrityTokenRequestJsonString).contains(INTEGRITY_TOKEN); + } - assertThat(task.isSuccessful()).isFalse(); - Exception exception = task.getException(); - assertThat(exception).isInstanceOf(IOException.class); + private static String createGeneratePlayIntegrityChallengeResponse() throws Exception { + JSONObject responseBodyJson = new JSONObject(); + responseBodyJson.put(GeneratePlayIntegrityChallengeResponse.CHALLENGE_KEY, CHALLENGE); + responseBodyJson.put(GeneratePlayIntegrityChallengeResponse.TIME_TO_LIVE_KEY, TIME_TO_LIVE); + + return responseBodyJson.toString(); } } diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java index 81c8d7a0b94..c1040671568 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java @@ -57,6 +57,8 @@ public class NetworkClient { "https://firebaseappcheck.googleapis.com/v1beta/projects/%s/apps/%s:exchangeDebugToken?key=%s"; private static final String PLAY_INTEGRITY_EXCHANGE_URL_TEMPLATE = "https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:exchangePlayIntegrityToken?key=%s"; + private static final String PLAY_INTEGRITY_CHALLENGE_URL_TEMPLATE = + "https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:generatePlayIntegrityChallenge?key=%s"; private static final String CONTENT_TYPE = "Content-Type"; private static final String APPLICATION_JSON = "application/json"; private static final String UTF_8 = "UTF-8"; @@ -119,6 +121,29 @@ public AppCheckTokenResponse exchangeAttestationForAppCheckToken( throw new FirebaseException("Too many attempts."); } URL url = new URL(String.format(getUrlTemplate(tokenType), projectId, appId, apiKey)); + String response = makeNetworkRequest(url, requestBytes, retryManager); + return AppCheckTokenResponse.fromJsonString(response); + } + + /** + * Calls the App Check backend using {@link HttpURLConnection} in order to generate a challenge + * nonce for the Play Integrity attestation flow. + */ + @NonNull + public String generatePlayIntegrityChallenge( + @NonNull byte[] requestBytes, @NonNull RetryManager retryManager) + throws FirebaseException, IOException, JSONException { + if (!retryManager.canRetry()) { + throw new FirebaseException("Too many attempts."); + } + URL url = + new URL(String.format(PLAY_INTEGRITY_CHALLENGE_URL_TEMPLATE, projectId, appId, apiKey)); + return makeNetworkRequest(url, requestBytes, retryManager); + } + + private String makeNetworkRequest( + @NonNull URL url, @NonNull byte[] requestBytes, @NonNull RetryManager retryManager) + throws FirebaseException, IOException, JSONException { HttpURLConnection urlConnection = createHttpUrlConnection(url); try { @@ -163,7 +188,7 @@ public AppCheckTokenResponse exchangeAttestationForAppCheckToken( + httpErrorResponse.getErrorMessage()); } retryManager.resetBackoffOnSuccess(); - return AppCheckTokenResponse.fromJsonString(responseBody); + return responseBody; } finally { urlConnection.disconnect(); } diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java index a105dbf7042..e7be0747ccf 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java @@ -69,7 +69,9 @@ public class NetworkClientTest { "https://firebaseappcheck.googleapis.com/v1beta/projects/projectId/apps/appId:exchangeSafetyNetToken?key=apiKey"; private static final String DEBUG_EXPECTED_URL = "https://firebaseappcheck.googleapis.com/v1beta/projects/projectId/apps/appId:exchangeDebugToken?key=apiKey"; - private static final String PLAY_INTEGRITY_EXPECTED_URL = + private static final String PLAY_INTEGRITY_CHALLENGE_EXPECTED_URL = + "https://firebaseappcheck.googleapis.com/v1/projects/projectId/apps/appId:generatePlayIntegrityChallenge?key=apiKey"; + private static final String PLAY_INTEGRITY_EXCHANGE_EXPECTED_URL = "https://firebaseappcheck.googleapis.com/v1/projects/projectId/apps/appId:exchangePlayIntegrityToken?key=apiKey"; private static final String JSON_REQUEST = "jsonRequest"; private static final int SUCCESS_CODE = 200; @@ -78,6 +80,7 @@ public class NetworkClientTest { private static final String TIME_TO_LIVE = "3600s"; private static final String ERROR_MESSAGE = "error message"; private static final String HEART_BEAT_HEADER_TEST = "test-header"; + private static final String CHALLENGE_RESPONSE = "challengeResponse"; @Mock HeartBeatController mockHeartBeatController; @Mock HttpURLConnection mockHttpUrlConnection; @@ -228,7 +231,7 @@ public void exchangePlayIntegrityToken_successResponse_returnsAppCheckTokenRespo assertThat(tokenResponse.getAttestationToken()).isEqualTo(ATTESTATION_TOKEN); assertThat(tokenResponse.getTimeToLive()).isEqualTo(TIME_TO_LIVE); - URL expectedUrl = new URL(PLAY_INTEGRITY_EXPECTED_URL); + URL expectedUrl = new URL(PLAY_INTEGRITY_EXCHANGE_EXPECTED_URL); verify(networkClient).createHttpUrlConnection(expectedUrl); verify(mockOutputStream) .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length); @@ -254,7 +257,7 @@ public void exchangePlayIntegrityToken_errorResponse_throwsException() throws Ex JSON_REQUEST.getBytes(), NetworkClient.PLAY_INTEGRITY, mockRetryManager)); assertThat(exception.getMessage()).contains(ERROR_MESSAGE); - URL expectedUrl = new URL(PLAY_INTEGRITY_EXPECTED_URL); + URL expectedUrl = new URL(PLAY_INTEGRITY_EXCHANGE_EXPECTED_URL); verify(networkClient).createHttpUrlConnection(expectedUrl); verify(mockOutputStream) .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length); @@ -306,6 +309,68 @@ public void exchangeAttestation_cannotRetry_throwsException() { verify(mockRetryManager, never()).resetBackoffOnSuccess(); } + @Test + public void generatePlayIntegrityChallenge_successResponse_returnsJsonString() throws Exception { + when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream); + when(mockHttpUrlConnection.getInputStream()) + .thenReturn(new ByteArrayInputStream(CHALLENGE_RESPONSE.getBytes())); + when(mockHttpUrlConnection.getResponseCode()).thenReturn(SUCCESS_CODE); + + String challengeResponse = + networkClient.generatePlayIntegrityChallenge(JSON_REQUEST.getBytes(), mockRetryManager); + assertThat(challengeResponse).isEqualTo(CHALLENGE_RESPONSE); + + URL expectedUrl = new URL(PLAY_INTEGRITY_CHALLENGE_EXPECTED_URL); + verify(networkClient).createHttpUrlConnection(expectedUrl); + verify(mockOutputStream) + .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length); + verify(mockRetryManager, never()).updateBackoffOnFailure(anyInt()); + verify(mockRetryManager).resetBackoffOnSuccess(); + verifyRequestHeaders(); + } + + @Test + public void generatePlayIntegrityChallenge_errorResponse_throwsException() throws Exception { + JSONObject responseBodyJson = createHttpErrorResponse(); + + when(mockHttpUrlConnection.getOutputStream()).thenReturn(mockOutputStream); + when(mockHttpUrlConnection.getErrorStream()) + .thenReturn(new ByteArrayInputStream(responseBodyJson.toString().getBytes())); + when(mockHttpUrlConnection.getResponseCode()).thenReturn(ERROR_CODE); + + FirebaseException exception = + assertThrows( + FirebaseException.class, + () -> + networkClient.generatePlayIntegrityChallenge( + JSON_REQUEST.getBytes(), mockRetryManager)); + + assertThat(exception.getMessage()).contains(ERROR_MESSAGE); + URL expectedUrl = new URL(PLAY_INTEGRITY_CHALLENGE_EXPECTED_URL); + verify(networkClient).createHttpUrlConnection(expectedUrl); + verify(mockOutputStream) + .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length); + verify(mockRetryManager).updateBackoffOnFailure(ERROR_CODE); + verify(mockRetryManager, never()).resetBackoffOnSuccess(); + verifyRequestHeaders(); + } + + @Test + public void generatePlayIntegrityChallenge_cannotRetry_throwsException() { + when(mockRetryManager.canRetry()).thenReturn(false); + + FirebaseException exception = + assertThrows( + FirebaseException.class, + () -> + networkClient.generatePlayIntegrityChallenge( + JSON_REQUEST.getBytes(), mockRetryManager)); + + assertThat(exception.getMessage()).contains("Too many attempts"); + verify(mockRetryManager, never()).updateBackoffOnFailure(anyInt()); + verify(mockRetryManager, never()).resetBackoffOnSuccess(); + } + private void verifyRequestHeaders() { verify(networkClient).getHeartBeat(); verify(mockHttpUrlConnection).setRequestProperty(X_FIREBASE_CLIENT, HEART_BEAT_HEADER_TEST); From ae078047769d04d209becf8a2d96f39855e5559e Mon Sep 17 00:00:00 2001 From: Rosalyn Tan Date: Fri, 15 Apr 2022 10:46:20 -0700 Subject: [PATCH 4/8] Update test app. (#3655) --- .../googletest/firebase/appcheck/MainActivity.java | 14 ++++++++++++++ .../test-app/src/main/res/layout/activity_main.xml | 5 +++++ .../test-app/src/main/res/values/strings.xml | 1 + .../firebase-appcheck/test-app/test-app.gradle | 1 + 4 files changed, 21 insertions(+) diff --git a/appcheck/firebase-appcheck/test-app/src/main/java/com/googletest/firebase/appcheck/MainActivity.java b/appcheck/firebase-appcheck/test-app/src/main/java/com/googletest/firebase/appcheck/MainActivity.java index 41299c2276d..38eca67b561 100644 --- a/appcheck/firebase-appcheck/test-app/src/main/java/com/googletest/firebase/appcheck/MainActivity.java +++ b/appcheck/firebase-appcheck/test-app/src/main/java/com/googletest/firebase/appcheck/MainActivity.java @@ -30,6 +30,7 @@ import com.google.firebase.appcheck.FirebaseAppCheck; import com.google.firebase.appcheck.FirebaseAppCheck.AppCheckListener; import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory; +import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory; import com.google.firebase.appcheck.safetynet.SafetyNetAppCheckProviderFactory; import com.google.firebase.storage.FirebaseStorage; import com.google.firebase.storage.ListResult; @@ -42,6 +43,7 @@ public class MainActivity extends AppCompatActivity { private FirebaseAppCheck firebaseAppCheck; private FirebaseStorage firebaseStorage; private AppCheckListener appCheckListener; + private Button installPlayIntegrityButton; private Button installSafetyNetButton; private Button installDebugButton; private Button getAppCheckTokenButton; @@ -80,6 +82,7 @@ public void onAppCheckTokenChanged(@NonNull AppCheckToken token) { } private void initViews() { + installPlayIntegrityButton = findViewById(R.id.install_play_integrity_app_check_button); installSafetyNetButton = findViewById(R.id.install_safety_net_app_check_button); installDebugButton = findViewById(R.id.install_debug_app_check_button); getAppCheckTokenButton = findViewById(R.id.exchange_app_check_button); @@ -89,6 +92,17 @@ private void initViews() { } private void setOnClickListeners() { + installPlayIntegrityButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + firebaseAppCheck.installAppCheckProviderFactory( + PlayIntegrityAppCheckProviderFactory.getInstance()); + Log.d(TAG, "Installed PlayIntegrityAppCheckProvider"); + showToast("Installed PlayIntegrityAppCheckProvider."); + } + }); + installSafetyNetButton.setOnClickListener( new OnClickListener() { @Override diff --git a/appcheck/firebase-appcheck/test-app/src/main/res/layout/activity_main.xml b/appcheck/firebase-appcheck/test-app/src/main/res/layout/activity_main.xml index eccdbf9ec6e..43a4d771cb7 100644 --- a/appcheck/firebase-appcheck/test-app/src/main/res/layout/activity_main.xml +++ b/appcheck/firebase-appcheck/test-app/src/main/res/layout/activity_main.xml @@ -6,6 +6,11 @@ android:gravity="center" android:orientation="vertical"> +