Google SafetyNet API JwsResult is a hashed string instead of JSON

1.1k Views Asked by At

I've successfully implemented Google SafetyNet API, even have a successful response. Problem is that the JWSResult from AttestationResponse is a hashed string, whereas my expectation was to get a JSON in response.

May I ask where do I need to first look for problems?

Here is the code where attest() is called:

    fun callSafetyNetAttentationApi(context: Activity, callback: SafetyNetCallback) {

    if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS) {

        val nonce: ByteArray? = getRequestNonce("Safety Net Sample: " + System.currentTimeMillis())
        val client = SafetyNet.getClient(context)

        nonce?.let {
            val task: Task<AttestationResponse> = client.attest(it, BuildConfig.SAFETY_NET_KEY)

            task.addOnSuccessListener { response -> safetyNetSuccess(response, callback) }
                    .addOnFailureListener { callback.isDeviceTrusted(false) }

        } ?: kotlin.run {
            callback.isDeviceTrusted(false)
        }

    } else {
        MaterialDialog.Builder(context)
                .title("The app cannot be used")
                .content("Please update Google Play Services and try again.")
                .cancelable(false)
                .positiveText("Dismiss")
                .onPositive { dialog, which -> context.finish() }
                .show()
    }
}
2

There are 2 best solutions below

1
On

This is a typical JSON response that you'll receive after performing safetyNetClient.attest(nonce, apiKey):

{
   "jwsResult": "foo.bar.zar",
   "uid": "11426643",
   "authCode": "H3o28i\/ViJUPRAW\/q4IUe1AMAbD-2jYp82os9v",
   "app": 1,
   "androidId": "vece15a43449afa9"
}

Here foo.bar.zar is a base64 encoded string, something like aisnfaksdf.8439hundf.ghbadsjn, where each part corresponds to:

<Base64 encoded header>.<Base64 encoded JSON data>.<Base64 encoded signature>

You need to take the bar and Base64 decode that in order to get the SafetyNet result JSON:

    private fun extractJwsData(jws: String?): ByteArray? {
        val parts = jws?.split("[.]".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
        if (parts?.size != 3) {
            System.err.println("Failure: Illegal JWS signature format. The JWS consists of "
                    + parts?.size + " parts instead of 3.")
            return null
        }
        return Base64.decode(parts[1], Base64.DEFAULT)
    }

Then construct java object using the JSON library you like, e.g. GSON:

val model = Gson().fromJson(extractJwsData(jws).toString(), SafetyNetApiModel::class.java)

Where SafetyNetApiModel is:

class SafetyNetApiModel {
    @SerializedName("nonce")
    var nonce: String? = null

    @SerializedName("timestampMs")
    var timestampMs: Long? = null

    @SerializedName("apkPackageName")
    var apkPackageName: String? = null

    @SerializedName("apkCertificateDigestSha256")
    var apkCertificateDigestSha256: List<String>? = null

    @SerializedName("apkDigestSha256")
    var apkDigestSha256: String? = null

    @SerializedName("ctsProfileMatch")
    var ctsProfileMatch: Boolean? = null

    @SerializedName("basicIntegrity")
    var basicIntegrity: Boolean? = null
}

Have a look at this for reference.

6
On

JWS response is always the sign result. After getting the JWS response, you have to verify it from the server-side code with the nonce then the server will be verified the request and if it is a valid request then it returns JSON response like this

Check out the link send JWS response to the server for verification

For sample app:

private OnSuccessListener<SafetyNetApi.AttestationResponse> mSuccessListener =
        new OnSuccessListener<SafetyNetApi.AttestationResponse>() {
            @Override
            public void onSuccess(SafetyNetApi.AttestationResponse attestationResponse) {
                /*
                 Successfully communicated with SafetyNet API.
                 Use result.getJwsResult() to get the signed result data. See the server
                 component of this sample for details on how to verify and parse this result.
                 */
                mResult = attestationResponse.getJwsResult();
                Log.d(TAG, "Success! SafetyNet result:\n" + mResult + "\n");

                    /*
                     TODO(developer): Forward this result to your server together with
                     the nonce for verification.
                     You can also parse the JwsResult locally to confirm that the API
                     returned a response by checking for an 'error' field first and before
                     retrying the request with an exponential backoff.
                     NOTE: Do NOT rely on a local, client-side only check for security, you
                     must verify the response on a remote server!
                    */
            }
        };

Read the comment inside the success response, this code is from GitHub SafetyNet sample app