Flutter Secure Storage
ライブラリ
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のセキュリティ機能エラーの対応が必要
主要リンク
- Flutter Secure Storage pub.dev
- GitHub リポジトリ
- API ドキュメント
- Flutter 公式プラグイン
- iOS Keychain Services
- Android EncryptedSharedPreferences
書き方の例
基本的なセットアップ
// 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;
}
}
}