
Security should never be an afterthought in mobile app development. With increasing cyber threats and strict privacy regulations like GDPR and CCPA, protecting user data is both a legal requirement and a trust imperative.
This comprehensive guide covers essential security practices for Flutter apps, from basic data protection to advanced threat mitigation. Whether you're building a simple utility app or a complex fintech solution, these practices will help you build secure, trustworthy applications.
DATA ENCRYPTION & SECURE STORAGE
Protect sensitive data both at rest and in transit. Never store sensitive information in plain text, and always encrypt data before transmission.
// Secure Storage Implementation import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:crypto/crypto.dart'; import 'dart:convert'; class SecureStorageService { static const _storage = FlutterSecureStorage( aOptions: AndroidOptions( encryptedSharedPreferences: true, sharedPreferencesName: 'secure_prefs', preferencesKeyPrefix: 'app_', ), iOptions: IOSOptions( groupId: 'com.yourapp.security', accessibility: IOSAccessibility.first_unlock_this_device, synchronizable: false, ), ); // Store encrypted sensitive data static Future<void> storeSecureData(String key, String value) async { try { final encryptedValue = _encryptData(value); await _storage.write(key: key, value: encryptedValue); } catch (e) { throw SecurityException('Failed to store secure data: $e'); } } // Retrieve and decrypt sensitive data static Future<String?> getSecureData(String key) async { try { final encryptedValue = await _storage.read(key: key); if (encryptedValue == null) return null; return _decryptData(encryptedValue); } catch (e) { throw SecurityException('Failed to retrieve secure data: $e'); } } // Store authentication tokens securely static Future<void> storeAuthToken(String token) async { await storeSecureData('auth_token', token); await storeSecureData('token_timestamp', DateTime.now().toIso8601String()); } // Check if token is valid and not expired static Future<bool> isTokenValid() async { final token = await getSecureData('auth_token'); final timestampStr = await getSecureData('token_timestamp'); if (token == null || timestampStr == null) return false; final timestamp = DateTime.parse(timestampStr); final now = DateTime.now(); const tokenLifetime = Duration(hours: 24); return now.difference(timestamp) < tokenLifetime; } // Clear all sensitive data static Future<void> clearAllSecureData() async { await _storage.deleteAll(); } // Basic encryption for additional security layer static String _encryptData(String data) { final bytes = utf8.encode(data); final digest = sha256.convert(bytes); return base64.encode(bytes); // In production, use proper encryption } static String _decryptData(String encryptedData) { final bytes = base64.decode(encryptedData); return utf8.decode(bytes); } } // Advanced Encryption Service import 'package:pointycastle/export.dart'; class AdvancedEncryptionService { static final _key = _generateKey(); static final _iv = _generateIV(); static Uint8List encrypt(String plainText) { final plainBytes = Uint8List.fromList(utf8.encode(plainText)); final cipher = AESFastEngine(); final cbcCipher = CBCBlockCipher(cipher); final paddedCipher = PaddedBlockCipher('AES/CBC/PKCS7'); paddedCipher.init(true, PaddedBlockCipherParameters( ParametersWithIV(KeyParameter(_key), _iv), null, )); return paddedCipher.process(plainBytes); } static String decrypt(Uint8List cipherBytes) { final cipher = AESFastEngine(); final cbcCipher = CBCBlockCipher(cipher); final paddedCipher = PaddedBlockCipher('AES/CBC/PKCS7'); paddedCipher.init(false, PaddedBlockCipherParameters( ParametersWithIV(KeyParameter(_key), _iv), null, )); final decrypted = paddedCipher.process(cipherBytes); return utf8.decode(decrypted); } static Uint8List _generateKey() { // In production, derive from secure random or user password final keyBytes = Uint8List(32); final secureRandom = SecureRandom('Fortuna'); secureRandom.seed(KeyParameter(Uint8List.fromList('your-secret-key'.codeUnits))); for (int i = 0; i < keyBytes.length; i++) { keyBytes[i] = secureRandom.nextUint8(); } return keyBytes; } static Uint8List _generateIV() { final ivBytes = Uint8List(16); final secureRandom = SecureRandom('Fortuna'); for (int i = 0; i < ivBytes.length; i++) { ivBytes[i] = secureRandom.nextUint8(); } return ivBytes; } }
✅ STORAGE DO'S
- • Use Flutter Secure Storage for tokens
- • Encrypt sensitive data before storage
- • Implement key rotation policies
- • Set data expiration timestamps
❌ STORAGE DON'TS
- • Never store passwords in plain text
- • Don't use SharedPreferences for secrets
- • Avoid hardcoded encryption keys
- • Don't store PII without encryption
API SECURITY & AUTHENTICATION
Secure API communication is crucial for protecting data in transit. Implement proper authentication, authorization, and request signing to prevent unauthorized access.
// Secure API Client Implementation import 'package:dio/dio.dart'; import 'package:dio_certificate_pinning/dio_certificate_pinning.dart'; import 'package:crypto/crypto.dart'; class SecureApiClient { late final Dio _dio; final String _baseUrl; final String _apiKey; SecureApiClient({ required String baseUrl, required String apiKey, }) : _baseUrl = baseUrl, _apiKey = apiKey { _setupDio(); } void _setupDio() { _dio = Dio(); // Certificate pinning for enhanced security _dio.interceptors.add( CertificatePinningInterceptor( allowedSHAFingerprints: [ 'YOUR_SERVER_CERTIFICATE_SHA256_FINGERPRINT', ], ), ); // Request/Response interceptor for security _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) { // Add security headers options.headers['Content-Type'] = 'application/json'; options.headers['User-Agent'] = 'YourApp/1.0.0'; options.headers['X-Requested-With'] = 'XMLHttpRequest'; // Add timestamp and nonce for replay attack prevention final timestamp = DateTime.now().millisecondsSinceEpoch.toString(); final nonce = _generateNonce(); options.headers['X-Timestamp'] = timestamp; options.headers['X-Nonce'] = nonce; // Add request signature final signature = _signRequest( method: options.method, path: options.path, timestamp: timestamp, nonce: nonce, body: options.data?.toString() ?? '', ); options.headers['X-Signature'] = signature; handler.next(options); }, onResponse: (response, handler) { // Verify response signature if provided final responseSignature = response.headers['x-response-signature']?.first; if (responseSignature != null) { if (!_verifyResponseSignature(response.data, responseSignature)) { throw DioError( requestOptions: response.requestOptions, error: 'Invalid response signature', type: DioErrorType.other, ); } } handler.next(response); }, onError: (error, handler) { _handleApiError(error); handler.next(error); }, ), ); } // Authenticated GET request Future<Map<String, dynamic>> get(String endpoint) async { try { final token = await SecureStorageService.getSecureData('auth_token'); if (token == null) throw UnauthorizedException('No auth token found'); final response = await _dio.get( '$_baseUrl$endpoint', options: Options( headers: {'Authorization': 'Bearer $token'}, ), ); return response.data; } catch (e) { throw _handleApiException(e); } } // Authenticated POST request with body encryption Future<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> data) async { try { final token = await SecureStorageService.getSecureData('auth_token'); if (token == null) throw UnauthorizedException('No auth token found'); // Encrypt sensitive data in request body final encryptedData = _encryptRequestBody(data); final response = await _dio.post( '$_baseUrl$endpoint', data: encryptedData, options: Options( headers: { 'Authorization': 'Bearer $token', 'Content-Encoding': 'encrypted', }, ), ); return response.data; } catch (e) { throw _handleApiException(e); } } // OAuth 2.0 / JWT Authentication Future<void> authenticate(String username, String password) async { try { // Never send passwords in plain text final hashedPassword = _hashPassword(password); final response = await _dio.post( '$_baseUrl/auth/login', data: { 'username': username, 'password': hashedPassword, 'client_id': 'your_client_id', 'grant_type': 'password', }, ); final accessToken = response.data['access_token']; final refreshToken = response.data['refresh_token']; await SecureStorageService.storeAuthToken(accessToken); await SecureStorageService.storeSecureData('refresh_token', refreshToken); } catch (e) { throw AuthenticationException('Authentication failed: $e'); } } // Token refresh mechanism Future<void> refreshToken() async { final refreshToken = await SecureStorageService.getSecureData('refresh_token'); if (refreshToken == null) throw UnauthorizedException('No refresh token'); try { final response = await _dio.post( '$_baseUrl/auth/refresh', data: { 'refresh_token': refreshToken, 'grant_type': 'refresh_token', }, ); final newAccessToken = response.data['access_token']; await SecureStorageService.storeAuthToken(newAccessToken); } catch (e) { // Clear tokens if refresh fails await SecureStorageService.clearAllSecureData(); throw AuthenticationException('Token refresh failed: $e'); } } String _signRequest({ required String method, required String path, required String timestamp, required String nonce, required String body, }) { final message = '$method$path$timestamp$nonce$body'; final key = utf8.encode(_apiKey); final messageBytes = utf8.encode(message); final hmac = Hmac(sha256, key); final digest = hmac.convert(messageBytes); return digest.toString(); } String _generateNonce() { final random = Random.secure(); final values = List<int>.generate(16, (i) => random.nextInt(256)); return base64.encode(values); } Map<String, dynamic> _encryptRequestBody(Map<String, dynamic> data) { // Encrypt sensitive fields final sensitiveFields = ['password', 'ssn', 'credit_card', 'personal_data']; final encryptedData = Map<String, dynamic>.from(data); for (final field in sensitiveFields) { if (encryptedData.containsKey(field)) { final plainText = encryptedData[field].toString(); encryptedData[field] = base64.encode( AdvancedEncryptionService.encrypt(plainText), ); } } return encryptedData; } String _hashPassword(String password) { // Use bcrypt or Argon2 in production final salt = 'your_app_salt'; // Use proper salt generation final combined = password + salt; final digest = sha256.convert(utf8.encode(combined)); return digest.toString(); } }
🔐 API Security Checklist:
- • Implement certificate pinning
- • Use HMAC signatures for request verification
- • Add timestamp and nonce for replay protection
- • Encrypt sensitive data in request/response
- • Implement proper token refresh mechanisms
BIOMETRIC AUTHENTICATION & DEVICE SECURITY
Implement biometric authentication and device security checks to ensure your app runs only on trusted devices and provides secure user authentication.
// Biometric Authentication Service import 'package:local_auth/local_auth.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/services.dart'; class BiometricAuthService { static final LocalAuthentication _localAuth = LocalAuthentication(); // Check if biometric authentication is available static Future<bool> isBiometricAvailable() async { try { final isAvailable = await _localAuth.canCheckBiometrics; final isDeviceSupported = await _localAuth.isDeviceSupported(); return isAvailable && isDeviceSupported; } catch (e) { return false; } } // Get available biometric types static Future<List<BiometricType>> getAvailableBiometrics() async { try { return await _localAuth.getAvailableBiometrics(); } catch (e) { return []; } } // Authenticate with biometrics static Future<bool> authenticateWithBiometrics({ required String reason, bool stickyAuth = true, }) async { try { final isAvailable = await isBiometricAvailable(); if (!isAvailable) return false; return await _localAuth.authenticate( localizedReason: reason, authMessages: const [ AndroidAuthMessages( signInTitle: 'Biometric Authentication', biometricHint: 'Verify your identity', biometricNotRecognized: 'Biometric not recognized', biometricRequiredTitle: 'Biometric Required', biometricSuccess: 'Authentication successful', cancelButton: 'Cancel', deviceCredentialsRequiredTitle: 'Device credentials required', deviceCredentialsSetupDescription: 'Please set up device credentials', goToSettingsButton: 'Go to Settings', goToSettingsDescription: 'Please set up biometric authentication', ), IOSAuthMessages( lockOut: 'Biometric authentication is disabled', goToSettingsButton: 'Go to Settings', goToSettingsDescription: 'Please set up biometric authentication', cancelButton: 'Cancel', ), ], options: AuthenticationOptions( stickyAuth: stickyAuth, biometricOnly: false, // Allow fallback to device credentials ), ); } catch (e) { return false; } } // Enhanced authentication with retry logic static Future<AuthResult> authenticateWithRetry({ required String reason, int maxRetries = 3, }) async { int attempts = 0; while (attempts < maxRetries) { try { final success = await authenticateWithBiometrics(reason: reason); if (success) { return AuthResult.success(); } attempts++; } catch (e) { if (e is PlatformException) { switch (e.code) { case 'UserCancel': return AuthResult.cancelled(); case 'BiometricOnlyNotSupported': return AuthResult.fallbackRequired(); case 'DeviceNotSupported': return AuthResult.notSupported(); default: attempts++; } } } } return AuthResult.failed('Maximum authentication attempts exceeded'); } } // Device Security Checks class DeviceSecurityService { static final DeviceInfoPlugin _deviceInfo = DeviceInfoPlugin(); // Check if device is rooted/jailbroken static Future<bool> isDeviceCompromised() async { try { if (Platform.isAndroid) { return await _checkAndroidRootedDevice(); } else if (Platform.isIOS) { return await _checkIOSJailbrokenDevice(); } return false; } catch (e) { return true; // Assume compromised if check fails } } static Future<bool> _checkAndroidRootedDevice() async { final androidInfo = await _deviceInfo.androidInfo; // Check for common root indicators final rootIndicators = [ '/system/app/Superuser.apk', '/sbin/su', '/system/bin/su', '/system/xbin/su', '/data/local/xbin/su', '/data/local/bin/su', '/system/sd/xbin/su', '/system/bin/failsafe/su', '/data/local/su', ]; for (final path in rootIndicators) { if (await File(path).exists()) { return true; } } // Check for root apps try { await Process.run('which', ['su']); return true; } catch (e) { // su command not found, likely not rooted } return false; } static Future<bool> _checkIOSJailbrokenDevice() async { final iosInfo = await _deviceInfo.iosInfo; // Check for jailbreak indicators final jailbreakPaths = [ '/Applications/Cydia.app', '/Library/MobileSubstrate/MobileSubstrate.dylib', '/bin/bash', '/usr/sbin/sshd', '/etc/apt', '/private/var/lib/apt/', ]; for (final path in jailbreakPaths) { if (await File(path).exists()) { return true; } } return false; } // Check if app is running in debug mode static bool isDebugMode() { return kDebugMode; } // Verify app integrity static Future<bool> verifyAppIntegrity() async { try { // Check app signature (platform-specific implementation required) // This is a simplified check final packageInfo = await PackageInfo.fromPlatform(); // Verify package name and version const expectedPackageName = 'com.yourapp.package'; if (packageInfo.packageName != expectedPackageName) { return false; } return true; } catch (e) { return false; } } // Complete security check static Future<SecurityCheckResult> performSecurityCheck() async { final results = await Future.wait([ isDeviceCompromised(), BiometricAuthService.isBiometricAvailable(), verifyAppIntegrity(), ]); final isCompromised = results[0] as bool; final biometricAvailable = results[1] as bool; final integrityValid = results[2] as bool; return SecurityCheckResult( isDeviceCompromised: isCompromised, isBiometricAvailable: biometricAvailable, isAppIntegrityValid: integrityValid, isDebugMode: isDebugMode(), ); } } // Result classes class AuthResult { final bool isSuccess; final String? error; final AuthResultType type; AuthResult._(this.isSuccess, this.error, this.type); factory AuthResult.success() => AuthResult._(true, null, AuthResultType.success); factory AuthResult.failed(String error) => AuthResult._(false, error, AuthResultType.failed); factory AuthResult.cancelled() => AuthResult._(false, 'User cancelled', AuthResultType.cancelled); factory AuthResult.notSupported() => AuthResult._(false, 'Not supported', AuthResultType.notSupported); factory AuthResult.fallbackRequired() => AuthResult._(false, 'Fallback required', AuthResultType.fallbackRequired); } enum AuthResultType { success, failed, cancelled, notSupported, fallbackRequired } class SecurityCheckResult { final bool isDeviceCompromised; final bool isBiometricAvailable; final bool isAppIntegrityValid; final bool isDebugMode; SecurityCheckResult({ required this.isDeviceCompromised, required this.isBiometricAvailable, required this.isAppIntegrityValid, required this.isDebugMode, }); bool get isSecure => !isDeviceCompromised && isAppIntegrityValid && !isDebugMode; }
🛡️ Device Security Features:
- • Biometric authentication with fallback
- • Root/jailbreak detection
- • App integrity verification
- • Debug mode detection
COMMON VULNERABILITIES & PREVENTION
Understanding common security vulnerabilities helps you proactively protect your app. Here are the most critical threats and how to prevent them.
⚠️ COMMON THREATS
- • Man-in-the-middle attacks
- • SQL injection through APIs
- • Reverse engineering
- • Data leakage through logs
- • Insecure data transmission
- • Code tampering
✅ PREVENTION MEASURES
- • Certificate pinning
- • Input validation & sanitization
- • Code obfuscation
- • Secure logging practices
- • End-to-end encryption
- • App integrity checks
// Security Utilities and Best Practices class SecurityUtils { // Secure logging that prevents sensitive data leakage static void secureLog(String message, {LogLevel level = LogLevel.info}) { if (kReleaseMode) { // In production, only log non-sensitive information final sanitizedMessage = _sanitizeLogMessage(message); _logToSecureEndpoint(sanitizedMessage, level); } else { // In debug mode, allow detailed logging developer.log(message, level: level.value); } } static String _sanitizeLogMessage(String message) { // Remove sensitive patterns final sensitivePatterns = [ RegExp(r'password[=:]s*S+', caseSensitive: false), RegExp(r'token[=:]s*S+', caseSensitive: false), RegExp(r'api[_-]?key[=:]s*S+', caseSensitive: false), RegExp(r'd{4}[s-]?d{4}[s-]?d{4}[s-]?d{4}'), // Credit card numbers RegExp(r'd{3}-d{2}-d{4}'), // SSN ]; String sanitized = message; for (final pattern in sensitivePatterns) { sanitized = sanitized.replaceAll(pattern, '[REDACTED]'); } return sanitized; } // Input validation and sanitization static String sanitizeInput(String input) { // Remove potentially dangerous characters return input .replaceAll(RegExp(r'[<>"']'), '') .replaceAll(RegExp(r'script', caseSensitive: false), '') .trim(); } // Email validation with security considerations static bool isValidEmail(String email) { final emailRegex = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$'); return emailRegex.hasMatch(email) && email.length <= 254; } // Password strength validation static PasswordStrength checkPasswordStrength(String password) { int score = 0; final feedback = <String>[]; if (password.length >= 8) score += 1; else feedback.add('Use at least 8 characters'); if (RegExp(r'[a-z]').hasMatch(password)) score += 1; else feedback.add('Include lowercase letters'); if (RegExp(r'[A-Z]').hasMatch(password)) score += 1; else feedback.add('Include uppercase letters'); if (RegExp(r'd').hasMatch(password)) score += 1; else feedback.add('Include numbers'); if (RegExp(r'[!@#$&*~]').hasMatch(password)) score += 1; else feedback.add('Include special characters'); // Check against common passwords if (_isCommonPassword(password)) { score = 0; feedback.add('Password is too common'); } return PasswordStrength(score: score, feedback: feedback); } static bool _isCommonPassword(String password) { final commonPasswords = [ 'password', '123456', 'password123', 'admin', 'qwerty', 'letmein', 'welcome', 'monkey', '1234567890', 'dragon' ]; return commonPasswords.contains(password.toLowerCase()); } // Generate secure random strings static String generateSecureToken(int length) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; final random = Random.secure(); return List.generate(length, (index) => chars[random.nextInt(chars.length)]).join(); } // Rate limiting for API calls static final Map<String, List<DateTime>> _requestHistory = {}; static bool isRateLimited(String identifier, {int maxRequests = 10, Duration window = const Duration(minutes: 1)}) { final now = DateTime.now(); final windowStart = now.subtract(window); _requestHistory[identifier] ??= []; final requests = _requestHistory[identifier]!; // Remove old requests outside the window requests.removeWhere((time) => time.isBefore(windowStart)); if (requests.length >= maxRequests) { return true; // Rate limited } requests.add(now); return false; } } // Security exceptions class SecurityException implements Exception { final String message; SecurityException(this.message); @override String toString() => 'SecurityException: $message'; } class AuthenticationException extends SecurityException { AuthenticationException(String message) : super(message); } class UnauthorizedException extends SecurityException { UnauthorizedException(String message) : super(message); } // Password strength result class PasswordStrength { final int score; final List<String> feedback; PasswordStrength({required this.score, required this.feedback}); PasswordStrengthLevel get level { if (score < 2) return PasswordStrengthLevel.weak; if (score < 4) return PasswordStrengthLevel.medium; return PasswordStrengthLevel.strong; } } enum PasswordStrengthLevel { weak, medium, strong } enum LogLevel { debug, info, warning, error }