Menyimpan JSON Web Token (JWT) atau refresh token dalam AsyncStorage adalah celah keamanan kritis pada aplikasi React Native. AsyncStorage beroperasi tanpa enkripsi bawaan: data disimpan dalam format file SQLite unencrypted atau XML plain text di sandbox aplikasi. Siapa pun yang memiliki akses fisik atau perantara ke perangkat dapat mengekstraksi kredensial sesi tanpa perlu membongkar payload kode.

Vektor Serangan: Ekstraksi Data dari Storage Plain Text

Dua vektor serangan umum yang mengeksploitasi penyimpanan token tanpa enkripsi adalah akses root filesystem dan ADB backup.

1. Root Filesystem Dump

Pada perangkat Android yang telah di-root atau perangkat iOS yang di-jailbreak, mekanisme isolasi sandbox aplikasi dapat ditembus. Penyerang dapat membaca direktori lokal secara langsung melalui shell:

adb shell
su
cat /data/data/com.aplikasi.anda/databases/RKStorage
# atau membaca file SharedPreferences default
cat /data/data/com.aplikasi.anda/shared_prefs/RKStorage.xml

Token autentikasi yang tersimpan di AsyncStorage langsung terekspos dalam bentuk teks biasa, memungkinkan pembajakan sesi (account takeover) tanpa interaksi korban.

2. Ekstraksi ADB Backup

Secara default, Android mengizinkan pencadangan data aplikasi via Android Debug Bridge (ADB) jika flag android:allowBackup bernilai true di AndroidManifest.xml. Penyerang cukup menghubungkan perangkat yang tidak terkunci ke komputer via USB:

adb backup -f backup.ab -noapk com.aplikasi.anda
# Konversi format Android Backup (.ab) ke tar archive
dd if=backup.ab bs=24 skip=1 | openssl zlib -d > backup.tar
tar -xvf backup.tar

Hasil ekstraksi tersebut mencakup basis data SQLite AsyncStorage secara utuh.

Arsitektur Hardware-Backed Security: Keystore & Keychain

Untuk memitigasi vektor ekstraksi, kunci enkripsi dan data kredensial harus didelegasikan ke subsistem keamanan berbasis perangkat keras.

  • Android Keystore: Kunci kriptografi dibuat dan disimpan di dalam Trusted Execution Environment (TEE) atau StrongBox (chip HSM terpisah seperti Titan M). Operasi enkripsi/dekripsi dieksekusi di dalam secure domain. Kunci privat tidak pernah dimuat ke memori OS utama (Linux Kernel/Android runtime), sehingga memblokir ekstraksi oleh root exploit.
  • iOS Keychain Services: Kredensial dienkripsi menggunakan mekanisme hardware crypto engine dari Secure Enclave. Melalui konfigurasi atribut kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, data hanya didekripsi saat perangkat telah dibuka kuncinya pertama kali pasca-reboot dan dibatasi agar tidak dapat dipindahkan ke perangkat lain via iCloud maupun unencrypted iTunes backup.

Implementasi Secure Storage Service

Gunakan library react-native-keychain untuk mengikat interaksi native Keystore dan Keychain ke React Native runtime.

npm install react-native-keychain
# atau
yarn add react-native-keychain

Implementasikan storage service menggunakan TypeScript dengan penegakan level hardware security dan opsi fallback:

// src/services/SecureAuthStorage.ts
import * as Keychain from 'react-native-keychain';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface AuthTokens {
  accessToken: string;
  refreshToken: string;
}

const SERVICE_NAME = 'com.aplikasi.auth';
const LEGACY_STORAGE_KEY = '@auth_tokens_legacy';

export class SecureAuthStorage {
  /**
   * Menyimpan token dengan prioritas SECURE_HARDWARE (TEE/StrongBox).
   */
  static async setTokens(tokens: AuthTokens): Promise {
    const payload = JSON.stringify(tokens);

    try {
      const result = await Keychain.setGenericPassword('auth_session', payload, {
        service: SERVICE_NAME,
        accessible: Keychain.ACCESSIBLE.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY,
        securityLevel: Keychain.SECURITY_LEVEL.SECURE_HARDWARE,
      });
      return Boolean(result);
    } catch (error) {
      // Graceful fallback jika hardware-backed Keystore tidak didukung perangkat
      return this.setTokensFallback(payload);
    }
  }

  /**
   * Fallback penyimpanan ke software-backed Keystore.
   */
  private static async setTokensFallback(payload: string): Promise {
    try {
      const fallbackResult = await Keychain.setGenericPassword('auth_session', payload, {
        service: SERVICE_NAME,
        accessible: Keychain.ACCESSIBLE.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY,
        securityLevel: Keychain.SECURITY_LEVEL.SECURE_SOFTWARE,
      });
      return Boolean(fallbackResult);
    } catch (fallbackError) {
      console.error('Gagal menyimpan token ke secure storage:', fallbackError);
      return false;
    }
  }

  /**
   * Mengambil token dengan mekanisme Silent Migration dari AsyncStorage lama.
   */
  static async getTokens(): Promise {
    try {
      // 1. Cek kredensial di hardware storage terlebih dahulu
      const credentials = await Keychain.getGenericPassword({
        service: SERVICE_NAME,
      });

      if (credentials) {
        return JSON.parse(credentials.password) as AuthTokens;
      }

      // 2. Jalankan silent migration jika hardware storage kosong
      return await this.migrateFromLegacyStorage();
    } catch (error) {
      console.error('Gagal membaca token secure storage:', error);
      return null;
    }
  }

  /**
   * Membaca AsyncStorage, migrasi ke Keychain, lalu menghapus data lama.
   */
  private static async migrateFromLegacyStorage(): Promise {
    try {
      const legacyData = await AsyncStorage.getItem(LEGACY_STORAGE_KEY);
      if (!legacyData) {
        return null;
      }

      const parsedTokens = JSON.parse(legacyData) as AuthTokens;

      // Amankan ke Keychain/Keystore
      const migrated = await this.setTokens(parsedTokens);
      if (migrated) {
        // Hapus token plain text setelah migrasi sukses
        await AsyncStorage.removeItem(LEGACY_STORAGE_KEY);
        return parsedTokens;
      }

      return null;
    } catch (migrationError) {
      console.error('Migrasi data gagal:', migrationError);
      return null;
    }
  }

  /**
   * Menghapus seluruh sesi autentikasi.
   */
  static async clearTokens(): Promise {
    try {
      await AsyncStorage.removeItem(LEGACY_STORAGE_KEY);
      return await Keychain.resetGenericPassword({ service: SERVICE_NAME });
    } catch (error) {
      console.error('Gagal menghapus token:', error);
      return false;
    }
  }
}

Menangani Kasus Edge: Fallback dan Fragmentasi Android

Spesifikasi perangkat Android sangat bervariasi. Perangkat murah atau model lama sering kali tidak memiliki implementasi StrongBox Keymaster atau bahkan TEE yang stabil.

  • Deteksi Security Level: Jika pemanggilan dengan Keychain.SECURITY_LEVEL.SECURE_HARDWARE melempar error runtime (seperti KeyStoreException), wrapper di atas secara otomatis mencoba Keychain.SECURITY_LEVEL.SECURE_SOFTWARE. Meskipun tidak seaman modul hardware, software-backed Keystore tetap mengenkripsi kredensial dengan kunci yang diproteksi oleh Android OS master key dan tidak dapat diekstrak lewat file transfer ADB standar.
  • Device Lock State: Atribut ACCESSIBLE.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY memastikan background service tidak gagal mengakses token saat perangkat menyala, namun tetap mengunci data jika perangkat di-clone atau datanya dicadangkan ke eksternal storage.

Verifikasi Proteksi Manifest

Selain mengamankan storage via kode, nonaktifkan fitur auto-backup sistem operasi Android pada android/app/src/main/AndroidManifest.xml untuk mencegah backup system-level membaca artefak aplikasi yang tidak terenkripsi:

<application
    android:name=".MainApplication"
    android:allowBackup="false"
    android:fullBackupContent="false"
    ... >
</application>

Kombinasi antara react-native-keychain berbasis TEE/Secure Enclave, strategi silent migration tanpa memutus sesi user, dan deaktivasi allowBackup menutup celah ekstraksi kredensial offline pada aplikasi React Native produksi.