Manage secrets in a flutter project with Blackbox
October 22, 2019
For every flutter mobile app there are certain secrets which have to be integrated into the app. These might include API Keys or private keys for services like Firebase, Analytics, etc. Or provisioning profiles for iOS.
This is the first part of what will become three parts on how to build flutter apps on a CI.
- Setting up Blackbox & Managing Signing Keys (This article)
- Using Blackbox on a CI (Coming soon)
- Building release builds on Travis (Android) and Cirrus CI. (iOS) (Coming soon)
Outline
In this article you will get a quick overview on how to manage secrets you usually encounter in flutter apps:
- First a quick explanation of Blackbox, a tool we will use to encrypt all secrets.
- Signing keys for android and how to configure gradle.
- Provisioning profiles for iOS with fastlane match.
Blackbox Overview
It is generally not a good idea to add plaintext API keys, passwords, etc. to your git repository. But it is very convenient. This is where Blackbox comes in and allows you to store encrypted secrets along side your flutter app.
Possible secrets could include:
- Upload or signing keys.
- Secret API Keys for services used in the app (like Google Analytics, etc.).
- Provisioning Profiles or similar.
Installation & Set Up
Blackbox are a collection of shell scripts based on GPG to store secrets inside public git repositories.
Installation is as easy as cloning the git repository and adding it’s
bin/
directory to your $PATH
. I would recommend cloning my fork
because I’ve added a useful blackbox_create_role_account
script.
git clone --branch create_role_account https://github.com/hpoul/blackbox/
export PATH=$(pwd)/blackbox/bin
Afterwards to get started you have to set up and create a GPG signing key.
Now change into your git repository and we’ll add blackbox. Make sure to
exchange my@email.com
with your actual email address which
includes your GPG key.
bash$ blackbox_initialize
> Enable blackbox for this git repo? (yes/no) yes
> VCS_TYPE: git
bash$ blackbox_addadmin my@email.com
gpg: keybox '/myrepo/.blackbox/pubring.kbx' created
gpg: /myrepo/.blackbox/trustdb.gpg: trustdb created
gpg: key 3F58C943BC3603F4: public key "Max Mustermann <my@email.com>" imported
gpg: Total number processed: 1
gpg: imported: 1
NEXT STEP: You need to manually check these in:
git commit -m'NEW ADMIN: herbert@poul.at' .blackbox/pubring.kbx .blackbox/trustdb.gpg .blackbox/blackbox-admins.txt
bash$ git add .
bash$ git commit -m 'initialized blackbox'
[master (root-commit) 65384bd] initialized blackbox
6 files changed, 6 insertions(+)
create mode 100644 .blackbox/.gitattributes
create mode 100644 .blackbox/blackbox-admins.txt
create mode 100644 .blackbox/blackbox-files.txt
create mode 100644 .blackbox/pubring.kbx
create mode 100644 .blackbox/trustdb.gpg
create mode 100644 .gitignore
bash$
Configure signing keys for android
It’s a good practice to keep your secrets in one place instead of scattering
them across the whole project. So I tend to have a _tools/secrets
folder in
my projects which contain all non-public files.
The easiest way to keep your android release and upload keys secure is to add
a new build_secrets.gradle.kts
file which is conditionally imported into your
main gradle file if it is available.
(My examples use gradle kotlin dsl. it should only require minor adjustments for gradle groovy syntax).
_tools/secrets/android_build_secrets.gradle.kts
allprojects {
// requires path relative to the `android` directory.
extra["secrets.keyAlias"] = "MyKeyAlias"
extra["secrets.storeFile"] = "../_tools/secrets/android_upload_key.jks"
extra["secrets.storePassword"] = "MySecretPassword"
}
Now copy your upload or signing key to _tools/secrets/android_upload_key.jks
and add both to blackbox:
bash$ blackbox_register_new_file _tools/secrets/android_build_secrets.gradle.kts _tools/secrets/android_upload_key.jks
========== PLAINFILE _tools/secrets/android_build_secrets.gradle.kts
========== ENCRYPTED _tools/secrets/android_build_secrets.gradle.kts.gpg
========== PLAINFILE _tools/secrets/android_upload_key.jks
========== ENCRYPTED _tools/secrets/android_upload_key.jks.gpg
Local repo updated. Please push when ready.
git push
bash$
You should now be left with only .gpg
files and have the real files
encrypted and added to your .gitignore
. To be able to continue using those
files, run blackbox_postdeploy
bash$ blackbox_postdeploy
========== EXTRACTED _tools/secrets/android_build_secrets.gradle.kts
========== EXTRACTED _tools/secrets/android_upload_key.jks
========== Decrypting new/changed files: DONE
bash$
android/app/build.gradle.kts
val secretConfig = file("../_tools/secrets/build_secrets.gradle.kts")
if (secretConfig.exists()) {
apply { from(secretConfig) }
} else {
println("Warning: Secrets not found, release signing disabled.")
}
// ...
android {
signingConfigs {
release {
keyAlias project.properties["secrets.keyAlias"] ?: ""
keyPassword project.properties['secrets.storePassword']
// Provide a dummy default value, null would break builds.
storeFile file(project.properties['secrets.storeFile'] ?: "invalid")
storePassword project.properties['secrets.storePassword']
}
}
}
🎉️ aaaand now you are done. Feel save to commit and push your changes. Your secrets are save, but you can still conveniently export your signed appbundles/apks.
Secret API Keys for your flutter app
To access “secret” API keys (for example for accessing services like firebase or analytics) you can use a separate entry point for your flutter app.
So instead of using the default lib/main.dart
you create your custom
lib/env/production.dart
with a main method like:
lib/env/production.dart
import '../main.dart';
import './production_secrets.dart';
void main() {
startApp(ProductionSecrets());
}
Now modify your main.dart so we start our app with the provided secrets. We can then use the Provider package to make it available throughout the app.
lib/main.dart
import 'package:provider/provider.dart';
abstract class Secrets {
abstract String get firebaseApiKey;
}
void startApp(Secrets secrets) {
runApp(MyApp(secrets: secrets));
}
class MyApp extends StatelessWidget {
const MyApp({Key key, this.secrets}) : super(key: key);
final Secrets secrets;
@override
Widget build(BuildContext context) {
return Provider<Secrets>.value(
value: secrets,
child: // ...
);
}
}
Now simply add your secrets as an implementation of the Secrets
class
and you are ready to go:
lib/env/production_secrets.dart
class ProductionSecrets implements Secrets {
@override String get firebaseApiKey => 'D484fDKJE4';
}
To keep things tidy you can put this file into _tools/secrets/
and
symlink it into your lib/env/
folder.
Make sure to register the file with blackbox so it is encrypted correctly.
bash$ blackbox_register_new_file lib/env/production_secrets.dart
========== PLAINFILE lib/env/production_secrets.dart
========== ENCRYPTED lib/env/production_secrets.dart.gpg
========== UPDATING VCS: DONE
Local repo updated. Please push when ready.
git push
Update release script to use new secrets.
Now you have to tell flutter build
(and flutter run
) to use your new
secrets file. To do this simply pass in the -t
parameter:
bash$ flutter build appbundle -t lib/env/production.dart
If you require it during production you can also set the entrypoint in Android Studio:
iOS: Manage Provisioning Profiles
Fastlane match is a convenient way to share provisioning profiles with your team or different CI environments.
It uses a remote git repository (or google cloud storage) to securely store
provisioning profiles and install them using fastlane match
.
My recommendation is to store the fastlane configuration inside your ios
project but reference your secrets inside _tools/secrets
to keep all
secrets in one place.
Follow the setup instructions on fastlane’s website the “secret” configuration basically consists of the following variables:
_tools/secrets/fastlane_match_password
export MATCH_KEYCHAIN_NAME="mykeychainname"
export MATCH_PASSWORD="fastlaneMatchEncryptionPassword"
export FASTLANE_PASSWORD="itunes connect store password"
and don’t forget it to register it with blackbox:
bash$ blackbox_register_new_file _tools/fastlane_match_password
========== CREATED: _tools/fastlane_match_password.gpg
========== UPDATING VCS: DONE
Local repo updated. Please push when ready.
git push
SSH Access
For fastlane to access the git repository where you store the provisioning profile you should create a new private key - for example on GitHub or GitLab you can register deploy keys. So if you do not yet have a ssh key you can create a new one using
bash$ ssh-keygen -f _tools/fastlane_match_certificates_id_rsa
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in _tools/fastlane_match_certificates_id_rsa.
Your public key has been saved in _tools/fastlane_match_certificates_id_rsa.pub
bash$ blackbox_register_new_file _tools/fastlane_match_certificates_id_rsa
Using your secrets with fastlane
Now if you are want to actually access the repository to install all
provisioning profiles (for example on a CI server) you can simply import the
private key, source the password file and launch fastlane match
:
cat _tools/secrets/fastlane_match_certificates_id_rsa | ssh-add -
source _tools/secrets/fastlane_match_password
fastlane match
More on how to create a full CI release cycle for your flutter app in a later article 😉️