LoginSignup
13
12

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-02-13

はじめに

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」

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

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

動作原理

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

ビルドツールプラグインには、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

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.directorycontext.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

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

makeコマンドの実行

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

自動生成用のシェルスクリプトの実行

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

スクリーンショット 2023-02-14 1.56.13.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インポート後下記のように扱うことが可能です。

let mysecretapikey: String = SwiftPackageKeys.mysecretapikey

終わりに

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

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

13
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
12