Mateusz Siatrak
Posted on February 16, 2024
In a world where mobile apps frequently interact with web services, securing API keys has become a paramount concern for developers. As someone who has experienced the repercussions of stolen keys firsthand, I've learned that the traditional method of storing API keys in .xcconfig
files and injecting them into the Info.plist
at build time is not foolproof. While the method keeps API keys out of the source code, it does little to prevent bad actors from reading them in plain text from the plist
.
In this article, we will explore different methods to secure API keys in SwiftUI apps, examining their effectiveness both in protecting keys from appearing in the source control and in making it harder for attackers to extract keys.
Methods of Securing API Keys
Method 1: Storing Keys in .plist
Files
Traditionally, developers would inject API keys into Info.plist
using .xcconfig
files which are not checked into source control.
- Source Control: ✅ (API keys are not checked into source)
- Extraction Difficulty: ❌ (It's easy for bad actors to extract plain text keys)
Method 2: Server Retrieval and Secure Storage
Retrieve the API Key from a server at runtime and securely store it in the Keychain.
- Source Control: ✅ (API keys are not checked into source)
- Extraction Difficulty: ✅ (More difficult for attackers with encrypted transmission and Keychain storage)
Method 3: Obfuscating Keys
Obfuscate the API key by splitting, encoding, or adding dummy code to confuse decompilers.
- Source Control: ✅ (API keys are not checked into source if obfuscation is done at build time)
- Extraction Difficulty: ⚠️ (Increases difficulty but not foolproof)
Method 4: GYB Technique with Custom Salt
Utilize Swift GYB (Generate Your Boilerplate) to obfuscate keys with every build using a custom salt.
- Source Control: ✅ (API keys are not checked into source and are obfuscated in source code)
- Extraction Difficulty: ✅ (Higher difficulty, as keys change with every build)
Method 5: CloudKit Key Management
Use CloudKit to distribute keys to all app users and store them in the Keychain.
- Source Control: ✅ (API keys are not included in source code)
- Extraction Difficulty: ✅ (Stored securely in the Keychain with CloudKit's security)
More Detailed Pros and Cons
Here's a closer look at the pros and cons of each approach:
.plist
Injection via .xcconfig
- Pros: Easy to implement; keeps keys out of source control.
- Cons: Does not protect against binary inspection; plist files can easily be read.
Server Retrieval and Keychain Storage
- Pros: Secure storage; keys are not bundled with the app; Keychain provides encryption.
- Cons: Dependent on network availability; initial setup may be complex.
Obfuscating Keys
- Pros: Provides a basic level of security; relatively simple to implement.
- Cons: Can be reversed by skilled attackers; adds maintenance overhead.
GYB with Custom Salt
- Pros: Creates unique obfuscations with each build; more advanced protection technique.
- Cons: Requires understanding of GYB tooling; may be complex for new developers.
CloudKit Key Management
- Pros: Removes keys from code; leverages Apple’s security for transfer and storage.
- Cons: Dependent on CloudKit uptime and user iCloud accounts; initial learning curve.
Example Using CloudKit for Key Management
One effective way to manage API keys is through CloudKit by storing the key securely on Apple's servers and distributing it to authenticated users of your app. Here’s a basic implementation of how you can store and retrieve an encrypted API key using CloudKit and save it in the Keychain:
import CloudKit
import KeychainAccess
class APIKeyManager {
private let container = CKContainer.default()
private let keychain = Keychain(service: "com.yourapp.service")
func retrieveAPIKey() {
let publicDB = container.publicCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "APIKeys", predicate: predicate)
publicDB.perform(query, inZoneWith: nil) { [weak self] (records, error) in
if let record = records?.first, let encryptedKey = record["encryptedKey"] as? String {
let apiKey = self?.decryptKey(encryptedApiKey: encryptedKey)
self?.keychain["apiKey"] = apiKey
}
}
}
private func decryptKey(encryptedApiKey: String) -> String {
// Implement your decryption logic here
}
}
GYB with Obfuscation and Custom Salts
The GYB (Generate Your Boilerplate) tool is a powerful Python script that can preprocess Swift files to generate Swift code. It's particularly useful when you need to create repetitive code or, as in this case, when you want to obfuscate values like API keys before compile time. We'll expand upon how to use GYB to create an obfuscated API key, how to integrate it into your build process and decode the key in the SwiftUI app.
Preparing the GYB Template
You'll first want to create a .gyb
file that contains Python code for obfuscation logic. Create a file named APIKey.swift.gyb
and save it in your Xcode project directory, preferably in a folder that also contains an Xcode group for clarity.
# APIKey.swift.gyb
%{
import os
# Generate a random salt
salt = os.urandom(16)
encoded_key = ''.join([f'\\x{b:02x}' for b in 'Your-API-Key'.encode('utf-8')])
}%
// Your Swift file contents
enum APIKeys {
static let obfuscatedKey: String = "${encoded_key}"
static let salt: [UInt8] = [% for b in salt %]${hex(b)},[% end %]
}
Running GYB to Generate Swift Code
GYB needs to be executed as part of the build process before compiling your Swift code.
- Download
gyb.py
from the Swift repository or install it using Homebrew:
brew install gyb
- Add a Run Script build phase in Xcode to your target:
cd $SRCROOT
gyb --line-directive '' -o APIKeys.swift APIKey.swift.gyb
This script navigates to the source root of your Xcode project, runs gyb
, and outputs an APIKeys.swift
file. The --line-directive
option removes line directives from the output, making the resultant Swift file cleaner.
Decoding the Key in SwiftUI
Now your app needs to decode this obfuscated key at runtime before using it. The decoding process—in this case, simply reversing the obfuscation—would be applied in the app's source-code when the key is required.
// APIKeys.swift (Generated by GYB)
enum APIKeys {
static let obfuscatedKey: String = "\x68\x65\x6c\x6c\x6f"
static let salt: [UInt8] = [0xae, 0xbf, 0x20, ...] // Randomly generated each build
}
// Your Swift file where you're decoding the key
class APIManager {
static let shared = APIManager()
private(set) var apiKey: String?
init() {
decodeAPIKey()
}
private func decodeAPIKey() {
let obfuscatedBytes = [UInt8](APIKeys.obfuscatedKey.utf8)
let decodedBytes = zip(obfuscatedBytes, APIKeys.salt).map { $0 ^ $1 }
apiKey = String(bytes: decodedBytes, encoding: .utf8)
}
// Rest of your API Manager code
}
// Access the key in your SwiftUI Views or elsewhere
let apiManager = APIManager.shared
print(apiManager.apiKey ?? "No API Key found")
In this example, we've created an APIManager
class that is responsible for decoding and providing the API key. It zips the obfuscated key bytes with the salt bytes and uses bitwise XOR to retrieve the original key's bytes, then converts it back to a string.
Final Thoughts
While there is no perfect method to secure API keys absolutely, combining several of these methods can strengthen your app’s defense against key theft.
- Obfuscation Techniques: At a minimum, implement key obfuscation to frustrate casual inspection of the binary.
- CloudKit Distribution: For a more secure approach, distribute the key to users via CloudKit, removing the key from the source code entirely and relying on secure communication between the device and Apple’s servers.
Through these methods, we enhance our resilience against the theft and misuse of sensitive API keys in our SwiftUI applications. Remember to keep monitoring for any unusual activity in your API usage, and combine these strategies with API usage limits to give you more control and minimize potential damage.
Posted on February 16, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.