All articles
8 min read
October 23, 20268 min read

Hardening Flutter Apps: SSL Pinning, Biometric Keychain & Code Obfuscation

Secure production Flutter applications against decompilation, API sniffing, and reverse engineering using SSL pinning, biometric secure storage, and ProGuard/R8 obfuscation.

Tags:#Flutter#Mobile Security#SSL Pinning#Keychain#Cryptography
Hardening Flutter Apps: SSL Pinning, Biometric Keychain & Code Obfuscation

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.

Mobile Security Architecture
Mobile Security Architecture

Defense-in-depth spans transport encryption, hardware-backed keychains, and binary symbol stripping.

Mobile Threat Model

Attack VectorThreatMitigation
Proxy Inspection (Charles / Burp)Intercepting auth tokens and financial payloadsCustom SecurityContext with Certificate Pinning
Root / Jailbreak TamperingMemory injection via Frida / MagiskEnvironment integrity checks & safety APIs
Stolen Device Data ExtractionUnencrypted SQLite / SharedPreferences extractionAndroid Keystore & iOS Keychain via flutter_secure_storage
Binary DisassemblyExtracting secret keys and business logic from APK/IPADart 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:

dart
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:

dart
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:

bash
# 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/ios

Store the ./debug_symbols folder safely in your CI artifacts. You will need it to symbolicate crash reports received from Sentry or Crashlytics.