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.