はじめに
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の実行
targets: [
.target(
name: "SwiftPackageKeys",
dependencies: [],
plugins: [.plugin(name: "EnvironmentKeyPlugin")]
),
.plugin(
name: "EnvironmentKeyPlugin",
capability: .buildTool()
)
]
Package.swift
に定義した、EnvironmentKeyPlugin
がprebuildCommandです。
ファイル階層としては、Pluginsディレクトリ配下にある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
が取得できます。
prebuildCommand
で、これら2つのPathを渡しています。
makeコマンドの実行
Makeコマンドでは、.env
からコードを自動生成するシェルスクリプトを叩くだけです。
自動生成用のシェルスクリプトの実行
主にアプリのディレクトリから.env
ファイルの中身を取得し、コードを自動生成する処理を行なっています。
ただ、スクリプト自体はDerivedData
のcheckout
ディレクトリ配下に存在するため、開発中のアプリのディレクトリパスは、そのまま取得することはできません。
なので、.env
のPathを取得するには、もう一手間必要です。
肝となるのは、DerivedData
直下にある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
フレームワーク本体には、下記の空クラスが含まれています。
public final class SwiftPackageKeys {
// Empty
}
シェルスクリプトで実行後、生成されるコードは、SwiftPackageKeys
のExtensionコードになります。
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などあげていただけると嬉しいです。