Categories
Foundation iOS Swift Xcode

Using on-demand resources for securely storing API keys in iOS apps

Many apps use API keys when authenticating network requests. Although there are better ways of authenticating requests like OAuth with PKCE, but it might not always be possible. One thing what we must keep in mind is that it is fairly simple to extract strings from IPA files and therefore, if we store API keys in code, someone else can get access to these. This is of course a security issue. One of the approaches how to avoid it is using Apple’s on-demand resources with prefetching enabled. This means that as soon as we install the app, iOS will download additional resources separately and these resources can contain our API keys. This separation enables not putting any API keys into the IPA file. No one can go and inspect the IPA file any more and try to extract string constants. Let’s see how to set it up.

First step is that we create a prefetching enabled tag. Apple uses tags to identify on-demand resources. Open your Xcode project settings, app target and then “Resource Tags” tab. Let’s add a new resource tag named “APIKeys”.

The next step is to attach a resource to the tag. We’ll use a JSON file for our API keys, so go ahead and add a new JSON file for API keys. We’ll just create a key-value pairs in that file and assign a resource tag to the file, which can be found in the utilities area > file inspector tab. In our example, the tag has the same name as the file “APIKeys”.

So far we have created a resource tag and assigned a tag to the JSON file. The default behaviour is that the tag is treated as on-demand resource and only downloaded when it is required by the app. With API keys, it makes sense to download it along with the app binary when the user installs that app. Then on the first launch we can immediately store the API key in keychain for future usage. Prefetching can be enabled in the “Resource Tags” tab. Tap on the “Prefetched” button and drag the “APIKeys” tag under “Initial Install Tags”.

An important thing to note is that even though we have set that tag to be part of initial install tags there is still the possibility that the tag has been purged. This happens when the user installs the app and then waits a long time. In that case, the system needs to go and download it again when we want to access it. Therefore, the code accessing the tag could still take some time. Let’s see a simple function which accesses the JSON file through NSBundleResourceRequest API and makes the API keys available for the app.

enum Constants {
static func loadAPIKeys() async throws {
let request = NSBundleResourceRequest(tags: ["APIKeys"])
try await request.beginAccessingResources()
let url = Bundle.main.url(forResource: "APIKeys", withExtension: "json")!
let data = try Data(contentsOf: url)
// TODO: Store in keychain and skip NSBundleResourceRequest on next launches
APIKeys.storage = try JSONDecoder().decode([String: String].self, from: data)
request.endAccessingResources()
}
enum APIKeys {
static fileprivate(set) var storage = [String: String]()
static var mySecretAPIKey: String { storage["MyServiceX"] ?? "" }
static var mySecretAPIKey2: String { storage["MyServiceY"] ?? "" }
}
}
view raw APIKeys.swift hosted with ❤ by GitHub

With a setup like this, we need to make sure that the loadAPIkeys function is called before we access mySecretAPIKey and mySecretAPIKey2. If we have a centralized place for network requests, let’s say some networking module which wraps URLSession then that could be an excellent place where to run this async code. Another way could be delaying showing the main UI before the function completes. Personally, I would go for the former and integrate it into the networking stack.

OnDemandAPIKeyExample (GitHub, Xcode 15.0.1)

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

One reply on “Using on-demand resources for securely storing API keys in iOS apps”