OWASPによるモバイルアプリのセキュリティ基準を9つFlutterで実装する
はじめに
OWASPとは、Open Web Application Security Projectという非営利団体です。OWASPはモバイルアプリ用のセキュリティ基準 を設けています。 github.com 本記事ではFlutterに関わる部分の実装方法について説明します。
1. 機密データを格納するために、システムの資格情報保存機能を使用している。
flutter_secure_storageというパッケージを使います。
資格情報保存機能とは、
です。flutter_secure_storageでは、Keychainはデフォルトで使用されますが、EncryptedSharedPreferencesはパラメーターで指定する必要があります。
2.機密データを処理するテキスト入力では、キーボードキャッシュが無効にされている。
TextFieldのautocorrectをfalseに指定することで、キーボードキャッシュを無効にできます。
TextField( autocorrect: false )
3.機密データは、ユーザーインタフェースを介して公開されていない。
TextFieldのobscureTextをtrueに指定することで、パスワード入力の際に画面にパスワードが表示されません。
TextField( obscureText: true )
4. 機密データはモバイルオペレーティングシステムにより生成されるバックアップに含まれていない。
androidではデフォルトで自動バックアップをしてしまいますので、これをオフにします。 android/app/src/main/AndroidManifest.xml を下記ように編集します。
<manifest ... > ... <application android:allowBackup="false" ... > ... </manifest>
iosに関してはここに詳しく書いてありますが、Flutterから逸脱してしまうので触れません。 github.com
5. バックグラウンドへ移動した際にアプリはビューから機密データを削除している。
バックグラウンドに移動したかの検知はWidgetsBindningObserverを継承することで可能になります。
テキストの削除はTextEditingControllerで行います。
class SampleView extends StatelessWidget with WidgetsBindingObserver { final _controller = TextEditingController(); @override void initState() { super.initState(); WidgetsBinding.instance!.addObserver(this); } @override void dispose() { WidgetsBinding.instance!.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if(state == fecycleState.paused): _controller.clear() } @override Widget build(BuildContext context) { ... TextField( controller: _controller ), ... } }
6. アプリは必要以上に長くメモリ内に機密データを保持せず、使用後は明示的にメモリがクリアされている。
DartはGC(Garbage Collection)を採用しているため、必要以上に長くメモリ内に機密データを保持することはありません。
自動でメモリがセキュアに割り当て、解放されることは大きなメリットです。 しかし、デメリットとして明示的にメモリをクリアできません。
javeなどでは、
- nullを代入すること
- primitive型変数に別の値を格納すること
で明示的にメモリを削除します。
ですがDartはnull safetyな言語ですし、primitive型はありません。(すべての型はObject型の派生です)
ただ、Providerで機密データを扱っていれば .autoDisposeを付けることを考えるべきです。.autoDisposeにより、参照されなくなったProviderのStateは破棄されます。
7. 明示的に必要でない限りWebViewでJavaScriptが無効化されている。
WebViewはアプリ内でWebページを開くWidgetです。
パラメーターでjavascriptをdisableに指定することができます。
WebView( initialUrl: url, javascriptMode: JavascriptMode.disable )
8. WebViewを破棄する前にWebViewのキャッシュをクリアしている。
WebViewControllerを使ってキャッシュをクリアします。WebView Widget 内のonWebViewCreatedでコントローラを登録する。
import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'dart:async'; class WebPageView extends StatelessWidget { final Completer<WebViewController> _controller = Completer<WebViewController>(); @override Widget build(BuildContext context) { ... WebView( initialUrl: url, javascriptMode: JavascriptMode.disabled, onWebViewCreated: _controller.complete, onPageFinished: (String url) async{ final controller = await _controller.future; controller.clearCache(); }, ), ... } }
9. リバースエンジニアリング対策
難読化はビルドする際に下記のコマンドで行うことができます。
flutter build apk --obfuscate --split-debug-info=<project名>/android flutter build ios --obfuscate --split-debug-info=<project名>/ios
リバースエンジニアリング対策を含んだパッケージを使う手もあるようです。 pub.dev
おわりに
Flutter側からSwift側を扱えばより改善される部分もありそうですね。 以上です、ありがとうございました。
Flutterのtear-offとは?使うべき理由
tear-off とは
Flutterで関数のコールバックに匿名関数を用いない方法です。他の言語でいうfunction pointerです。
・tear-offの例
TextButton( onPressed:button.toggle )
・tear-offを使わない例
TextButton( onPressed: () { button.toggle(); } )
TextButton( onPressed: () => button.toggle() )
tear-offのメリット
匿名関数分のアロケーションが必要なくなります。tear-offでは関数をただ参照しているだけです。これにより、メモリと実行速度が改善されます。
関数の決定がruntimeでなくcompile時になります。これにより、実行速度が速くなります。*
可読性があがります。
* compile時になることでメリットもデメリットもあります(Compile time Polymorphism VS Run time Polymorphism) www.geeksforgeeks.org
forEachではtear-offを使おう
forEachはbreak, continue, return, awaitができないため使われず、for-inが使われるのが一般的です。
しかし、下記のようにtear-offを使うときだけは推奨されます。
list.forEach(print);
以上です!ありがとうございました。
参考
FlutterにおけるRepository Pattern
Repository Patternとは?
アーキテクチャにRepositoryを組み込むパターンです。
Repositoryは、UIやビジネスロジックとは関係ない、ネットワークからデータを所得するような部分です。データの所得だけでなく、データのローカルキャッシングや、取ってきたデータのモデルへの変換等も行います。
Repository Patternのメリット
UI、ビジネスロジックと明確に分けることで、以下のメリットがあります。
- データ所得部分の変更をしても他に影響が出ない
- 型安全性が保証される
例えばデータベースの構造を変更したことで、データ所得のコードも変更されても、UIやビジネスロジックのコードには影響が出ません。
また、型安全性とはType Errorが起きないということです。関数でreturnする型が保証されるので、間違って別の型やnullが返ってくることはありません。
Repository Patternのデメリット
- boilerplate code*が増える
- 学習が少し必要?
*boilerplate code ... 複数の場所で使われる定型コード
Repository Patternを実際に使ってみる
以下のFirebaseUsersRepositoryが今回作ったRepositoryのクラスです。
final usersRepositoryProvider = Provider<FirebaseUsersRepository>((ref) => FirebaseUsersRepository()); class FirebaseUsersRepository { static const int fetchLength = 10; Future<List<UserModel>> fetchUsers( List<String> genderQuery, List<UserModel> before) async { var from = before.isEmpty ? Timestamp.fromDate(DateTime.now()) : before[before.length - 1].timestamp; // Firebaseからデータを所得 QuerySnapshot<Map<String, dynamic>> docs = await FirebaseFirestore.instance .collection('users') .doc('v1') .collection('public') .where('query.gender', whereIn: genderQuery) .orderBy('timestamp', descending: true) .startAfter([from]) .limit(fetchLength) .get(); // 所得したデータをUserModel型のListに変換 String userid = FirebaseAuth.instance.currentUser!.uid; List<UserModel> newList = []; docs.docs.forEach((element) { if (element.exists && element.id != userid && element.data().containsKey('id')) { newList.add(UserModel(element)); } }); return newList; } }
Repositoryクラス内の関数fetchUsersでは、Firebaseからユーザーのデータを10個所得し、UserModel型のListで返します。
1行目のusersRepositoryProviderを通してビジネスロジック側がRepositoryを使うことができます。
List<UserModel> userList = await ref .read(usersRepositoryProvider) .fetchUsers(genderQuery, state.allUsers);
Providerを使っているのは、どこからでもFirebaseUsersRepositoryにアクセスできるようにするためです。
アプリの様々な場所からステートにアクセスできるようになります。 つまり、プロバイダはシングルトンやサービスロケータのようなパターン、依存性注入、あるいはInheritedWidget を完全に代替することができます。 引用元: riverpod.dev
以上です!ありがとうございました。