Skip to content

Безпека у Flutter

Безпека мобільних застосунків — критичний аспект розробки. Flutter надає інструменти та підходи для захисту даних користувачів, мережевої комунікації та самого застосунку.

Безпечне зберігання даних

flutter_secure_storage

Зберігання конфіденційних даних у Keychain (iOS) та Keystore (Android):

dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureStorageService {
  final _storage = const FlutterSecureStorage(
    aOptions: AndroidOptions(encryptedSharedPreferences: true),
    iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
  );

  /// Зберегти токен
  Future<void> saveToken(String token) async {
    await _storage.write(key: 'auth_token', value: token);
  }

  /// Отримати токен
  Future<String?> getToken() async {
    return await _storage.read(key: 'auth_token');
  }

  /// Видалити токен
  Future<void> deleteToken() async {
    await _storage.delete(key: 'auth_token');
  }

  /// Очистити все сховище
  Future<void> clearAll() async {
    await _storage.deleteAll();
  }
}

Шифрування даних

dart
import 'package:encrypt/encrypt.dart';

class EncryptionService {
  late final Key _key;
  late final IV _iv;
  late final Encrypter _encrypter;

  EncryptionService(String secretKey) {
    _key = Key.fromUtf8(secretKey.padRight(32, '0').substring(0, 32));
    _iv = IV.fromLength(16);
    _encrypter = Encrypter(AES(_key));
  }

  /// Зашифрувати текст
  String encrypt(String plainText) {
    final encrypted = _encrypter.encrypt(plainText, iv: _iv);
    return encrypted.base64;
  }

  /// Розшифрувати текст
  String decrypt(String encryptedText) {
    final decrypted = _encrypter.decrypt64(encryptedText, iv: _iv);
    return decrypted;
  }
}

Безпечне зберігання у SQLite

dart
import 'package:sqflite_sqlcipher/sqflite.dart';

class SecureDatabase {
  static Database? _db;

  Future<Database> get database async {
    _db ??= await _initDb();
    return _db!;
  }

  Future<Database> _initDb() async {
    final path = await getDatabasesPath();
    return openDatabase(
      '$path/secure_app.db',
      password: 'your-secure-password', // отримати з secure storage
      version: 1,
      onCreate: (db, version) async {
        await db.execute('''
          CREATE TABLE users (
            id INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            email TEXT NOT NULL
          )
        ''');
      },
    );
  }
}

Мережева безпека

SSL Pinning

Прив'язка до конкретного SSL-сертифіката для захисту від MITM-атак:

dart
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'dart:io';

class SecureApiClient {
  late final Dio _dio;

  SecureApiClient() {
    _dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));

    // SSL Pinning
    (_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
      final client = HttpClient();
      client.badCertificateCallback = (cert, host, port) {
        // Перевірка відбитка сертифіката
        final fingerprint = cert.sha256;
        const expectedFingerprint = 'AA:BB:CC:DD:...'; // ваш відбиток
        return fingerprint.toString() == expectedFingerprint;
      };
      return client;
    };
  }
}

Certificate Transparency

dart
// Використання http_certificate_pinning
import 'package:http_certificate_pinning/http_certificate_pinning.dart';

Future<void> secureRequest() async {
  final response = await HttpCertificatePinning.check(
    serverURL: 'https://api.example.com',
    headerHttp: {},
    sha: SHA.SHA256,
    allowedSHAFingerprints: [
      'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99',
    ],
    timeout: 50,
  );

  if (response.contains('CONNECTION_SECURE')) {
    // З'єднання безпечне
  }
}

Безпечні HTTP-запити

dart
class ApiService {
  final Dio _dio;

  ApiService(this._dio) {
    _dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) {
          // Додавання токена авторизації
          options.headers['Authorization'] = 'Bearer $token';
          // Заборона кешування конфіденційних даних
          options.headers['Cache-Control'] = 'no-store';
          handler.next(options);
        },
        onError: (error, handler) {
          if (error.response?.statusCode == 401) {
            // Токен протермінований — оновити або вийти
            _handleUnauthorized();
          }
          handler.next(error);
        },
      ),
    );
  }
}

Аутентифікація та авторизація

Безпечне зберігання токенів

dart
class AuthTokenManager {
  final SecureStorageService _storage;

  AuthTokenManager(this._storage);

  Future<void> saveTokens({
    required String accessToken,
    required String refreshToken,
  }) async {
    await _storage.saveToken(accessToken);
    await _storage.write('refresh_token', refreshToken);
  }

  Future<String?> getValidToken() async {
    final token = await _storage.getToken();
    if (token == null) return null;

    // Перевірка терміну дії
    if (_isTokenExpired(token)) {
      return await _refreshToken();
    }
    return token;
  }

  bool _isTokenExpired(String token) {
    try {
      final parts = token.split('.');
      if (parts.length != 3) return true;

      final payload = json.decode(
        utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))),
      );

      final exp = payload['exp'] as int;
      final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
      return now >= exp;
    } catch (_) {
      return true;
    }
  }
}

Біометрична аутентифікація

dart
import 'package:local_auth/local_auth.dart';

class BiometricService {
  final _auth = LocalAuthentication();

  /// Перевірка доступності біометрії
  Future<bool> isBiometricAvailable() async {
    final canCheck = await _auth.canCheckBiometrics;
    final isDeviceSupported = await _auth.isDeviceSupported();
    return canCheck && isDeviceSupported;
  }

  /// Отримати доступні типи біометрії
  Future<List<BiometricType>> getAvailableBiometrics() async {
    return await _auth.getAvailableBiometrics();
  }

  /// Аутентифікація
  Future<bool> authenticate() async {
    try {
      return await _auth.authenticate(
        localizedReason: 'Підтвердіть свою особу для входу',
        options: const AuthenticationOptions(
          stickyAuth: true,
          biometricOnly: false,
        ),
      );
    } catch (e) {
      return false;
    }
  }
}

Захист від реверс-інжинірингу

Обфускація коду

bash
# Збірка з обфускацією
flutter build apk --obfuscate --split-debug-info=./debug-info/
flutter build ios --obfuscate --split-debug-info=./debug-info/

Захист від відладки

dart
import 'dart:io';

class SecurityCheck {
  /// Перевірка чи застосунок запущено у відладчику
  static bool get isDebuggerAttached {
    bool isDebug = false;
    assert(() {
      isDebug = true;
      return true;
    }());
    return isDebug;
  }

  /// Перевірка на root/jailbreak
  static Future<bool> isDeviceCompromised() async {
    if (Platform.isAndroid) {
      return await _checkAndroidRoot();
    } else if (Platform.isIOS) {
      return await _checkIOSJailbreak();
    }
    return false;
  }

  static Future<bool> _checkAndroidRoot() async {
    final paths = [
      '/system/app/Superuser.apk',
      '/sbin/su',
      '/system/bin/su',
      '/system/xbin/su',
    ];
    for (final path in paths) {
      if (await File(path).exists()) return true;
    }
    return false;
  }

  static Future<bool> _checkIOSJailbreak() async {
    final paths = [
      '/Applications/Cydia.app',
      '/Library/MobileSubstrate/MobileSubstrate.dylib',
      '/bin/bash',
      '/usr/sbin/sshd',
    ];
    for (final path in paths) {
      if (await File(path).exists()) return true;
    }
    return false;
  }
}

Валідація введених даних

dart
class InputValidator {
  /// Валідація email
  static String? validateEmail(String? value) {
    if (value == null || value.isEmpty) return 'Введіть email';
    final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
    if (!regex.hasMatch(value)) return 'Невірний формат email';
    return null;
  }

  /// Валідація пароля
  static String? validatePassword(String? value) {
    if (value == null || value.isEmpty) return 'Введіть пароль';
    if (value.length < 8) return 'Мінімум 8 символів';
    if (!value.contains(RegExp(r'[A-Z]'))) return 'Потрібна велика літера';
    if (!value.contains(RegExp(r'[a-z]'))) return 'Потрібна мала літера';
    if (!value.contains(RegExp(r'[0-9]'))) return 'Потрібна цифра';
    if (!value.contains(RegExp(r'[!@#\$%^&*(),.?":{}|<>]'))) {
      return 'Потрібен спеціальний символ';
    }
    return null;
  }

  /// Санітизація вводу — захист від injection
  static String sanitize(String input) {
    return input
        .replaceAll(RegExp(r'<[^>]*>'), '') // видалення HTML тегів
        .replaceAll(RegExp(r'[<>"\']'), '') // видалення небезпечних символів
        .trim();
  }
}

Безпечна конфігурація

Зберігання секретів

dart
// НЕ зберігайте секрети в коді!

// Використовуйте --dart-define для передачі секретів
// flutter run --dart-define=API_KEY=your_key

class AppConfig {
  static const apiKey = String.fromEnvironment('API_KEY');
  static const apiUrl = String.fromEnvironment(
    'API_URL',
    defaultValue: 'https://api.example.com',
  );
}

ProGuard (Android)

proguard
# android/app/proguard-rules.pro
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
-dontwarn io.flutter.embedding.**

Чек-лист безпеки

  • [ ] Конфіденційні дані зберігаються у flutter_secure_storage
  • [ ] SSL Pinning налаштовано для API-запитів
  • [ ] Токени оновлюються та перевіряються на термін дії
  • [ ] Код обфускований у release-збірках
  • [ ] Введені дані валідуються та санітизуються
  • [ ] Секрети не зашиті в код (використовуються змінні середовища)
  • [ ] Біометрична аутентифікація для чутливих операцій
  • [ ] Логування вимкнене у release-збірках
  • [ ] Перевірка на root/jailbreak для критичних застосунків
  • [ ] ProGuard налаштовано для Android
  • [ ] Network Security Config налаштовано
  • [ ] Кешування конфіденційних даних вимкнене