Episode #542

Managing Secrets with Arkana

28 minutes
Published on December 6, 2022

This video is only available to subscribers. Get access to this video and 572 others.

Storing secrets in plain-text in your source code is not a great idea. It can lead to leaking this information to the wrong people (or to a wider audience than is necessary), it makes rotating these keys difficult, and it makes it trivial for people to see these secrets in your compiled application binary. In this episode we will utilize an obfuscation approach that may just be good enough for many use cases.

Links

In the Async Await Series, I demoed this application, which fetches images from the Unsplash API.

Doing so requires a pair of secret keys that should not be shared. To accomplish this for my demo, I utilized 2 features of Xcode:

  1. Creating a new non-shared Scheme
  2. Adding environment variables to the scheme that are read at runtime by ProcessInfo.processInfo.environment

This solved the problem of not sharing the keys in the committed source code, however it only works in the simulator (for iOS projects) and from Xcode for macOS projects. If I tried to run this outside of Xcode, these values would not be present and the app would not work.

This brings me to a point that I should mention up front:

Should you store secrets in your application’s source code?

There are 2 main reasons why you may not want to do this.

  • You don’t want to expose production secrets to anyone with access to a git repo (this means the entire world for open source projects)
  • You don’t want prying users who install your app to have access to these keys

Both of these are good ideas, however it ends up being fraught, as there is literally no guaranteed way of ensuring that keys shipped with your application don’t end up being readable by the motivated or extremely curious end-user.

I’m mainly concerned with the good practice of not storing plain-text keys in your project’s source code, allowing you to isolate and protect certain keys (such as production credentials) and making it obfuscated enough that it isn’t super easy for a curious individual to dig them up in your application binary.

You don’t have to outrun the bear, you just have to outrun the person next to you.

For more reading on this topic, I’ll point you to this excellent article on NSHipster.

So now that we’ve side-stepped the giant elephant, let’s proceed with an obfuscation approach that may be good enough for most needs.

Enter Arkana

Arkana is a gem that can read your .env file and generate an obfuscated Swift Package that your app depends on. (.env files are a common practice in the industry for storing machine-local values that are not checked in to source control).

You may already be using a Gemfile to manage ruby dependencies like cocoapods or fastlane, but if not, you can initialize one:

$ bundle init

Then open the created Gemfile in your editor and add:

gem "arkana"

Then run bundle install to install it.

This gives you a new command you can use to generate the secrets package. Before you get started, I find it a good idea to generate binstubs:

$ bundle binstubs arkana

This gives you bin/arkana which is a safe way to run the gem's executable without having to prefix it with bundle exec.

Before continuing, we must create a configuration file for Arkana. This will tell Arkana which env keys we care about, what the package should be called, and which environment variants we want to support.

We also need to define our .env file and put our secret values in there.

$ vim .env

UnsplashAccessKey=unsplshxs
UnsplashApiSecret=unseekrit

Make sure to ignore this file in your .gitignore so you don't accidentally check it in. Git commit history is a super useful tool, but if you ever check in secrets, it's a giant pain to remove them.

$ echo ".env" >> .gitignore

Now we can configure Arkana. Create a new file called .arkana.yml and add this:

import_name: ArkanaKeys
namespace: Keys
package_manager: spm

environments:
  - Debug
  - Release

global_secrets: 
  - UnsplashAccessKey
  - UnsplashSecretKey

environment_secrets:

We currently only have one set of Unsplash keys, so we put those in the global_secrets section. These need to match the names you put in the .env file.

We’ll come back to environment_secrets later.

Generating secrets

This is the easy part. Run:

$ bin/arkana

And take a look at what was generated.

We can now drag the ArkanaKeys folder into our Xcode project and add these packages as dependencies.

Next let’s update UnsplashApiClient to read the keys from the ArkanaKeys package:

    convenience init() {
        let accessKey = ArkanaKeys.Keys.Global().unsplashAccessKey
        let secretKey = ArkanaKeys.Keys.Global().unsplashSecretKey
        self.init(
            accessKey: accessKey,
            secretKey: secretKey
        )
    }

If we run the app now it will fail to load because we added bogus keys to our .env file! But if we (privately) update the .env file with real keys, we can re-run arkana and run the project again to see it working. Nice!

Environment Secrets

Let’s say we wanted to use this system to have a separate set of keys for an analytics provider, depending on the environments we specified in our config. We created Debug and Release , but these could be whatever you want (like Staging and Production, for instance).

Let’s add a couple environment specific keys to our .env:

AnalyticsKeyDebug=debug123
AnalyticsKeyRelease=release123

The suffixes have to match the names of your environments.

We also need to tell Arkana about this new value. Open the .arkana.yml config file:

...

environment_secrets:
  - AnalyticsKey

Note that we use the raw name of the key here. It will be suffixed with the environment name automatically.

Let’s re-run Arkana and see how to use these.

This episode uses Xcode 14.1, Swift 5.7.