How to Securely Implement API Keys in Flutter Using Envied

Securely Implement API Keys in flutter using Envied

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.

Important Security Notice! Traditional .env approaches in Flutter mobile apps are inherently insecure. This article provides a more robust solution using the envied package, which significantly improves the security of your sensitive credentials.
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:

Security Advantages!
  1. Generates Dart code at build time based on your .env values
  2. The .env file itself is never included in your app bundle
  3. Values can be obfuscated in the generated code
  4. 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.

Tip! Always check for the latest version of these packages on pub.dev as they may receive security updates and improvements over time.

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
Critical Security Step! Add this file to your .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;
}
Security Note! Notice the important 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.

Developer Tip! For continuous development, you can use 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:

  1. Always obfuscate sensitive values by setting obfuscate: true for any credential that needs protection
  2. Enable code obfuscation in your Flutter release builds:
    flutter build apk --obfuscate --split-debug-info=build/app/outputs/symbols
  3. Use different .env files for different environments
  4. Consider implementing different Env classes for different environments
  5. 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
}
Best Practice! For production builds, consider using CI/CD pipelines to inject the correct environment files during the build process rather than committing them to source control.

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.

Security Principle! The most secure API key is the one that never exists in your client-side code at all. When possible, proxy sensitive operations through your own backend.

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.

Security In Layers! No single approach provides complete security. Implementing multiple security layers makes your app significantly more resilient against attacks.

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

Next Steps! Now that you've learned how to secure your API keys and credentials in Flutter, consider implementing additional security measures like certificate pinning, app attestation, and backend proxies to create a comprehensive security strategy for your mobile applications.

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.

Post a Comment