把 iOS 專案中的 Secret API Key 都藏好吧

通常都會漏掉一個最重要的…

藏 secret key

如果我們想要開源一個已上線的 app,通常都要把我們使用服務的 secret api key 給放進 .gitignore 中,不要把它們 commit 進 git 中。

這邊先假設我們的專案中有使用到 AdMob 這個服務:

GADMobileAds.configure(withApplicationID: "ca-app-pub-3395100423612831~123456778")

要藏這個 secret key 其實很簡單,在專案中新增一個檔案 admob.apikey,並且在裡面放進 admob 的 api key,並從檔案中讀取出來即可。

// The string that results from reading the bundle resource contains a trailing
// newline character, which we must remove now because Fabric/Crashlytics
// can't handle extraneous whitespace.
let resourceURL = Bundle.main.url(forResource: "admob.apikey", withExtension: nil)!
let admobAPIKey = try! String(contentsOf: resourceURL).trimmingCharacters(in: .whitespacesAndNewlines)
GADMobileAds.configure(withApplicationID: admobAPIKey)

那 Crashlytics 呢?

Crashlytics 是一套很常使用的錯誤追蹤分析工具,大家還記得安裝方式嗎?

先回顧一下安裝方式

先在 Podfile 中加入 dependencies:

use_frameworks!
pod 'Fabric'
pod 'Crashlytics'

接著在 terminal 中輸入:

$pod install

裝好 Crashlytics 後要在專案中加一段 Run Script Build Phase:

https://cdn-cf.fabric.io/static_assets/discover/install/run_script_build_phase.mp4

"${PODS_ROOT}/Fabric/run" long_api_key_for_fabric long_build_secret_for_fabric

最後在 info.plist 中加入 fabric api key:

<key>Fabric</key>
<dict>
<key>APIKey</key>
<string>fabric_api_key</string>
<key>Kits</key>
<array>
<dict>
<key>KitInfo</key>
<dict/>
<key>KitName</key>
<string>Crashlytics</string>
</dict>
</array>
</dict>

你發現問題了嗎?

info.plist 跟 run script 都是無法被 ignore 的,如果我們沒有把這些東西 commit 進 git,之後我們 clone 專案下來後會發生找不到專案檔跟必要資訊的問題。如果開源給別人使用也會遇到別人只能單純閱讀 code,但不能把專案成功執行起來的問題。

如何解決呢?

這個問題困擾我很久,在之前寫某個上線的專案時,就有想要把某專案開源,但一直找不到方法把 Crashlytics api key 給藏起來的方法。但今天再次 google 後竟然發現一個解法,嘗試過後發現可行。

  1. 一樣在專案中建立兩個檔案 fabric.apikeyfabric.buildsecret
  2. 把這兩個檔案放進 .gitignore 中
  3. fabric.apikey 拉進 file navigator 中,fabric.buildsecret 不需要
  4. 把 info.plist 中的 api key 移除
  5. 把 run script 改掉,改成以下的 code
FABRIC_APIKEY_FILE="${SRCROOT}/fabric.apikey"
FABRIC_BUILDSECRET_FILE="${SRCROOT}/fabric.buildsecret"

if test ! -f "$FABRIC_APIKEY_FILE" -o ! -f "$FABRIC_BUILDSECRET_FILE"; then
echo "This build wants to upload dSYM files to Crashlytics."
echo "Uploading is possible only if a Fabric API key and a Fabric build secret are"
echo "available. This build is failing because at least one of these pieces of"
echo "information is missing."
echo ""
echo "To fix the problem, create the following files and store the API key and"
echo "build secret, respectively, within those files:"
echo ""
echo " $FABRIC_APIKEY_FILE"
echo " $FABRIC_BUILDSECRET_FILE"
echo ""
echo "If you forked the project then you must register with Crashlytics and
echo "get your own API key and build secret."

# Let the build fail
exit 1
fi

FABRIC_APIKEY=$(cat "$FABRIC_APIKEY_FILE")
if test $? -ne 0; then
echo "Cannot read $FABRIC_APIKEY_FILE"
exit 1
fi

FABRIC_BUILDSECRET=$(cat "$FABRIC_BUILDSECRET_FILE")
if test $? -ne 0; then
echo "Cannot read $FABRIC_BUILDSECRET_FILE"
exit 1
fi

echo "Uploading dSYM files to Crashlytics"
"${PODS_ROOT}/Fabric/run" "$FABRIC_APIKEY" "$FABRIC_BUILDSECRET"

最後回到 AppDelegate.swift 中把

Fabric.with([Crashlytics.self])

改成

let fabricAPIKey = fetch_from_file
Crashlytics.start(withAPIKey: fabricAPIKey)

就完成啦!

最後有幾點要注意

  1. 注意你放 *.apikey *.buildsecret 位置,他應該在跟藍色專案檔在同一層位置,如果不是 run script 要更改讀取位置。
  2. 如果你單純使用 Answer 跟 Crashlytics,這樣做不會有問題。但如果你還有使用 Fabric 提供的更多服務,我不知道 Crashlytics.start(withAPIKey: fabricAPIKey) 跟 Fabric.with([Crashlytics.self, Answer.self, …]) 一起呼叫會怎樣,至少我自己嘗試的時候 Fabric 會警告說:你已經呼叫兩次 Fabric.with 這個方法。