😇

Flutter向けにアクセストークンとかを暗号化保存しておく機構を作ったというお話

2024/03/05に公開2

コードにこういうの混ざってるの嫌ですよね

動作確認の時も含めて、コードにこの手のセンシティブな情報を入れて作業してると、ついウッカリとGitHubに保存しちゃったりとか、疲れてる時に「ちょっとこのコード見てよ」ってChatGPTにまるッとコピペしたやつとか投げちゃいそうで怖いじゃないですか。自分に対するそういう疑いの姿勢、大事

そんなわけで前回Azure用のChatServiceを公開した時から、クライアントアプリ側でこういうの使ってもらうなら、アプリ内で安全にトークン等の情報を保存・管理できるようになってるのとセットだよなー、と思ってたワケです。

そこで今回も作っちゃいました

https://github.com/hamatz/research_flutter/blob/main/lib/src/services/crypt_service.dart
アプリ初回起動時にユーザーに入力してもらったパスワードを使ってPBKDF2のアルゴリズムでコンテンツ鍵を生成、その鍵を使って、設定画面を経由してユーザー入力されたトークン等の情報は暗号化され、FlutterSecureStorage を通じてセキュアに保存(なんかflutter_secure_storageは「iOSではKeychain、AndroidではKeyStoreを使ってデータを保存してくれるライブラリ」ということらしい)される、みたいな形で使うようにしました(早口)

こんな画面で使うイメージ
設定画面

アプリに暗号化用の鍵を抱き込ませたりしても、鍵が漏れたらダメなので、まぁユーザの頭の中にしかないパスワードで鍵を作る、というのがひとまずは良い手なのかな、と。

まあ、基本的には呼び出して

crypt_sample.dart
import 'package:chat_sample_app/src/services/crypt_service.dart'; // パスは自分で置いたところで
final cryptoService = CryptoService();

新規ユーザーに鍵作ってもらう時には

new_key_gen.dart
await CryptoService().generateNewKey(passwordController.text);

で鍵作って、再ログイン後は

key_gen.dart
await CryptoService().generateKey(passwordController.text);

で、前に鍵作った時のパラメータとパスワードを使って鍵を再生成するイメージで

暗号化は

encrypt.dart
value = await cryptoService.encrypt(value);

復号は

decrypt.dart
value = await cryptoService.decrypt(settingData['value']);

するだけなので、まぁそんな難しくはないと思います。使い方のサンプルになりそうなコードは、前回同様のココに転がってますので宜しければご確認ください。まぁまぁ、必要最低限のChatGPTクローン的なやつができたかなー、という感じにはなってきたので、気が向いたら別途、専用のリポジトリを置くかもです。ホワイトレーベルで色々な人がチャットアプリを作る基礎部品的な位置付けで改めてメンテしていってもいいかもな、と(まだ思っただけ)

https://github.com/hamatz/research_flutter

おまけ

ただアレですね。まだFlutter歴5日目とかなんで全然分かってなくて苦労したんですが、せっかく設定画面でトークンの情報とか保存してても、すでに表示済み扱いの画面に戻る場合、「戻ったタイミングで勝手にページをリロード」みたいなゆるふわな事は起きてくれないので、「更新されたぜー!設定情報読み込みしなおせよお前らー!」って通知しないといけないんですね。いやー、EventBus君、君がいてくれて本当に助かった... (そして、そういうのを雑に話しながら教えてくれるChatGPT君も本当にありがとうな!)

グローバルに参照できるようにして

global.dart
import 'package:event_bus/event_bus.dart';

final EventBus eventBus = EventBus();

イベント定義して

global.dart
class SettingsUpdatedEvent {
  // 必要に応じてイベントに関するデータをフィールドとして持たせることができる
  final String sendBy = "settingApp";
}

イベント起きたら発報して

sample.dart
eventBus.fire(SettingsUpdatedEvent());

処理しなきゃいけないヤツがそれを受け取って処理を動かすだけでOK

sample2.dart
  void _subscribeToSettingsUpdate() {
    _settingsUpdatedSubscription = eventBus.on<SettingsUpdatedEvent>().listen((event) {
      _loadInitialSettings();
    });
  }

うーん、シンプル!
とはいえ、Flutter、そんな楽チン簡単ではない感じですね〜

Discussion

すさすさ

「戻ったタイミングで勝手にページをリロード」みたいなゆるふわな事は起きてくれないので、

riverpod使えば簡単にできます!