Almost all iOS apps need private values, such as API keys, secrets and passwords. Some of these may need to be used in the source code, to setup third-party SDKs or backend APIs. Some secrets may be needed during the build process or to use developer tools, such as communicating with an Apple Developer account.
Simple approaches for including secrets into an app would include: putting values directly into the code, placing them into build scripts or keeping them in the Info.plist
file. Unfortunately, these approaches mean the secrets are committed to source control and are visible to anyone with access to the the code. Furthermore, scripts and plist files added to the app bundle can be extracted from the compiled app.
We are going to look through some more secure ways of managing secrets within our iOS projects, in order to access them from code. It is worth noting that it is practically impossible to keep anything within an app 100% secret, as apps are "out in the wild" on users' devices. A good approach is to enforce a suitable level of protection for the situation, for example a banking app should probably have a higher level of security than a to-do list app.
Control the sources
It is important to keep secrets out of source control for both open and closed source projects for various reasons.
- By committing secrets to source control, you are sharing these values with all users who have read-access to the repository, including anyone who gets access in the future. You may have users who need to access the project, but not necessarily use all of the secrets.
- After a user checks out the repository, they would gain a copy of all the secrets stored on their local machine.
- If your source control system is compromised, not only would the source code be obtained, so would all of the passwords and other secrets.
Git allows us to include a .gitignore
file, where we can list files (or expressions) that should be kept out. If these files are required by our app, each developer just creates these files and adds any required values. We could include template versions of these files in Git or a guide for what is needed in documentation, including contact details for who to provide secrets if they are required.
Cocoapods Keys
As most iOS developers are aware, many iOS projects use Cocoapods as a dependency manager. Cocoapods includes a plugin system that allows its processes to be hooked into. One such plugin that can be used to manage your app secrets securely is cocoapods-keys.
Not only does cocoapods-keys keep the secrets out of the project sources, it holds them securely in your system keychain. When pods are installed or updated, the developer is asked for each key that has no value already stored. An Objective-C class is generated that contains obfuscated versions of keys and their values. As this class is built by running Cocoapods, the Pods/CocoaPodsKeys
directory can be added to the .gitignore
.
The plugin has many advantages:
- It asks for a value to be provided for each key, avoiding the need to document the required secrets. This makes it very easy for each developer to get their project environment configured.
- The source file is generated, which allows it to be easily kept out of source control.
- Secrets are scrambled within the generated sources, to protect against the keys being extracted from the app binary.
- It can be used from both Swift and Objective-C sources.
- By reading from the keychain, we could access the secrets within build scripts if we needed to.
- We can share keys between different projects that use cocoapods-keys.
Let's setup our keys
Incorporating cocoapods-keys into our project is very simple and starts with including the plugin into our Podfile
.
plugin 'cocoapods-keys', {
:project => "ChatApp",
:target => "ChatApp",
:keys => [
"ChatAPIClientSecretProd",
"ChatAPIClientSecretTest",
"AnalyticsWriteKey"
]
}
After we next run pod install
, we will be asked to enter the value for each of our keys listed above. Once all keys are configured, the plugin will generate a source file that can be referenced in our code to read the keys.
import Keys
let keys = ChatAppKeys()
Analytics.setup(with: keys.analyticsWriteKey)
For values like our backend API secret, where we may want it to be different for debug and release, we can include both keys and then read a different one at runtime. There are obviously many different ways of handling this, depending on the exact use case we have. Therefore, we aren't going to discuss all possible options here and will simply look at switching based on whether the app is built in the debug configuration.
var apiClientSecret {
#if DEBUG
return keys.chatApiClientSecretTest
#else
return keys.chatApiClientSecretProd
#endif
}
If you are using Cocoapods in your project, I would recommend looking into cocoapods-keys over other options, due to it being a secure and easy-to-use way to solve the problem.
Doing things ourselves
Many projects don't use Cocoapods or we may not want to use a plugin and so we can also consider implementing a solution ourselves. Secrets will be passed into the build process, where they will be made available as environment variables. We can then read these variables and use them to generate a source file that will give our code access to our secret values.
We can have different values for different environments
Xcode offers xcconfig files that can be linked to a particular build configuration in order to load in settings specified within them. An xcconfig file can be specified for each build configuration, allowing us to have different values for each environment. We may wish to point our app at our production API for release apps, but a test backend for debug apps.
Note that if a particular app doesn't need different values depending on the build configuration, we can use a shell script instead of the xcconfig files and source the file into our final build phase later on.
We start off by creating example versions of our xcconfig files. If we place these files within a directory such as BuildConfig
it will keep them separate from other project files. It is recommended to include these files into the project, so that they appear within Xcode, but we should ensure they aren't added to any targets within the Target Membership area of the File Inspector.
→ debug.example.xccconfig
CHAT_API_CLIENT_SECRET = CHAT_API_CLIENT_SECRET_TEST
ANALYTICS_WRITE_KEY = ANALYTICS_WRITE_KEY
→ release.example.xccconfig
CHAT_API_CLIENT_SECRET = CHAT_API_CLIENT_SECRET_PROD
ANALYTICS_WRITE_KEY = ANALYTICS_WRITE_KEY
By duplicating and renaming the example files we can easily create the real files that will be used by the project. As with the example files, we want these files in the project, but not attached to any targets.
→ debug.xccconfig
CHAT_API_CLIENT_SECRET = 123456789
ANALYTICS_WRITE_KEY = abcdefgh
→ release.xccconfig
CHAT_API_CLIENT_SECRET = 987654321
ANALYTICS_WRITE_KEY = abcdefgh
To avoid the real xcconfig files being added to source control, they should be listed in the .gitignore file. We can use a regular expression in the rule to catch the files for all configurations.
BuildConfig/*.example.xcconfig
The final step, is telling Xcode to use our xcconfig files, which is specified within the project file under Info → Configurations
. Make sure to select an xcconfig for each build configuration and for the target you need.
Generating the source
When Xcode builds your project, the values in the xcconfig files are made available as environment variables. You could simply use these values in your Info.plist
file if you wished with the form $(CHAT_API_CLIENT_SECRET)
. We have already discussed that putting secrets into plist files isn't very secure, but have mentioned it for completeness.
We are going to generate a source file using a tool called Sourcery and then reference this source file in our code to access our secrets. Needless to say, we will need to start by adding Sourcery to our project, for example including a standalone version within our repository or using Cocoapods.
Sourcery uses a template system, where we create a stencil file to show the tool how to generate our code. We will create AppSecrets.stencil
including some syntax to substitute in the secret values when the file is generated from the template.
struct AppSecrets {
static let chatApiClientSecret="{{ argument.chatApiClientSecret }}"
static let analyticsWriteKey="{{ argument.analyticsWriteKey }}"
}
We next need to add a build phase to our project by selecting the project file, then selecting the correct target and going to the Build Phases tab.
Tools/Sourcery/bin/sourcery
--sources Sources
--templates Templates/AppSecrets.stencil
--output Generated
--args chatApiClientSecret=\"$CHAT_API_CLIENT_SECRET\",
analyticsWriteKey=\"$ANALYTICS_WRITE_KEY\"
- The path to the Sourcery executable will depend on how it is installed.
- The sources argument needs to be specified, even though it isn't used in this situation. We can simply point it to our main sources, or any valid directory.
- The templates argument is a path from the root of the project to our template file.
- The output directory is where the generated source file is written. We need to ensure this folder exists, possibly by adding
mkdir -p Generated
at the start of the build phase. - Within args, values are separated by commas in the form:
arg1=one,arg2=two
. It is a good idea to escape as above incase the secret values contain any special characters.
We can use a more complex script instead of manually specifying secrets within the build phase. This can be beneficial if there are more than a couple of secrets or the build phase is hard to maintain.
After adding the build phase, we can build the app as normal and then add the generated AppSecrets.swift
file to the project so that it is compiled and linked to the project target. As with the xcconfig files, we should add AppSecrets.swift
to our .gitignore file to keep it out of Git.
Using secrets within our code is as simple as referencing our generated struct.
Analytics.setup(with: AppSecrets.analyticsWriteKey)
Wrap up
The goal we wanted to achieve was being able to reference secret values within our source code, without these values themselves needing to be kept in source control. The two solutions we have looked at are quite different, but both achieve the same goal.
By using cocoapods-keys, we can avoid the manual setup and also avoid the values being stored in plain-text anywhere in the project. However, our solution using Sourcery can be used without Cocoapods and will still require very little maintenance. There will definitely be even more solutions available online for more use cases, it will come down to using the most appropriate solution for the situation.
I hope the article was useful. If you have any feedback or questions please feel free to reach out.
Thanks for reading!
WRITTEN BY
Andrew Lord
A software developer and tech leader from the UK. Writing articles that focus on all aspects of Android and iOS development using Kotlin and Swift.
Want to read more?
Here are some other articles you may enjoy.