Flutter'da Biyometrik Kimlik Doğrulama ve Uygulama Güvenliği
Mobil uygulama güvenliği çok katmanlıdır: biyometrik kimlik doğrulama, şifreli yerel depolama, ağ güvenliği ve cihaz güvenlik kontrolü. Bu yazıda her katmanı Flutter ile nasıl uygulayacağınızı inceliyoruz.
Biyometrik Kimlik Doğrulama: local_auth
// pubspec: local_auth: ^2.3.0
// iOS — Info.plist
// NSFaceIDUsageDescription: Hızlı giriş için yüz tanıma kullanılır.
// Android — AndroidManifest.xml
// <uses-permission android:name="android.permission.USE_BIOMETRIC"/>
// <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
class BiometricService {
final LocalAuthentication _auth = LocalAuthentication();
Future<bool> isAvailable() async {
try {
final canCheck = await _auth.canCheckBiometrics;
final isDeviceSupported = await _auth.isDeviceSupported();
return canCheck && isDeviceSupported;
} on PlatformException {
return false;
}
}
Future<List<BiometricType>> getAvailableBiometrics() async {
try {
return await _auth.getAvailableBiometrics();
} on PlatformException {
return [];
}
}
Future<BiometricResult> authenticate({
required String localizedReason,
bool useErrorDialogs = true,
bool stickyAuth = false,
}) async {
try {
final authenticated = await _auth.authenticate(
localizedReason: localizedReason,
options: AuthenticationOptions(
useErrorDialogs: useErrorDialogs,
stickyAuth: stickyAuth,
biometricOnly: false, // PIN/şifre fallback'ine izin ver
sensitiveTransaction: true,
),
);
return authenticated ? BiometricResult.success : BiometricResult.failed;
} on PlatformException catch (e) {
return switch (e.code) {
auth_error.notAvailable => BiometricResult.notAvailable,
auth_error.notEnrolled => BiometricResult.notEnrolled,
auth_error.lockedOut => BiometricResult.lockedOut,
auth_error.permanentlyLockedOut => BiometricResult.permanentlyLockedOut,
_ => BiometricResult.error,
};
}
}
void cancelAuthentication() => _auth.stopAuthentication();
}
// Kullanım — kilit ekranı
class AppLockScreen extends ConsumerWidget {
const AppLockScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.lock, size: 64, color: Colors.grey),
const SizedBox(height: 24),
const Text('Uygulamaya erişmek için doğrulama yapın'),
const SizedBox(height: 32),
ElevatedButton.icon(
icon: const Icon(Icons.fingerprint),
label: const Text('Biyometrik Doğrulama'),
onPressed: () async {
final result = await BiometricService().authenticate(
localizedReason: 'Uygulamaya erişmek için parmak izinizi kullanın',
);
if (result == BiometricResult.success && context.mounted) {
ref.read(appLockProvider.notifier).unlock();
}
},
),
],
),
),
);
}
}
flutter_secure_storage: Şifreli Yerel Depolama
// pubspec: flutter_secure_storage: ^9.2.2
// Android: AES 256 (Android Keystore)
// iOS: Keychain Services
class SecureStorageService {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
synchronizable: false, // iCloud sync istemiyoruz
),
);
static const _tokenKey = 'auth_token';
static const _refreshKey = 'refresh_token';
static const _pinKey = 'app_pin';
static const _biometricEnabledKey = 'biometric_enabled';
Future<void> saveTokens({
required String accessToken,
required String refreshToken,
}) async {
await Future.wait([
_storage.write(key: _tokenKey, value: accessToken),
_storage.write(key: _refreshKey, value: refreshToken),
]);
}
Future<String?> get accessToken => _storage.read(key: _tokenKey);
Future<String?> get refreshToken => _storage.read(key: _refreshKey);
Future<void> savePin(String pin) async {
// PIN'i hashle, ham olarak kaydetme
final salt = base64Encode(List.generate(32, (_) => Random.secure().nextInt(256)));
final hash = base64Encode(sha256.convert(utf8.encode('$salt:$pin')).bytes);
await _storage.write(key: _pinKey, value: '$salt:$hash');
}
Future<bool> verifyPin(String pin) async {
final stored = await _storage.read(key: _pinKey);
if (stored == null) return false;
final parts = stored.split(':');
if (parts.length != 2) return false;
final hash = base64Encode(sha256.convert(utf8.encode('${parts[0]}:$pin')).bytes);
return hash == parts[1];
}
Future<bool> get isBiometricEnabled async =>
await _storage.read(key: _biometricEnabledKey) == 'true';
Future<void> setBiometricEnabled(bool enabled) =>
_storage.write(key: _biometricEnabledKey, value: enabled.toString());
Future<void> clearAll() => _storage.deleteAll();
}
SSL Pinning: Ağ Güvenliği
// pubspec: dio: ^5.7.0, dio_smart_retry: ^6.0.0
class SecureHttpClient {
late final Dio _dio;
SecureHttpClient() {
_dio = Dio(BaseOptions(baseUrl: Env.apiUrl));
_setupSslPinning();
_setupInterceptors();
}
void _setupSslPinning() {
(_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
final client = HttpClient();
client.badCertificateCallback = (cert, host, port) => false;
// Sertifika parmak izini kontrol et
SecurityContext context = SecurityContext.defaultContext;
// Sertifikanızı assets'e koyun
context.setTrustedCertificatesBytes(
(rootBundle.loadString('assets/certs/api_cert.pem') as dynamic)
.toString()
.codeUnits,
);
return HttpClient(context: context);
};
}
void _setupInterceptors() {
_dio.interceptors.addAll([
// JWT token yenileme
QueuedInterceptorsWrapper(
onRequest: (options, handler) async {
final token = await getIt<SecureStorageService>().accessToken;
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
final refreshed = await _refreshToken();
if (refreshed) {
// Token yenilendi, isteği tekrarla
final token = await getIt<SecureStorageService>().accessToken;
error.requestOptions.headers['Authorization'] = 'Bearer $token';
final response = await _dio.fetch(error.requestOptions);
return handler.resolve(response);
}
}
handler.next(error);
},
),
LogInterceptor(responseBody: kDebugMode),
]);
}
}
Root / Jailbreak Tespiti
// pubspec: flutter_jailbreak_detection: ^1.9.0
class DeviceSecurityChecker {
Future<SecurityCheckResult> check() async {
final isJailbroken = await FlutterJailbreakDetection.jailbroken;
final isDeveloperMode = await FlutterJailbreakDetection.developerMode;
if (isJailbroken) {
return SecurityCheckResult.jailbroken;
}
if (isDeveloperMode && kReleaseMode) {
return SecurityCheckResult.developerMode;
}
return SecurityCheckResult.safe;
}
}
// Uygulama başlangıcında kontrol
class SecurityGateWidget extends ConsumerWidget {
final Widget child;
const SecurityGateWidget({super.key, required this.child});
@override
Widget build(BuildContext context, WidgetRef ref) {
return FutureBuilder<SecurityCheckResult>(
future: DeviceSecurityChecker().check(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const SplashScreen();
if (snapshot.data == SecurityCheckResult.jailbroken) {
return const Scaffold(
body: Center(
child: Text(
'Bu cihaz güvenlik politikamızla uyumlu değil.',
textAlign: TextAlign.center,
),
),
);
}
return child;
},
);
}
}
Güvenlik katmanları birbirini tamamlar. Biyometrik doğrulama UX'i iyileştirir; flutter_secure_storage hassas verileri (token, PIN) korur; SSL pinning man-in-the-middle saldırılarını önler. Root/jailbreak tespiti finansal veya sağlık uygulamaları için özellikle önemlidir.