Skip to content

Commit 20da1e7

Browse files
authored
Add Play Integrity support to App Check (#3658)
* 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. * Implement Play Integrity token exchange. (#3613) * Implement Play Integrity exchange. * Update and add unit tests. * Add @nonnull annotation. * 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. * Update test app. (#3655) * Update version in `gradle.properties`. * Fix tests broken after merge. * Address review comments. * Address review comment.
1 parent e6b01db commit 20da1e7

23 files changed

+985
-11
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
plugins {
16+
id 'firebase-library'
17+
}
18+
19+
firebaseLibrary {
20+
publishSources = true
21+
}
22+
23+
android {
24+
adbOptions {
25+
timeOutInMs 60 * 1000
26+
}
27+
28+
compileSdkVersion project.targetSdkVersion
29+
defaultConfig {
30+
targetSdkVersion project.targetSdkVersion
31+
minSdkVersion project.minSdkVersion
32+
versionName version
33+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
34+
}
35+
compileOptions {
36+
sourceCompatibility JavaVersion.VERSION_1_8
37+
targetCompatibility JavaVersion.VERSION_1_8
38+
}
39+
40+
testOptions.unitTests.includeAndroidResources = false
41+
}
42+
43+
dependencies {
44+
implementation project(':firebase-common')
45+
implementation project(':firebase-components')
46+
implementation project(':appcheck:firebase-appcheck')
47+
implementation 'com.google.android.gms:play-services-base:18.0.1'
48+
implementation 'com.google.android.gms:play-services-tasks:18.0.1'
49+
implementation 'com.google.android.play:integrity:1.0.1'
50+
51+
testImplementation 'junit:junit:4.13.2'
52+
testImplementation 'org.mockito:mockito-core:3.4.6'
53+
testImplementation "com.google.truth:truth:$googleTruthVersion"
54+
testImplementation "org.robolectric:robolectric:$robolectricVersion"
55+
testImplementation 'androidx.test:core:1.4.0'
56+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
version=16.0.0
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- Copyright 2022 Google LLC -->
3+
<!-- -->
4+
<!-- Licensed under the Apache License, Version 2.0 (the "License"); -->
5+
<!-- you may not use this file except in compliance with the License. -->
6+
<!-- You may obtain a copy of the License at -->
7+
<!-- -->
8+
<!-- http://www.apache.org/licenses/LICENSE-2.0 -->
9+
<!-- -->
10+
<!-- Unless required by applicable law or agreed to in writing, software -->
11+
<!-- distributed under the License is distributed on an "AS IS" BASIS, -->
12+
<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -->
13+
<!-- See the License for the specific language governing permissions and -->
14+
<!-- limitations under the License. -->
15+
16+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
17+
package="com.google.firebase.appcheck.playintegrity">
18+
<!--Although the *SdkVersion is captured in gradle build files, this is required for non gradle builds-->
19+
<!--<uses-sdk android:minSdkVersion="16"/>-->
20+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appcheck.playintegrity;
16+
17+
import androidx.annotation.NonNull;
18+
import com.google.firebase.FirebaseApp;
19+
import com.google.firebase.appcheck.AppCheckProvider;
20+
import com.google.firebase.appcheck.AppCheckProviderFactory;
21+
import com.google.firebase.appcheck.playintegrity.internal.PlayIntegrityAppCheckProvider;
22+
23+
/**
24+
* Implementation of an {@link AppCheckProviderFactory} that builds {@link
25+
* PlayIntegrityAppCheckProvider}s. This is the default implementation.
26+
*/
27+
public class PlayIntegrityAppCheckProviderFactory implements AppCheckProviderFactory {
28+
29+
private static final PlayIntegrityAppCheckProviderFactory instance =
30+
new PlayIntegrityAppCheckProviderFactory();
31+
32+
/**
33+
* Gets an instance of this class for installation into a {@link
34+
* com.google.firebase.appcheck.FirebaseAppCheck} instance.
35+
*/
36+
@NonNull
37+
public static PlayIntegrityAppCheckProviderFactory getInstance() {
38+
return instance;
39+
}
40+
41+
@NonNull
42+
@Override
43+
public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) {
44+
return new PlayIntegrityAppCheckProvider(firebaseApp);
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appcheck.playintegrity.internal;
16+
17+
import androidx.annotation.NonNull;
18+
import androidx.annotation.VisibleForTesting;
19+
import org.json.JSONException;
20+
import org.json.JSONObject;
21+
22+
/**
23+
* Client-side model of the ExchangePlayIntegrityTokenRequest payload from the Firebase App Check
24+
* Token Exchange API.
25+
*/
26+
class ExchangePlayIntegrityTokenRequest {
27+
28+
@VisibleForTesting static final String PLAY_INTEGRITY_TOKEN_KEY = "playIntegrityToken";
29+
30+
private final String playIntegrityToken;
31+
32+
public ExchangePlayIntegrityTokenRequest(@NonNull String playIntegrityToken) {
33+
this.playIntegrityToken = playIntegrityToken;
34+
}
35+
36+
@NonNull
37+
public String toJsonString() throws JSONException {
38+
JSONObject jsonObject = new JSONObject();
39+
jsonObject.put(PLAY_INTEGRITY_TOKEN_KEY, playIntegrityToken);
40+
41+
return jsonObject.toString();
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appcheck.playintegrity.internal;
16+
17+
import androidx.annotation.NonNull;
18+
import org.json.JSONObject;
19+
20+
/**
21+
* Client-side model of the GeneratePlayIntegrityChallengeRequest payload from the Firebase App
22+
* Check Token Exchange API.
23+
*/
24+
class GeneratePlayIntegrityChallengeRequest {
25+
26+
public GeneratePlayIntegrityChallengeRequest() {}
27+
28+
@NonNull
29+
public String toJsonString() {
30+
JSONObject jsonObject = new JSONObject();
31+
32+
// GeneratePlayIntegrityChallenge takes an empty POST body since the app ID is in the URL.
33+
return jsonObject.toString();
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appcheck.playintegrity.internal;
16+
17+
import static com.google.android.gms.common.internal.Preconditions.checkNotNull;
18+
import static com.google.android.gms.common.util.Strings.emptyToNull;
19+
20+
import androidx.annotation.NonNull;
21+
import androidx.annotation.VisibleForTesting;
22+
import com.google.firebase.FirebaseException;
23+
import org.json.JSONException;
24+
import org.json.JSONObject;
25+
26+
/**
27+
* Client-side model of the GeneratePlayIntegrityChallengeResponse payload from the Firebase App
28+
* Check Token Exchange API.
29+
*/
30+
class GeneratePlayIntegrityChallengeResponse {
31+
32+
@VisibleForTesting static final String CHALLENGE_KEY = "challenge";
33+
@VisibleForTesting static final String TIME_TO_LIVE_KEY = "ttl";
34+
35+
private String challenge;
36+
private String timeToLive;
37+
38+
@NonNull
39+
public static GeneratePlayIntegrityChallengeResponse fromJsonString(@NonNull String jsonString)
40+
throws FirebaseException, JSONException {
41+
JSONObject jsonObject = new JSONObject(jsonString);
42+
String challenge = emptyToNull(jsonObject.optString(CHALLENGE_KEY));
43+
String timeToLive = emptyToNull(jsonObject.optString(TIME_TO_LIVE_KEY));
44+
if (challenge == null || timeToLive == null) {
45+
throw new FirebaseException("Unexpected server response.");
46+
}
47+
return new GeneratePlayIntegrityChallengeResponse(challenge, timeToLive);
48+
}
49+
50+
private GeneratePlayIntegrityChallengeResponse(
51+
@NonNull String challenge, @NonNull String timeToLive) {
52+
checkNotNull(challenge);
53+
checkNotNull(timeToLive);
54+
this.challenge = challenge;
55+
this.timeToLive = timeToLive;
56+
}
57+
58+
@NonNull
59+
public String getChallenge() {
60+
return challenge;
61+
}
62+
63+
@NonNull
64+
public String getTimeToLive() {
65+
return timeToLive;
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appcheck.playintegrity.internal;
16+
17+
import androidx.annotation.NonNull;
18+
import androidx.annotation.VisibleForTesting;
19+
import com.google.android.gms.tasks.Task;
20+
import com.google.android.gms.tasks.Tasks;
21+
import com.google.android.play.core.integrity.IntegrityManager;
22+
import com.google.android.play.core.integrity.IntegrityManagerFactory;
23+
import com.google.android.play.core.integrity.IntegrityTokenRequest;
24+
import com.google.android.play.core.integrity.IntegrityTokenResponse;
25+
import com.google.firebase.FirebaseApp;
26+
import com.google.firebase.appcheck.AppCheckProvider;
27+
import com.google.firebase.appcheck.AppCheckToken;
28+
import com.google.firebase.appcheck.internal.DefaultAppCheckToken;
29+
import com.google.firebase.appcheck.internal.NetworkClient;
30+
import com.google.firebase.appcheck.internal.RetryManager;
31+
import java.util.concurrent.ExecutorService;
32+
import java.util.concurrent.Executors;
33+
34+
public class PlayIntegrityAppCheckProvider implements AppCheckProvider {
35+
36+
private static final String UTF_8 = "UTF-8";
37+
38+
private final String projectNumber;
39+
private final IntegrityManager integrityManager;
40+
private final NetworkClient networkClient;
41+
private final ExecutorService backgroundExecutor;
42+
private final RetryManager retryManager;
43+
44+
public PlayIntegrityAppCheckProvider(@NonNull FirebaseApp firebaseApp) {
45+
this(
46+
firebaseApp.getOptions().getGcmSenderId(),
47+
IntegrityManagerFactory.create(firebaseApp.getApplicationContext()),
48+
new NetworkClient(firebaseApp),
49+
Executors.newCachedThreadPool(),
50+
new RetryManager());
51+
}
52+
53+
@VisibleForTesting
54+
PlayIntegrityAppCheckProvider(
55+
@NonNull String projectNumber,
56+
@NonNull IntegrityManager integrityManager,
57+
@NonNull NetworkClient networkClient,
58+
@NonNull ExecutorService backgroundExecutor,
59+
@NonNull RetryManager retryManager) {
60+
this.projectNumber = projectNumber;
61+
this.integrityManager = integrityManager;
62+
this.networkClient = networkClient;
63+
this.backgroundExecutor = backgroundExecutor;
64+
this.retryManager = retryManager;
65+
}
66+
67+
@NonNull
68+
@Override
69+
public Task<AppCheckToken> getToken() {
70+
return getPlayIntegrityAttestation()
71+
.onSuccessTask(
72+
integrityTokenResponse -> {
73+
ExchangePlayIntegrityTokenRequest request =
74+
new ExchangePlayIntegrityTokenRequest(integrityTokenResponse.token());
75+
return Tasks.call(
76+
backgroundExecutor,
77+
() ->
78+
networkClient.exchangeAttestationForAppCheckToken(
79+
request.toJsonString().getBytes(UTF_8),
80+
NetworkClient.PLAY_INTEGRITY,
81+
retryManager));
82+
})
83+
.onSuccessTask(
84+
appCheckTokenResponse -> {
85+
return Tasks.forResult(
86+
DefaultAppCheckToken.constructFromAppCheckTokenResponse(appCheckTokenResponse));
87+
});
88+
}
89+
90+
@NonNull
91+
private Task<IntegrityTokenResponse> getPlayIntegrityAttestation() {
92+
GeneratePlayIntegrityChallengeRequest generateChallengeRequest =
93+
new GeneratePlayIntegrityChallengeRequest();
94+
Task<GeneratePlayIntegrityChallengeResponse> generateChallengeTask =
95+
Tasks.call(
96+
backgroundExecutor,
97+
() ->
98+
GeneratePlayIntegrityChallengeResponse.fromJsonString(
99+
networkClient.generatePlayIntegrityChallenge(
100+
generateChallengeRequest.toJsonString().getBytes(UTF_8), retryManager)));
101+
return generateChallengeTask.onSuccessTask(
102+
generatePlayIntegrityChallengeResponse -> {
103+
return integrityManager.requestIntegrityToken(
104+
IntegrityTokenRequest.builder()
105+
.setCloudProjectNumber(Long.parseLong(projectNumber))
106+
.setNonce(generatePlayIntegrityChallengeResponse.getChallenge())
107+
.build());
108+
});
109+
}
110+
}

0 commit comments

Comments
 (0)