1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

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

実装

利用する Flutter Packages

以下の3つのパッケージを導入します。

pubspec.yaml
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 をラップするように書いていくと便利なコードになるかと思います。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?