Fetching API Keys from Property List Files

Fetching API Keys from Property List Files

Many APIs require developers to provide an API key and/or API secret in order to be able to access the API.

This is both to identify the app that is accessing the API, and to limit access to the API for apps that are known to the API.

Both the API key and the secret (if you have one) should be treated as a ... secret: anyone who knows these can access and use the API, impersonating as your app. This results in all sorts of security concerns: depending on the type of API, an attacker might be able to access your application's data, compromise your users' data, and access information that is protected by the terms of service established between you and the service provider. They might also thrash the API, causing a large bill for you at the end of the month.

All of these are good reasons to make sure to keep your API keys and secrets safe and secure.

In this article, we're going to look at how to make sure your API keys and secrets don't accidentally leak to your version control system. The easiest, but also most dangerous way to store your API key is to define a constant in your app's source code. You might have seen code like this:

  let apiKey = "s33cr3t!k3y"

  func search(queryString: String) {
    TMDB.set(apiKey: apiKey)

    TMDB.Search.movies(query: queryString) { pagedListResult in
      // handle result
    }
  }

When committing code like this to your version control system, anyone who has access to your repository can go ahead and use the API key in their app to access the API. This might not be a big deal if your code lives in an in-house repository with tight access control, but it is a huge security risk for open source projects.

The easiest way to work around this is to externalise your API key into a configuration file that you don't check in to your repository. You can then keep the API key in a secure location (such as a password manager), and hand it out to developers on a need-to-know basis. For example, you might want to use the API key for accessing the production endpoints only on your CI/CD server, and provide developers with API keys to the development endpoints (which might have stricter rate limiting and tighter cost caps).

In iOS, we traditionally use Plist (short for property list) files to store and manage configuration data. Plist files essentially are XML files with benefits: for example, Xcode provides a graphical editor to make editing Plist files more pleasant, and there's an easy-to-use API for reading Plist files.

Let's have a look at how this helps us to keep the API key in the above code snippet safe and make our code more secure.

Externalising the API Key

The steps to properly externalise an API key are:

  1. Add a Plist file to your project
  2. Define the keys/values in the Plist file
  3. Read the API key from the Plist file
  4. Use the API key
  5. Handle error scenarios

To add a new Plist file to your project, make sure to select your project's root folder in the project navigator, and then choose New File ... from the root folder's context menu. Type property into the filter field, and then choose the Property File type from the Resources section:

Adding a new Property List file

Choosing a good name for your property list file is essential, as we will later write a build script to automate handling of this file. I recommend the following naming scheme: <name of the API>-Info.plist. In our example, we access TMDB (The Movie Database), so we'll name the file TMDB-Info.plist.

Next, go ahead and add a new key/value pair to the newly created file. I chose API_KEY as the name for the key. Most API keys are strings, so choose String as the data type, and then insert the key itself in the value field. Here is how it should look like:

Editing the Plist file

Let's now write some code to read the API key from the Plist file and use it when accessing the API.

Reading a Plist file is just a one-liner: let plist = NSDictionary(contentsOfFile: filePath) - this will give you a dictionary, which makes reading the API key as simple as let value = plist?.object(forKey: "API_KEY") as? String. To make reading the API key and using it in our code as easy as possible, we will wrap it in a computed property. This will also give us the opportunity to perform some error handling.

private var apiKey: String {
  get {
    // 1
    guard let filePath = Bundle.main.path(forResource: "TMDB-Info", ofType: "plist") else {
      fatalError("Couldn't find file 'TMDB-Info.plist'.")
    }
    // 2
    let plist = NSDictionary(contentsOfFile: filePath)
    guard let value = plist?.object(forKey: "API_KEY") as? String else {
      fatalError("Couldn't find key 'API_KEY' in 'TMDB-Info.plist'.")
    }
    return value
  }
}

First, we use Bundle.main.path(forResource:ofType) to obtain the path to our Plist file. In case the file doesn't exist, we crash fatally (not providing the file is a programming error after all, and there is no way the app can recover), issuing a helpful error message.

Next, we try to load the Plist file into a dictionary (2), and then read the value for the API key from the dictionary. In case the value doesn't exist, or is of the wrong type, we issue another error message (and crash the app).

And finally, we return the value. Since we named the computed property apiKey - just like the constant - we can now delete the constant from our code, and our app will continue working as before.

With that, we've got the basic infrastructure in place. Before you can relax with a hot or cold beverage of your choice, we'll have to talk about source control.

Source Control

If you've accidentally checked in an API key to a public repository, there are two ways to fix the situation:

  1. Rotating the API key (i.e. remove the old one from your code, get a new one, and then make sure to not check the new one in).
  2. Rewriting history (if you're using git). Before doing this, I urge you to watch Scott Hanselman's video Git Push --Force will destroy the timeline and kill us all from his brilliant series Computer Stuff They Didn't Teach You, and then choose option 1.

Even better than having to revoke an API key is to prevent accidentally checking it in. To this end, add any configuration files that contain API keys to your .gitignore file. This also means each developer can have their own copy of this file, with their specific values. For example, you might be using an API key and URL for the production endpoint, whereas your colleague who is working on a new feature is using the development key and URL for the development endpoint.

To make it easier for new team members (or when you need to check out your code on a different machine), provide a sample configuration file. I suggest naming it <name of the Plist file>-Sample.plist. Then, add all required keys to this file and provide placeholder values. By prefixing the placeholder values with an underscore (or any other character that usually doesn't occur in your configuration), you can then extend your error handling code:

private var apiKey: String {
  get {
    // 1
    guard let filePath = Bundle.main.path(forResource: "TMDB-Info", ofType: "plist") else {
      fatalError("Couldn't find file 'TMDB-Info.plist'.")
    }
    // 2
    let plist = NSDictionary(contentsOfFile: filePath)
    guard let value = plist?.object(forKey: "API_KEY") as? String else {
      fatalError("Couldn't find key 'API_KEY' in 'TMDB-Info.plist'.")
    }
    // 3
    if (value.starts(with: "_")) {
      fatalError("Register for a TMDB developer account and get an API key at https://developers.themoviedb.org/3/getting-started/introduction.")
    }
    return value
  }

The code in (3) checks if the value read from the Plist file starts with an underscore (which denotes a placeholder), and issues an error message that tells the developer how to obtain an API key.

When adding this sample Plist file to your project, make sure you do not include it in any build target - we don't want to include this file in our application binary, it's only relevant when checking out the project from your repository.

Bonus: Provisioning the Plist File After Checking Out

Speaking of your repository - wouldn't it be nice if the config file (in our case, TMDB-Info.plist) was created automatically after checking out the project from source control?

We can use Xcode build phases to achieve this. Here is how:

  • Select your target in the Xcode project editor
  • Navigate to Build Phases
  • Add a new Run Script build phase, making sure it is the first build phase in your target. Name this new phase Copy sample API key.

Adding a new build phase

  • Paste the following code
CONFIG_FILE_BASE_NAME="TMDB-Info"

CONFIG_FILE_NAME=${CONFIG_FILE_BASE_NAME}.plist
SAMPLE_CONFIG_FILE_NAME=${CONFIG_FILE_BASE_NAME}-Sample.plist

CONFIG_FILE_PATH=$SRCROOT/$PRODUCT_NAME/$CONFIG_FILE_NAME
SAMPLE_CONFIG_FILE_PATH=$SRCROOT/$PRODUCT_NAME/$SAMPLE_CONFIG_FILE_NAME

if [ -f "$CONFIG_FILE_PATH" ]; then
  echo "$CONFIG_FILE_PATH exists."
else
  echo "$CONFIG_FILE_PATH does not exist, copying sample"
  cp -v "${SAMPLE_CONFIG_FILE_PATH}" "${CONFIG_FILE_PATH}"
fi

Finally, change the variable CONFIG_FILE_BASE_NAME to match your config file name. The code in this build phase will create a new API key config file based on the sample file you've checked in to the repository.

This means, whenever you (or one of your team members) checks out your project and builds it, the API key config file will be created. When you run the app, the error handling code in the computed property will detect that this is a pristine copy and tell you to replace the placeholder value with a proper API key.

Conclusion

Keeping your API keys and secrets secure is just one piece of the puzzle of implementing and operating your app safely and securely, but it is an important one. I hope this article helped you to take this important step on the path to a more secure app, while making this a pleasant experience for you and your team.


The header image is based on Security by Komkrit Noenpoempisut from the Noun Project