Anatomi Masalah: Client Tampering dan Batasan Logika Frontend
Aplikasi React Native rentan terhadap dekompilasi, reverse engineering, dan injeksi kode runtime menggunakan runtime instrumentation toolkit seperti Frida atau Xposed. Logika validasi yang sepenuhnya bertumpu pada JavaScript bundle (index.android.bundle) mudah di-bypass dengan mengubah conditional statement sederhana atau mengaitkan (hooking) return value modul keamanan.
Untuk memvalidasi bahwa request API backend berasal dari binary aplikasi resmi, berjalan di sistem operasi resmi yang belum di-root, dan tidak dijalankan dalam lingkungan emulator otomatis (seperti pada skenario credential stuffing), backend memerlukan jaminan kriptografis dari platform OS penyedia runtime: Google Play Integrity API untuk Android dan DCAppAttestService (App Attest) untuk iOS.
Arsitektur Nonce-Based Attestation Handshake
Attestation tanpa parameter unik rentan terhadap replay attacks, di mana penyerang merekam token integritas valid lalu mengirimkannya berulang kali bersama payload yang dimanipulasi. Handshake yang aman mewajibkan pembuatan nonce sekali pakai (one-time nonce) yang di-generate oleh backend.
Alur kerja validasi integritas berjalan sebagai berikut:
- Nonce Challenge: Client meminta tantangan kriptografis sebelum mengeksekusi aksi sensitif (misal: login, transfer dana, order checkout). Backend membuat nonce acak (minimal 16 byte, di-hash via SHA-256) dan mengaitkannya ke session/TTL sementara di cache (Redis).
- Platform Request: JavaScript mengirimkan nonce ke Native Module (Kotlin/Swift). Native Module meneruskan nonce ke Play Integrity SDK / App Attest API.
- Attestation Token Generation: Google Play Services atau Secure Enclave iOS menghasilkan payload terenkripsi yang memuat hardware identity, hash binary aplikasi, dan nonce challenge.
- Verification Request: React Native mengirimkan token integritas mentah beserta payload bisnis ke backend.
- Server-side Decrypt & Verdict: Backend mendekripsi token via Google Play API, memverifikasi kesesuaian nonce, package name, signing certificate SHA-256, serta memeriksa level integritas perangkat sebelum menyetujui transaksi.
Implementasi Native Bridging
1. Android: Kotlin Native Module (Play Integrity)
Tambahkan dependency resmi pada android/app/build.gradle:
dependencies {
implementation 'com.google.android.play:integrity:1.3.0'
}Implementasikan Native Module untuk mengeksekusi request token ke Google Play Services:
package com.yourapp.integrity
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.google.android.play.core.integrity.IntegrityManagerFactory
import com.google.android.play.core.integrity.IntegrityTokenRequest
import com.google.android.play.core.integrity.model.IntegrityErrorCode
class PlayIntegrityModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
override fun getName() = "PlayIntegrity"
@ReactMethod
fun requestIntegrityToken(projectNumber: String, nonce: String, promise: Promise) {
try {
val integrityManager = IntegrityManagerFactory.create(reactApplicationContext)
val request = IntegrityTokenRequest.builder()
.setCloudProjectNumber(projectNumber.toLong())
.setNonce(nonce)
.build()
integrityManager.requestIntegrityToken(request)
.addOnSuccessListener { response ->
promise.resolve(response.token())
}
.addOnFailureListener { exception ->
promise.reject("INTEGRITY_FETCH_FAILED", exception.message, exception)
}
} catch (e: Exception) {
promise.reject("UNEXPECTED_ERROR", e.message, e)
}
}
}2. iOS: Swift Native Module (DeviceCheck / App Attest)
Di iOS, Apple menyediakan DCAppAttestService untuk menguji keabsahan aplikasi:
import Foundation
import DeviceCheck
import CryptoKit
@objc(AppAttestModule)
class AppAttestModule: NSObject {
@objc
func requestAttestation(_ nonce: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
let service = DCAppAttestService.shared
guard service.isSupported else {
reject("UNSUPPORTED", "App Attest is not supported on this device", nil)
return
}
service.generateKey { keyId, error in
if let error = error {
reject("KEY_GEN_FAILED", error.localizedDescription, error)
return
}
guard let keyId = keyId else {
reject("KEY_NULL", "KeyId generation returned null", nil)
return
}
guard let clientData = nonce.data(using: .utf8) else {
reject("INVALID_NONCE", "Failed to convert nonce to data", nil)
return
}
let clientDataHash = Data(SHA256.hash(data: clientData))
service.attestKey(keyId, clientDataHash: clientDataHash) { attestation, error in
if let error = error {
reject("ATTESTATION_FAILED", error.localizedDescription, error)
return
}
guard let attestation = attestation else {
reject("ATTEST_NULL", "Attestation payload is null", nil)
return
}
let result: [String: String] = [
"keyId": keyId,
"attestation": attestation.base64EncodedString()
]
resolve(result)
}
}
}
}3. React Native TypeScript Layer
Abstraksikan modul platform ke antarmuka tunggal:
import { NativeModules, Platform } from 'react-native';
const { PlayIntegrity, AppAttestModule } = NativeModules;
interface AttestationResult {
token: string;
keyId?: string;
}
export async function fetchDeviceAttestation(nonce: string, googleProjectNumber: string): Promise<AttestationResult> {
if (Platform.OS === 'android') {
const token = await PlayIntegrity.requestIntegrityToken(googleProjectNumber, nonce);
return { token };
} else if (Platform.OS === 'ios') {
const result = await AppAttestModule.requestAttestation(nonce);
return { token: result.attestation, keyId: result.keyId };
}
throw new Error('Platform not supported for hardware attestation');
}Validasi Verdict di Sisi Backend
Hindari validasi token di sisi client. Seluruh token mentah harus diteruskan ke server. Untuk Android, gunakan Google APIs Client Library untuk mendekripsi verdict token:
import { google } from 'googleapis';
const playIntegrity = google.playintegrity('v1');
async function verifyAndroidToken(integrityToken: string, expectedNonce: string) {
const auth = new google.auth.GoogleAuth({
scopes: ['https://www.googleapis.com/auth/playintegrity'],
});
const authClient = await auth.getClient();
const res = await playIntegrity.v1.decodeIntegrityToken({
packageName: 'com.yourapp',
requestBody: { integrityToken },
auth: authClient as any,
});
const payload = res.data.tokenPayloadExternal;
if (!payload) {
throw new Error('DECODE_EMPTY');
}
// 1. Verifikasi Nonce Binding
if (payload.requestDetails?.nonce !== expectedNonce) {
throw new Error('NONCE_MISMATCH');
}
// 2. Evaluasi App Recognition
const appVerdict = payload.appIntegrity?.appRecognitionVerdict;
if (appVerdict !== 'PLAY_RECOGNIZED') {
throw new Error(`UNRECOGNIZED_BINARY: ${appVerdict}`);
}
// 3. Evaluasi Device Recognition
const deviceVerdict = payload.deviceIntegrity?.deviceRecognitionVerdict || [];
const meetsBasicIntegrity = deviceVerdict.includes('MEETS_BASIC_INTEGRITY');
const meetsStrongIntegrity = deviceVerdict.includes('MEETS_STRONG_INTEGRITY');
if (!meetsBasicIntegrity) {
throw new Error('COMPROMISED_DEVICE');
}
// 4. Evaluasi Account Licensing
if (payload.accountDetails?.appLicensingVerdict !== 'LICENSED') {
// Sideloaded APK atau modded distribution
throw new Error('UNLICENSED_INSTALL');
}
return { verified: true, strongHardwareProtected: meetsStrongIntegrity };
}Handling Edge Cases dan Error Fallback
- Google Play Services Usang: Jika perangkat pengguna memiliki Google Play Services versi lama, request token akan mengembalikan error
API_NOT_AVAILABLEatauSERVICE_DISABLED. Aplikasi harus mengecek ketersediaan viaGoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)dan memandu pengguna memperbarui layanan Play Services. - Quota Limits: Standard tier Play Integrity memiliki limit harian request decode (biasanya 10.000 per hari jika menggunakan Classic Request API tanpa Play Integrity Cloud console linking). Daftarkan aplikasi ke Google Cloud Console untuk menaikkan tier ke Standard (Unlimited requests) jika volume trafik tinggi.
- Network Timeout: Handshake attestation melibatkan komunikasi jaringan antara Google Play Services dan server Google. Buat mekanisme retry terikat (maksimal 2x dengan exponential backoff) khusus untuk network error native.
Strategi Caching dan Efisiensi Performa
Memanggil Play Integrity API secara sinkronis pada setiap HTTP request akan mendegradasi performa dan latensi (overhead 300ms–1200ms per pemanggilan hardware native). Gunakan strategi berikut:
Aturan Prinsip Caching: Jangan memvalidasi token integritas pada read-only endpoint biasa. Batasi handshake hanya pada transaksi berdampak tinggi (State-Changing Transactions) seperti authentication, pembayaran, dan eksekusi transfer data kritis.
- Device Attestation Session Token: Setelah client lolos validasi integrity token pada fase otentikasi awal, backend menerbitkan short-lived verified device session token (misal: disimpan di Redis dengan TTL 15–30 menit, dipetakan ke
device_idterenkripsi). - Risk-Based Step-Up Challenge: Request API reguler hanya mengecek session token tersebut. Ketika request mendeteksi anomali (perubahan IP drastis, transaksi melewati threshold, deteksi emulator heuristic dasar), backend secara dinamis mengembalikan HTTP 403 Challenge yang mewajibkan React Native melakukan re-attestation ke Play Integrity API.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!