Because mobile devices are client environments controlled by untrusted end-users, mobile applications face reverse engineering, Man-in-the-Middle (MitM) traffic inspection, and runtime tampering.
Hardening a Flutter application involves defensive layers across network transport, data persistence, and compiled binary assembly.
Defense-in-depth spans transport encryption, hardware-backed keychains, and binary symbol stripping.
Mobile Threat Model
| Attack Vector | Threat | Mitigation |
|---|---|---|
| Proxy Inspection (Charles / Burp) | Intercepting auth tokens and financial payloads | Custom SecurityContext with Certificate Pinning |
| Root / Jailbreak Tampering | Memory injection via Frida / Magisk | Environment integrity checks & safety APIs |
| Stolen Device Data Extraction | Unencrypted SQLite / SharedPreferences extraction | Android Keystore & iOS Keychain via flutter_secure_storage |
| Binary Disassembly | Extracting secret keys and business logic from APK/IPA | Dart AOT --obfuscate and --split-debug-info |
Implementing SSL Certificate Pinning
Default HTTP clients trust any CA installed in the device's root store—enabling users to install self-signed certificates and decrypt traffic. Pinning enforces your server's exact public key certificate:
import 'dart:io';
import 'package:flutter/services.dart';
Future<HttpClient> createPinnedClient() async {
// Load official public certificate from embedded asset
final certBytes = await rootBundle.load('assets/certs/api.nishak.me.pem');
final securityContext = SecurityContext(withTrustedRoots: false);
securityContext.setTrustedCertificatesBytes(certBytes.buffer.asUint8List());
final client = HttpClient(context: securityContext);
client.badCertificateCallback = (X509Certificate cert, String host, int port) {
// Strictly reject any certificate not matching the pinned bundle
return false;
};
return client;
}Secure Credential Storage
Never store JWT tokens, refresh credentials, or encryption keys in SharedPreferences (which writes plaintext XML on Android). Instead, leverage hardware-backed storage:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureVault {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
keyCipherAlgorithm: KeyCipherAlgorithm.RSA_ECB_PKCS1Padding,
storageCipherAlgorithm: StorageCipherAlgorithm.AES_GCM_NoPadding,
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
static Future<void> saveToken(String token) async {
await _storage.write(key: 'auth_jwt', value: token);
}
static Future<String?> getToken() async {
return await _storage.read(key: 'auth_jwt');
}
}Obfuscating Dart Binaries in Release Builds
When compiling production builds, enable Dart symbol obfuscation to strip function and class names:
# Android App Bundle with Obfuscation
flutter build appbundle --release \
--obfuscate \
--split-debug-info=./debug_symbols/android
# iOS Archive with Obfuscation
flutter build ipa --release \
--obfuscate \
--split-debug-info=./debug_symbols/iosStore the ./debug_symbols folder safely in your CI artifacts. You will need it to symbolicate crash reports received from Sentry or Crashlytics.