/*
* Copyright 2016 The Android Open Source Project
*
* 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.afwsamples.testdpc.safetynet;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.text.method.ScrollingMovementMethod;
import android.util.Base64;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import com.afwsamples.testdpc.R;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallbacks;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.safetynet.SafetyNet;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.SecureRandom;
import static com.google.android.gms.safetynet.SafetyNetApi.AttestationResult;
/**
* Demonstrate how to use SafetyNet API to check device compatibility.
* Please notice that you should verifying the payload in your server.
* For more details, please check http://developer.android.com/training/safetynet/index.html.
*/
public class SafetyNetFragment extends DialogFragment implements
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
private GoogleApiClient mGoogleApiClient;
private TextView mMessageView;
private @ColorInt int BLACK, DARK_RED;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Kick start the checking
mGoogleApiClient = buildGoogleApiClient();
}
@Override
public void onStart() {
super.onStart();
updateMessageView(R.string.safetynet_running, false);
mGoogleApiClient.connect();
}
@Override
public void onStop() {
super.onStop();
mGoogleApiClient.disconnect();
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
BLACK = ContextCompat.getColor(getActivity(), R.color.text_black);
DARK_RED = ContextCompat.getColor(getActivity(), R.color.dark_red);
LayoutInflater inflater = LayoutInflater.from(getActivity());
View rootView = inflater.inflate(R.layout.safety_net_attest_dialog, null);
mMessageView = (TextView) rootView.findViewById(R.id.message_view);
// Show scrollbar in textview.
mMessageView.setMovementMethod(new ScrollingMovementMethod());
return new AlertDialog.Builder(getActivity())
.setView(rootView)
.setTitle(R.string.safetynet_dialog_title)
.setNeutralButton(android.R.string.ok, null)
.create();
}
@Override
public void onConnected(@Nullable Bundle bundle) {
if (hasInternetConnection()) {
runSaftyNetTest();
} else {
updateMessageView(R.string.safetynet_fail_reason_no_internet, true);
}
}
@Override
public void onConnectionSuspended(int i) {
updateMessageView(R.string.cancel_safetynet_msg, true);
}
@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
if (connectionResult.getErrorCode() == ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED) {
updateMessageView(R.string.safetynet_fail_reason_gmscore_upgrade, true);
} else {
updateMessageView(getString(R.string.safetynet_fail_reason_error_code,
connectionResult.getErrorCode()), true);
}
}
private GoogleApiClient buildGoogleApiClient() {
return new GoogleApiClient.Builder(getActivity())
.addApi(SafetyNet.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}
/**
* For simplicity, we generate the nonce in the client. However, it should be generated on the
* server for anti-replay protection.
*/
private byte[] generateNonce() {
byte[] nonce = new byte[32];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(nonce);
return nonce;
}
private void runSaftyNetTest() {
final byte[] nonce = generateNonce();
SafetyNet.SafetyNetApi.attest(mGoogleApiClient, nonce)
.setResultCallback(new ResultCallbacks<AttestationResult>() {
@Override
public void onSuccess(@NonNull AttestationResult attestationResult) {
if (isDetached()) {
return;
}
final String jws = attestationResult.getJwsResult();
try {
final JSONObject jsonObject = retrievePayloadFromJws(jws);
final String jsonString = jsonObject.toString(4);
final String verifyOnServerString
= getString(R.string.safetynet_verify_on_server);
updateMessageView(verifyOnServerString + "\n" + jsonString, false);
} catch (JSONException ex) {
updateMessageView(R.string.safetynet_fail_reason_invalid_jws, true);
}
}
@Override
public void onFailure(@NonNull Status status) {
if (isDetached()) {
return;
}
updateMessageView(R.string.safetynet_fail_to_run_api, true);
}
});
}
private void updateMessageView(int message, boolean isError) {
updateMessageView(getString(message), isError);
}
private void updateMessageView(String message, boolean isError) {
mMessageView.setText(message);
mMessageView.setTextColor((isError) ? DARK_RED : BLACK);
}
private boolean hasInternetConnection() {
ConnectivityManager cm =
(ConnectivityManager) getActivity().getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
return activeNetwork != null && activeNetwork.isConnected();
}
private static JSONObject retrievePayloadFromJws(String jws) throws JSONException {
String[] parts = jws.split("\\.");
if (parts.length != 3) {
throw new JSONException("Invalid JWS");
}
return new JSONObject(new String(Base64.decode(parts[1], Base64.URL_SAFE)));
}
}