Safely distributing API keys in your mobile app

The safest way is to keep your API keys server side. But if you have a library that's useable offline and it requires a license key to unlock, you will need a way to securely store that license key at runtime, without access to a server. Strap your hat, you are going for a ride.

First, what not to do:

  • Don't store your license/API key as a string in your source code

  • Don't store your license/API key as a value in Info.plist

  • Don't store your license/API key in a .xcconfig file that is used to populate your Info.plist

  • Don't store your license/API key in an environment variable used to populate your source code at compile time which will just store the value as a string in your source code.

  • Don't base64/base32 encode your key and think it's safe

  • Don't XOR your key.

Yikes, so.. with all that, what can we do then?

You will want to use a cryptographically proven algorithm, like AES.GCM to encrypt the key at compile time, then use AES.GCM to decrypt the key at runtime.

I hear you, AES.GCM requires an encryption key, so how will you store the key? And that, my friends, is how the ride starts.

Let's ride.

Pick a password

Come up with a good password. If you are not sure, uuidgen command in terminal is a relatively safe bet. Plus it won't look like a password so, bonus point for confusing your attackers.

Pick a shared secret

Next, at some point you will need a "shared secret" to derive your key. Hit that uuidgen command once more.

Pick an obfuscator

Grab an obfuscator library of your choice. SwiftSecretKeys (https://github.com/MatheusMBispo/SwiftSecretKeys) seems decent enough, there are others. Most of them are not cryptographically safe but that's okay. It will be better than storing your password and shared secret as strings in your source code.

Make sure you understand how to use the chosen obfuscator. Store your password and shared secret in there. Please give them creative names. If your obfuscator works with string keys, go ahead and generate two more UUID to use as keys.

output: Generated/
keys:
  kE2110F7359B34F6EB11AC01F96652F44: '77783393-62DE-499E-8848-68FF3D4A2979'
  k3A0535981E94408E86BAB3CFE466C399: '849C3BB6-2519-4BF4-8E7A-3E6D7D9C003B'
let mySharedSecret = SecretKeys.kE2110F7359B34F6EB11AC01F96652F44
let myPassword = SecretKeys.k3A0535981E94408E86BAB3CFE466C399

Assuming your stuff is setup right, this should give you a mySharedSecret variable that contains the string "77783393-62DE-499E-8848-68FF3D4A2979" and myPassword that contains the string "849C3BB6-2519-4BF4-8E7A-3E6D7D9C003B"

Learn how to use HKDF

Apple's doc on HKDF is at https://developer.apple.com/documentation/cryptokit/hkdf

In short, HDKF is a key derivation algorithm, you give it a password, a salt, and tell it how many bytes of data you need for the key, and it's going to do its magic until there's enough data to fill the key. In our case we will be using AES.GCM with 256-bit keys. Given 8-bits per byte, we will need 32 bytes.

let passwordData = myPassword.data(using: .utf8)!
let sharedSecretData = mySharedSecret.data(using: .utf8)!
let aesKey = HKDF<SHA512>.deriveKey(
            inputKeyMaterial: passwordData,
            info: sharedSecretData,
            outputByteCount: 32)

Learn how to use AES.GCM

Apple's doc on AES.GCM is at https://developer.apple.com/documentation/cryptokit/aes/gcm

AES is the encryption algorithm, GCM is the block cipher which also includes some AEAD authentication data at the end. No need to go into too much details here, it allows you to encrypt some plain text data into gibberish unintelligible data commonly known as cipher text.

plain text = stuff not encrypted

cipher text = gibberish

AES.GCM uses something called a SealedBox to store the cipher text. So when you want to encrypt your plain text, you use AES.GCM.seal() to create a SealedBox, and when you want to decrypt your cipher text back to plain text you first create a SealedBox using the cipher text, and you open the SealedBox using AES.GCM.open().

Creating the [UInt8] sequence to include in your source code, assuming you already have the HKDF to generate the aesKey:

let myApiKey = "I love Apple"
let myApiKeyPlainText: Data = myApiKey.data(using: .utf8)!
let sealedBox = try! AES.GCM.seal(myApiKeyPlainText, using: aesKey)
let myApiKeyCipherText: Data = sealedBox.combined!
let cipherRepresentation = myApiKeyCipherText.map { String(format: "0x%02X", $0 }.joined(separator: ",")
print("private let apiKeyCipherText = [\(cipherRepresentation)]")

// will print:
// private let apiKeyCipherText: [UInt8] = [0xA2,0xC3,0xC7,0x56,0x66,0x28,0x73,0x87,0x1B,0xC6,0xF8,0x64,0x8D,0x8F,0x80,0x2C,0xB0,0xF2,0x87,0x57,0xFF,0x87,0x75,0x74,0x03,0x7B,0x92,0xD6,0xFC,0xF7,0xDF,0xB9,0xD6,0xBA,0x80,0xB9,0x1D,0xCD,0x6E,0xAB]

In your source code, assuming you already have the HKDF to generate the aesKey:

 private let apiKeyCipherText: [UInt8] = [0xA2,0xC3,0xC7,0x56,0x66,0x28,0x73,0x87,0x1B,0xC6,0xF8,0x64,0x8D,0x8F,0x80,0x2C,0xB0,0xF2,0x87,0x57,0xFF,0x87,0x75,0x74,0x03,0x7B,0x92,0xD6,0xFC,0xF7,0xDF,0xB9,0xD6,0xBA,0x80,0xB9,0x1D,0xCD,0x6E,0xAB]
 let sealedBox = try! AES.GCM.SealedBox(combined: Data(apiKeyCipherText))
 let myApiKeyPlainText = try! AES.GCM.open(sealedBox, using: aesKey)
 let myApiKey = String(data: myAPiKeyPlainText, encoding: .utf8)!
 print(myApiKey)

 // will print
 // I love Apple

Congrats!

You can now securely store license keys and secrets, in your mobile app.

You are not done.

If you went through all this trouble just to send your API key over some network request using HTTPS, your API key will be easily accessible to hackers using TLS bypass attacks.

You can learn more about this by searching for Radare2, Frida or Objection. You can detect TLS bypass attacks but you can't really prevent them. So your API key will be exposed the moment you send it over the network, unless you encrypt before sending it over to the server.

Most online services expect you to send your API key as is, so most of your hard work to keep your API key secret will, well, be for naught.

But, if your API key is used locally in a commercial library, then you are all set, or almost.

You will want to finish protecting your application with jailbreak detection, debugger detection, etc.

Happy coding!