Secrets Management on iOS

A common question in iOS development remains: How do I securely store secrets on a client device?

The concern behind this question is the assumption that, without adequate precautions, secrets will inevitably be exposed. Whether it’s through version control systems like GitHub, analysis tools inspecting .ipa files from the App Store, or other methods, sensitive information is often vulnerable.

While security of credentials may not always be a top priority, growing academic research has highlighted the importance of addressing these threats. For example, researchers at North Carolina State University have found thousands of API keys, multi-factor secrets, and other sensitive credentials leaking daily on GitHub (Meli et al., 2019). Another study from 2018 found that 68 out of 100 popular iOS apps misused SDK credentials (Wen et al., 2018).

This article delves into increasingly advanced strategies for securing your secrets.

Basic Approach

Hard-Coding Secrets in Source Code

Suppose you have an app that communicates with a web service, and it requires an API key to authenticate outgoing requests. This could be handled by a third-party framework or through custom setup in your AppDelegate. Regardless of the method, you still need to securely store the API key.

One common but risky practice is embedding the secret directly in your code:

struct Secrets {
    static let apiKey = "6a0f0731d84afa4082031e3a72354991"
}

Although compiling the app transforms your code into a binary, it doesn’t remove the secret. Using reverse engineering tools like Radare2, anyone can extract the key from the compiled executable:

$ r2 ~/Developer/Xcode/Archives/.../MyApp.app/MyApp
[0x1000051fc]> iz
[Strings]
Num Paddr      Vaddr      Len Size Section  Type  String
000 0x00005fa0 0x100005fa0  30  31 (3.__TEXT.__cstring) ascii _TtC9MyApp14ViewController
001 0x00005fc7 0x100005fc7  13  14 (3.__TEXT.__cstring) ascii @32@0:8@16@24
002 0x00005fe0 0x100005fe0  36  37 (3.__TEXT.__cstring) ascii 6a0f0731d84afa4082031e3a72354991

The takeaway: Avoid committing secrets in your source code.

As Benjamin Franklin famously said, If you would keep your secret from an enemy, tell it not to a friend.

More Secure Approach

Storing Secrets in Xcode Configuration and Info.plist

A better solution is to use .xcconfig files to separate configuration from the code, which keeps secrets out of version control:

// Development.xcconfig
API_KEY = 6a0f0731d84afa4082031e3a72354991

// Production.xcconfig
API_KEY = d9b3c5d63229688e4ddbeff6e1a04a49

By doing this and ensuring the files are excluded from source control, you reduce the chance of exposing credentials in your code. At first glance, this appears to address the concern of static analysis tools.

However, secrets stored in Info.plist are still bundled with the app’s payload and can be extracted easily:

$ plutil -p .../MyApp.app/Info.plist
{
  "API_KEY" => "6a0f0731d84afa4082031e3a72354991"
}

This method provides better separation, but it’s not a complete solution. Secrets stored in Info.plist are still vulnerable to inspection by anyone analyzing the app’s payload.

Advanced Approach

Obfuscating Secrets with Code Generation

An even more advanced approach involves using code generation tools like GYB (Generate Your Boilerplate). This can help obfuscate secrets before including them in your source code. For example, you can encode the secrets using a simple XOR cipher:

import os

# Function to obfuscate the secret
def obfuscate(secret, salt):
    return [ord(secret[i]) ^ salt[i % len(salt)] for i in range(len(secret))]

# Generate obfuscated secrets
salt = os.urandom(64)
api_key = os.getenv("API_KEY")  # Retrieve from environment
encoded_api_key = obfuscate(api_key, salt)

# Now you can embed `encoded_api_key` into your source code

This example encrypts the secret using a salt and then stores the encoded value. In Swift, you would decode it on the client side:

enum Secrets {
    private static let salt: [UInt8] = [0xa2, 0x00, 0xcf, 0x45, ...]

    static var apiKey: String {
        let encoded: [UInt8] = [0x94, 0x61, 0xff, 0x88, ...]
        return decode(encoded, cipher: salt)
    }

    static func decode(_ encoded: [UInt8], cipher: [UInt8]) -> String {
        String(decoding: encoded.enumerated().map { (offset, element) in
            element ^ cipher[offset % cipher.count]
        }, as: UTF8.self)
    }
}

In this case, the secret is encoded and embedded as an array of bytes, making it more difficult for reverse engineering tools to extract it. While this is not an impenetrable solution, it adds an extra layer of protection.

Ultimate Best Practice

Don’t Store Secrets On-Device

The most secure approach is to avoid storing secrets directly on the device. No matter how much obfuscation you apply, an attacker with enough time and resources will likely extract the secret.

The ideal way to store secrets is on the server, where they can be securely fetched when needed. If you must use secrets on the client, Apple offers services like On-Demand Resources and CloudKit to securely manage them.

Once a secret is stored in the Secure Enclave, it can be safely used for authentication or other purposes. However, ensuring that secrets remain secure throughout their lifecycle requires ongoing vigilance.

As always, security is not a one-time fix but a continuous process.

Final Conclusion

Client-Side Secrecy is Unavoidable but Manageable

While no approach guarantees absolute security for secrets on the client, maintaining security on the server is always the best practice. Instead of trying to “solve” client-side secret management, focus on avoiding it whenever possible.

Any SDK that relies on client-side secrets is inherently insecure, so consider moving such logic to the server. If you cannot, assess the risks of a potential leak and explore ways to obfuscate secrets to minimize the chances of exposure.

Answering the original question: “How do I securely store secrets on the client?”
The safest answer is: Don’t — but if you must, obfuscation can reduce risks.

 

Similar Posts