Flutter Secure Storage

認証セキュアストレージFlutterDartモバイルクロスプラットフォーム

ライブラリ

Flutter Secure Storage

概要

Flutter Secure Storageは、機密データを安全に保存するためのFlutterプラグインで、プラットフォーム固有のセキュアストレージを利用します。

詳細

Flutter Secure Storageは、認証トークン、APIキー、ユーザー資格情報などの機密データをキー・バリュー形式で暗号化して保存するFlutterプラグインです。Android、iOS、macOS、Windows、Linuxといった複数のプラットフォームで動作し、各プラットフォームの最適なセキュリティ機能を活用して、統一されたAPIを提供します。

プラグインは連合型プラグインアーキテクチャを採用しており、プラットフォーム非依存のAPIとプラットフォーム固有の実装を分離しています。これにより、各プラットフォームで最適なネイティブセキュリティ機能を使用しながら、開発者には一貫したAPIを提供できます。

各プラットフォームでの実装は以下の通りです:

  • Android: EncryptedSharedPreferencesとGoogle Tinkを使用して暗号化
  • iOS/macOS: Apple KeychainサービスAPIを利用
  • Windows: Data Protection API (DPAPI)で暗号化し、JSONファイルに保存
  • Linux: Secret Service API (libsecret)を使用してシステムキーリングにアクセス
  • Web: WebCrypto APIによる暗号化とlocalStorage/sessionStorageを使用(HTTPSまたはlocalhostでのみ動作)

データフローでは、FlutterアプリケーションがFlutterSecureStorageインスタンスのメソッドを呼び出し、FlutterSecureStoragePlatformインスタンスに委譲します。このプラットフォームインターフェースは、通常MethodChannelFlutterSecureStorageを通じて、メソッドチャネル経由でプラットフォーム固有のコードを呼び出します。ネイティブレイヤーでは、セキュアな仕組みを使用して暗号化と保存を処理し、結果をFlutterアプリに戻します。

メリット・デメリット

メリット

  • マルチプラットフォーム対応: 単一のAPIで複数のプラットフォームをサポート
  • プラットフォーム最適化: 各OSの最適なセキュリティ機能を活用
  • 暗号化: データは保存前に自動的に暗号化される
  • 簡単な実装: 直感的なキー・バリューAPIで使いやすい
  • リスナーシステム: キーの変更を監視できる機能
  • 設定オプション: プラットフォーム固有の設定が可能
  • アクティブメンテナンス: 継続的な開発とコミュニティサポート

デメリット

  • プラットフォーム依存: 各プラットフォームのセキュリティ機能に依存
  • デバッグの困難さ: 暗号化されたデータのデバッグが難しい場合がある
  • iOS Keychain制限: iOSのKeychain容量制限の影響を受ける可能性
  • Web制限: HTTPS環境またはlocalhostでのみ動作
  • 大量データ非対応: 大きなデータの保存には適さない
  • プラットフォーム固有エラー: 各OSのセキュリティ機能エラーの対応が必要

主要リンク

書き方の例

基本的なセットアップ

// pubspec.yamlに追加
dependencies:
  flutter_secure_storage: ^9.2.2

// 使用前の初期化
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

// インスタンスの作成(デフォルト設定)
const storage = FlutterSecureStorage();

// カスタム設定でのインスタンス作成
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',
  ),
);

基本的なCRUD操作

class SecureStorageService {
  static const _storage = FlutterSecureStorage();

  // データの書き込み
  static Future<void> writeData(String key, String value) async {
    try {
      await _storage.write(key: key, value: value);
      print('データが保存されました: $key');
    } catch (e) {
      print('書き込みエラー: $e');
      throw Exception('データの保存に失敗しました');
    }
  }

  // データの読み込み
  static Future<String?> readData(String key) async {
    try {
      final value = await _storage.read(key: key);
      return value;
    } catch (e) {
      print('読み込みエラー: $e');
      return null;
    }
  }

  // データの削除
  static Future<void> deleteData(String key) async {
    try {
      await _storage.delete(key: key);
      print('データが削除されました: $key');
    } catch (e) {
      print('削除エラー: $e');
      throw Exception('データの削除に失敗しました');
    }
  }

  // すべてのデータを削除
  static Future<void> deleteAllData() async {
    try {
      await _storage.deleteAll();
      print('すべてのデータが削除されました');
    } catch (e) {
      print('全削除エラー: $e');
      throw Exception('データの全削除に失敗しました');
    }
  }

  // キーの存在確認
  static Future<bool> containsKey(String key) async {
    try {
      return await _storage.containsKey(key: key);
    } catch (e) {
      print('キー確認エラー: $e');
      return false;
    }
  }

  // すべてのキー・バリューを取得
  static Future<Map<String, String>> readAllData() async {
    try {
      return await _storage.readAll();
    } catch (e) {
      print('全読み込みエラー: $e');
      return {};
    }
  }
}

ユーザー認証情報の管理

class AuthStorageService {
  static const _storage = FlutterSecureStorage();
  
  // キー定数
  static const _accessTokenKey = 'access_token';
  static const _refreshTokenKey = 'refresh_token';
  static const _userIdKey = 'user_id';
  static const _userEmailKey = 'user_email';

  // アクセストークンの保存
  static Future<void> saveAccessToken(String token) async {
    await _storage.write(key: _accessTokenKey, value: token);
  }

  // アクセストークンの取得
  static Future<String?> getAccessToken() async {
    return await _storage.read(key: _accessTokenKey);
  }

  // リフレッシュトークンの保存
  static Future<void> saveRefreshToken(String token) async {
    await _storage.write(key: _refreshTokenKey, value: token);
  }

  // リフレッシュトークンの取得
  static Future<String?> getRefreshToken() async {
    return await _storage.read(key: _refreshTokenKey);
  }

  // ユーザー情報の保存
  static Future<void> saveUserInfo(String userId, String email) async {
    await Future.wait([
      _storage.write(key: _userIdKey, value: userId),
      _storage.write(key: _userEmailKey, value: email),
    ]);
  }

  // ユーザー情報の取得
  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],
    };
  }

  // ログアウト(すべての認証情報を削除)
  static Future<void> clearAuthData() async {
    await Future.wait([
      _storage.delete(key: _accessTokenKey),
      _storage.delete(key: _refreshTokenKey),
      _storage.delete(key: _userIdKey),
      _storage.delete(key: _userEmailKey),
    ]);
  }

  // ログイン状態の確認
  static Future<bool> isLoggedIn() async {
    final accessToken = await getAccessToken();
    return accessToken != null && accessToken.isNotEmpty;
  }
}

JSON形式データの保存・取得

import 'dart:convert';

class JsonStorageService {
  static const _storage = FlutterSecureStorage();

  // JSONオブジェクトの保存
  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('JSON データの保存に失敗しました: $e');
    }
  }

  // JSONオブジェクトの読み込み
  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 データの読み込みエラー: $e');
      return null;
    }
  }

  // リストデータの保存
  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('JSON リストの保存に失敗しました: $e');
    }
  }

  // リストデータの読み込み
  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 リストの読み込みエラー: $e');
      return [];
    }
  }
}

// 使用例:ユーザー設定の保存
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'],
  );
}

// 設定の保存・読み込み
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,
    );
  }
}

プラットフォーム固有の設定

class PlatformSpecificStorage {
  // iOS固有の設定
  static const iosStorage = FlutterSecureStorage(
    iOptions: IOSOptions(
      // Keychainアクセス可能性の設定
      accessibility: KeychainAccessibility.first_unlock_this_device,
      // App Groupでの共有
      groupId: 'group.com.example.myapp',
      // アカウント名の設定
      accountName: 'MyAppAccount',
      // 同期設定
      synchronizable: false,
    ),
  );

  // Android固有の設定
  static const androidStorage = FlutterSecureStorage(
    aOptions: AndroidOptions(
      // EncryptedSharedPreferences使用
      encryptedSharedPreferences: true,
      // 生体認証が必要な場合
      sharedPreferencesName: 'secure_prefs',
      preferencesKeyPrefix: 'myapp_',
    ),
  );

  // Windows固有の設定
  static const windowsStorage = FlutterSecureStorage(
    wOptions: WindowsOptions(
      // カスタムプレフィックス
      useBackwardCompatibility: true,
    ),
  );

  // Linux固有の設定
  static const linuxStorage = FlutterSecureStorage(
    lOptions: LinuxOptions(
      // セッションキーリング使用
      useSessionKeyring: true,
    ),
  );

  // Web固有の設定
  static const webStorage = FlutterSecureStorage(
    webOptions: WebOptions(
      // データベース名の設定
      dbName: 'MyAppSecureStorage',
      // 公開キーの設定
      publicKey: 'myapp_public_key',
    ),
  );
}

リスナーシステムの活用

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

  // リスナーの登録
  static void registerListener(String key, ValueChanged<String?> listener) {
    if (!_listeners.containsKey(key)) {
      _listeners[key] = [];
    }
    _listeners[key]!.add(listener);
    
    // Flutter Secure Storageのリスナー機能を使用
    // 注意:現在のバージョンでは実装されていない場合があります
  }

  // リスナーの削除
  static void unregisterListener(String key, ValueChanged<String?> listener) {
    _listeners[key]?.remove(listener);
    if (_listeners[key]?.isEmpty == true) {
      _listeners.remove(key);
    }
  }

  // 特定キーのすべてのリスナーを削除
  static void unregisterAllListenersForKey(String key) {
    _listeners.remove(key);
  }

  // すべてのリスナーを削除
  static void unregisterAllListeners() {
    _listeners.clear();
  }

  // データ変更時の手動リスナー呼び出し
  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);
  }

  // リスナーへの通知
  static void _notifyListeners(String key, String? value) {
    _listeners[key]?.forEach((listener) {
      listener(value);
    });
  }
}

// 使用例
class AuthStateManager {
  void setupAuthListeners() {
    // アクセストークンの変更を監視
    StorageListenerService.registerListener('access_token', (token) {
      if (token != null) {
        print('ユーザーがログインしました');
        onUserLoggedIn();
      } else {
        print('ユーザーがログアウトしました');
        onUserLoggedOut();
      }
    });
  }

  void onUserLoggedIn() {
    // ログイン後の処理
  }

  void onUserLoggedOut() {
    // ログアウト後の処理
  }
}

エラーハンドリングとベストプラクティス

class SecureStorageManager {
  static const _storage = FlutterSecureStorage();

  // 安全なデータ読み込み
  static Future<String?> safeRead(String key) async {
    try {
      return await _storage.read(key: key);
    } on PlatformException catch (e) {
      print('プラットフォームエラー: ${e.message}');
      return null;
    } catch (e) {
      print('予期しないエラー: $e');
      return null;
    }
  }

  // 安全なデータ書き込み
  static Future<bool> safeWrite(String key, String value) async {
    try {
      await _storage.write(key: key, value: value);
      return true;
    } on PlatformException catch (e) {
      print('プラットフォームエラー: ${e.message}');
      return false;
    } catch (e) {
      print('予期しないエラー: $e');
      return false;
    }
  }

  // バッチ操作
  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;
  }

  // キーの一括削除
  static Future<void> deleteMultiple(List<String> keys) async {
    await Future.wait(
      keys.map((key) => _storage.delete(key: key)),
    );
  }

  // データのバックアップ・復元
  static Future<Map<String, String>> backup() async {
    try {
      return await _storage.readAll();
    } catch (e) {
      print('バックアップエラー: $e');
      return {};
    }
  }

  static Future<bool> restore(Map<String, String> data) async {
    try {
      // 既存データを削除
      await _storage.deleteAll();
      
      // 新しいデータを書き込み
      await Future.wait(
        data.entries.map((entry) => 
          _storage.write(key: entry.key, value: entry.value)
        ),
      );
      
      return true;
    } catch (e) {
      print('復元エラー: $e');
      return false;
    }
  }
}