Menggunakan verdict integritas

Play Integrity API menggunakan verdict integritas untuk menyampaikan informasi tentang validitas perangkat, aplikasi, dan pengguna. Server aplikasi Anda dapat menggunakan payload yang dihasilkan dalam verdict yang telah didekripsi dan diverifikasi guna menentukan cara terbaik untuk melanjutkan dengan tindakan atau permintaan tertentu di aplikasi Anda.

Membuat nonce

Nonce adalah nomor identifikasi permintaan sekali pakai yang digunakan untuk memverifikasi integritas pesan. Idealnya, ID permintaan ini terikat dengan konteks tempatnya dibuat, seperti hash ID pengguna atau stempel waktu.

Nonce harus unik dan tidak dapat diprediksi. Jika pengguna mendapatkan verdict integritas untuk tindakan tertentu yang dilindungi, pengguna tidak akan dapat menggunakannya kembali untuk tindakan perlindungan berikutnya. Hal ini dikenal sebagai serangan replay dan dicegah oleh nonce.

Untuk membuat nonce, lakukan salah satu hal berikut:

Anda harus memilih teknik pembuatan nonce berdasarkan nilai tindakan yang dilindungi dan ancaman yang diketahui oleh aplikasi. Aplikasi klien Anda mungkin berada di bawah kontrol penyerang, sehingga akan jauh lebih aman untuk membuat nonce secara acak di sisi server daripada di sisi klien.

Nonce yang dibuat server

Langkah-langkah berikut menjelaskan cara membuat nonce menggunakan generator angka acak yang aman secara kriptografis di lingkungan server yang aman untuk setiap permintaan klien yang masuk:

  1. Aplikasi Anda memberi tahu backend aplikasi tentang suatu tindakan, seperti mendaftarkan pengguna baru.
  2. Backend aplikasi akan menghasilkan nonce acak dan unik.
  3. Backend aplikasi akan menambahkan pasangan nonce/permintaan ke tabel permintaan yang tertunda.
  4. Backend aplikasi akan meminta token integritas dari aplikasi, yang meneruskan nonce bersamaan dengan permintaan ini.
  5. Setelah berkomunikasi dengan Play Integrity API, aplikasi akan meneruskan token respons ke backend aplikasi. Token yang merepresentasikan verdict integritas ini kemudian didekripsi dan diverifikasi. Biasanya, backend aplikasi akan meneruskan token ke server Play untuk mendekripsi dan memverifikasi verdict, lalu meneruskan payload token kembali ke backend aplikasi. Backend aplikasi juga dapat melakukan langkah-langkah dekripsi dan verifikasi secara lokal.
  6. Backend aplikasi akan mengekstrak nonce dari payload yang didekripsi.
  7. Backend aplikasi akan memeriksa apakah nonce muncul di entri tabel permintaan yang tertunda, dan apakah permintaan yang sesuai tersebut cocok.
  8. Backend aplikasi akan menghapus entri dari tabel permintaan yang tertunda.
  9. Backend aplikasi akan mengekstrak stempel waktu dari payload yang didekripsi dan memverifikasi bahwa stempel waktu berasal dari masa lalu, dalam periode maksimum yang diizinkan antara waktu tersebut dan saat ini.
  10. Backend aplikasi akan mengekstrak nama paket dari payload yang didekripsi dan memverifikasi bahwa nama paket dalam payload cocok dengan nama paket aplikasi yang sebenarnya.

Gambar 1 menampilkan diagram urutan yang menggambarkan langkah-langkah ini.

Nonce bersifat unik dan tidak mungkin bisa diprediksi, sehingga pendekatan ini menawarkan jaminan kuat bahwa permintaan tidak dapat di-replay.

Gambar 1. Diagram urutan yang menunjukkan cara membuat nonce sisi server untuk digunakan dengan Play Integrity API.

Nonce yang dibuat klien

Beberapa klien mungkin tidak memiliki kemampuan untuk menyimpan semua permintaan yang masuk di sisi server atau mungkin tidak dapat menoleransi penundaan round-trip tambahan. Untuk kasus tersebut, ada solusi lain yang mengorbankan beberapa jaminan replay demi kemudahan. Hal ini didasarkan pada hashing parameter permintaan dan menyertakan stempel waktu:

  1. Aplikasi menyelesaikan tindakan, seperti mendaftarkan pengguna baru.
  2. Aplikasi membuat nonce, menggunakan hash parameter seperti waktu saat ini untuk menghitung nilai unik bagi permintaan khusus ini.
  3. Aplikasi meminta token integritas dari Play Integrity API, yang meneruskan nonce.
  4. Play Integrity API merespons dengan token yang menampilkan verdict integritas.
  5. Aplikasi meneruskan token, bersamaan dengan parameter yang digunakan untuk membuat nonce, ke backend aplikasi.
  6. Token tersebut didekripsi dan diverifikasi. Biasanya, backend aplikasi akan meneruskan token ke server Play untuk mendekripsi dan memverifikasi verdict, lalu meneruskan payload token kembali ke backend aplikasi. Backend aplikasi juga dapat melakukan langkah-langkah dekripsi dan verifikasi secara lokal.
  7. Backend aplikasi akan mengekstrak nonce dari payload yang didekripsi.
  8. Backend aplikasi akan menghitung hash parameter sisi klien yang diberikan dari aplikasi, dan memeriksa apakah nilai penghitungan ini cocok dengan nonce dari payload yang didekripsi.
  9. Backend aplikasi akan mengekstrak stempel waktu dari payload yang didekripsi dan memverifikasi bahwa stempel waktu berasal dari masa lalu, dalam periode maksimum yang diizinkan antara waktu tersebut dan saat ini.
  10. Backend aplikasi akan mengekstrak nama paket dari payload yang didekripsi dan memverifikasi bahwa nama paket dalam payload cocok dengan nama paket aplikasi yang sebenarnya.

Gambar 2 menampilkan diagram urutan yang menggambarkan langkah-langkah ini.

Stempel waktu harus disertakan dalam parameter permintaan, sehingga setiap nonce bersifat unik. Namun, karena stempel waktu dan parameter permintaan dibuat di klien, dan Anda tidak tahu apakah klien berada dalam kontrol penyerang, Anda harus berasumsi bahwa perilaku yang berlawanan mungkin terjadi.

Gambar 2. Diagram urutan yang menunjukkan cara membuat nonce sisi klien untuk digunakan dengan Play Integrity API.

Meminta verdict integritas

Setelah membuat nonce, Anda dapat meminta verdict integritas dari Google Play. Caranya, selesaikan langkah-langkah berikut:

  1. Buat IntegrityManager seperti yang ditunjukkan dalam cuplikan kode berikut.
  2. Gunakan pengelola untuk memanggil requestIntegrityToken() yang menyediakan nonce melalui metode setNonce() dalam builder IntegrityTokenRequest terkait.

Kotlin

// Receive the nonce from the secure server.
val nonce: String = ...

// Create an instance of a manager.
val integrityManager =
    IntegrityManagerFactory.create(applicationContext)

// Request the integrity token by providing a nonce.
val integrityTokenResponse: Task<IntegrityTokenResponse> =
    integrityManager.requestIntegrityToken(
        IntegrityTokenRequest.builder()
            .setNonce(nonce)
            .build())

// Allows your app to interpret error codes from the API.
integrityTokenResponse.addOnFailureListener(error -> ...)

Java

// Receive the nonce from the secure server.
String nonce = ...

// Create an instance of a manager.
IntegrityManager integrityManager =
    IntegrityManagerFactory.create(getApplicationContext());

// Request the integrity token by providing a nonce.
Task<IntegrityTokenResponse> integrityTokenResponse =
    integrityManager
      .requestIntegrityToken(
          IntegrityTokenRequest.builder().setNonce(nonce).build());

// Allows your app to interpret error codes from the API.
integrityTokenResponse.addOnFailureListener(error -> ...);

Unity

IEnumerator RequestIntegrityTokenCoroutine()
{
    // Receive the nonce from the secure server.
    var nonce = ...

    // Create an instance of a manager.
    var integrityManager = new IntegrityManager();

    // Request the integrity token by providing a nonce.
    var tokenRequest = new IntegrityTokenRequest(nonce);
    var requestIntegrityTokenOperation =
        integrityManager.RequestIntegrityToken(tokenRequest);

    // Wait for PlayAsyncOperation to complete.
    yield return requestIntegrityTokenOperation;

    // Check the resulting error code.
    if (requestIntegrityTokenOperation.Error != IntegrityErrorCode.NoError)
    {
        AppendStatusLog("IntegrityAsyncOperation failed with error: " +
                requestIntegrityTokenOperation.Error);
        yield break;
    }

    // Get the response.
    var tokenResponse = requestIntegrityTokenOperation.GetResult();
}

Native

/// Create an IntegrityTokenRequest opaque object.
const char* nonce = RequestNonceFromServer();
IntegrityTokenRequest* request;
IntegrityTokenRequest_create(&request);
IntegrityTokenRequest_setNonce(request, nonce);

/// Prepare an IntegrityTokenResponse opaque type pointer before calling
/// IntegerityManager_requestIntegrityToken().
IntegrityTokenResponse* response;
IntegrityErrorCode error_code = IntegrityManager_requestIntegrityToken(request, &response);

/// Proceed to polling iff error_code == INTEGRITY_NO_ERROR
if (error_code != INTEGRITY_NO_ERROR) {
    /// Remember to call the *_destroy() functions.
    return;
}

/// Use polling to wait for the async operation to complete.
IntegrityResponseStatus response_status;

/// Check for error codes.
IntegrityErrorCode error_code = IntegrityTokenResponse_getStatus(response, &response_status);
if (error_code == INTEGRITY_NO_ERROR && response_status == INTEGRITY_RESPONSE_COMPLETED) {
        const char* integrity_token = IntegrityTokenResponse_getToken(response);
        SendTokenToServer(integrity_token);
}

/// Remember to free up resources.
IntegrityTokenRequest_destroy(request);
IntegrityTokenResponse_destroy(response);
IntegrityManager_destroy();

Mendekripsi dan memverifikasi verdict integritas

Saat Anda meminta verdict integritas, Play Integrity API akan menyediakan token respons yang ditandatangani. Nonce yang Anda sertakan dalam permintaan Anda akan menjadi bagian dari token respons.

Format token

Token berupa JSON Web Token (JWT) bertingkat, yaitu JSON Web Encryption (JWE) dari JSON Web Signature (JWS). Komponen JWE dan JWS direpresentasikan menggunakan serialisasi ringkas.

Algoritme enkripsi dan penandatanganan didukung dengan baik di berbagai implementasi JWT:

  • JWE menggunakan A256KW untuk alg dan A256GCM untuk enc {: .external}
  • JWS menggunakan ES256.

Mendekripsi dan memverifikasi di server Google

Play Integrity API memungkinkan Anda mendekripsi dan memverifikasi verdict integritas di server Google, yang meningkatkan keamanan aplikasi. Untuk melakukannya, selesaikan langkah-langkah berikut:

  1. Buat akun layanan dalam project Google Cloud yang ditautkan ke aplikasi Anda.
  2. Di server aplikasi, buat permintaan berikut:

    playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \
      '{ "integrity_token": "INTEGRITY_TOKEN", "nonce": "NONCE"}'
  3. Baca respons JSON.

Mendekripsi dan memverifikasi secara lokal

Jika memilih untuk mengelola kunci API secara mandiri, Anda dapat mendekripsi dan memverifikasi token yang ditampilkan dalam lingkungan server aman Anda sendiri. Anda bisa mendapatkan token yang ditampilkan menggunakan metode IntegrityTokenResponse#token().

Contoh berikut menunjukkan cara mendekode kunci AES dan kunci EC publik yang dienkode DER untuk verifikasi tanda tangan dari Konsol Play ke kunci khusus bahasa di backend aplikasi. Perhatikan bahwa kunci dienkode dengan base64 menggunakan flag default.

Kotlin

// base64OfEncodedDecryptionKey is provided through Play Console.
var decryptionKeyBytes: ByteArray = Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT)

// Deserialized encryption (symmetric) key.
var decryptionKey: SecretKey = SecretKeySpec(
    decryptionKeyBytes,
    /* offset= */ 0,
    AES_KEY_SIZE_BYTES,
    AES_KEY_TYPE
)

// base64OfEncodedVerificationKey is provided through Play Console.
var encodedVerificationKey: ByteArray =
    Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT)

// Deserialized verification (public) key.
var verificationKey: PublicKey = KeyFactory.getInstance(EC_KEY_TYPE)
    .generatePublic(X509EncodedKeySpec(encodedVerificationKey))

Java

// base64OfEncodedDecryptionKey is provided through Play Console.
byte[] decryptionKeyBytes =
    Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT);

// Deserialized encryption (symmetric) key.
SecretKey decryptionKey =
    new SecretKeySpec(
        decryptionKeyBytes,
        /* offset= */ 0,
        AES_KEY_SIZE_BYTES,
        AES_KEY_TYPE);

// base64OfEncodedVerificationKey is provided through Play Console.
byte[] encodedVerificationKey =
    Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT);
// Deserialized verification (public) key.
PublicKey verificationKey =
    KeyFactory.getInstance(EC_KEY_TYPE)
        .generatePublic(new X509EncodedKeySpec(encodedVerificationKey));

Berikutnya, gunakan kunci ini untuk mendekripsi token integritas (bagian JWE) terlebih dahulu, lalu verifikasi dan ekstrak bagian JWS bertingkat.

Kotlin

val jwe: JsonWebEncryption =
    JsonWebStructure.fromCompactSerialization(integrityToken) as JsonWebEncryption
jwe.setKey(decryptionKey)

// This also decrypts the JWE token.
val compactJws: String = jwe.getPayload()

val jws: JsonWebSignature =
    JsonWebStructure.fromCompactSerialization(compactJws) as JsonWebSignature
jws.setKey(verificationKey)

// This also verifies the signature.
val payload: String = jws.getPayload()

Java

JsonWebEncryption jwe =
 (JsonWebEncryption)JsonWebStructure.fromCompactSerialization(integrityToken);
jwe.setKey(decryptionKey);

// This also decrypts the JWE token.
String compactJws = jwe.getPayload();

JsonWebSignature jws =
    (JsonWebSignature) JsonWebStructure.fromCompactSerialization(compactJws);
jws.setKey(verificationKey);

// This also verifies the signature.
String payload = jws.getPayload();

Payload yang dihasilkan adalah token teks biasa yang berisi sinyal integritas.

Anda juga harus memverifikasi bagian requestDetails payload JSON dengan memastikan bahwa nonce dan nama paket cocok dengan yang dikirim dalam permintaan asal:

Kotlin

val requestDetails: JSONObject =
    JSONObject(payload).getJSONObject("requestDetails")
val requestPackageName: String =
    requestDetails.getString("requestPackageName");
val nonce: String = requestDetails.getString("nonce");
val timestampMillis: Long = requestDetails.getLong("timestampMillis");
val currentTimestampMillis: Long = ...;

if (!requestPackageName.equals(expectedPackageName)
    // See "Generate nonce" section of the doc on how to store/compute
    // the expected nonce.
    || !nonce.equals(expectedNonce)
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
  // The token is invalid!
  ...
}

Java

JSONObject requestDetails =
    new JSONObject(payload).getJSONObject("requestDetails");
String requestPackageName =
    requestDetails.getString("requestPackageName");
String nonce = requestDetails.getString("nonce");
long timestampMillis = requestDetails.getLong("timestampMillis");
long currentTimestampMillis = ...;

if (!requestPackageName.equals(expectedPackageName)
    // See "Generate nonce" section of the doc on how to store/compute
    // the expected nonce.
    || !nonce.equals(expectedNonce)
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
  // The token is invalid!
  ...
}

Format payload yang ditampilkan

Payload berupa JSON teks biasa dan berisi sinyal integritas bersama informasi yang disediakan developer.

Berikut adalah struktur payload umum:

{
  requestDetails: { ... }
  appIntegrity: { ... }
  deviceIntegrity: { ... }
  accountDetails: { ... }
}

Bagian berikut menjelaskan setiap kolom secara lebih mendetail.

Kolom detail permintaan

Kolom requestDetails berisi informasi yang disediakan dalam permintaan, termasuk nonce. Nilai ini harus cocok dengan permintaan asal.

requestDetails: {
  // Application package name this attestation was requested for.
  // Note that this field might be spoofed in the middle of the
  // request.
  requestPackageName: "com.package.name"
  // base64-encoded web-safe no-wrap nonce provided by the developer.
  nonce: "aGVsbG8gd29scmQgdGhlcmU"
  // The timestamp in milliseconds when the request was made
  // (computed on the server).
  timestampMillis: 1617893780
}

Kolom integritas aplikasi

Kolom appIntegrity berisi informasi terkait paket.

appIntegrity: {
  // PLAY_RECOGNIZED, UNRECOGNIZED_VERSION, or UNEVALUATED.
  appRecognitionVerdict: "PLAY_RECOGNIZED"
  // The package name of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  packageName: "com.package.name"
  // The sha256 digest of app certificates on device.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  certificateSha256Digest: ["6a6a1474b5cbbb2b1aa57e0bc3"]
  // The version of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  versionCode: 42
}

appRecognitionVerdict dapat memiliki nilai berikut:

PLAY_RECOGNIZED
Aplikasi dan sertifikat cocok dengan versi yang didistribusikan oleh Google Play.
UNRECOGNIZED_VERSION
Sertifikat atau nama paket tidak cocok dengan data Google Play.
UNEVALUATED
Integritas aplikasi tidak dievaluasi. Persyaratan yang diperlukan tidak terpenuhi, seperti perangkat tidak cukup tepercaya.

Kolom integritas perangkat

Kolom deviceIntegrity berisi satu nilai, device_recognition_verdict, yang menunjukkan seberapa baik perangkat dapat menerapkan integritas aplikasi.

deviceIntegrity: {
  // "MEETS_DEVICE_INTEGRITY" is one of several possible values.
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
}

Secara default, device_recognition_verdict dapat memiliki salah satu label berikut:

MEETS_DEVICE_INTEGRITY
Aplikasi ini berjalan pada perangkat Android yang didukung oleh layanan Google Play. Perangkat ini lulus pemeriksaan integritas sistem dan memenuhi persyaratan kompatibilitas Android.
Tanpa label (nilai kosong)
Aplikasi sedang berjalan di perangkat yang memiliki tanda-tanda serangan (seperti hook API) atau penyusupan sistem (seperti di-root), atau aplikasi tidak berjalan di perangkat fisik (seperti emulator yang tidak lulus pemeriksaan integritas Google Play).

Jika Anda memilih untuk menerima label tambahan dalam verdict integritas, device_recognition_verdict dapat memiliki label tambahan berikut:

MEETS_BASIC_INTEGRITY
Aplikasi berjalan di perangkat yang telah lulus pemeriksaan integritas sistem dasar. Perangkat mungkin tidak memenuhi persyaratan kompatibilitas Android dan mungkin tidak disetujui untuk menjalankan layanan Google Play. Misalnya, perangkat mungkin menjalankan versi Android yang tidak dikenal, mungkin memiliki bootloader yang tidak terkunci, atau mungkin belum disertifikasi oleh produsen.
MEETS_STRONG_INTEGRITY
Aplikasi ini berjalan di perangkat Android yang didukung oleh layanan Google Play dan memiliki jaminan kuat atas integritas sistem seperti keystore yang didukung hardware. Perangkat ini melewati pemeriksaan integritas sistem dan memenuhi persyaratan kompatibilitas Android.

Untuk membantu menerima verdict yang dimaksud saat menguji akurasi label verdict, pastikan kondisi berikut terpenuhi di perangkat pengujian Anda:

  1. Proses debug USB dinonaktifkan.
  2. Bootloader terkunci.

Pelajari lebih lanjut cara membuat pengujian Play Integrity API untuk aplikasi Anda.

Kolom detail akun

Kolom accountDetails berisi nilai tunggal, licensingVerdict, yang menunjukkan status pemberian lisensi/hak aplikasi.

accountDetails: {
  // This field can be LICENSED, UNLICENSED, or UNEVALUATED.
  licensingVerdict: "LICENSED"
}

licensingVerdict dapat memiliki nilai berikut:

LICENSED
Pengguna memiliki hak aplikasi. Dengan kata lain, pengguna menginstal atau membeli aplikasi Anda di Google Play.
UNLICENSED
Pengguna tidak memiliki hak aplikasi. Hal ini terjadi jika, misalnya, pengguna melakukan sideload aplikasi Anda atau tidak mendapatkan aplikasi dari Google Play.
UNEVALUATED

Detail pemberian lisensi tidak dievaluasi karena persyaratan yang diperlukan tidak terjawab.

Hal ini dapat terjadi karena beberapa alasan, termasuk:

  • Perangkat tidak cukup tepercaya.
  • Versi aplikasi yang diinstal di perangkat tidak dikenal oleh Google Play.