Flutterアプリの機能作りに慣れてきたところで
個人開発ではありますが、Flutterでアプリケーションを作成しています。
Play Storeへのリリースはしていませんのでそのあたりの知識はからっきしですが、以下が構築くらいはできるようになりました。
- Googleアカウント認証機能
- DeepLinkを用いた画面表示機能
- FCM(Firebase Cloud Messaging)を用いた通知機能
- CI/CDパイプラインによるApp Distributionを用いたapkファイルリリース
そんな中で、セキュリティを考慮した実装ができている?と聞かれると怪しい部分があります。
注意が必要とわかっていても、どういった形で問題が顕在するか見えていないことがほとんどです。
アプリケーション作成の方が落ち着いてきたので、この機会に少しずつ進めてみることにしました。
この記事はFlutter初学者向けではないため、以下は特に記載していません。
- Flutterの環境構築
- プロジェクト作成や各パッケージのインストールなど、コマンド関係の説明
- 利用する各パッケージの詳細説明
やってみたこと
作業方針
実際にそんなことあるなしは一旦置いといて、
すべき対応がなされていないと、どういったことをされてしまう可能性があるのか、そこに焦点を当てた内容になります。
apkファイル作成時に難読化オプションを設定しても、環境変数自体の暗号化を設定していない場合は、APIキーなどの機密情報が取得できてしまう。
これを実際に確認します。
実際にリリース済みのアプリに対する検証
実際にリリースされているアプリのapkファイルについて、解析だけであれば大丈夫かもしれませんが、悪さをした場合は犯罪(不正アクセス禁止法・著作権法違反あたり?)になってしまいます。
知らない間に犯罪行為となる可能性もあるかもしれないので、自分で作成したflutterプロジェクトを使って試してください。
進めてみよう
簡単なプロジェクトを作成して、きちんと設定しないと環境変数の情報を読み取れることを体験します。その後、コードを適切な形で修正して、読み取れない状態にすることが今回のゴールです。
まずはアプリケーションを準備していきます。
アプリケーション側の準備
必要なパッケージ
デフォルトから追加したパッケージは以下3つです。
dependencies
- envied:環境変数を型安全に管理するためのパッケージ
dev_dependencies
- build_runner:コード生成やビルドタスクを実行するためのツール
- envied_generator:enviedのコード生成を行うジェネレーター
追加後のpubspec.yaml
実装内容
name: intro_reverse_engineering
description: "A new Flutter project."
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.9.2
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
envied: ^1.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
build_runner: ^2.5.4
envied_generator: ^1.1.1
flutter:
uses-material-design: true
アプリケーション実装
変数を2つ定義します。
- 暗号化設定なし
- 暗号化設定あり(
obfuscate: true を記載した方)
SIMPLE_URL=https://xyzcompany.supabase.co
SECRET_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh5emNvbXBhbnkuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTg5NTY3MDYsImV4cCI6MTk4NDkzMjcwNn0.D1bX4YkYJH3p1k3jv8nX9cX0mX1f5r6
import 'package:envied/envied.dart';
part 'env.g.dart';
@Envied(path: '.env')
final class Env {
@EnviedField(varName: 'SIMPLE_URL')
static const String simpleUrl = _Env.simpleUrl;
@EnviedField(varName: 'SECRET_KEY', obfuscate: true)
static String secretKey = _Env.secretKey;
}
以下コマンドで、env.g.dartファイルを作成します。
その後、2つの変数をmain.dartで呼び出すように実装します。
dart run build_runner build
main.dart
import 'package:flutter/material.dart';
import 'package:intro_reverse_engineering/constants/app.dart';
void main() {
AppConstants.initialize();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('テスト'),
// 環境変数の情報を出力(試験的にこうしている)
Text(AppConstants.simpleUrl),
Text(AppConstants.secretKey),
],
),
),
);
}
}
apkファイル作成
最初は難読化を指定せずに進めます。
flutter build apk --split-per-abi
Running Gradle task 'assembleRelease'... 53.6s
√ Built build\app\outputs\flutter-apk\app-armeabi-v7a-release.apk (11.2MB)
√ Built build\app\outputs\flutter-apk\app-arm64-v8a-release.apk (13.9MB)
√ Built build\app\outputs\flutter-apk\app-x86_64-release.apk (14.9MB)
解析準備ができたので、実際にやってみます。
apkファイル解析
apktoolでDecompile
apktoolを使って、作成したapkファイルをDecompileします。以下インストールガイドです。
# d : decompile時の設定オプション
apktool d app-arm64-v8a-release.apk
以下のログが出力されれば成功です。
I: Using Apktool 2.12.1 on app-arm64-v8a-release.apk with 8 threads
I: Baksmaling classes.dex...
I: Loading resource table...
I: Decoding file-resources...
I: Loading resource table from file: C:\Users{username}\AppData\Local\apktool\framework\1.apk
I: Decoding values / XMLs...
I: Decoding AndroidManifest.xml with resources...
I: Copying original files...
I: Copying assets...
I: Copying lib...
I: Copying unknown files...
以下の通り、フォルダが新しく作成されています。
作成されたフォルダ内で以下コマンドを実行します。
# バイナリファイルから印刷可能な文字列を抽出
strings lib/arm64-v8a/libapp.so > signed_strings.txt
# 作成したテキストファイルから該当する文字列箇所を出力
grep -E "https?://[a-zA-Z0-9.-]+" signed_strings.txt
grep -E ".*eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.*" signed_strings.txt
https://xyzcompany.supabase.coという環境変数の値がそのまま抽出されていることが分かります。
難読化オプションを追加してapkファイル作成
難読化する設定を追加して、apkファイルを作成します。
flutter build apk --split-per-abi --obfuscate --split-debug-info=build/app/outputs/logs
結果ですが、さきほどと同じで、https://xyzcompany.supabase.coが読み取れてしまいます。
環境変数の暗号化(obfuscateをtrueに)
先ほどの結果から、apkファイルの難読化しても環境変数自体は平文で読み取れてしまうことが分かりました。
env.dartで、各フィールドにobfuscate: trueを設定し、再度apkファイル作成とDecompileを行います。
@Envied(path: '.env')
final class Env {
@EnviedField(varName: 'SIMPLE_URL', obfuscate: true)
static final String simpleUrl = _Env.simpleUrl;
@EnviedField(varName: 'SECRET_KEY', obfuscate: true)
static String secretKey = _Env.secretKey;
}
Grep検索で環境変数に関する情報はヒットしなくなりましたので、最初に記載したゴール、環境変数に対する最低限の対策(※)は取れました。
※最低限の対策
リポジトリにも記載されていますが、完全に安全が担保されている訳ではないようです。
Keep in mind that this only makes it more difficult to access the obfuscated or encrypted values, it does NOT make them completely secure. A determined individual may still be able to retrieve them. For more details, see frencojobs/envify#28 and #4!
以下PRで暗号化に関する実装が追加されており、「XOR暗号」という暗号化手法を用いているようです。
packages/envied_generator/lib/src/generate_line_encrypted.dart
まとめ
理解事項の整理
apk作成時にobfuscateを追加しても環境変数の情報は平文のままで抽出できたことから、以下のように整理できます。
- Flutter ビルド時の難読化(--obfuscate)
- 対象: Dart コードのクラス名、メソッド名、変数名など
- 効果: コードの構造を読みにくくする
- 制限: コンパイル時定数(const)は難読化されない
- envied パッケージの暗号化(obfuscate: true)
- 対象: 環境変数の値そのもの
- 効果: 値を暗号化してAPKに埋め込む
- 動作: 実行時に復号化して使用
この後の作業
今回ほんとに基礎です。確認内容は以下の通り盛りだくさんです。
少しずつ理解箇所を増やしていければと思います。
- XOR暗号化に対する理解
- 作業前に読んでいた Flutter セキュリティ Tips の内容深堀り
- デバッグシンボル に対する理解
- 作業中の調べもので見つけた
apksigner(署名に関する話)の話
参考資料


