← Articles

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 strings on the APK/IPA
  • Disassembly with Ghidra or IDA Pro
  • Dynamic analysis with Frida

Mitigations:

  1. Obfuscation (--obfuscate) makes extraction harder but not impossible
  2. Short-lived tokens: use tokens that expire quickly (1 hour) rather than long-lived API keys
  3. 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.

Storing secrets safely in Flutter with --dart-define — ANN Tech