How To Securely Build and Sign Your Android App With GitHub Actions

Implement automated release signing without uploading your KeyStore file to your repository

Yanneck Reiß
ProAndroidDev

--

Photo by Mati Mango from Pexels

Releasing an app to an app store like the Google Play Store is a common task that Android developers have to go through.

Because you need to verify that you are the owner of the respective app, you need to digitally sign your APK (Android application package) or AAB (Android App Bundle) before being able to upload it.

To be able to sign your application, you need to generate a .jks (Java KeyStore) file that contains your certificate.

If you are working alone on your app, you can easily use the Android Studio’s built-in Generate Signed Bundle/APK function.

However, if you work in a team, in most cases you want everyone to be able to access the respective Keystore and credentials.

Because sharing the credentials and KeyStore is a security issue, a common approach is to upload the KeyStore to version control and store the credentials in the respective secrets.

While this might be an acceptable trade-off for private repositories, it can not be an option for a public repository. Even if your credentials are safely stored in the secrets, you don’t want everyone to be able to access your KeyStore file.

The solution to this problem I want to share with you in this article is to encode the KeyStore file and also safely store it in your secrets, just like the credentials.

We can then use a CI/CD (Continuous Integration / Continous Delivery) to decode the secret on the flight back to a KeyStore file and safely sign and upload your release application.

For example, I will show you how to create a GitHub Workflow to achieve that behavior.

Table of contents

  1. Gradle Set-Up
  2. Encoding The KeyStore
  3. The GitHub Workflow
  4. Conclusion

1 Gradle Set-Up

Before we can start with the actual implementation, we need to set up signing-config in our app-level build.gradle to use environment variables from our GitHub secrets, which we will later set up.

The following code snippet first takes our decoded KeyStore file from the temporary GitHub Workflow folder and copies it to our app folder.

By using this approach we can locally leave the KeyStore file in the respective path, add it to the .gitignore, and keep being able to build our gradle file.

The proceeding lines set environment variables to our release config.

2 Encoding the KeyStore

The next step treats the encoding of the KeyStore file. At this point, I assume you already own your KeyStore file. If you don’t have experience with app-signing, I suggest you take a look at the already mentioned documentation.

For encoding, we will make use of the popular Base64 encoding scheme. Base64 doesn’t stand for specific but various encoding schemes that allow you to convert binary data into a text representation.

In our case, the encoding of the KeyStorefile will allow us to store the file as text in our GitHub Secrets and later on in the GitHub Workflow process decode it back to our original KeyStore file.

The encryption step can easily be done by using OpenSSL. Download and install it, then navigate to the folder that contains your .jks file. Within the respective folder, execute the following command in your Unix terminal or just use Git bash on Windows:

openssl base64 < your_signing_keystore.jks | tr -d '\n' | tee your_signing_keystore_base64_encoded.txt

If everything went right, you should see a newly created file your_signing_keystore_base64_encoded.txt which contains a cryptic text that represents your KeyStore file.

3 The GitHub Actions Workflow

To build our CI/CD pipeline, we will use GitHub Actions. But before we can start implementing our Workflow, we first need to set up our GitHub secrets.

3.1 Set up your GitHub Secrets

In the following section, I assume that you used the identifiers from the mentioned build.gradle file. If you renamed the environment variables, you need to adapt the GitHub Secret names accordingly.

The first secret we will add is the encoded Base64 representation of our KeyStore file. To do so, go into your project's GitHub secrets and add a new GitHub Secret called KEYSTORE.

Copy the content from the your_signing_keystore_base64_encoded.txt file and paste it into the value field.

Next, create a secret that is called SIGNING_STORE_PASSWORD and contains your KeyStore password.

Afterward, create one that is called SIGNING_KEY_PASSWORD and contains your key alias password.

The last secret we need to add is called SIGNING_KEY_ALIAS and should contain the alias of your app.

3.2 The Workflow

Now that we set up our secrets, we can proceed with the actual Workflow.

Because we later want to be able to manually trigger our Workflow, we will define it as on: workflow_dispatch.

To decode our encoded KeyStore file, we use the base64-to-file GitHub Action by Tim Heuer.

The GitHub Action allows us to define a parameter encodedString that will refer to our GitHub secret KEYSTORE. With the fileName parameter, we set the directory and filename of our KeyStore file in the temporary directory of our Workflow.

As we discussed in the first part of this article, our build.gradle will then be able to copy and use that file as the KeyStore.

Alternative: Using a custom statement

If you don’t want to rely on a third party GitHub Action for this task, you can decode the secret by yourself and save it to the temporary directory of your runner.

Use the code snippet from below in replacement for GitHub Action snippet to achieve this behavior. You will be able to proceed with the article without any drawbacks.

In the next step, we build the actual bundle file. First, we use the checkout GitHub Action to checkout our repository. By doing so our Workflow can access the files within it.

We then use the Setup-Java GitHub Action to set up JDK 1.8.

By using the chmod +x ./gradlew command, we make the Gradle wrapper executable for our Workflow.

With the .gradlew app:bundleRelease step we then actually build our app bundle file. Note how we set our GitHub secrets to the environment variables. Without this set up the build.gradle won’t be able to access the defined environment variables.

In the last step of our Workflow, we upload our whole outputs folder with the help of the Upload Artifact GitHub Action.

The summarized Workflow looks like the following:

3.3 The Workflow in Action

Now that we implemented our Workflow, we can access it via the Action tab in our GitHub repository.

By clicking on the Workflow at the left inside the tab, we should now be able to see the following.

workflow_dispatch event trigger

Whenever you want to build a new release, you can now click on Run workflow and start the build process.

Afterward, you will be able to download the built artifacts at the bottom of the respective workflow.

Finished workflow with generated build artifacts

4 Conclusion

Within this article, we discussed how to implement a GitHub Workflow that doesn’t force us to upload our KeyStore file directly into our repository anymore.

Instead, we decode the file and put it in the GitHub Secrets. Within our workflow, we decode this secret back to a KeyStore file and then can use it to sign our app bundle as usual.

This approach is especially useful if you own a public repository. However, a trade-off is that the KeyStore file temporarily gets decoded to the original file inside the workflow process.

Since you can’t access the file directly anyway, I think this compromise is acceptable to get a much more secure way to sign your app bundles.

Of course, the explained approach isn’t exclusively possible with GitHub Workflows but with any other CI pipeline that allows storing secrets.

If you came that far, I hope you learned something and hopefully can improve your CI/CD process.

--

--

Follow me on my journey as a professional mobile and fullstack developer