Skip to content

Attest

Register this device's signing key with DEA. The SDK creates the key the first time it is needed, inside the Android Keystore — in StrongBox where the device has it — where the private half cannot be read by your app or by the SDK. Where possible, the registration also carries a Google Play Integrity token, so DEA can check that the device and the app are genuine.

DEA files the registration under the app your appKey belongs to. See Accounts and keys.

When to use it

  • Before a sensitive step, to make sure the device is registered
  • To show attestation status in a diagnostics screen

Prerequisites

  • The SDK is installed and initialized with your appId, appKey and playIntegrityProject. See the Quick Start and Accounts and keys.
  • Android 6.0 (API 23) or later. Below that, attest() answers UNSUPPORTED.
  • The Play Integrity setup below.

Platform setup

Play Integrity

Play only issues a genuine token to an app it recognizes: installed from Google Play, or signed with a key registered in your app's Play Console entry. When Play gives no token — on a device without Google Play services, or when Play refuses the request — the SDK sends the registration without one, and DEA may refuse it. attest() then answers FAILED, with DEA's answer in detail.

Pass the Google Cloud project number DEA issued for your app as playIntegrityProject. It must be the project your app is linked to on the App integrity page of Play Console. With any other number, attest() answers FAILED. See Accounts and keys.

DeaSdk.init(
    this,
    DeaConfig(appId = "YOUR_APP_ID", appKey = "YOUR_APP_KEY", playIntegrityProject = 123456789012L),
)

Usage

Attest

attest() makes network requests and blocks the calling thread, so call it off the main thread. These samples use Kotlin coroutines:

import ai.dea.sdk.DeaSdk
import ai.dea.sdk.attest.Verdict
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

suspend fun attestDevice() {
    val attestation = withContext(Dispatchers.IO) { DeaSdk.instance?.attest() } ?: return

    when (attestation.verdict) {
        Verdict.PASSED -> {
            // DEA accepted this device's key.
        }
        Verdict.UNSUPPORTED -> {
            // No key could be created on this device, or no appKey was given. See attestation.detail.
        }
        Verdict.FAILED -> {
            // DEA refused, or could not be reached. See attestation.detail.
        }
    }
}
  • attest() never throws.
  • The result is cached on the device until shortly before it expires, including across app restarts, so calling it again usually costs nothing.
  • The first call turns on automatic renewal. From then on the SDK renews the registration shortly before it expires, and re-checks whenever the app returns to the foreground. An app that never calls attest() never registers a key.
  • After a registration that used a Play Integrity token fails, the SDK waits before it tries again: one hour, doubling with each further failure up to seven days, also across app restarts. Until then attest() answers FAILED without a request, and detail says until when.

The registration sends DEA the key's public half, the Play Integrity token when there is one, and codes for any signs of a rooted, hooked, debugged or emulated device that the SDK found. See Privacy and network.

Check whether Play Integrity was part of it

A PASSED verdict means DEA accepted the key. platformAttested is true when a Play Integrity token went with the accepted registration:

if (attestation.verdict == Verdict.PASSED && attestation.platformAttested) {
    // DEA accepted the key, with Google's token about the device and app.
}

integrity carries DEA's own reading of the device, when DEA returns one.

Read the last result without attesting again

val last = DeaSdk.instance?.lastAttestation // null before the first attest()

To read the registration this install already holds, including one from an earlier launch, without any request:

val stored = withContext(Dispatchers.IO) { DeaSdk.instance?.storedAttestation() } // null if none is fresh

storedAttestation() reads the device's storage. When it finds a fresh registration, it turns on automatic renewal as attest() would.

Renew now

val renewed = withContext(Dispatchers.IO) { DeaSdk.instance?.renewAttestation() }

renewAttestation() registers again right away, even if the cached result is still valid. If the renewal fails, the registration already held is kept. While the SDK is waiting after refused registrations, it answers FAILED without a request, like attest(). Blocking, like attest().

Start over

DeaSdk.instance?.forgetAttestation()

Forgets the stored registration and any wait after refused attempts, and stops automatic renewal. The device key is kept, so the next attest() registers the same key again.

Watch automatic renewal

import ai.dea.sdk.DeaSdk
import ai.dea.sdk.attest.AttestRefresher
import android.util.Log

val dea = DeaSdk.instance!!

// Set before the first attest(), which reports the first schedule.
dea.onAttestationEvent = { event ->
    when (event) {
        is AttestRefresher.Event.Scheduled -> Log.d("MyApp", "renewal in ${event.afterSeconds} s")
        is AttestRefresher.Event.Fired -> Log.d("MyApp", "renewed: ${event.verdict}")
        AttestRefresher.Event.Idle -> Log.d("MyApp", "nothing to renew")
    }
}

Log.d("MyApp", "next renewal at ${dea.nextAttestationRefresh}") // epoch seconds, or null

The callback runs on the thread that made the decision, which is not always the main thread.

Examples

Attest before opening a payment screen

import ai.dea.sdk.DeaSdk
import ai.dea.sdk.attest.Verdict
import android.content.Intent
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

fun AppCompatActivity.openPayment() {
    lifecycleScope.launch {
        val attestation = withContext(Dispatchers.IO) { DeaSdk.instance?.attest() }

        if (attestation?.verdict != Verdict.PASSED) {
            Log.w("MyApp", "DEA attestation: $attestation")
        }

        startActivity(Intent(this@openPayment, PaymentActivity::class.java))
    }
}

API

Member Returns Description
attest() Attestation Registers the device key and returns the result. Blocking
renewAttestation() Attestation Registers again now. Blocking
lastAttestation Attestation? The last result, without attesting again
storedAttestation() Attestation? The fresh registration this install holds, read from storage without a request
forgetAttestation() Unit Forgets the registration and stops automatic renewal; keeps the key
nextAttestationRefresh Long? When the next automatic renewal runs, epoch seconds
onAttestationEvent ((AttestRefresher.Event) -> Unit)? Called on each automatic renewal decision

Attestation:

Field Type Description
supported Boolean Whether the SDK could register: false when no key could be created, or no appKey was given
verdict Verdict PASSED, FAILED or UNSUPPORTED
platformAttested Boolean Whether a Play Integrity token went with an accepted registration
platformTokenSpent Boolean Whether this attempt obtained a Play Integrity token, whatever DEA answered
integrity String? DEA's reading of the device's integrity, when DEA returned one
expiresAt Long? Expiry, in epoch seconds
kid String? The key's identifier
tier String? The trust tier DEA assigned
backing String? Where the key is kept: strongbox, tee or software
detail String? Why the SDK did not register, or what DEA or the network answered when it failed