Skip to content

Commit 1341ce7

Browse files
committed
Implement Play Integrity attestation flow.
1 parent 2ddbaf3 commit 1341ce7

File tree

4 files changed

+160
-15
lines changed

4 files changed

+160
-15
lines changed
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.JSONException;
19+
import org.json.JSONObject;
20+
21+
/**
22+
* Client-side model of the GeneratePlayIntegrityChallengeRequest payload from the Firebase App
23+
* Check Token Exchange API.
24+
*/
25+
public class GeneratePlayIntegrityChallengeRequest {
26+
27+
public GeneratePlayIntegrityChallengeRequest() {}
28+
29+
@NonNull
30+
public String toJsonString() throws JSONException {
31+
JSONObject jsonObject = new JSONObject();
32+
33+
return jsonObject.toString();
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 org.json.JSONException;
23+
import org.json.JSONObject;
24+
25+
/**
26+
* Client-side model of the GeneratePlayIntegrityChallengeResponse payload from the Firebase App
27+
* Check Token Exchange API.
28+
*/
29+
public class GeneratePlayIntegrityChallengeResponse {
30+
31+
@VisibleForTesting static final String CHALLENGE_KEY = "challenge";
32+
@VisibleForTesting static final String TIME_TO_LIVE_KEY = "ttl";
33+
34+
private String challenge;
35+
private String timeToLive;
36+
37+
@NonNull
38+
public static GeneratePlayIntegrityChallengeResponse fromJsonString(@NonNull String jsonString)
39+
throws JSONException {
40+
JSONObject jsonObject = new JSONObject(jsonString);
41+
String challenge = emptyToNull(jsonObject.optString(CHALLENGE_KEY));
42+
String timeToLive = emptyToNull(jsonObject.optString(TIME_TO_LIVE_KEY));
43+
return new GeneratePlayIntegrityChallengeResponse(challenge, timeToLive);
44+
}
45+
46+
private GeneratePlayIntegrityChallengeResponse(
47+
@NonNull String challenge, @NonNull String timeToLive) {
48+
checkNotNull(challenge);
49+
checkNotNull(timeToLive);
50+
this.challenge = challenge;
51+
this.timeToLive = timeToLive;
52+
}
53+
54+
@NonNull
55+
public String getChallenge() {
56+
return challenge;
57+
}
58+
59+
@NonNull
60+
public String getTimeToLive() {
61+
return timeToLive;
62+
}
63+
}

appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
import com.google.android.gms.tasks.Continuation;
2020
import com.google.android.gms.tasks.Task;
2121
import com.google.android.gms.tasks.Tasks;
22+
import com.google.android.play.core.integrity.IntegrityManager;
23+
import com.google.android.play.core.integrity.IntegrityManagerFactory;
24+
import com.google.android.play.core.integrity.IntegrityTokenRequest;
25+
import com.google.android.play.core.integrity.IntegrityTokenResponse;
2226
import com.google.firebase.FirebaseApp;
2327
import com.google.firebase.appcheck.AppCheckProvider;
2428
import com.google.firebase.appcheck.AppCheckToken;
@@ -33,19 +37,26 @@ public class PlayIntegrityAppCheckProvider implements AppCheckProvider {
3337

3438
private static final String UTF_8 = "UTF-8";
3539

40+
private final IntegrityManager integrityManager;
3641
private final NetworkClient networkClient;
3742
private final ExecutorService backgroundExecutor;
3843
private final RetryManager retryManager;
3944

4045
public PlayIntegrityAppCheckProvider(@NonNull FirebaseApp firebaseApp) {
41-
this(new NetworkClient(firebaseApp), Executors.newCachedThreadPool(), new RetryManager());
46+
this(
47+
IntegrityManagerFactory.create(firebaseApp.getApplicationContext()),
48+
new NetworkClient(firebaseApp),
49+
Executors.newCachedThreadPool(),
50+
new RetryManager());
4251
}
4352

4453
@VisibleForTesting
4554
PlayIntegrityAppCheckProvider(
55+
@NonNull IntegrityManager integrityManager,
4656
@NonNull NetworkClient networkClient,
4757
@NonNull ExecutorService backgroundExecutor,
4858
@NonNull RetryManager retryManager) {
59+
this.integrityManager = integrityManager;
4960
this.networkClient = networkClient;
5061
this.backgroundExecutor = backgroundExecutor;
5162
this.retryManager = retryManager;
@@ -54,24 +65,60 @@ public PlayIntegrityAppCheckProvider(@NonNull FirebaseApp firebaseApp) {
5465
@NonNull
5566
@Override
5667
public Task<AppCheckToken> getToken() {
57-
// TODO(rosalyntan): Obtain the Play Integrity challenge nonce.
58-
ExchangePlayIntegrityTokenRequest request =
59-
new ExchangePlayIntegrityTokenRequest("placeholder");
60-
Task<AppCheckTokenResponse> networkTask =
68+
return getPlayIntegrityAttestation()
69+
.continueWithTask(
70+
new Continuation<IntegrityTokenResponse, Task<AppCheckTokenResponse>>() {
71+
@Override
72+
public Task<AppCheckTokenResponse> then(@NonNull Task<IntegrityTokenResponse> task) {
73+
if (task.isSuccessful()) {
74+
ExchangePlayIntegrityTokenRequest request =
75+
new ExchangePlayIntegrityTokenRequest(task.getResult().token());
76+
return Tasks.call(
77+
backgroundExecutor,
78+
() ->
79+
networkClient.exchangeAttestationForAppCheckToken(
80+
request.toJsonString().getBytes(UTF_8),
81+
NetworkClient.PLAY_INTEGRITY,
82+
retryManager));
83+
}
84+
return Tasks.forException(task.getException());
85+
}
86+
})
87+
.continueWithTask(
88+
new Continuation<AppCheckTokenResponse, Task<AppCheckToken>>() {
89+
@Override
90+
public Task<AppCheckToken> then(@NonNull Task<AppCheckTokenResponse> task) {
91+
if (task.isSuccessful()) {
92+
return Tasks.forResult(
93+
DefaultAppCheckToken.constructFromAppCheckTokenResponse(task.getResult()));
94+
}
95+
// TODO: Surface more error details.
96+
return Tasks.forException(task.getException());
97+
}
98+
});
99+
}
100+
101+
@NonNull
102+
Task<IntegrityTokenResponse> getPlayIntegrityAttestation() {
103+
GeneratePlayIntegrityChallengeRequest generateChallengeRequest =
104+
new GeneratePlayIntegrityChallengeRequest();
105+
Task<GeneratePlayIntegrityChallengeResponse> generateChallengeTask =
61106
Tasks.call(
62107
backgroundExecutor,
63108
() ->
64-
networkClient.exchangeAttestationForAppCheckToken(
65-
request.toJsonString().getBytes(UTF_8),
66-
NetworkClient.PLAY_INTEGRITY,
67-
retryManager));
68-
return networkTask.continueWithTask(
69-
new Continuation<AppCheckTokenResponse, Task<AppCheckToken>>() {
109+
GeneratePlayIntegrityChallengeResponse.fromJsonString(
110+
networkClient.generatePlayIntegrityChallenge(
111+
generateChallengeRequest.toJsonString().getBytes(UTF_8), retryManager)));
112+
return generateChallengeTask.continueWithTask(
113+
new Continuation<GeneratePlayIntegrityChallengeResponse, Task<IntegrityTokenResponse>>() {
70114
@Override
71-
public Task<AppCheckToken> then(@NonNull Task<AppCheckTokenResponse> task) {
115+
public Task<IntegrityTokenResponse> then(
116+
@NonNull Task<GeneratePlayIntegrityChallengeResponse> task) {
72117
if (task.isSuccessful()) {
73-
return Tasks.forResult(
74-
DefaultAppCheckToken.constructFromAppCheckTokenResponse(task.getResult()));
118+
return integrityManager.requestIntegrityToken(
119+
IntegrityTokenRequest.builder()
120+
.setNonce(task.getResult().getChallenge())
121+
.build());
75122
}
76123
// TODO: Surface more error details.
77124
return Tasks.forException(task.getException());

appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public String generatePlayIntegrityChallenge(
142142
}
143143

144144
private String makeNetworkRequest(
145-
URL url, @NonNull byte[] requestBytes, @NonNull RetryManager retryManager)
145+
@NonNull URL url, @NonNull byte[] requestBytes, @NonNull RetryManager retryManager)
146146
throws FirebaseException, IOException, JSONException {
147147
HttpURLConnection urlConnection = createHttpUrlConnection(url);
148148

0 commit comments

Comments
 (0)