はじめに
アプリを開発していると設定などを Key/Value として保存するケースが多く、簡単に実現する場合は shared_preferences パッケージを使うことで簡単に実現できます。
しかし、多くの場合で平文は避けたいこともあり、暗号化して保存したいということになります。
Flutter には、 flutter_secure_storage という KeyChain などを利用してセキュアに保存できるパッケージもありますが、この保存された値はアプリをアンインストールしても消えないというデメリット?もあります。
そこで、これらを組み合わせることで、セキュアでかつ簡単に Key/Value を保存できるようにしてみました。
実装
利用する Flutter Packages
以下の3つのパッケージを導入します。
dependencies:
encrypt: ^5.0.3
flutter_secure_storage: ^9.2.2
shared_preferences: ^2.3.2
コード
import 'package:encrypt/encrypt.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
class CryptedPreference {
// Singleton
static final CryptedPreference _instance = CryptedPreference._internal();
factory CryptedPreference() {
return _instance;
}
CryptedPreference._internal();
/// SecureStorage
final secureStorage = const FlutterSecureStorage();
/// 指定された Key で SharedPreferences に保存された文字列を取得し、復号して返す.
Future<String?> getString(String key) async {
final prefs = await SharedPreferences.getInstance();
var encryptedBase64Text = prefs.getString(key);
if (encryptedBase64Text != null) {
return await _decrypt(encryptedBase64Text);
}
return null;
}
/// 指定された Key で SharedPreferences に文字列を暗号化して保存する.
Future<void> setString(String key, String value) async {
final prefs = await SharedPreferences.getInstance();
var encryptedBase64Text = await _encrypt(value);
prefs.setString(key, encryptedBase64Text);
}
/// 文字列を暗号化する.
Future<String> _encrypt(String plainText) async {
final key = await _getKey();
final iv = await _getIV();
final encrypter = Encrypter(AES(key, mode: AESMode.cbc, padding: 'PKCS7'));
final encrypted = encrypter.encrypt(plainText, iv: iv);
return encrypted.base64;
}
/// 暗号化された文字列を復号する.
Future<String> _decrypt(String encryptedBase64Text) async {
final key = await _getKey();
final iv = await _getIV();
final encrypter = Encrypter(AES(key, mode: AESMode.cbc, padding: 'PKCS7'));
String? plainText;
try {
plainText = encrypter.decrypt(Encrypted.fromBase64(encryptedBase64Text), iv: iv);
} catch (e) {
logger.e('Failed to decrypt: $e');
}
return plainText;
}
/// Keyを取得する
/// SecureStorage に Key が存在しない場合は生成する.
Future<Key> _getKey() async {
String? key = await secureStorage.read(key: 'key');
if (key == null) {
key = Key.fromLength(32).base64;
await secureStorage.write(key: 'key', value: key);
}
return Key.fromBase64(key);
}
/// IV を取得する
/// SecureStorage に IV が存在しない場合は生成する.
Future<IV> _getIV() async {
String? iv = await secureStorage.read(key: 'iv');
if (iv == null) {
iv = IV.fromLength(16).base64;
await secureStorage.write(key: 'iv', value: iv);
}
return IV.fromBase64(iv);
}
}
解説
簡単に言えば、文字列を暗号化して shared_preferences に保存しているだけです。
暗号化・復号は encrypt パッケージを使い、AES/CBC/PKCS7 で行います。
final encrypter = Encrypter(AES(key, mode: AESMode.cbc, padding: 'PKCS7'));
暗号化に利用する Key と IV は、復号時にも同じものが必要なため、flutter_secure_storage を使ってセキュアに保存します。初回に生成し、2回目以降は保存された値を利用します。
Future<Key> _getKey()
Future<IV> _getIV()
データ(暗号化された文字列) は shared_preferences を使って読み書きしています。
final prefs = await SharedPreferences.getInstance();
// 読み込み
var encryptedBase64Text = prefs.getString(key);
// 書き込み
prefs.setString(key, encryptedBase64Text);
これでアプリがアンインスールされた際、KEY, IV は残ってしまいますが、データは削除されるので、flutter_secure_storage のみを使った場合よりセキュアになったかと思います。
最後に
とりあえず文字列だけを対象のサンプルコードを書きましたが、他の型への対応や remove()
など SharedPreferences をラップするように書いていくと便利なコードになるかと思います。