Securing Secrets in iOS apps using Xcode build scripts

Adarsh A
5 min readMar 27, 2023

--

Inject sensitive information such as API keys in iOS applications during build time using build scripts in Xcode.

We all might have come across the need to store sensitive information in iOS apps. There are a variety of ways to store secrets in iOS apps. We can directly store secrets as hardcoded strings in swift files, but this is highly risky as it exposes your secrets in your repo. To avoid this, we can use a combination of xcconfig and .plist files to store secrets. Though this prevents your secrets from being available in the repo, it is very easy for anyone to extract .plist files from your .ipa file.

Thus, it is important that we inject secrets directly into the application during build time so that they are not stored in your VCS or in your app bundle. We can use several third-party dependencies like cocoapod-keys for this purpose.

The approach I came up with is inspired by this article in which the author uses build scripts to inject secrets during build time using Sourcery. However, we would be achieving this without any third-party dependencies.

Without further ado, let’s see how we can inject secrets during build time using Xcode build scripts and python.

Step 1: Create a python script

Create a new python file generate_secrets.py inside the project directory with the following code.

import os


def generate_secret_manager(filename: str, directory_path: str):
api_key_1 = os.environ["API_KEY_1"]
api_key_2 = os.environ["API_KEY_2"]

filepath = os.path.join(directory_path, filename)

with open(filepath, 'w') as f:
f.write(
f'''// AUTOGENERATED USING PYTHON. DO NOT EDIT OR COMMIT THIS FILE.
//
// {filename}
//

import Foundation

struct SampleAppSecretManager {{
static let shared: Self = .init()
let apiKey1: String
let apiKey2: String

private init() {{
self.apiKey1 = "{api_key_1}"
self.apiKey2 = "{api_key_2}"
}}
}}''')


if __name__ == '__main__':

directory_path = "Generated"
filename = "SampleAppSecretManager.generated.swift"

generate_secret_manager(filename, directory_path)

Let’s analyse what the above code does

  • The method generate_secret_manager takes in a filename and directory_path and generates a swift file with the given filename inside the given directory.
  • In this case, the filename is SampleAppSecretManager.generated.swift and the file is to be created in the directory named Generated.
  • The generated swift file consists of a struct SampleAppSecretManager with two API keys stored as instance variables.
  • The values of the API keys are fetched from environment variables using the os library.

Step 2: Create a run phase script in Xcode

Open Xcode, go to Build Phases and add a new Run Script Phase. Drag your build phase and place it above Compile Sources phase. In the script editor, type in python3 generate_secrets.py to execute our python script as shown below.

Creating build script

Don’t forget to add the generated file in the Output Files section as shown above. If you had changed the filename or the directory_path in the python script, do change it here as required.

Step 3: Load environment variables in Xcode

At this point, if you build your application, you will see an error as shown below.

This error is thrown by the python code because it could not find the environment variable API_KEY_1. Even if you set the environment variable in your bash profile, you will still see this error in Xcode because Xcode will not have access to those environment variables.

Thus, we will have to load the environment variables in Xcode before executing our python script. Let us create a file env.sh that will contain our environment variables. This file must be added to .gitignore and each developer should create this file locally before building the application.

export API_KEY_1=d030f1f2-a06c-4fcb-aff2-f9bbce105046
export API_KEY_2=0633edda-baf5-4b25-babb-51c40ed73860

Please make sure that the environment variables are named as same as the names used in the python script.

Now let’s load the environment variables using this script in Xcode. Replace the existing code in your build script with the following code.

if [ -f ./env.sh ]
then
source ./env.sh
fi
python3 generate_secrets.py

For CI/CD, you just need to set the required environment variables in your CI environment as builds triggered through the command line (using xcodebuild or fastlane) will not require env.sh.

Step 4: Add the generated file in Xcode

Before we try to build the application, let us create the directory in which the file is to be generated. In this case, the directory is named Generated. So, let’s create a new Group in Xcode with the same name.

Now, let’s build the application. You will notice that the build has succeeded but still, the generated file is not visible in Xcode. We can add the file by simply right-clicking on the Generated folder and select Add files option. Select the generated file and add the necessary targets and Voila! We can now use this autogenerated struct anywhere in our application.

Autogenerated file using python

An example of how we can access the injected secrets using the SampleAppSecretManager is shown below.

Using SampleAppSecretManager to access the injected secrets

Step 5: Update the .gitignore file

Add the following in your .gitignore

*.generated.swift
env.sh

This will ensure that the autogenerated file and the env.sh files are not pushed to your remote repository.

Conclusion

The above mentioned is a simple way to inject secrets into swift code without any external dependency during build time. The example codes used above are available here.

Like many others, I would also like to emphasize the fact that storing secrets in client-side applications is unsafe. There is always a chance for anyone to reverse engineer your application and retrieve them. However, you can use this approach along with code obfuscation to make your keys more secure.

References

Thanks for reading !! Please drop your comments or suggestions if any.

--

--