Storing secrets safely in Flutter with --dart-define
By Ann Tech · 23 November 2025
Secrets in Flutter apps are a common vulnerability. API keys embedded in source code appear in git history, stack traces, and binary analysis. The right approach: use --dart-define to inject secrets at build time from CI secrets storage.
What not to do
// NEVER do this:
const apiKey = 'sk-live-abc123...';
const firebaseApiKey = 'AIzaSy...';
// Also wrong — still in source:
const apiKey = String.fromEnvironment('API_KEY', defaultValue: 'sk-live-abc123');
// ^^^^^^^^^^^^^^^^
// Default is visible in source!
# .env bundled as an asset — readable with a hex editor on the APK/IPA:
STRIPE_SECRET_KEY=sk_live_xxx # WRONG — never do this
The correct approach: dart-define from CI secrets
# .github/workflows/build.yml
- name: Build production app
run: |
flutter build appbundle \
--dart-define=API_KEY=${{ secrets.API_KEY }} \
--dart-define=STRIPE_KEY=${{ secrets.STRIPE_KEY_PROD }} \
--dart-define=SENTRY_DSN=${{ secrets.SENTRY_DSN }}
// No default that leaks the secret
const apiKey = String.fromEnvironment('API_KEY', defaultValue: '');
const stripeKey = String.fromEnvironment('STRIPE_KEY', defaultValue: '');
The key is never in your source code. It's only available in the compiled binary.
Environment-specific secrets with JSON files
# CI creates this file from secrets, builds, then deletes it
cat > config.json << EOF
{
"API_KEY": "${{ secrets.API_KEY }}",
"STRIPE_KEY": "${{ secrets.STRIPE_KEY_PROD }}"
}
EOF
flutter build appbundle --dart-define-from-file=config.json
rm config.json # Delete immediately
Secure storage for runtime secrets
For secrets acquired at runtime (auth tokens, session keys):
const storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
keyCipherAlgorithm: KeyCipherAlgorithm.RSA_ECB_PKCS1Padding,
storageCipherAlgorithm: StorageCipherAlgorithm.AES_GCM_NoPadding,
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
// Store auth token after login
await storage.write(key: 'auth_token', value: token);
await storage.write(key: 'refresh_token', value: refreshToken);
// Read
final token = await storage.read(key: 'auth_token');
// Delete on logout
await storage.deleteAll();
What can still be extracted from the binary
Even with --dart-define, keys baked into the binary can be extracted by:
- Running
stringson the APK/IPA - Disassembly with Ghidra or IDA Pro
- Dynamic analysis with Frida
Mitigations:
- Obfuscation (
--obfuscate) makes extraction harder but not impossible - Short-lived tokens: use tokens that expire quickly (1 hour) rather than long-lived API keys
- Backend proxy: for truly sensitive APIs, call your backend which holds the secret — the mobile app never needs the raw key
Backend proxy pattern
For payment processing or other sensitive operations:
// WRONG: Stripe secret key in mobile app
final stripe = Stripe(secretKey: 'sk_live_xxx');
await stripe.createPaymentIntent(amount: 1000);
// RIGHT: backend creates the payment intent
final response = await dio.post('/api/payment/create-intent', data: {
'amount': 1000,
'currency': 'usd',
'order_id': order.id,
});
final clientSecret = response.data['client_secret'] as String;
// Use the client_secret to confirm payment on the client side
await Stripe.instance.confirmPayment(paymentIntentClientSecret: clientSecret);
Android network security config
Prevent cleartext HTTP (which could leak tokens):
<!-- android/app/src/main/res/xml/network_security_config.xml -->
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
</network-security-config>
Local development
For local dev, use a .env.development file that's in .gitignore:
# .gitignore
*.env
*.env.*
config/*.json
!config/example.json # Keep an example with placeholder values
# scripts/run_dev.sh
set -a; source .env.development; set +a
flutter run $(env | grep -E '^(API_KEY|STRIPE_KEY)' | sed 's/^/--dart-define=/' | tr '\n' ' ')
Common pitfalls
Checking secrets into git. Even if deleted, secrets in git history are compromised. Rotate immediately if this happens. Use git-secrets or similar tools to prevent accidental commits.
Using the same key for dev and prod. A dev key accidentally used in production means a breach can't be contained by rotating the prod key alone. Keep environments completely separate.
Not having a rotation plan. API keys will eventually need rotation. Document how to rotate each secret and test the process before an emergency forces you to do it under pressure.
Sign in to like, dislike, or report.