Secret Management on iOS

One of the great unsolved questions in iOS development is, “How do I store secrets securely on the client?”

Baked into this question is the assumption that, without taking proper measures, those secrets will be leaked in one way or another — either in source revision control (GitHub and the like), from analysis tools running on .ipa files distributed through the App Store, or some other way.

Although credential security is often a second-tier concern at best, there’s a large and growing body of academic work that compels us, as developers, to take these threats seriously.

For instance, researchers at North Carolina State University found that thousands of API keys, multi-factor secrets, and other sensitive credentials are leaked every day on GitHub. (Meli et al., 2019) Another paper published in 2018 found SDK credential misuse in 68 out of a sample of 100 popular iOS apps. (Wen et al., 2018)

This week on NSHipster, we’re shamelessly invoking a meme from 2017 to disclose progressively more sophisticated countermeasures for keeping your secrets safe.

If your app is among the myriad others out there with an embedded Twitter access token, AWS Key ID, or any other manner of credential, read on and expand your mind.


🧠 Normal Brain Hard-Code Secrets in Source Code

Imagine an app that communicates with a web application that requires an API key (a secret) to be attached to outbound requests for purposes of authentication. This integration could be managed entirely by a third-party framework, or it might be brokered with a URLSession you set up in AppDelegate. In either case, there’s a question of how and where to store the required API key.

Let’s consider the consequences of embedding the secret in code as a string literal:

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

When you archive your app for distribution, all traces of this code appear to vanish. The act of compilation transforms human-readable text into machine-executable binary, leaving no trace of the secret… right?

Not quite.

Using a reverse-engineering tool like Radare2, that secret is plainly visible from inside the compiled executable. You can prove this to yourself with your own project by selecting “Generic iOS Device” in the active scheme in your own project, creating an archive (Product > Archive), and inspecting the resulting .xcarchive bundle:

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

While a typical user downloading your app from the App Store would never even think to try opening the .ipa on their device, there are plenty of individuals and organizations whomst’d’ve carved out a niche in the mobile app industry analyzing app payloads for analytics and security purposes. And even if the majority of these firms are above-board, there’s likely to be a few bots out there combing through apps just to pick out hard-coded credentials.

Much of that is just conjecture, but here’s what we know for sure:

If you hard-code secrets in source code, they live forever in source control — and forever’s a long time when it comes to keeping a secret. All it takes is one misconfiguration to your repo permissions or a data breach with your service provider for everything to be out in the open. And as one human working with many other humans, chances are good that someone, at some point, will eventually mess up.

Not to put too fine a point on it, but:

To quote Benjamin Franklin:

If you would keep your secret from an enemy, tell it not to a friend.

🧠 Big Brain Store Secrets in Xcode Configuration and Info.plist

In a previous article, we described how you could use .xcconfig files to externalize configuration from code according to 12-Factor App best practices. When reified through Info.plist, these build settings can serve as makeshift .env files:

// Development.xcconfig
API_KEY = 6a0f0731d84afa4082031e3a72354991

// Release.xcconfig
API_KEY = d9b3c5d63229688e4ddbeff6e1a04a49

So long as these files are kept out of source control, this approach solves the problem of leaking credentials in code. And at first glance, it would appear to remediate our concern about leaking to static analysis:

If we fire up our l33t hax0r tools and attenuate them to our executable as before (izz to list strings binary, ~6a0f07 to filter using the first few characters of our secret), our search comes up empty:

$ r2 ~/Developer/Xcode/Archives/.xcarchive/Products/Applications/Swordfish.app/Swordfish
[0x100005040]> izz~6a0f
[0x100005040]>

But before you start celebrating, you might want to reacquaint yourself with the rest of the app payload:

$ tree /Swordfish.app
├── Base.lproj
│   ├── LaunchScreen.storyboardc
│   │   ├── 01J-lp-oVM-view-Ze5-6b-2t3.nib
│   │   ├── Info.plist
│   │   └── UIViewController-01J-lp-oVM.nib
│   └── Main.storyboardc
│       ├── BYZ-38-t0r-view-8bC-Xf-vdC.nib
│       ├── Info.plist
│       └── UIViewController-BYZ-38-t0r.nib
├── Info.plist
├── PkgInfo
├── Swordfish
├── _CodeSignature
│   └── CodeResources
└── embedded.mobileprovision

That Info.plist file we used to store our secrets? Here it is, packaged right alongside our executable.

By some measures, this approach might be considered less secure than writing secrets in code, because the secret is accessible directly from the payload, without any fancy tooling.

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


There doesn’t seem to be a way to configure the environment when a user launches your app on their device. And as we saw in the previous section, using Info.plist as a clearinghouse for secrets qua build settings is also a no-go.

But maybe there’s another way we could capture our environment in code…

🧠 Cosmic Brain Obfuscate Secrets Using Code Generation

A while back, we wrote about GYB, a code generation tool used in the Swift standard library. Although that article focuses on eliminating boilerplate code, GYB’s metaprogramming capabilities go far beyond that.

For example, we can use GYB to pull environment variables into generated code:

$ API_KEY=6a0f0731d84afa4082031e3a72354991 \
gyb --line-directive '' <<"EOF"
%{ import os }%
let apiKey = "${os.environ.get('API_KEY')}"
EOF

let apiKey = "6a0f0731d84afa4082031e3a72354991"

Generating (and not committing) Swift files from GYB while pulling secrets from environment variables solves the problem of leaking credentials in source code, but it fails to guard against common static analysis tools. However, we can use a combination of Swift and Python code (via GYB) to obfuscate secrets in a way that’s more difficult to reverse-engineer.

For example, here’s an (admittedly crude) solution that implements an XOR cipher using a salt that’s generated randomly each time:

// Secrets.swift.gyb
%{
import os

def chunks(seq, size):
    return (seq[i:(i + size)] for i in range(0, len(seq), size))

def encode(string, cipher):
    bytes = string.encode("UTF-8")
    return [ord(bytes[i]) ^ cipher[i % len(cipher)] for i in range(0, len(bytes))]
}%
enum Secrets {
    private static let salt: [UInt8] = [
    %{ salt = [ord(byte) for byte in os.urandom(64)] }%
    % for chunk in chunks(salt, 8):
        ${"".join(["0x%02x, " % byte for byte in chunk])}
    % end
    ]

    static var apiKey: String {
        let encoded: [UInt8] = [
        % for chunk in chunks(encode(os.environ.get('API_KEY'), salt), 8):
            ${"".join(["0x%02x, " % byte for byte in chunk])}
        % end
        ]

        return decode(encoded, cipher: salt)
    }

    
}

Secrets are pulled from the environment and encoded by a Python function before being included in the source code as [UInt8] array literals. Those encoded values are then run through an equivalent Swift function to retrieve the original value without exposing any secrets directly in the source.

The resulting code looks something like this:

// Secrets.swift
enum Secrets {
    private static let salt: [UInt8] = [
        0xa2, 0x00, 0xcf, , 0x06, 0x84, 0x1c,
    ]

    static var apiKey: String {
        let encoded: [UInt8] = [
            0x94, 0x61, 0xff,  0x15, 0x05, 0x59,
        ]

        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)
    }
}

Secrets.apiKey // "6a0f0731d84afa4082031e3a72354991"

While security through obscurity may be theoretically unsound, it can be an effective solution in practice. (Wang et al., 2018)

As the old saying goes:

You don’t have to outrun a bear to be safe. You just have to outrun the guy next to you.

🧠 Galaxy Brain Don’t Store Secrets On-Device

No matter how much we obfuscate a secret on the client, it’s only a matter of time before the secret gets out. Given enough time and sufficient motivation, an attacker will be able to reverse-engineer whatever you throw their way.

The only true way to keep secrets in mobile apps is to store them on the server.

Following this tack, our imagined plot shifts from an Oceans 11-style heist movie to a high-stakes Behind Enemy Lines escort mission movie. (Your mission: Transfer a payload from the server and store it in the Secure Enclave without an attacker compromising it.)

If you don’t have a server that you can trust, there are a few Apple services that can serve as a transport mechanism:

Then again, once your secret reaches the Secure Enclave, it’ll be sent right back out with the next outbound request for which it’s used. It’s not enough to get it right once; security is a practice.

Or put another way:

A secret in Secure Enclave is safe, but that is not what secrets are for.

🧠 Universe Brain Client Secrecy is Impossible

There’s no way to secure secrets stored on the client. Once someone can run your software on their own device, it’s game over.

And maintaining a secure, closed communications channel between client and server incurs an immense amount of operational complexity — assuming it’s possible in the first place.

Perhaps Julian Assange said it best:

The only way to keep a secret is to never have one.


Rather than looking at client secret management as a problem to be solved, we should see it instead as an anti-pattern to be avoided.

What is an API_KEY other than an insecure, anonymous authentication mechanism, anyway? It’s a blank check that anyone can cash, a persistent liability the operational integrity of your business.

Any third-party SDK that’s configured with a client secret is insecure by design. If your app uses any SDKs that fits this description, you should see if it’s possible to move the integration to the server. Barring that, you should take time to understand the impact of a potential leak and consider whether you’re willing to accept that risk. If you deem the risk to be substantial, it wouldn’t be a bad idea to look into ways to obfuscate sensitive information to reduce the likelihood of exposure.


Restating our original question: “How do I store secrets securely on the client?”

Our answer: “Don’t (but if you must, obfuscation wouldn’t hurt).”


References
  1. Meli, M., McNiece, M. R., & Reaves, B. (2019). How Bad Can It Git? Characterizing Secret Leakage in Public GitHub Repositories. Proceedings of the NDSS Symposium 2019. https://www.ndss-symposium.org/ndss-paper/how-bad-can-it-git-characterizing-secret-leakage-in-public-github-repositories/
  2. Wen, H., Li, J., Zhang, Y., & Gu, D. (2018). An Empirical Study of SDK Credential Misuse in iOS Apps. 2018 25th Asia-Pacific Software Engineering Conference (APSEC), 258–267. https://doi.org/10.1109/APSEC.2018.00040
  3. Wang, P., Wu, D., Chen, Z., & Wei, T. (2018). Protecting Million-User iOS Apps with Obfuscation: Motivations, Pitfalls, and Experience. 2018 IEEE/ACM 40th International Conference on Software Engineering: Software Engineering in Practice Track (ICSE-SEIP), 235–244.
NSMutableHipster

Questions? Corrections? Issues and pull requests are always welcome.

This article uses Swift version 5.1. Find status information for all articles on the status page.

Written by Mattt
Mattt

Mattt (@mattt) is a writer and developer in Portland, Oregon.

Next Article

A look at an obscure, little collection type that challenges our fundamental distinctions between Array, Set, and Dictionary.