Flutter でセキュアに Key/Value を保存する

# はじめに
アプリを開発していると設定などを Key/Value として保存するケースが多く、簡単に実現する場合は [shared_preferences](https://pub.dev/packages/shared_preferences) パッケージを使うことで簡単に実現できます。
しかし、多くの場合で平文は避けたいこともあり、暗号化して保存したいということになります。
Flutter には、 [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) という KeyChain などを利用してセキュアに保存できるパッケージもありますが、この保存された値はアプリをアンインストールしても消えないというデメリット？もあります。

そこで、これらを組み合わせることで、セキュアでかつ簡単に Key/Value を保存できるようにしてみました。

# 実装
## 利用する Flutter Packages
以下の３つのパッケージを導入します。
```pubspec.yaml
dependencies:
  encrypt: ^5.0.3
  flutter_secure_storage: ^9.2.2
  shared_preferences: ^2.3.2
```

## コード

```dart
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](https://pub.dev/packages/shared_preferences) に保存しているだけです。

暗号化・復号は　[encrypt](https://pub.dev/packages/encrypt) パッケージを使い、AES/CBC/PKCS7 で行います。
```
final encrypter = Encrypter(AES(key, mode: AESMode.cbc, padding: 'PKCS7'));
```
暗号化に利用する Key と IV は、復号時にも同じものが必要なため、[flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage)　を使ってセキュアに保存します。初回に生成し、２回目以降は保存された値を利用します。
```
Future<Key> _getKey()
Future<IV> _getIV()
```
データ（暗号化された文字列）　は　[shared_preferences](https://pub.dev/packages/shared_preferences)　を使って読み書きしています。
```
final prefs = await SharedPreferences.getInstance();
// 読み込み
var encryptedBase64Text = prefs.getString(key);
// 書き込み
prefs.setString(key, encryptedBase64Text);
```

これでアプリがアンインスールされた際、KEY, IV は残ってしまいますが、データは削除されるので、flutter_secure_storage のみを使った場合よりセキュアになったかと思います。

# 最後に
とりあえず文字列だけを対象のサンプルコードを書きましたが、他の型への対応や `remove()` など　SharedPreferences をラップするように書いていくと便利なコードになるかと思います。