
Securely storing API keys and sensitive credentials in mobile applications has long been a challenge for Flutter developers. In our previous article, we exposed the critical security vulnerabilities in popular packages like flutter_dotenv and dotenv. Now, let's explore a more secure alternative: the envied package.
Table of Contents
What Makes Envied More Secure?
Unlike flutter_dotenv
, which bundles your plain text .env file with your app, Envied takes a fundamentally different approach to credential management:
- Generates Dart code at build time based on your .env values
- The .env file itself is never included in your app bundle
- Values can be obfuscated in the generated code
- Leverages compile-time constants rather than runtime loading
This approach significantly improves security by eliminating the easily accessible plain text file while providing convenient access to environment variables in your code. It's not perfect security (no client-side solution truly is), but it's a substantial improvement over the plaintext alternatives.
Step-by-Step Implementation Guide
Let's walk through the process of implementing Envied in your Flutter application:
Step 1: Add Package Dependencies
First, add envied and its code generation dependencies to your pubspec.yaml
:
dependencies:
flutter:
sdk: flutter
envied: ^0.5.2
dev_dependencies:
envied_generator: ^0.5.2
build_runner: ^2.4.6
Run flutter pub get
to install these packages.
Step 2: Create Your .env File
Create a .env
file in the root of your project with your environment variables:
API_KEY=your_secret_api_key_here
BASE_URL=https://api.example.com/v1
ENABLE_ANALYTICS=true
.gitignore
to prevent it from being committed to your repository:
# .gitignore
.env
*.g.dart
Step 3: Create the Env Class
Create a new file, for example lib/env/env.dart
:
import 'package:envied/envied.dart';
part 'env.g.dart';
@Envied(path: '.env')
abstract class Env {
@EnviedField(varName: 'API_KEY', obfuscate: true)
static const apiKey = _Env.apiKey;
@EnviedField(varName: 'BASE_URL')
static const baseUrl = _Env.baseUrl;
@EnviedField(varName: 'ENABLE_ANALYTICS', defaultValue: 'false')
static const enableAnalytics = _Env.enableAnalytics;
}
obfuscate: true
parameter for sensitive values like API keys. This is crucial for improving security.
Step 4: Generate the Code
Run the build_runner to generate the code:
flutter pub run build_runner build
This will create the env.g.dart
file with the generated code that contains your environment variables. If you make changes to your .env
file or env.dart
class, you'll need to run this command again to regenerate the code.
flutter pub run build_runner watch
instead to automatically regenerate code whenever changes are detected.
Step 5: Use the Environment Variables in Your Code
Now you can use your environment variables throughout your application:
import 'package:your_app/env/env.dart';
void main() {
// Access your environment variables
final apiKey = Env.apiKey;
final baseUrl = Env.baseUrl;
final enableAnalytics = Env.enableAnalytics == 'true';
print('Connecting to: $baseUrl');
// Use them in your API client
final apiClient = ApiClient(
baseUrl: baseUrl,
apiKey: apiKey,
enableAnalytics: enableAnalytics,
);
runApp(MyApp(apiClient: apiClient));
}
Understanding Obfuscation in Envied
The obfuscate: true
parameter is key for sensitive values. Let's examine how it works:
// Generated code without obfuscation
abstract class _Env {
static const apiKey = 'your_secret_api_key_here';
static const baseUrl = 'https://api.example.com/v1';
}
// Generated code with obfuscation
abstract class _Env {
static const List _enviedKeyapiKey = [1, 34, 78, 12, ...]; // Encoded bytes
static const apiKey = String.fromCharCodes(
List.generate(_enviedKeyapiKey.length,
(i) => _enviedKeyapiKey[i] ^ _enviedKeystemp[i % _enviedKeystemp.length])
);
static const baseUrl = 'https://api.example.com/v1';
}
This obfuscation makes it significantly harder (though not impossible) for an attacker to extract your API keys through static analysis. It applies a simple XOR cipher to the string data, which requires additional effort to reverse-engineer compared to plain text values.
Best Practices When Using Envied
To maximize security when using Envied:
- Always obfuscate sensitive values by setting
obfuscate: true
for any credential that needs protection - Enable code obfuscation in your Flutter release builds:
flutter build apk --obfuscate --split-debug-info=build/app/outputs/symbols
- Use different .env files for different environments
- Consider implementing different Env classes for different environments
- Be cautious with extremely sensitive keys - for truly critical keys, consider a backend proxy approach
Managing Multiple Environments
For larger projects, you might want to maintain separate environment files:
.env.development
.env.staging
.env.production
You can then create environment-specific Env classes:
import 'package:envied/envied.dart';
// Define this based on build configuration
const String flavor = 'development'; // or 'staging' or 'production'
@Envied(path: '.env.${flavor}')
abstract class Env {
// environment variables here
}
Additional Security Layers
While Envied significantly improves security over dotenv packages, it's not a silver bullet. Consider these additional security measures:
1. Backend Proxy for Sensitive APIs
For highly sensitive APIs, implement a backend proxy service:
// Instead of direct API calls with embedded keys
Future fetchData() async {
// Your backend handles the actual API key
return http.get(Uri.parse('${Env.yourBackendUrl}/api/proxy-request'));
}
With this approach, your mobile app only needs to authenticate with your own backend service, which then makes the actual API calls using credentials stored securely on the server. The third-party API keys never exist in your mobile app code.
2. App Attestation
Implement Google Play Integrity API or Apple App Attest to verify your app hasn't been tampered with:
import 'package:play_integrity_flutter/play_integrity_flutter.dart';
Future verifyAppIntegrity() async {
final integrityToken = await PlayIntegrityFlutter().requestIntegrityToken(
'YOUR_CLOUD_PROJECT_NUMBER',
);
// Verify the token on your backend
final response = await http.post(
Uri.parse('${Env.backendUrl}/verify-integrity'),
body: {'token': integrityToken},
);
return response.statusCode == 200;
}
App attestation ensures that your app hasn't been modified or is running in an unsafe environment. This can prevent attackers from modifying your app to extract credentials.
3. Certificate Pinning
Implement certificate pinning to prevent man-in-the-middle attacks:
import 'package:http_certificate_pinning/http_certificate_pinning.dart';
Future secureApiCall() async {
final SecurityContext context = SecurityContext.defaultContext;
final fingerprints = [
SHA1Fingerprint('fingerprint1'),
SHA1Fingerprint('fingerprint2'),
];
final client = HttpCertificatePinning.createClient(
fingerprints: fingerprints,
context: context,
);
return client.get(Uri.parse('${Env.apiUrl}/endpoint'));
}
Certificate pinning ensures that your app only connects to servers with specific, pre-defined SSL certificates, preventing man-in-the-middle attacks that could intercept API calls.
Conclusion
The envied package offers a significantly more secure approach to handling environment variables in Flutter applications compared to dotenv-based solutions. By generating code at compile time, obfuscating sensitive values, and never including the .env file in your app bundle, it addresses the fundamental security flaws present in dotenv packages.
However, remember that no client-side solution is completely secure. For the highest level of security, sensitive operations should be handled by a backend service you control, with proper authentication and authorization mechanisms in place.
By combining envied with additional security measures like code obfuscation, certificate pinning, and app attestation, you can create a robust security posture for your Flutter applications.
Frequently Asked Questions
Is envied completely secure for storing API keys?
No, no client-side solution is completely secure. While envied is significantly more secure than dotenv packages because it doesn't include plaintext files, a determined attacker with sufficient knowledge can still potentially extract the values from your compiled application. For highly sensitive keys, the best approach is to use a backend proxy where the keys never exist in the client application.
Do I need to regenerate code every time I change the .env file?
Yes, you need to run flutter pub run build_runner build
after any changes to your .env file or Env class. For development convenience, you can use flutter pub run build_runner watch
to automatically regenerate the code whenever changes are detected.
What's the difference between envied and flutter_config?
Flutter_config uses native code to access environment variables, while envied generates Dart code at build time. Envied is generally considered more secure for mobile applications because it supports obfuscation and doesn't rely on readable files in your app bundle. Flutter_config still requires the .env file to be bundled with your app, which can be extracted.
Can I use envied in Flutter web applications?
Yes, envied works with Flutter web, but the security implications are different. In web applications, all client-side code can be inspected by users through browser developer tools, so even obfuscated values might be discoverable. For web applications, it's even more important to handle sensitive operations on your backend.
How do I handle different environment configurations in CI/CD pipelines?
For CI/CD pipelines, you can generate different .env files during the build process or store them securely in your CI/CD platform's secrets manager. Many CI/CD platforms like GitHub Actions, CircleCI, and GitLab CI support environment variables and secrets that can be used to create the appropriate .env file before building your app. Some developers also use flavors in Flutter to distinguish between environments.
Additional Resources
- Envied Package Documentation
- Flutter Security Best Practices
- OWASP Mobile Security Testing Guide
- Google Play Integrity API
- Apple App Attest
Remember: security is a continuous process, not a one-time implementation. Stay informed about the latest security practices and regularly audit your code for potential vulnerabilities.