Gunjan Sharma

Security · Flutter

Shifting the Paradigm: Introducing `envault` — Secure Secrets for Flutter

· Updated

If you are a mobile engineer, you already know the golden rule of client-side security: Never trust the client.

Yet, when we build Flutter applications, we routinely break this rule out of necessity. We need Stripe publishable keys, Google Maps API tokens, and Firebase configurations to initialize our apps.

So, what do we do? We look at pub.dev. And what we find there is, frankly, terrifying for any enterprise or fintech application.

I spent the last few weeks auditing how Flutter apps handle environment variables, and the results forced me to build an entirely new architecture from the ground up. Here is why current tools fail, and why I built envault to fix them.

The State of Flutter Secrets

There are basically two ways developers handle .env files in Flutter right now. Both are fundamentally broken for high-stakes applications.

1. The "Configuration File" Approach (flutter_dotenv)

You drop your .env file into your pubspec.yaml assets block. When your APK or IPA is built, the .env file is literally zipped into the binary as plaintext.

The Exploit: An attacker downloads your app, renames app.apk to app.zip, unzips it, and reads your .env file in 5 seconds. This isn't security; it's just remote configuration.

2. The "Toy Cipher" Approach (envied, envify)

This is the current "best practice" in the community. You use build_runner to generate a Dart class where your secrets are obfuscated using an XOR cipher.

The Exploit: XOR is a symmetric toy cipher. If you run the unix strings command against the compiled Dart AOT snapshot, you can easily extract both the XOR key and the ciphertext. A junior security researcher can reverse this in under 60 seconds with a simple Python script.

Furthermore, both of these approaches inject the plaintext secret directly into the Dart VM heap as a standard String. If a junior developer accidentally types print(Env.apiKey) or includes the config class in a toJson() serialization, your production secrets are permanently leaked into Crashlytics, Sentry, or your Datadog logs.

For a fintech application subject to PCI-DSS or SOC2 compliance, this is a catastrophic failure waiting to happen.

Shifting the Paradigm: Introducing envault

I built envault to shift Flutter secret management from "script-kiddie obfuscation" to verifiable, hardware-accelerated cryptography.

[**Check out envault on pub.dev**](https://pub.dev/packages/envault)

Here is how we fixed the architecture:

1. Real Cryptography (AES-256-GCM)

Instead of XOR, envault encrypts your secrets at build time using AES-256-GCM (NIST FIPS 197). Every individual field gets its own 96-bit CSPRNG Initialization Vector (IV). The ciphertext injected into your binary is mathematically opaque.

2. The Master Key is NEVER in the Binary

The fatal flaw of obfuscation tools is that they store the encryption key adjacent to the ciphertext. envault assumes the binary is compromised. The master key is derived dynamically at runtime using PBKDF2-HMAC-SHA256 with 310,000 iterations. We derive the key from non-secret app metadata (or a CI-injected master password that never enters the Dart code).

3. Hardware Acceleration (No UI Jank)

Doing 310,000 rounds of PBKDF2 in pure Dart will drop frames and cause "App Not Responding" (ANR) crashes on older Android devices. envault fixes this by delegating decryption to the underlying OS java.security on Android 21+, CryptoKit on iOS 13+). The heavy math is offloaded to native background threads, ensuring your 120fps UI stays buttery smooth.

4. Scoped Memory Safety

To prevent log leakage, envault introduces the SecureString type.

* Calling .toString() simply yields [REDACTED:SecureString].

* To access the plaintext, you must use an asynchronous closure: await Vault.apiKey.use((key) => ...)

Because the plaintext is scoped to the closure, the string reference is dropped immediately upon completion. It becomes instantly eligible for the Dart Garbage Collector, dramatically reducing the window of opportunity for an attacker to dump the heap.

5. Zero build_runner Coupling

If you work in a monorepo with hundreds of Freezed or JSON-Serializable models, you know the pain of build_runner conflicts and 5-minute generation times. envault ships as a standalone CLI envault generate). It executes in milliseconds, completely isolated from your main build graph.

The Architecture Whitepaper

If you are a Security Auditor, a Staff Engineer, or just want to dive deep into the cryptographic math and threat models behind this package (including how we handle rooted devices and Frida hooks), I have published a comprehensive architectural whitepaper.

📄 [Read the Technical Whitepaper (PDF)](https://smallpdf.com/file#s=5ad5c618-a4e1-44eb-8e43-5097da5328c3)

Conclusion

Client-side security is about defense in depth. While no client-side secret is ever 100% immune to a dedicated attacker with a rooted device and infinite time, we have a professional obligation to raise the cost of an attack. Moving from XOR obfuscation to AES-256-GCM with hardware-accelerated key derivation does exactly that.

It's time to stop baking plaintext secrets into your Flutter apps. Give envault a try on your next project, and let me know what you think.

---

Gunjan Sharma is a Software Engineer specializing in scalable architectures and cryptographic systems. Connect with him on LinkedIn (https://www.linkedin.com/in/gunjan1sharma/).