If you discover a major security vulnerability, please report it responsibly by emailing mfazrinizar@gmail.com instead of creating a public GitHub issue. We will respond within 72 hours and work toward a fix.
flutter_secure_dotenv encrypts your .env values at build time. At runtime the app needs the same key to decrypt. The critical question is: how does the key reach the running app securely?
| # | Approach | Key in binary? | Key in source control? | Notes |
|---|---|---|---|---|
| 1 | Server-fetched key + flutter_secure_storage |
❌ | ❌ | Only approach where the key never exists in the binary. |
| 2 | Hardcoded key in gitignored env.dart + --obfuscate |
❌ (gitignored) | Key is in the binary but protected from source control leaks. | |
| 3 | --dart-define / String.fromEnvironment |
✅ base64 string | Same binary risk as #2 but key also leaks into source control. | |
| 4 | JSON file shipped in app bundle | ✅ plaintext file | ❌ | Extractable without decompiling — just unzip. Worst option. |
Note on
flutter_secure_storagewithout a server: Writing a hardcoded key toflutter_secure_storageon first launch does not improve security. Thestatic constis already compiled into the binary — an attacker extracts it from the binary before the app ever runs. Copying it to secure storage at runtime doesn't remove it from the compiled code.flutter_secure_storageis only meaningful when the key comes from an external source (a server) and never exists in the binary.
- Build-time embedding —
--dart-definevalues are compiled directly into the binary (libapp.soon Android, Mach-O on iOS) as base64-encoded strings. - Binary decompilation — Anyone with the APK/IPA can extract them using
apktool,jadx, or Hopper Disassembler. - Memory exposure —
String.fromEnvironment()values exist as plain text in process memory. - Security paradox — An insecure channel for the key undermines the entire encryption layer.
Shipping encryption_key.json as a Flutter asset or alongside the binary means the key is plaintext on disk inside the APK/IPA. An attacker can extract it with a simple unzip — no decompilation needed. Never bundle the key file in your release.
The JSON file generated by build_runner (OUTPUT_FILE) is a temporary transfer mechanism to move the key from the build tool to your gitignored env.dart. Delete it immediately after copying the values.
The simplest working approach. The key lives in a source file that is never committed. Combined with --obfuscate, this is the best you can do without a server.
NOTE: The key IS in the compiled binary.
--obfuscatemakes it harder to find but not impossible for a determined attacker. This approach protects against source control leaks and raises the bar for reverse engineering, but it is not bulletproof.
project/
├── lib/
│ ├── env.dart ← GITIGNORED (contains real key)
│ ├── env.example.dart ← Committed (template with placeholders)
│ └── env.g.dart ← Generated by build_runner
├── .env ← Your secrets
└── .gitignore ← Must include: .env, env.dart, encryption_key.json
env.example.dart (committed):
import 'package:flutter_secure_dotenv/flutter_secure_dotenv.dart';
part 'env.g.dart';
@DotEnvGen(filename: '.env', fieldRename: FieldRename.screamingSnake)
abstract class Env {
// Replace with real values from encryption_key.json, then delete the JSON.
static const _encryptionKey = 'PASTE_BASE64_ENCRYPTION_KEY_HERE';
static const _iv = 'PASTE_BASE64_IV_HERE';
static Env create() => Env(_encryptionKey, _iv);
const factory Env(String encryptionKey, String iv) = _$Env;
const Env._();
String get apiKey;
}Setup steps:
# 1. Copy the template
cp lib/env.example.dart lib/env.dart
# 2. Generate encrypted env + random key
dart run build_runner build \
--define flutter_secure_dotenv_generator:flutter_secure_dotenv=OUTPUT_FILE=encryption_key.json
# 3. Copy ENCRYPTION_KEY and IV from encryption_key.json into env.dart
# 4. Delete the JSON file — it was only a transfer mechanism
rm encryption_key.json
# 5. Build release with obfuscation
flutter build apk --obfuscate --split-debug-info=build/debug-infoWhat this protects:
- ✅ Key never enters source control or git history
- ✅
.envvalues are encrypted — not plaintext in the binary ⚠️ Key is in the binary but obfuscated — raises the difficulty bar for reverse engineering
What this does NOT protect:
- ❌ A determined attacker with decompilation skills can still find the key in the binary
- ❌ No key rotation without rebuilding and re-deploying
The only approach where the key never exists in the app binary. It is fetched from a backend secrets manager on first launch and cached in platform secure storage (Android Keystore / iOS Keychain).
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
static Future<Env> create() async {
const storage = FlutterSecureStorage();
var key = await storage.read(key: 'env_encryption_key');
var iv = await storage.read(key: 'env_iv');
if (key == null || iv == null) {
// Fetch from your backend over HTTPS
final response = await http.get(
Uri.parse('https://your-api.com/env-keys'),
headers: {'Authorization': 'Bearer $token'},
);
final keys = json.decode(response.body) as Map<String, dynamic>;
key = keys['ENCRYPTION_KEY'] as String;
iv = keys['IV'] as String;
await storage.write(key: 'env_encryption_key', value: key);
await storage.write(key: 'env_iv', value: iv);
}
return Env(key, iv);
}Use a secrets manager such as:
What this protects:
- ✅ Key never in the binary — cannot be extracted by decompilation
- ✅ Key never in source control
- ✅ Supports key rotation without rebuilding the app
- ✅ After first launch, key lives in OS-encrypted hardware-backed storage
What this does NOT protect:
- ❌ Memory dump on rooted/jailbroken device (decrypted values in RAM)
- ❌ Requires a backend server (needs to be secure) and internet on first launch
| Threat | Protected? | Notes |
|---|---|---|
Source code leak of .env file |
✅ Yes | Values are encrypted at build time |
| Source control leak of encryption key | ✅ Yes | Key is in gitignored file, never committed |
| Reverse-engineering APK/IPA for secrets | Encrypted values are safe; key security depends on approach + --obfuscate |
|
| Reverse-engineering APK/IPA for the key | Approach 2 (server): ✅. Approach 1 (hardcoded): |
|
| Memory dump on rooted device | See Memory Protection below | |
| Man-in-the-middle attacks | ❌ No | Use HTTPS + certificate pinning separately |
Decrypted values must exist in app memory to be used — a root-level attacker who can dump memory at the right moment can read them. This is a fundamental limitation of every app on every platform, not specific to this package.
Note on
flutter_secure_storage: The data at rest in Android Keystore or iOS Keychain is hardware-encrypted and safe even on rooted devices. However, the moment you callstorage.read(), the returnedStringis a plain Dart object in app memory — and that is dumpable. The storage protects the persisted data; the vulnerability is the read-into-memory step, which is unavoidable if you need to actually use the value.
Available mitigations (none are bulletproof, but they stack):
| Mitigation | Effectiveness | Notes |
|---|---|---|
| Root/jailbreak detection | Refuse to run on compromised devices. Bypassable via Frida hooks or patched binaries. Packages: flutter_jailbreak_detection. |
|
| Hardware-backed crypto | Use Android Keystore TEE/StrongBox or iOS Secure Enclave to decrypt in hardware — the key never enters app memory. But the decrypted values still must. | |
| Minimize exposure window | Decrypt lazily (on-demand), don't cache, null-out references after use. Limited in Dart — strings are immutable and the GC controls when memory is freed, so you can't reliably zero it. | |
| Anti-tampering / anti-debugging | Detect Frida, debuggers, and memory inspection tools at runtime. Bypassable by skilled attackers. | |
| Short-lived tokens | ✅ | Design your backend so secrets are short-lived tokens that expire quickly. Even if dumped, they become useless. This is an architectural solution, not a client-side one. |
Bottom line: The only robust defense against memory inspection is making stolen secrets useless — short-lived tokens, server-side rate limiting, and device attestation. No amount of client-side hardening fully solves this.
No purely client-side approach can fully protect an encryption key from a determined attacker. If the key must be in the binary (because there's no server), it can be extracted — --obfuscate raises the bar but doesn't eliminate the risk.
This is a fundamental limitation of all client-side secret management, not specific to this package.
Your .env file should contain semi-secrets — values that need to be in the client at build time but are already protected by server-side rules. The encryption adds a layer that makes casual extraction and automated scraping harder, but it is not the primary security boundary.
Good candidates for .env (semi-secrets with server-side protection):
| Value | Why it's okay | Real protection layer |
|---|---|---|
| Firebase API key | Must be in client; public by design | Firestore Security Rules, App Check |
| API base URL | Not truly secret, but shouldn't be trivially scraped | Rate limiting, authentication |
| Sentry DSN | Needed at build time | Server-side rate limiting, project access controls |
| Third-party SDK keys (Maps, Analytics) | Must be in client | Usage quotas, domain/bundle restrictions |
| Feature flags / config | Non-sensitive but shouldn't be plaintext | Server-side validation |
Bad candidates for .env (truly secret — do NOT embed in client):
| Value | Why it's dangerous | What to do instead |
|---|---|---|
| Database credentials | Full data access if leaked | Server-side only — never in client |
| Admin API keys | Unrestricted access | Server-side only |
| Payment processor secret keys | Financial access | Server-side only |
| JWT signing secrets | Can forge tokens | Server-side only |
Think of flutter_secure_dotenv encryption as a speed bump, not a wall:
- It prevents casual leaks —
.envvalues aren't plaintext in your repo or binary - It raises the effort required — an attacker needs decompilation skills + obfuscation bypassing, not just
unziporstrings - It defeats automated scanning — secret-scanning bots and scrapers won't find encrypted values
- It reduces spam/abuse potential — even semi-public keys like Firebase API keys benefit from not being trivially extractable, because it reduces the pool of people who can abuse them
The real security comes from the server side — rules, quotas, authentication, rate limiting, and App Check. The encryption just makes it harder for someone to reach the point of attempting abuse.
- Never pass encryption keys via
--dart-define. - Never ship
encryption_key.jsonor any key file in your app bundle — it is plaintext. - Never commit
env.dartto source control — commitenv.example.dartas a template. - Always build release builds with
--obfuscate --split-debug-info=.... - Always add
env.dart,encryption_key.json, and.env*to.gitignore. - For maximum security, use a server to provision the key (Approach 2).
- Without a server, use the gitignored
env.dart+--obfuscatepattern (Approach 1) and accept the trade-off.
| Version | Supported |
|---|---|
| 2.x | ✅ Current |
| 1.x |