[Flutter] How to secure secret keys and build app bundles with specific target file (not using flutter_dotenv)
There are several ways how to secure secret keys such as api access tokens in mobile development. The most widely-used method is to store them in .env file supported by the third party library named flutter_dotenv and it has been regarded safer than the hard-coded way. Although the best variant is to get them from server, we decided to secure our keys in the flutter project but not to use .env file, but code them inside the lib directory and declare private variables to get accessed to call apis.
Please note that this is one of suggestions, especially, suitable for our own project, which is possible not to match your work.
1. Create a new directory where environment-related files will be located in "lib".
2. Set your env.dart file. Build type is just an option.
import 'package:flutter/material.dart';
import 'package:get/route_manager.dart';
import 'package:secure_store/home.dart';
enum BuildType { debug, release }
class BuildConfig {
static const String API_KEY = 'API_KEY';
static const String BUILD_MODE = 'BUILD_MODE';
static final Map<String, dynamic> _debug = {
API_KEY: "DEBUG KEY",
BUILD_MODE: "DEBUG MODE",
};
static final Map<String, dynamic> _release = {
API_KEY: "RELEASE KEY",
BUILD_MODE: "RELEASE MODE",
};
}
class Env {
static Env? _instance;
static Map<String, dynamic>? _config;
static get instance => _instance;
late BuildType _buildType;
Env(BuildType type) {
_buildType = type;
if (_buildType == BuildType.debug) {
_config = BuildConfig._debug;
} else if (_buildType == BuildType.release) {
_config = BuildConfig._release;
} else {
_config = BuildConfig._release;
}
}
static String getConfig(String key) {
if (_config == null && !_config!.containsKey(key)) return '';
return _config![key];
}
static bool getFlagConfig(String key) {
if (_config == null || !_config!.containsKey(key)) return false;
return _config![key];
}
factory Env.newInstance(BuildType type) {
_instance ??= Env(type);
return _instance!;
}
void run() async {
// all pre-run configurations here
// ex) native splash, firebase, localization, screen_util
runApp(
GetMaterialApp(
title: 'Secure Demo',
theme: ThemeData(primarySwatch: Colors.deepPurple),
initialRoute: '/',
debugShowCheckedModeBanner: false,
getPages: [
GetPage(name: '/', page: () => const HomeScreen()),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:secure_store/config/env.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final mode = Env.getConfig(BuildConfig.BUILD_MODE);
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: Center(
child: Text(
mode,
style: const TextStyle(
fontSize: 30,
fontWeight: FontWeight.w700,
),
),
),
),
);
}
}
3. Create file triggering "run()"method by each build type.
# debug.dart
main() => Env.newInstance(BuildType.debug).run();
#release.dart
main() => Env.newInstance(BuildType.release).run();
4. Create launch.json in "RUN AND DEBUG" and configure all build types which should be understandable for vs code.
5. Run the project with the target mode(release/debug). (button or cli)
flutter run -t lib/config/debug.dart
6. Build the project to upload to stores (Google Play/App Store).
6-1. AOS
# apk
flutter build apk -t lib/config/release.dart
# appbundle
flutter build appbundle -t lib/config/release.dart
6-2. iOS
- Open your project in Xcode.
- Go to "TARGETS/Runner" -> "Build Settings" -> "User-Defined" and click to expand "FLUTTER_TARGET".
- Specify target build file for each mode. Debug mode will be installed in your iPhone simulator and Release mode will be set up when you archive and upload it to appstore.