Pocket APIを利用したアプリケーションを開発しているのですが、コンシューマキーを commit してしまうという痛恨のミスを犯してしまいました(しかも二回...)。コンシューマキーはコード中にベタがきされていました。1度ミスを犯した後は commit しないように注意していたのですが、人間はミスを犯すものですね。
外部に漏らすべきでない情報を含んだコードをバージョン管理する場合、どうすべきか?というと、そういった情報は別ファイルに切り出すなどして、バージョン管理システムの管理下におかなければ良い、ということになります。
Xcode を使用する iOS 開発でこれを実現する場合にはどのような方法があるのか気になったので、調べました。先にまとめを書いておくと、自分の要求にあった方法は二種類ほどあるようで、各々の概要は以下のような感じです。
-
cocoapods-keys
- CUI 上からコマンドでキーを設定する
- Qiita にはすでにいくつかこちらに関する記事はありますね
- 設定したキーは keychain 内に保持されるらしい
-
pod install
orpod update
時にキーを参照可能なオブジェクトの定義を含んだコードが生成される - コード内で秘密のキーを扱う場合は有用
-
Info.plist
に適用できない
-
Run Script 内で Info.plist を書き換える
- 元の Info.plist を書き換えると差分がでてしまうため、Preprocess された Info.plist を編集
- Info.plist の編集には PlistBuddy を使用する
自分の中の解決策として、コード中で参照する必要のあるコンシューマキーは cocoapods-keys、動的に指定したい Info.plist 中の値の設定には Run Script を使用しました。
それぞれについて少し詳しく調べたので、以下にまとめます。
cocoapods-keys
cocoapods-keys はライブラリ管理用の CocoaPods のプラグインらしく、pod install
もしくは pod update
時に、設定されたキーをコードから参照できるようなオブジェクトを生成してくれるようです。
# インストール方法
$ gem install cocoapods-keys
例えば、プロジェクト名が LinkedNote
、管理したいキー名が PocketAPIConsumerKey
である場合、以下のような記述を Podfile に追記します。
plugin 'cocoapods-keys', {
:project => "LinkedNote",
:keys => [
"PocketAPIConsumerKey"
]
}
これを設定した状態で pod install
もしくは pod update
を行うと、キーの入力を求められるはずです。また、キーの設定、確認は、pod keys
コマンドで行えます。
# キーの設定
$ pod keys set PocketAPIConsumerKey <my-consumer-key>
Saved PocketAPIConsumerKey to LinkedNote.
# キーの確認
$ pod keys
Keys for LinkedNote
└ PocketAPIConsumerKey - <my-consumer-key>
キーを設定後は、pod install
or pod update
時に、キーを参照するためのコードが自動生成されます。具体的には、「プロジェクト名 + Keys」という名前のオブジェクトが、設定したキー名と同名のプロパティを持ったもの、が定義されるようです。Swift の場合、Keys
を import した後にこれを参照します。
import Keys
// キーの参照方法:
// <プロジェクト名> + Keys という名前のオブジェクト にキーを参照するためのプロパティがセットされている
let keys = LinkedNoteKeys()
// 各キー名をプロパティとして参照することでキー値を取得できる
// 先頭は小文字になるようだ
val key = keys.pocketAPIConsumerKey
cocoapods-keysで設定値をプロジェクトから切り離す - Qiita
cocoapods-keysをSwiftのプロジェクトで利用する - Qiita
Info.plist に動的に値を設定するためには
cocoapods-keys の問題点
コードからセキュアな情報を切り出すだけなら cocoapods-keys
だけで十分なのだけど、Pocket API では Info.plist にアプリケーション ID を設定する必要があります。このアプリケーション ID はバレたところで害はないのですが、コンシューマキーとセットで意味のあるものだし、バージョン管理の対象にはしたくありません。
cocoapods-keys
はコードからキー値を参照できます。しかし、アプリケーションバンドルから Info.plist を書き換えることはできないので、コードから動的に Info.plist を書き直すことはできなさそうです。
objective c - Editing Info.plist possible programmatically? - Stack Overflow
ビルド時にスクリプトを走らせ Info.plist を直接書き換える
ビルド時に Info.plist を書き換える方法は、ビルド番号の採番の自動化のためなどに利用されているようです。実現のためのテクニックをまとめると以下のようになります。
- Info.plist を動的に書き換えるためには、 Run Script Build Phases を新規に作成し、スクリプト内に書き換えるためのコマンドを記述すると良い
- Info.plist を書き換えるには、PlistBuddy を使用する
- 元々の Info.plist を書き換えると差分ができてしまうので、Info.plist をプリプロセスするよう設定し、プリプロセスされた Info.plist を編集する
各々について調べたことをまとめておきます。
Run Script Build Phases
Xcode では、ビルド時にユーザが定義したスクリプトを実行できます。スクリプトの定義は、Build Phase
> Editor-Add Build Phase-Add Run Sctipt Build Phase
から追加します。ここで定義したスクリプトはビルド時に Build Phase の1ステップとして実行されるので、スクリプト内部に Info.plist を書き換える処理を追加すれば、ビルド時に Info.plist を編集できます。
この時、Info.plist の更新に使用する値は、もちろん外部ファイルとして別に定義しておく必要があります。スクリプトはシェルスクリプトとして実行可能なので、環境変数と値のセットを定義したファイルを用意し、スクリプト内でロードすれば良いと思います(このときロードするファイルは、バージョン管理システムの管理下からは外しておきます)。
例えば、以下のような .env
を用意します。
POCKET_APP_ID=pocketapp67646
Run Script 内で以下のように記述すれば、Info.plist を更新できます(Preprocessed-Info.plist
, PlistBuddy
については後述します)。
if [ ${CONFIGURATION} = "Debug" ]; then
plistBuddy="/usr/libexec/PlistBuddy"
infoPlistFileSource="${SRCROOT}/${INFOPLIST_FILE}"
infoPlistFileDestination="${TEMP_DIR}/Preprocessed-Info.plist"
. ./.env
$plistBuddy -c "Set :CFBundleURLTypes:0:CFBundleURLSchemes:0 ${POCKET_APP_ID}" "${infoPlistFileDestination}"
fi
Run Script 内では、特定のパスを指し示すマクロを参照できます(上記の場合だと、${SRCROOT}
や ${INFOPLIST_FILE}
がそうです)。特に今回関心がある、Info.plist のパス関連について示すと以下のような感じになるようです。 これらは参考サイトで利用しているものをそのままひっぱってきてしまっているので、公式のドキュメントなどを参考にしたわけではありません。
-
${BUILT_PRODUCTS_DIR}/${FULL_PRODUCT_NAME}/Info.plist
- バンドル内に配置された Info.plist
-
${SRCROOT}/${INFOPLIST_FILE}
- ソースコード内の Info.plist
-
${TEMP_DIR}/Preprocessed-Info.plist
- preprocess された Info.plist
- preprocess については後述
$(SRCROOT) や $(BUILD_DIR) 等の Xcode で使用しているマクロの置換内容の一覧を調べる方法 - Over&Out その後
PlistBuddy
PlistBuddy は、.plist
ファイルを編集するためのコマンドです。例えば、値の設定は以下のように行えます。要素の入れ子は :
で区切って表現するようです。
# エントリの編集
$ /usr/libexec/PlistBuddy -c "Set :ProgramArguments:0 ABC" ./target.plist
Preprocessed-Info.plist
Info.plist を直接編集してしまうと、差分が生まれてしまうので意味がありません。あるいは、ビルドフェーズ中にバンドル内に配置された後の Info.plist を編集することもできるようなのですが、安定しないという例が見受けられたので受け入れ難いです。色々と調べてみると、Preprocess Info.plist
という設定項目があることがわかりました。
Preprocess Info.plist
を有効にすると、Build Phases のはじめに、ソース内の Info.plist を元にして同じ内容の ${TMP_DIR}/Preprocessed-Info.plist
が生成されます。これは、Preprocess が有効でない場合にソース内の Info.plist がバンドル内にコピーされるのと同じタイミングで、同様にバンドル内にコピーされます。Preprocessed-Info.plist
を有効にするには、Builde Settings
> Packaging
> Preprocess Info.plist file
を YES
に設定します。
この Preprocessed-Info.plist
を編集すれば、元々の Info.plist に差分を生じさせることなく、ビルド毎に動的に Info.plist を編集できる、ということです。しかし、 Info.plist が更新されていない場合には Preprocessed-Info.plist も更新されない? といったような情報も見かけたので、もう少し調べた方が良いかもしれません...(後日調査できれば、追記します)。とりあえず、自分の環境ではビルド時に元の Info.plist を変更しないままに環境変数がしっかり反映された、ということは確認できました。
蛇足: 参考サイトでは、タイミングによって Info.plist の編集内容がアプリに反映されないことがある、という問題をこれで解消できる、という話もあったのですが、自分の理解力がなく、なぜこの方法でそれが解決されるのかはいまいち分かりませんでした。
EZ-NET: Build 番号の自動更新スクリプトで Info.plist そのものに書き込まないための考察
これがXcodeでのバージョニングの決定版になるかも - TOKOROM BLOG
余談
余談1: Preprocess の威力
上記の話では、Info.plist に差分を出さないために preprocess された Info.plist を編集していましたが、Info.plist の preprocess は具体的に他にどのようなことを可能にするのかについて調べてみました。
例えば、ヘッダーファイルを別に定義してマクロを記述すると、それを Info.plist 内で利用できるようになるようです。
また、Info.plist 内、すなわち XML 上に直接 #if
などを記述し、定義を分岐させることもできるようです。
余談2: ビルドフェーズのチェック
参考サイトでは、Preprocesse の設定によりビルドフェーズがどのように変化するかを観測していました。どうやるのか気になったのですが、ビルドフェーズの詳細を確認したい場合には、⌘+8
で表示される Log Navigator を利用すれば良いようです。
その他参考にしたサイト群
Set: Entry, ":xxxxx", Does Not Exist と言われてしまった時の話 - Qiita
bkeepers/dotenv: A Ruby gem to load environment variables from.env
.
Dotenv使ってみた - Qiita
Xcodeと自動化 - Qiita
iOSの環境変数(Configuration, Build Settings, Plist)で知っておくべきこと - Qiita
Objective-C, Swiftでローカルファイル(.env)で定義した環境変数を参照する - Qiita