SwiftでAPI KEYなどの秘匿が推奨される情報の隠蔽化

## 何が問題なの？

普段あまり、AWSやFIREBASEなどのサービスの利用を何とか避けてニッチなアプリで凌いでいましたが、ついにそんな悠長な事も言って入れなくなってきました。

改めて、この種のサービスを利用する際に常に気になっていたのが、こんな記法です。SDKがこれらの情報を必要とするのは理解できますが、これらの情報をリテラルで渡すのはかなり気になります。スクリプトなどを介在して環境変数から、これらの情報を渡す場合もあるようですが、コンパイル時にはリテラルになっているのでやはり気になります。

```swift
WorldGreatService(apiKey: "123456", secretKey: "abcdef")
```
例えば、ターゲットとなるアプリがあったとします。`WorldGreatService`を利用しているのがわかっているものとします。そして、`WorldGreatService`がSDKを公開していたとします。

自分でも同SDKを組み込んでみるとします。自分用のapi-keyとsecret-keyを取得して上記同様にリテラルで組み込んでビルドしてみます。次にビルドしたアプリのバイナリから、自分のapi-keyとsecret-keyを探して見つけます。見つけたら、その前後のバイト列のメモします。

次に、ターゲットのアプリからその前後のバイト列を検索などで見つけます。この二つのバイナリー列を比較すると……

実際に試したわけではないので、ビルドされたコードからそんな単純に見つかるとは思いませんが、ハッカーがこんな感じでキーをクラックする手がかりになるのは容易に想像できると思います。

ちなみに、今回紹介する方法は、専門的な情報機関からのクラック対策を目標としたものではなく、あくまでもカジュアルハッカーからのクラック対策を目標としています。また、クラックされない事を保証したものではなく、気軽にハックできない程度を目標としています。

## Getting Started

サンプルプロジェクトは以下のURLから入手可能です。

https://github.com/codelynx/UnexposedCredencials

プロジェクト内には`.credentials`フォルダがあります。この中に`credentials.plist`というファイルがあるので、この中にAPI KEYやSECRET KEYなどの秘匿情報を記載します。が、間違っても、Xcodeのプロジェクトに組み込まないでください。`ipa`を開くと丸見えになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65634/0ac7a1eb-65a8-4b7e-fb28-08979b597de9.png" width="432"/>

次にTerminalから`./configure`を実行します。

```bash
./configure
```

`./configure`は`codegen.swift`を実行します。別に、この`codegen.swift`を直接走らせてもかまいません。同フォルダに`AES256.swift`がある事を確認してください。

実行すると、同フォルダ内に`Credentials.swift`が生成されます。このソースファイルをXcodeのプロジェクトに組み込みます。しつこいようですが間違っても`credentials.plist`を組み込まないでください。

ちなみに、のちに`credentials.plist`を変更した場合は`./configure`を再度実行し、`Credentials.swift`を再生成します。

## Using Credentials

`Credentials.swift`を組み込んだら、これで秘匿情報をアプリで取得する準備が完了です。

```swift
let my_api_key = CREDENTIALS.shared["MY_API_KEY"] as! String
let my_secert_key = CREDENTIALS.shared["MY_SECERT_KEY"] as! String 
WorldGreatService(apiKey: my_api_key, secretKey:my_secert_key)
``` 

### `Credentials.swift`

ちなみに、生成された`Credentials.swift`は以下のようなコードになります。

```swift
import Foundation
import CommonCrypto


struct AES256Key {

	static let length = Int(kCCKeySizeAES256)
	let key: [UInt8]

	init(string: String) {
		var buffer = [UInt8](repeating: 0, count: Self.length)
		if let data = string.data(using: .utf8) {
			let bytes = [UInt8](data)
			(0 ..< min(bytes.count, Self.length)).forEach { buffer[$0] = bytes[$0] }
		}
		assert(buffer.count == Self.length)
		self.key = buffer
	}

	init?(data: Data) {
		guard data.count == Self.length else { return nil }
		var buffer = [UInt8](repeating: 0, count: Self.length)
		(0 ..< Self.length).forEach { buffer[$0] = data[$0] }
		self.key = buffer
	}
}


extension Data {

	static private let keyLength = Int(kCCKeySizeAES256+1)

	func encryptAES256(key: AES256Key) -> Data? {
		let blockSize = Int(kCCBlockSizeAES128)
		var buffer = [UInt8](repeating: 0, count: self.count + blockSize)
		var numberOfBytesDecrypted: size_t = 0
		let status = CCCrypt(CCOperation(kCCEncrypt), CCAlgorithm(kCCAlgorithmAES128), CCOptions(kCCOptionPKCS7Padding),
				key.key, kCCKeySizeAES256,
				nil, // initialization vector (optional)
				(self as NSData).bytes, self.count, // input
				&buffer, buffer.count, // output
				&numberOfBytesDecrypted);
		if status == kCCSuccess {
			return Data(bytes: &buffer, count: Int(numberOfBytesDecrypted))
		}
		return nil
	}

	func decyptAES256(key: AES256Key) -> Data? {
		let blockSize = Int(kCCBlockSizeAES128)
		var buffer = [UInt8](repeating: 0, count: self.count + blockSize)
		var numberOfBytesDecrypted: size_t = 0
		let status = CCCrypt(CCOperation(kCCDecrypt), CCAlgorithm(kCCAlgorithmAES128), CCOptions(kCCOptionPKCS7Padding),
				key.key, kCCKeySizeAES256,
				nil, // initialization vector (optional)
				(self as NSData).bytes, self.count, // input
				&buffer, buffer.count, // output
				&numberOfBytesDecrypted);
		if status == kCCSuccess {
			return Data(bytes: &buffer, count: Int(numberOfBytesDecrypted))
		}
		return nil
	}

}

let aesKeyBin = Data([0xaa,0xda,0xfd,0x38,0x3f,0x3f,0x3d,0xe6,0x18,0xab,0x06,0x02,0x8a,0x99,0x65,0x4c,0x3d,0x41,0x65,0xea,0x00,0xf9,0x36,0x76,0xe6,0x57,0x71,0x2e,0x15,0x9f,0xb4,0x7f])
let aesKey = AES256Key(data: aesKeyBin)!
let encryptedBin = Data([0xd0,0xb2,0x82,0x17,0xaf,0xf7,0x62,0x50,0xde,0xcd,0xa1,0xc2,0x0f,0xcb,0x45,0x53,0xb5,0xc5,0x84,0x9f,0x32,0x8b,0x9e,0xd7,0xf9,0x1b,0x96,0xe0,0xad,0x19,0xd8,0xca,0x2e,0x5d,0xd2,0x07,0x24,0x0e,0x9f,0xa7,0x64,0x17,0xd4,0x7c,0xc7,0x24,0x0c,0x0f,0x17,0x9e,0x55,0x82,0x47,0x44,0x24,0xba,0x55,0x88,0x86,0x17,0xb8,0x29,0x1c,0xd0,0x0a,0x3b,0x55,0x67,0xd1,0xf5,0x58,0x41,0x4a,0x6f,0x3e,0x90,0x6d,0xe3,0xd5,0xe2,0xb8,0x47,0x87,0xaf,0xc0,0x81,0xd9,0xb5,0x03,0x31,0xb1,0x41,0x52,0x44,0xba,0xb9,0x9a,0x3b,0x06,0xed,0x0a,0x28,0xdb,0x07,0x1b,0xe2,0xf5,0xbd,0xb7,0x13,0xca,0x7a,0xcd,0x3e,0xa8,0x1f,0xce,0xc6,0xea,0xea,0x95,0x89,0xb7,0xea,0x58,0x1e,0x93,0xfb,0x50,0x66,0xdb,0x72,0x2c,0x97,0x98,0x6d,0xe6,0x5a,0x28,0xd8,0x7f,0x1f,0x53,0x47,0x84,0x8b,0xed,0x8c,0x0a,0xdc,0x80,0xf1,0xfe,0x48,0xbb,0xeb,0xaf,0xd6,0x32,0xc0,0x94,0x49,0x39,0xf9,0x91,0xba,0x7b,0xb7,0x11,0x1f,0x76,0x8f,0xcf,0x99,0xe5,0x2d,0x6d,0x22,0xa7,0x4d,0x10,0x66,0x4c,0x09,0x63,0x55,0x2d,0xc2,0x7c,0x99,0x6c,0x6f,0x39,0x6a,0xf1,0x2c,0x4b,0x92,0xcc,0xf9,0x6e,0x3a,0x4f,0xed,0x90,0xc6,0x49,0x6e,0x8a,0x27,0xd9,0x54,0x94,0x84,0x91,0xc9,0xf3,0xdc,0xfa,0xf7,0xa3,0x64,0x75,0x3a,0x97,0xa9,0xc7,0xa7,0x74,0x8a,0x9e,0x40,0x85,0xdc,0xa9,0x2e,0x7d,0x21,0xd0,0x5a,0x90,0x50,0x6d,0x53,0x9f,0x2a,0x31,0xc3,0x3c,0xd4,0xf9,0xee,0x0f,0xe2,0xe0,0x53,0x81,0xd2,0x53,0xda,0xdf,0x01,0x7b,0x90,0xd8,0x7f,0x33,0x24,0x95,0xa7,0x4b,0x96,0xcc,0x4b,0x25,0x5e,0x61,0x86,0x63,0xee,0x52,0x7c,0x30,0x82,0x9a,0x0c,0x22,0x61])
let decryptedBin = encryptedBin.decyptAES256(key: aesKey)!

class CBBA33F3DC70A4502851183DCF579CB1E {
	static let shared = CBBA33F3DC70A4502851183DCF579CB1E()
	let dictionary: [String: Any]?
	private init() {
		dictionary = try? PropertyListSerialization.propertyList(from: decryptedBin, options: [], format: nil) as? [String: Any]
	}
	subscript(key: String) -> Any? {
		return dictionary?[key]
	}
}
typealias CREDENTIALS = CBBA33F3DC70A4502851183DCF579CB1E
```

暗号キーと、その暗号キーで暗号化されたコンテンツはソースコードの形で埋め込まれています。そして、`CREDENTIALS`で利用するクラスは、`UUID`に類似した名前で実装されており、その`typealias`で`CREDENTIALS`を参照しています。こうする事で、`class_dump`のようなツールでSymbolを覗かれても、`CREDENTIALS`の類のシンボルが見つからないので、クラックされにくくなると考えています。

UUID似の名前は実行毎に新しくなりますが、`typealias`の`CREDENTIALS`で利用すれば、クライアントコードのブレークはないはずです。最も、シンボルの中にこんな不思議な名前のクラスがあれば、やはり不自然ですが、何より「MY_API_KEY」とか「MY_SECRET_KEY」とかいった名前でひくのは、もっと問題かと思いますが、あまり変な名前にすると、可読性が落ちるので、利用する場合はその辺りのバランスを考える必要があります。

## CHAT GPT

ちなみに、Chat GPTにリテラル埋め込みの危険性を訊いてみたらこんな答えが返ってきました。

(質問)

> There are many SDKs or other services require API keys or secret keys, and typical way to initialize services from app is like this.
>
> WorldService(apiKey: "123456", secretKey: "abcdef")
>
> I am curious how hard or easy to crack this this code for casual hacker to extract those keys?

（回答）

> If the API key and secret key are hardcoded in the app's binary, they can be extracted by reverse engineering the app. Therefore, it is not recommended to hardcode sensitive information like API keys and secret keys in the app's binary. Instead, it is better to store them securely on the server side and require authentication before granting access to the keys. Another approach is to use environment variables or a configuration file that is loaded at runtime, rather than hardcoding the keys in the source code.

その対策についても聞いてみました。この課題に取り組む前に聞けばよかったような気もしますが、まぁいいとします。

(質問)

> what is the typical way to avoid this?

（回答）

> To avoid exposing API keys and secret keys in your code, you can use environment variables or configuration files to store these sensitive information.
>
> For example, you can store your API keys and secret keys in a separate configuration file, and load them into your application at runtime. You can then configure your application to read the configuration file and use the keys to authenticate with your API.
>
> Another option is to use a service like HashiCorp Vault or AWS Secrets Manager to store and manage your API keys and secrets securely. These services provide a secure way to manage and distribute secrets to your applications without exposing them in your code.

### 環境

執筆時点での環境は以下のとおりです。

```bash
$ swift --version
swift-driver version: 1.75.2 Apple Swift version 5.8 (swiftlang-5.8.0.124.2 clang-1403.0.22.11.100)
Target: arm64-apple-macosx13.0
```