Posted at

Xcode による iOS 開発で秘匿したい情報をどう管理するか

More than 1 year has passed since last update.

Pocket APIを利用したアプリケーションを開発しているのですが、コンシューマキーを commit してしまうという痛恨のミスを犯してしまいました(しかも二回...)。コンシューマキーはコード中にベタがきされていました。1度ミスを犯した後は commit しないように注意していたのですが、人間はミスを犯すものですね。

外部に漏らすべきでない情報を含んだコードをバージョン管理する場合、どうすべきか?というと、そういった情報は別ファイルに切り出すなどして、バージョン管理システムの管理下におかなければ良い、ということになります。


project management - Strategy for keeping secret info such as API keys out of source control? - Software Engineering Stack Exchange


Xcode を使用する iOS 開発でこれを実現する場合にはどのような方法があるのか気になったので、調べました。先にまとめを書いておくと、自分の要求にあった方法は二種類ほどあるようで、各々の概要は以下のような感じです。



  • cocoapods-keys


    • CUI 上からコマンドでキーを設定する

    • Qiita にはすでにいくつかこちらに関する記事はありますね

    • 設定したキーは keychain 内に保持されるらしい


    • pod install or pod 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


Better Xcode Run Script Build Phases

Xcodeと自動化 - Qiita


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


A Simple PlistBuddy Tutorial

PlistBuddy 使い方 - Qiita



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 fileYES に設定します。

この 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 内で利用できるようになるようです。


Build Configuration毎でinfo.plistの内容を変更する - アルザえもんの手記


また、Info.plist 内、すなわち XML 上に直接 #if などを記述し、定義を分岐させることもできるようです。


Xcode Build Settings Part 1: Preprocessing



余談2: ビルドフェーズのチェック

参考サイトでは、Preprocesse の設定によりビルドフェーズがどのように変化するかを観測していました。どうやるのか気になったのですが、ビルドフェーズの詳細を確認したい場合には、⌘+8 で表示される Log Navigator を利用すれば良いようです。


Xcode のビルドログの詳細を確認する方法 - Qiita



その他参考にしたサイト群


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