Token Rahasia di Aplikasi Flutter: Kenapa String.fromEnvironment dan --dart-define Tidak Menyembunyikannya
- Authors

- Name
- Samsul Hadi
- Threads
- @Threads


Kamu menulis satu baris konfigurasi supaya aplikasi bisa mengirim log error ke Telegram. Token bot ditaruh sebagai nilai default, supaya developer lain tidak perlu repot mengisi apa pun saat build. Aplikasinya rilis, dipasang di HP pengguna, semuanya jalan.
Yang tidak terlihat: token itu ikut terkirim ke setiap HP yang memasang aplikasinya. Siapa pun yang membongkar file APK bisa membacanya, lalu mengirim pesan ke channel itu sesukanya.
Ini saya temukan di aplikasi HP untuk salah satu proyek klien yang saya pegang. Token bot Telegram untuk log error tertanam sebagai nilai default String.fromEnvironment. Lebih buruk lagi, bot itu bot pribadi, bukan akun proyek.
Artikel ini membahas kenapa nilai seperti itu selalu ikut ke dalam hasil build, kenapa menghapusnya dari kode tidak menarik kembali yang sudah beredar, kenapa --dart-define juga bukan tempat menyimpan rahasia, dan langkah yang benar-benar menutup aksesnya. Semua screenshot berasal dari demo Dart yang saya jalankan dengan token palsu.
Kenapa token di kode Flutter selalu ikut ke HP pengguna
String.fromEnvironment adalah konstanta yang nilainya ditentukan saat compile. Kalau tidak ada nilai yang diberikan saat build, Dart memakai defaultValue. Dalam kedua kasus, hasilnya adalah string biasa yang ditulis langsung ke dalam hasil build.
Di build release Flutter untuk Android, kode Dart dikompilasi ahead-of-time menjadi libapp.so di dalam APK. APK sendiri hanya file zip. Membukanya tidak butuh alat khusus: unzip untuk mengeluarkan libapp.so, lalu strings untuk menampilkan semua teks yang bisa dibaca di dalamnya. Teks yang dipakai aplikasi, termasuk URL dan token, muncul utuh.
Karena itu ada dua salinan yang tidak bisa ditarik kembali:
- Setiap APK yang sudah dibagikan. File di HP pengguna, di folder unduhan, atau di grup chat tidak berubah ketika kode sumbernya diubah.
- Riwayat git. Commit yang menghapus token hanya menambah versi baru. Versi lama tetap ada dan bisa dibaca siapa pun yang punya akses ke repo.
Ada satu jebakan lagi. Pindah dari defaultValue ke --dart-define terlihat seperti solusi, karena token tidak lagi tertulis di kode. Tapi --dart-define hanya mengganti dari mana nilai itu datang saat build. Nilainya tetap ditulis ke dalam libapp.so dengan cara yang sama.
Isi commit perbaikannya
Perbaikannya ada di satu commit, 26 Agustus 2026:
- Default dihapus. Token dan chat ID tidak lagi punya nilai default di kode. Kalau perlu log jarak jauh, nilainya diberikan lewat
--dart-definesaat build. - Build tanpa token tetap jalan. Logger memang sudah memeriksa token dan chat ID sebelum mengirim apa pun, dan berhenti diam-diam kalau salah satunya kosong. Jadi build tanpa
--dart-definehanya tidak mengirim log. - Satu-satunya penutup ditulis terang di pesan commit. Menghapus token dari kode tidak menarik kembali yang sudah beredar. Token itu ada di riwayat git dan di setiap APK yang sudah dibagikan. Yang benar-benar menutup akses hanya mencabut token itu lewat BotFather dan membuat yang baru.
Di commit yang sama ada perbaikan kecil yang berhubungan: fungsi logout dulu menelan error-nya. Di build release tidak ada yang terjadi dan tidak ada yang tercatat, padahal logout yang gagal berarti token login masih ada di HP sementara pengguna mengira sudah keluar. Sekarang kegagalan itu dicatat, dan pengguna diberi tahu bahwa sesinya masih aktif.
Untuk membuktikan setiap klaim di atas, saya membuat program Dart kecil yang memakai token persis seperti logger aslinya: token menjadi bagian URL API Telegram. Lalu saya compile tiga versi dengan dart compile aot-snapshot, compiler AOT yang sama yang menghasilkan libapp.so di Flutter:

Versi dengan default menyimpan token utuh di dalam URL. Versi tanpa default dan tanpa --dart-define bersih. Versi tanpa default tapi di-build dengan --dart-define kembali menyimpan token. Ketiganya berjalan sesuai harapan:

Jadi perbaikan di commit itu menutup satu hal: token tidak lagi ikut di setiap build secara otomatis. Build yang memakai --dart-define tetap membawa tokennya, dan itu harus diperlakukan sebagai token yang bisa dibaca orang.
Menutup kebocoran token, urut dari yang paling penting
Langkah 1: cabut tokennya dulu
Urutannya penting. Sebelum mengubah kode, cabut token yang bocor dan buat yang baru. Untuk bot Telegram, ini dilakukan lewat BotFather. Untuk API key lain, lewat dashboard penyedianya. Selama token lama masih berlaku, semua langkah berikutnya hanya kosmetik.
Langkah 2: cari rahasia yang tertanam di kode
grep -rn "defaultValue:" lib/ | grep -i -E "token|key|secret|password"
grep -rn -E "[0-9]{8,10}:[A-Za-z0-9_-]{30,}" lib/ # pola token bot Telegram
Periksa juga file konfigurasi yang ikut dibundel, misalnya file .env di folder assets.
Langkah 3: periksa hasil build, bukan hanya kode
Kode yang bersih belum tentu hasil build yang bersih. Buka APK release dan cari langsung:
unzip -o app-release.apk 'lib/arm64-v8a/libapp.so' -d apk/
strings apk/lib/arm64-v8a/libapp.so | grep -E "api\.telegram\.org/bot|sk-|AIza"
Kalau ada yang muncul, siapa pun yang memegang APK itu bisa melihatnya juga.
Langkah 4: hapus default, biarkan build tanpa rahasia tetap jalan
// Sebelum
static const String telegramBotToken = String.fromEnvironment(
'TG_BOT_TOKEN',
defaultValue: '<token asli di sini>',
);
// Sesudah: tanpa default
static const String telegramBotToken = String.fromEnvironment('TG_BOT_TOKEN');
Lalu pastikan kode yang memakainya berhenti dengan aman kalau nilainya kosong:
if (_botToken.isEmpty || _chatId.isEmpty) return;
Langkah 5: pindahkan rahasia ke server kalau memang harus rahasia
Kalau aplikasi perlu mengirim log ke Telegram tanpa membawa token, kirim lognya ke backend kamu sendiri, lalu backend yang meneruskan ke Telegram. Token cukup ada di server. Aplikasi hanya tahu alamat endpoint backend, yang memang boleh publik, dan endpoint itu bisa diberi batas jumlah request.
Kalau kamu tetap memakai --dart-define, anggap nilainya publik. Pakai token khusus dengan izin sekecil mungkin, bukan token pribadi atau token admin.
Langkah 6: jangan berharap riwayat git akan lupa
Menulis ulang riwayat git dengan alat seperti git filter-repo bisa menghapus token dari repo, tapi tidak dari salinan yang sudah di-clone orang lain dan tidak dari APK yang sudah beredar:

Karena itu langkah 1 tetap yang utama. Menulis ulang riwayat adalah pilihan tambahan, bukan pengganti.
Checklist sebelum rilis aplikasi HP
- Tidak ada
defaultValueberisi token, key, atau password di kode. stringspadalibapp.sodari APK release tidak menemukan token, key, atau URL yang memuat rahasia.- Nilai dari
--dart-definediperlakukan sebagai publik. - Rahasia yang benar-benar rahasia hanya ada di server.
- Token yang tetap ada di aplikasi punya izin sekecil mungkin dan bukan milik pribadi.
- Kalau ada rahasia yang terlanjur bocor: dicabut dulu, baru kode diperbaiki.
- Error di jalur penting seperti logout tidak ditelan diam-diam.
Yang belum dibuktikan demo ini
- Demo ini memakai
dart compile aot-snapshot, bukan build APK Flutter lengkap. Compiler AOT-nya sama, jadi string konstanta tertulis dengan cara yang sama, tapi saya tidak membongkar APK aplikasi klien untuk artikel ini. - Compiler bisa membuang string yang tidak dipakai. Dalam percobaan pertama, token yang hanya dicek
isEmptytidak muncul di hasil build. Ia baru muncul setelah dipakai untuk menyusun URL, seperti di logger aslinya. Jangan mengandalkan perilaku ini sebagai pengaman. - Obfuscation Dart tidak menyembunyikan isi string. Opsi
--obfuscatemengganti nama class dan fungsi, bukan teks konstanta seperti token. - Artikel ini tidak membahas apakah token itu sudah dicabut. Pesan commit menyebut pencabutan lewat BotFather sebagai satu-satunya penutup. Itu pekerjaan di luar kode, dan tidak tercatat di repo.
- Nama aplikasi dan proyek klien tidak disebut. Kode di artikel disederhanakan dari commit aslinya.
Dua perintah di langkah 3 cukup untuk tahu posisi kamu: hasil kosong berarti aman untuk pola yang dicari, hasil tidak kosong berarti rahasia itu sudah ada di HP pengguna.
Artikel terkait
Tulisan lain yang nyambung dengan topik ini.


Checksum Saja Tidak Cukup: Cara Memverifikasi Installer dengan SHA-256 dan Tanda Tangan OpenSSL
Checksum SHA-256 yang cocok belum membuktikan paket itu asli. Empat pemeriksaan installer sebelum memasang: checksum, kunci publik yang dipatok, tanda tangan OpenSSL, dan path arsip.


Ternak dan Kolam di Rumah Tanpa Lupa Jadwal: Cara Saya Membagi Tugas antara AI dan Pengingat Biasa
Jadwal penetasan, vitamin, kuras kolam, dan belanja pakan dipindah dari kepala ke satu grup WhatsApp. Bagian mana yang dikerjakan AI, mana yang sengaja tidak, dan templat yang bisa disalin.


Tenggat Kabur Bikin Siaga Seharian: Cara Mengubah "Secepatnya" Jadi Tanggal yang Disepakati
"Kirim sebelum pulang ya" membuat kamu siaga sampai malam. Kenapa tenggat kabur melelahkan, data 99 pekerjaan di tracker saya, dan satu kalimat konfirmasi untuk mengubahnya jadi kesepakatan.
Comments
Komentar akan tampil setelah disetujui.