Flutter Secure Storage

authenticationsecurestorageFlutterDartmobilecross-platform

Library

Flutter Secure Storage

Overview

Flutter Secure Storage is a Flutter plugin for securely storing sensitive data using platform-specific secure storage mechanisms.

Details

Flutter Secure Storage is a Flutter plugin that encrypts and stores sensitive data such as authentication tokens, API keys, and user credentials in key-value format. It works across multiple platforms including Android, iOS, macOS, Windows, and Linux, utilizing the optimal security features of each platform while providing a unified API.

The plugin adopts a federated plugin architecture, separating the platform-agnostic API from platform-specific implementations. This allows the use of optimal native security features for each platform while providing developers with a consistent API.

Platform-specific implementations are as follows:

  • Android: Uses EncryptedSharedPreferences with Google Tink for encryption
  • iOS/macOS: Utilizes Apple Keychain Services API
  • Windows: Encrypts using Data Protection API (DPAPI) and stores in JSON files
  • Linux: Uses Secret Service API (libsecret) to access system keyring
  • Web: Uses WebCrypto API for encryption and localStorage/sessionStorage (works only on HTTPS or localhost)

The data flow involves the Flutter application calling methods on the FlutterSecureStorage instance, which delegates to the FlutterSecureStoragePlatform instance. This platform interface, typically MethodChannelFlutterSecureStorage, invokes platform-specific code through method channels. The native layer handles encryption and storage using secure mechanisms and returns results to the Flutter app.

Pros and Cons

Pros

  • Multi-platform Support: Support for multiple platforms with a single API
  • Platform Optimization: Utilizes optimal security features of each OS
  • Encryption: Data is automatically encrypted before storage
  • Easy Implementation: Intuitive key-value API that's easy to use
  • Listener System: Ability to monitor key changes
  • Configuration Options: Platform-specific configurations available
  • Active Maintenance: Ongoing development and community support

Cons

  • Platform Dependency: Depends on each platform's security features
  • Debugging Difficulty: Debugging encrypted data can be challenging
  • iOS Keychain Limitations: May be affected by iOS Keychain capacity limits
  • Web Restrictions: Works only in HTTPS environment or localhost
  • Not for Large Data: Not suitable for storing large amounts of data
  • Platform-specific Errors: Need to handle errors from each OS's security features

Key Links

Usage Examples

Basic Setup

// Add to pubspec.yaml
dependencies:
  flutter_secure_storage: ^9.2.2

// Initialize before use
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

// Create instance with default settings
const storage = FlutterSecureStorage();

// Create instance with custom settings
const storage = FlutterSecureStorage(
  aOptions: AndroidOptions(
    encryptedSharedPreferences: true,
  ),
  iOptions: IOSOptions(
    groupId: 'com.example.app',
    accountName: 'MyApp',
    accessibility: KeychainAccessibility.first_unlock,
  ),
  lOptions: LinuxOptions(
    useSessionKeyring: false,
  ),
  webOptions: WebOptions(
    dbName: 'MyAppStorage',
    publicKey: 'MyPublicKey',
  ),
);

Basic CRUD Operations

class SecureStorageService {
  static const _storage = FlutterSecureStorage();

  // Write data
  static Future<void> writeData(String key, String value) async {
    try {
      await _storage.write(key: key, value: value);
      print('Data saved: $key');
    } catch (e) {
      print('Write error: $e');
      throw Exception('Failed to save data');
    }
  }

  // Read data
  static Future<String?> readData(String key) async {
    try {
      final value = await _storage.read(key: key);
      return value;
    } catch (e) {
      print('Read error: $e');
      return null;
    }
  }

  // Delete data
  static Future<void> deleteData(String key) async {
    try {
      await _storage.delete(key: key);
      print('Data deleted: $key');
    } catch (e) {
      print('Delete error: $e');
      throw Exception('Failed to delete data');
    }
  }

  // Delete all data
  static Future<void> deleteAllData() async {
    try {
      await _storage.deleteAll();
      print('All data deleted');
    } catch (e) {
      print('Delete all error: $e');
      throw Exception('Failed to delete all data');
    }
  }

  // Check if key exists
  static Future<bool> containsKey(String key) async {
    try {
      return await _storage.containsKey(key: key);
    } catch (e) {
      print('Key check error: $e');
      return false;
    }
  }

  // Get all key-value pairs
  static Future<Map<String, String>> readAllData() async {
    try {
      return await _storage.readAll();
    } catch (e) {
      print('Read all error: $e');
      return {};
    }
  }
}

User Authentication Information Management

class AuthStorageService {
  static const _storage = FlutterSecureStorage();
  
  // Key constants
  static const _accessTokenKey = 'access_token';
  static const _refreshTokenKey = 'refresh_token';
  static const _userIdKey = 'user_id';
  static const _userEmailKey = 'user_email';

  // Save access token
  static Future<void> saveAccessToken(String token) async {
    await _storage.write(key: _accessTokenKey, value: token);
  }

  // Get access token
  static Future<String?> getAccessToken() async {
    return await _storage.read(key: _accessTokenKey);
  }

  // Save refresh token
  static Future<void> saveRefreshToken(String token) async {
    await _storage.write(key: _refreshTokenKey, value: token);
  }

  // Get refresh token
  static Future<String?> getRefreshToken() async {
    return await _storage.read(key: _refreshTokenKey);
  }

  // Save user information
  static Future<void> saveUserInfo(String userId, String email) async {
    await Future.wait([
      _storage.write(key: _userIdKey, value: userId),
      _storage.write(key: _userEmailKey, value: email),
    ]);
  }

  // Get user information
  static Future<Map<String, String?>> getUserInfo() async {
    final results = await Future.wait([
      _storage.read(key: _userIdKey),
      _storage.read(key: _userEmailKey),
    ]);
    
    return {
      'userId': results[0],
      'email': results[1],
    };
  }

  // Logout (clear all auth data)
  static Future<void> clearAuthData() async {
    await Future.wait([
      _storage.delete(key: _accessTokenKey),
      _storage.delete(key: _refreshTokenKey),
      _storage.delete(key: _userIdKey),
      _storage.delete(key: _userEmailKey),
    ]);
  }

  // Check login status
  static Future<bool> isLoggedIn() async {
    final accessToken = await getAccessToken();
    return accessToken != null && accessToken.isNotEmpty;
  }
}

JSON Format Data Storage and Retrieval

import 'dart:convert';

class JsonStorageService {
  static const _storage = FlutterSecureStorage();

  // Save JSON object
  static Future<void> saveJsonData<T>(String key, T data) async {
    try {
      final jsonString = jsonEncode(data);
      await _storage.write(key: key, value: jsonString);
    } catch (e) {
      throw Exception('Failed to save JSON data: $e');
    }
  }

  // Load JSON object
  static Future<T?> loadJsonData<T>(
    String key, 
    T Function(Map<String, dynamic>) fromJson
  ) async {
    try {
      final jsonString = await _storage.read(key: key);
      if (jsonString == null) return null;
      
      final jsonMap = jsonDecode(jsonString) as Map<String, dynamic>;
      return fromJson(jsonMap);
    } catch (e) {
      print('JSON data load error: $e');
      return null;
    }
  }

  // Save list data
  static Future<void> saveJsonList<T>(String key, List<T> dataList) async {
    try {
      final jsonString = jsonEncode(dataList);
      await _storage.write(key: key, value: jsonString);
    } catch (e) {
      throw Exception('Failed to save JSON list: $e');
    }
  }

  // Load list data
  static Future<List<T>> loadJsonList<T>(
    String key,
    T Function(Map<String, dynamic>) fromJson
  ) async {
    try {
      final jsonString = await _storage.read(key: key);
      if (jsonString == null) return [];
      
      final jsonList = jsonDecode(jsonString) as List;
      return jsonList
          .cast<Map<String, dynamic>>()
          .map((json) => fromJson(json))
          .toList();
    } catch (e) {
      print('JSON list load error: $e');
      return [];
    }
  }
}

// Usage example: Save user settings
class UserSettings {
  final String theme;
  final bool notifications;
  final String language;

  UserSettings({
    required this.theme,
    required this.notifications,
    required this.language,
  });

  Map<String, dynamic> toJson() => {
    'theme': theme,
    'notifications': notifications,
    'language': language,
  };

  factory UserSettings.fromJson(Map<String, dynamic> json) => UserSettings(
    theme: json['theme'],
    notifications: json['notifications'],
    language: json['language'],
  );
}

// Settings save/load
class SettingsService {
  static const _settingsKey = 'user_settings';

  static Future<void> saveSettings(UserSettings settings) async {
    await JsonStorageService.saveJsonData(_settingsKey, settings.toJson());
  }

  static Future<UserSettings?> loadSettings() async {
    return await JsonStorageService.loadJsonData(
      _settingsKey,
      UserSettings.fromJson,
    );
  }
}

Platform-specific Configurations

class PlatformSpecificStorage {
  // iOS-specific settings
  static const iosStorage = FlutterSecureStorage(
    iOptions: IOSOptions(
      // Keychain accessibility settings
      accessibility: KeychainAccessibility.first_unlock_this_device,
      // App Group sharing
      groupId: 'group.com.example.myapp',
      // Account name setting
      accountName: 'MyAppAccount',
      // Sync settings
      synchronizable: false,
    ),
  );

  // Android-specific settings
  static const androidStorage = FlutterSecureStorage(
    aOptions: AndroidOptions(
      // Use EncryptedSharedPreferences
      encryptedSharedPreferences: true,
      // When biometric authentication is required
      sharedPreferencesName: 'secure_prefs',
      preferencesKeyPrefix: 'myapp_',
    ),
  );

  // Windows-specific settings
  static const windowsStorage = FlutterSecureStorage(
    wOptions: WindowsOptions(
      // Custom prefix
      useBackwardCompatibility: true,
    ),
  );

  // Linux-specific settings
  static const linuxStorage = FlutterSecureStorage(
    lOptions: LinuxOptions(
      // Use session keyring
      useSessionKeyring: true,
    ),
  );

  // Web-specific settings
  static const webStorage = FlutterSecureStorage(
    webOptions: WebOptions(
      // Database name setting
      dbName: 'MyAppSecureStorage',
      // Public key setting
      publicKey: 'myapp_public_key',
    ),
  );
}

Listener System Usage

class StorageListenerService {
  static const _storage = FlutterSecureStorage();
  static final Map<String, List<ValueChanged<String?>>> _listeners = {};

  // Register listener
  static void registerListener(String key, ValueChanged<String?> listener) {
    if (!_listeners.containsKey(key)) {
      _listeners[key] = [];
    }
    _listeners[key]!.add(listener);
    
    // Use Flutter Secure Storage listener feature
    // Note: May not be implemented in current version
  }

  // Unregister listener
  static void unregisterListener(String key, ValueChanged<String?> listener) {
    _listeners[key]?.remove(listener);
    if (_listeners[key]?.isEmpty == true) {
      _listeners.remove(key);
    }
  }

  // Unregister all listeners for specific key
  static void unregisterAllListenersForKey(String key) {
    _listeners.remove(key);
  }

  // Unregister all listeners
  static void unregisterAllListeners() {
    _listeners.clear();
  }

  // Manual listener call on data change
  static Future<void> writeWithNotification(String key, String value) async {
    await _storage.write(key: key, value: value);
    _notifyListeners(key, value);
  }

  static Future<void> deleteWithNotification(String key) async {
    await _storage.delete(key: key);
    _notifyListeners(key, null);
  }

  // Notify listeners
  static void _notifyListeners(String key, String? value) {
    _listeners[key]?.forEach((listener) {
      listener(value);
    });
  }
}

// Usage example
class AuthStateManager {
  void setupAuthListeners() {
    // Monitor access token changes
    StorageListenerService.registerListener('access_token', (token) {
      if (token != null) {
        print('User logged in');
        onUserLoggedIn();
      } else {
        print('User logged out');
        onUserLoggedOut();
      }
    });
  }

  void onUserLoggedIn() {
    // Post-login processing
  }

  void onUserLoggedOut() {
    // Post-logout processing
  }
}

Error Handling and Best Practices

class SecureStorageManager {
  static const _storage = FlutterSecureStorage();

  // Safe data reading
  static Future<String?> safeRead(String key) async {
    try {
      return await _storage.read(key: key);
    } on PlatformException catch (e) {
      print('Platform error: ${e.message}');
      return null;
    } catch (e) {
      print('Unexpected error: $e');
      return null;
    }
  }

  // Safe data writing
  static Future<bool> safeWrite(String key, String value) async {
    try {
      await _storage.write(key: key, value: value);
      return true;
    } on PlatformException catch (e) {
      print('Platform error: ${e.message}');
      return false;
    } catch (e) {
      print('Unexpected error: $e');
      return false;
    }
  }

  // Batch operations
  static Future<Map<String, String?>> readMultiple(List<String> keys) async {
    final results = <String, String?>{};
    
    await Future.wait(
      keys.map((key) async {
        results[key] = await safeRead(key);
      }),
    );
    
    return results;
  }

  // Batch key deletion
  static Future<void> deleteMultiple(List<String> keys) async {
    await Future.wait(
      keys.map((key) => _storage.delete(key: key)),
    );
  }

  // Data backup and restore
  static Future<Map<String, String>> backup() async {
    try {
      return await _storage.readAll();
    } catch (e) {
      print('Backup error: $e');
      return {};
    }
  }

  static Future<bool> restore(Map<String, String> data) async {
    try {
      // Delete existing data
      await _storage.deleteAll();
      
      // Write new data
      await Future.wait(
        data.entries.map((entry) => 
          _storage.write(key: entry.key, value: entry.value)
        ),
      );
      
      return true;
    } catch (e) {
      print('Restore error: $e');
      return false;
    }
  }
}