Dynamic (and Secure) Google Maps API Key in Flutter
Hard-coding and Distributing a Google Maps API Key 🤔? Nope.
One thing that I couldn’t wrap my head around when developing the (Android-flavor of my) location- and time-based reminder app Wyatt using Flutter was the intended hard-coding of a Google Maps API Key…
Here’s a rundown of an opinionated solution and my findings integrating it.
Google Maps for Flutter is a package which provides a widget that I use in my app as a location picker. The package documentation wants the developer to create an API key and include it into the application manifest android/app/src/main/AndroidManifest.xml as follows:
<manifest …
<application …
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="HARDCODED-KEY-HERE"/>When the resulting Android application package is built, this meta information is baked in into the resulting binary which then gets deployed on the target device. For release builds, it’s considered best practice to obfuscate the binary.
Sounds okay so far?
For me, there are a couple of problems with that approach:
- Even obfuscated code can be reverse engineered. This might be extremely hard, and I’m sure obfuscation is state of the art here, but it’s probably not impossible to obtain and extract the original API key.
- The released app with its key inside is shipped to n devices where n can (and likely should) be a potentially pretty big number, i.e., one and the same key can make it to many installations.
Now imagine a key gets reverse engineered and abused. Plus, here, we’re talking about a key for a popular API which generates cost if used above a certain limit. So, in such a worst case scenario, the only option is to revoke the compromised master key — breaking the application for potentially many users. Then a fix needs to be developed, a new release needs to be built, and the patch distributed…
Thinking it a bit further, such a single key approach doesn’t allow an installation-specific charge either, e.g., a software vendor may want to offer different tiers based upon actual consumer usage or time-restricted scenarios.
So, there’s both, at least a technical and a business reason to go for
- a dynamic key that is not hard-coded, but read from a secured storage and used per runtime
- a key per user/device/installation stored encrypted on the target
This way, blast radius is limited — a compromised key affects only one installation, not many. A user can obtain a key (think of it as something like a “license key” from a vendor perspective) as many times as necessary. A leaked key is easily identifiable and fixable. Just invalidate the affected key, create and communicate a new one to the user who updates the key in the app. No need to break all field installations, to rebuild and redistribute binary versions or your app.
Additionally, application tiers with their own quotas, limits, time frames can be created.
The drawbacks of this approach are obviously higher key maintenance (there are many individual keys rather than one key) and potentially a more complicated (sales) setup.
Google Map Dynamic Key (and Secure Storage) to the Rescue
The sketch of the solution is as follows:
- The user obtains an individual key (e.g. via an old school email or from a download page), that is an API key specifically created and managed for the user by the app producer.
- The app checks upon start-up its secure, encrypted device storage for such a key, and asks for this key if it can’t find one. The app stores a newly obtained key in its encrypted device storage.
- The key is set dynamically for the runtime of the app when needed.
Thankfully, there’s Google Map Dynamic Key. With that plugin added, there’s no more need for a static compile-time key.
Is it?
My initial a̶p̶p̶r̶o̶a̶c̶h̶mistake was to completely remove com.google.android.geo.API_KEY from AndroidManifest.xml as I thought this would no longer be needed at all.
Well, that left me with a FATAL EXCEPTION: androidmapsapi-ula-1 and crashed my app when instantiating the Google Maps widget:
FATAL EXCEPTION: androidmapsapi-ula-1My next best shot was to provide an empty key as the meta data value — android:value="" ended up in this Authorization failure:
My final solution has turned out to be twofold:
First, I added the following, non-empty dummy string value in my android/app/src/main/AndroidManifest.xml:
<manifest …
<application …
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="DUMMY-KEY-HERE"/>Second, in my LocationPicker widget, I figured that it requires setting the key dynamically at the right time, before the Google Maps widget actually checks the key.
Let’s have a look at the relevant code to dynamically set the API key:
@override
void initState() {
super.initState();
initGoogleMapKey();
}
// initializes _key as a side effect
Future<void> initGoogleMapKey() async {
final settings = ref.read(settingsNotifierProvider.notifier);
/* don't omit setState() and _isLoading here, otherwise the GoogleMap widget
will not be updated with the key, and tries to read the (dummy) key from
the manifest instead (results in a blank Google Maps screen and an error
message in the console): */
setState(() {
_isLoading = true;
});
_key = await settings.getKey();
log.debug('key from settings: $_key',
name:
'$runtimeType'); // if key is omitted, the app will crash with FATAL EXCEPTION: androidmapsapi-ula-1
await _googleMapDynamicKeyPlugin.setGoogleApiKey(_key).then((value) {
log.debug('GoogleMap key set dynamically', name: '$runtimeType');
setState(() {
_isLoading = false;
});
});
}That’s the initialization of the LocationPicker widget. Whenever the widget is instantiated it reads the API key from settingsNotifierProvider which is a Riverpod-based “wrapper” around Flutter Secure Storage using Android’s EncryptedSharedPreferences. Once retrieved, the key is set dynamically. Until then _isLoading is true which has an implication on the widget itself — In its build method, GoogleMap is created when _isLoading is set to false, i.e. after the key has been made known. Otherwise, while initializing, a circular progress indicator is shown. Here’s the related code snippet for that part:
@override
Widget build(BuildContext context) {
...
return Scaffold(
appBar: WyattAppBar(context: context, title: Screen.pickLocation),
body: _isLoading
? Center(child: const CircularProgressIndicator.adaptive())
: GoogleMap( ...That’s how, thanks to google_map_dynamic_key (and flutter_secure_storage), I reliably set a user-specific Google Maps API key dynamically, leveraging encrypted storage, avoiding hard-coding and failures.
What other Options are there?
Instead of a static key, an environment variable (using a dart-define) can inject a key. Still, that would be de facto one key per app and not really solve my original challenge.
Hope that helps folks playing especially around with the Google Map Dynamic Key for Flutter.
