Swift Package Managerだけで完結する機密情報などの環境変数を管理するFrameworkを作った話。

# はじめに
APIのアクセスキーなどの機密情報など設定値は、ソースコードにハードコードしたりInfo.plistに書き込んで利活用することは基本的には避けておくべきです。
そういった設定値が平文で混入した状態で、Github.com上のリポジトリなど社外のホスティング環境に対してCommitしてしまうと、Commit先のリポジトリがPrivateであっても第三者からの不正アクセスによって、Private Repository内から平文のAPI Keyが抜き出され、外部に漏洩してしまい悪用されてしまう可能性が生じます。

ipaファイルからリバースエンジニアリングを行いAPIKeyを抽出する可能性もあるため、完全なセキュアな状態とするのであれば、アプリそのものにAPIKeyなどの機密情報を埋め込まないことになります。
ただ、現実的にそんなことは運用上難しいため漏洩リスクを下げ、API側のアクセス制限などでカバーするのが一般的です。

# 従来、メジャーであった「cocoapods-keys」
アプリのコードと切り離して、機密情報の値を難読化して利用するために、Objective-Cの時代から「cocoapods-keys」がよく活用されてきました。
非常に長く使われてきたCocoapodsのプラグインなので、昔からサービスを提供し続けているアプリでは、まだまだ生き残っていると思います。

ただ、cocoapods-keysは、Cocoapodsのプラグインで提供されているため、Cocoapodsに依存しています。
CocoapodsやCarthageを脱却し、SwiftPackageManagerだけに統一する際の障害になってしまいます。

## Arkna（アルカナ）の登場
cocoapods-keysの後継として、2022年からArknaの開発がスタートしています。
こちらは、SwiftPackageManagerに対応しており、プロジェクトのルートディレクトリにある``.env``からKey-Valueのネイティブコードを持つPackageを自動で生成してくれます。

ただ、Packageの生成は都度Buildが必要になりアプリのビルドをトリガーにして自動で実行するには、任意のスクリプトであったりXcode Build Phaseなどに``Arkana run``コマンドを仕込んで実行する必要があります。
また、生成されるのはSwiftPackageなので、リポジトリ内にArkanaで生成したローカルパッケージを持たせ、ローカルで依存関係を持たせることになります。

できれば、Swift Package Managerだけで完結できて、AppのBuildを行うたび常に最新のKey-Valueを取得できるといいなと思うのでした...

# 思いつきで開発してみた「SwiftPackageKeys」

https://github.com/MasamiYamate/SwiftPackageKeys

Swift Package ManagerのPlugin機能を活用し、プロジェクトのルートディレクトリ配下にある``.env``からKey-Valueを扱うネイティブコードを自動生成するFrameworkが作れるんじゃないかと気づき、とりあえず作ってみました。

利用方法は、READMEに記載があるので、そちらをご覧ください。

## 動作原理
2022年3月にリリースされたSwift5.6以後で、Swift Package Managerにプラグイン機能が追加されています。
本Frameworkでは、プラグイン機能のビルドツールプラグインを活用しています。

https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md

ビルドツールプラグインには、2種類のコマンドが用意されています。

- buildCommand　（ビルド中に実行されるコマンド）
- prebuildCommand　（ビルド前に実行されるコマンド）

このうち、``prebuildCommand``を利用し、アプリ・Frameworkのビルド前に``.env``からKey-Valueのコードを自動生成するスクリプトを実行し、アプリのビルド時に自動生成を行なったコードをコンパイルし、アプリ内で機密情報を使えるようにしています。
大まかな実行順は、下記の5段階です。

```
1. prebuildCommandの実行
2. makeコマンドの実行
3. 自動生成用のシェルスクリプトの実行
4. Swift Packageのビルド
5. アプリ本体のビルド 
```

### prebuildCommandの実行

```Package.swift
targets: [
        .target(
            name: "SwiftPackageKeys",
            dependencies: [],
            plugins: [.plugin(name: "EnvironmentKeyPlugin")]
        ),
        .plugin(
            name: "EnvironmentKeyPlugin",
            capability: .buildTool()
        )
]
```

![スクリーンショット 2023-02-14 1.32.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114168/9da9c366-6c5a-bd69-b9fe-f47dcb8ad1de.png)

``Package.swift``に定義した、``EnvironmentKeyPlugin``がprebuildCommandです。
ファイル階層としては、Pluginsディレクトリ配下にあるEnvironmentKeyPlugin.swiftの内容が実行されます。

```EnvironmentKeyPlugin.swift
import PackagePlugin

@main
struct EnvironmentKeyPlugin: BuildToolPlugin {
    func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
        return [
            .prebuildCommand(
                displayName: "Run generate keys",
                executable: Path("/usr/bin/make"),
                arguments: [
                    "generate_keys",
                    "PACKAGE_DIR=\(context.package.directory)",
                    "WORK_DIR=\(context.pluginWorkDirectory.string)"
                ],
                outputFilesDirectory: context.pluginWorkDirectory
            ),
        ]
    }
}
```

``EnvironmentKeyPlugin.swift``には、``prebuildCommand``の内容が定義されており、内容としてはルートディレクトリ配下にあるMakefileを呼び出しています。
ここで重要なのは、``context.package.directory``と``context.pluginWorkDirectory``です。
``context.package.directory``は、``/Users/【ユーザー名】/Library/Developer/Xcode/DerivedData/【Appのディレクトリ】/SourcePackages/checkouts/SwiftPackageKeys``のパスが取得できます。
Packageのスクリプトなどを実行する場合は、このPathから任意のファイルまでのパスを生成し、実行することになります。

``context.pluginWorkDirectory``は、Pluginから書き出せるファイルPathになります。
今回の場合ですと、``/Users/【ユーザー名】/Library/Developer/Xcode/DerivedData/【Appのディレクトリ】/SourcePackages/plugins/swiftpackagekeys.output/SwiftPackageKeys/EnvironmentKeyPlugin``が取得できます。

![スクリーンショット 2023-02-14 1.51.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114168/d0bb982e-8281-9eda-c4ff-90c867a0a5a3.png)

``prebuildCommand``で、これら2つのPathを渡しています。

### makeコマンドの実行
Makeコマンドでは、``.env``からコードを自動生成するシェルスクリプトを叩くだけです。

### 自動生成用のシェルスクリプトの実行
主にアプリのディレクトリから``.env``ファイルの中身を取得し、コードを自動生成する処理を行なっています。
ただ、スクリプト自体は``DerivedData``の``checkout``ディレクトリ配下に存在するため、開発中のアプリのディレクトリパスは、そのまま取得することはできません。
なので、``.env``のPathを取得するには、もう一手間必要です。

![スクリーンショット 2023-02-14 1.56.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114168/2836b206-c8ab-0710-43ba-b0e5414465a0.png)

肝となるのは、``DerivedData``直下にある``info.plist``です。

```info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>LastAccessedDate</key>
	<date>2023-02-13T16:43:00Z</date>
	<key>WorkspacePath</key>
	<string>/Users/【UserName】/Documents/Workspace/TestApps/TestApps.xcodeproj</string>
</dict>
</plist>
```

info.plist内の``WorkspacePath``に開発中のアプリの``.xcodeproj``パスが入っています。
そこから必要なファイルへのパスを生成することが可能です。
今回ですと、``Users/【UserName】/Documents/Workspace/TestApps/.env``のパスを生成して、``.env``のファイル内容を取得しています。

``.env``が取得できれば、あとは適宜Swiftのコードを生成するだけです。

## 生成されるコード
``SwiftPackageKeys``フレームワーク本体には、下記の空クラスが含まれています。

```SwiftPackageKeys.swift
public final class SwiftPackageKeys {
    // Empty
}
```

シェルスクリプトで実行後、生成されるコードは、``SwiftPackageKeys``のExtensionコードになります。

```SwiftPackageKeys+Extension.swift
public extension SwiftPackageKeys { 
    static var mysecretapikey: String { return "hogehoge" }
    static var mysecretapikey2: String { return "hogehogehoge" }
}
```

アプリから参照する際は、Frameworkインポート後下記のように扱うことが可能です。

```swift
let mysecretapikey: String = SwiftPackageKeys.mysecretapikey
```


# 終わりに
Swift Package Manager出た当時は、Buildをトリガーにして何か行うことはできませんでしたがPlugin機能を活用することで、よりSwift Package Managerだけに統一できる環境になってきているので、ぜひPlugin機能を活用してSwift Package Manager環境への移行を推進してみてください。

また、今回はPlugin機能を調べてる中で、「あれ、いけるんじゃね？」と開発した荒削り感たっぷりなFrameworkです。
まだまだ未熟な実装ですが、今後も時間を見て手を入れて行きたいと思いますので、ご活用いただける方は、issueなどあげていただけると嬉しいです。