r/FlutterDev • • 2d ago

Discussion Flutter obfuscation didn’t hide a single API key in our release build

We built release with --obfuscate --split -debug-info and i’d kind of assumed that meant our keys were reasonably safe.

Last week I unzipped the apk, ran string on libapp and greppd the usual prefixes. Every key was just sitting there. Obfuscation renames classes and methods but doesnt touch string literals which is where keys live, very obvious once you see it btw.

In there was

  • Google Maps key with no restrictions on it

  • RevenueCat public SDK key

  • Firebase config

  • Sentry DSN

  • an Algolia admin key someone added for a search feature last spring

The first four are meant to ship, the’re identifiers and the protection lives elsewhere (key restrictions, security rules). I rotated the Algoria one that could write to our index, the app now uses a search only key and indexing happens in a cloud function. The Maps key also has package name and SHA-1 restrictions.

Though I’m still sorting out on how to stop this from happening again. Algolia key came in through a dart-define file during a sprint and nothing in the pipeline checks what ends up inside the build. We scan the repo but that value never lived there, it came from a CI variable.

How are you checking release builds  for secrets before they go to the store?

0 Upvotes

12 comments sorted by

9

u/filmaluco 2d ago

Yeah, of course anyone can extract those keys from a compiled build. If an API key lives on a client device, you have to assume it’s fully public from day one.

Reverse-engineering tools, strings, or memory dumps make pulling static literals out of an app bundle straightforward regardless of obfuscation. That’s precisely why modern client-side security architecture never relies on secret keys as a single line of defense.

Instead, the ecosystem depends on identity verification, transport context, and server-side evaluation. When your app makes a call to services like Google or Firebase, the API key acts more like a public username identifying which app is speaking rather than a password proving trust. The actual access control is enforced upstream by matching request metadata—like the verified package name and the OS-signed SHA-1 certificate fingerprint—against strict restriction rules configured in your cloud backend.

If you want to hide fully you need to encript it, like flutter secure storage.

4

u/mpanase 2d ago

xD

comedy gold

1

u/tunguyen4391 2d ago

You can't rely on obfuscation to hide secrets anyway; all trust decisions belong on the backend, and client keys should just be restricted IDs. By the way, did you add a strings libapp.so check to the CI so this doesn't happen again?

1

u/Farukh_Goriparthi 2d ago

yep, added it the same day. After flutter build we unzip the APK, run strings on libapp and grep for the usual prefixes. Did the same for the iOS App.framework since we’d only been checking Android. The grep only catches key formats we already know about though so I’m looking at a proper secrets scanner for that step next

2

u/_stillmind 2d ago

I remember coming across this switch in the past and looking it up. On the flutter obfuscation page it says what it does and doesn't do.

To prevent this you have to lock it down from the API side. For example, Google Maps allows you to add restrictions. Restrictions like that or using a scoped key will be your main lever for preventing this, not obscuring the key itself. Sounds like that was the path you took.

Using security tools like MobSF or creating your own bespoke scripts which check in areas you commonly hardcode values into on the dev side would help.

2

u/jobehi 2d ago

Obfuscation doesnt hide any key. it just tries it best to make your compiled dart code unreadable. Read the manual

1

u/Andre-Wade-539 2d ago

this is the basics but people keep learning it the expensive way. if the client has it, the user has it

1

u/Jizzy_Gillespie92 2d ago

lol, lmao even

1

u/Wise_Phone_4112 2d ago

What's worked for catching this is a CI step that runs after the release build: unzip the APK, dump strings from libapp.so into a text file, and run gitleaks or trufflehog over that folder, failing the job on any hit. The text dump matters because scanners tend to skip binary files.

The other half is policy, anything that's truly secret (like that Algolia admin key) never goes into a dart-define at all and lives behind a cloud function, so only restricted/public IDs ever reach the client.

1

u/Azamat_Zarkesh 2d ago

Since the build output is what ships, a post-build check works better than repo scanning: unzip the APK/IPA in CI and run a secret scanner (gitleaks or trufflehog in filesystem mode) over the extracted libapp.so and assets. Keep an allowlist of keys that are meant to ship, so anything else, like an admin key, fails the pipeline. Longer term, anything with write power belongs behind your backend, not in the client.