
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 }

