App 的安全性是軟體開發中非常重要的一環。我們 App 的使用者都期望自己的資料是保密的,所以 App 裡的敏感資料不應該輕易被人拿走。這篇文章我們將會探討一些開發者在 App 安全性方面的常見錯誤,以及如何輕易處理這些問題。
在錯誤的地方儲存敏感資料
我研究過一些在 AppStore 上的 App,很多都犯了同一個錯誤,就是將敏感資料存放在不對的地方。
如果你將敏感資料存放在 UserDefaults
裡,就有可能會暴露 App 的資訊。
UserDefaults
只是將資料儲存為一個屬性列表檔案,存放於 App 的 Preferences 資料夾裡。它們並沒有以任何形式加密儲存在 App 中。
基本上來說,藉由使用 Mac 的第三方程式像是 iMazing,即使手機沒有越獄 (Jailbreak),你也可以輕易看到任何從 AppStore 下載的 App 內 UserDefaults
的資料。
這些 Mac App 雖然只是設計來讓你瀏覽或管理你手機上的第三方程式檔案,但你也可以輕易地瀏覽任何 App 的 UserDefaults
。
促使我撰寫這篇文章的原因,就是因為我發現大部分從 AppStore 安裝的 App,都將敏感資料存放在 UserDefaults
中,例如是 Access Token(存取權杖)、Active Renewable subscription flags(啟用可續訂訂閱標誌)、可用代幣的數量等資料。
一旦這些資料都能夠被輕易地擷取、更改,就會對 App 造成傷害,例如可以免費使用付費功能,甚至是被駭入網路層等等的。
正確的做法
在 iOS App 上儲存資料時你必須記住一點,UserDefaults
只是設計來儲存小量資料,像是使用者在 App 裡的偏好設定、或是一些完全不敏感的東西。要儲存 App 的敏感資料,我們應該使用 Apple 提供的 Security 服務。
Keychain Services API 可以幫你解決這些問題,老會提供 App 一個方法來存放小量的使用者資料,在一個名為 Keychain 的加密資料庫內。
在 Keychain 裡,你可以自由儲存密碼、以及一些使用者特別在意的機密資料,像是信用卡資訊或是機密的筆記內容。你也可以存放一些用 Certificate, Key, and Trust Services 管理的加密金鑰和憑證等。
Keychain Services API
下文我們將描述如何儲存你使用者的密碼於 Keychain 內。
class KeychainService { func save(_ password: String, for account: String) { let password = password.data(using: String.Encoding.utf8)! let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: account, kSecValueData as String: password] let status = SecItemAdd(query as CFDictionary, nil) guard status == errSecSuccess else { return print("save error") } }
在 Query Dictionary 的部分,kSecClass:kSecClassGenericPassword 表示這個項目是一個密碼,Keychain Services 從中瞭解資料需要加密。
然後,我們利用已創建的 Query 呼叫 SecItemAdd,來把新密碼加到 Keychain。
要恢復資料,步驟亦十分相似:
func retrivePassword(for account: String) -> String? { let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: account, kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnData as String: kCFBooleanTrue] var retrivedData: AnyObject? = nil let _ = SecItemCopyMatching(query as CFDictionary, &retrivedData) guard let data = retrivedData as? Data else {return nil} return String(data: data, encoding: String.Encoding.utf8) }
我們可以寫個簡單的測試,來確保資料正確被儲存並恢復。
func testPaswordRetrive() { let password = "123456" let account = "User" keyChainService.save(password, for: account) XCTAssertEqual(keyChainService.retrivePassword(for: account), password) }
在第一次使用 Keychain API 時可能會有點複雜,如果你必須儲存多於一個密碼的話,我建議你建立一個外觀模式 (Facade) 介面,助你以最佳方法在 App 的使用情境上來幫助你儲存及改動資料。
如果你想要了解更多關於外觀模式、以及如何為複雜的子系統建立簡單的 Wrapper 的話,可以閱讀這篇文章。
同時,有很多開源程式庫可以簡化 Keychain API 的使用,例如是 SAMKeychain 和 SwiftKeychainWrapper。
儲存密碼和執行權限
在 iOS 開發者的職涯裡,我亦看過很多人不停犯同樣的錯誤。
很多時候開發者不是在 App 裡儲存密碼方便重複使用,就是直接以使用者帳戶及密碼進行網路登入請求。如果你正直接儲存密碼在 UserDefault
裡的話,那麼從本篇文章的開頭你就應該知道有多危險了。
儲存密碼到 Keychain 可以將安全性提高,但儲存密碼和敏感資料時,無論是儲存到 Keychain 或其他地方,我們都應該先將資料加密。假如攻擊者可以駭入 Keychain 的安全防護、或是透過網路攻擊我們,那麼他就可以直接以原始文件形式取得我們的密碼;所以更好的方法是儲存密碼,並將密碼以雜湊 (Hash) 形式加密,以用於登入請求中。
加密敏感資料
自己來實作雜湊加密可以很複雜,而且有點矯枉過正;所以在這篇文章中,我們將會使用 iOS 開源套件 CryptoSwift 來實作,CryptoSwift 是在 Swift 中實作的標準和安全加密演算法一個不斷增長的集合。
現在讓我們來嘗試 CryptoSwift 提供的演算法,來儲存並取回在 Keychain 中的密碼。
func saveEncryptedPassword(_ password: String, for account: String) { let salt = Array("salty".utf8) let key = try! HKDF(password: Array(password.utf8), salt: salt, variant: .sha256).calculate().toHexString() keychainService.save(key, for: account) }
這個方法會提取一個帳號和密碼,並以加密後的雜湊字串(而不是直接用字串)儲存於 Keychain 裡。
讓我們來拆解一下這個方法。
- Salt 是一個獨一無二的字串,用來與密碼混合。
- 用
sha256
來完成 SHA-2 類型的雜湊加密 - HKDF 是一個簡單的金鑰推衍函式 (Key derivation function, KDF),建基於 金鑰雜湊訊息鑑別碼 (Hash-based message authentication code, HMAC)
我們建立了一個 Salt 字串,來減低我們的資料被攻擊的可能性。如果我們只針對密碼進行雜湊加密,那麼駭客可能會用一個常見密碼列表,製作出他們的雜湊值來比對我們所建立的;這樣一來,他們就可能輕易針對一個帳號找出其密碼。
現在我們可以授權予伺服器,去使用我們的帳號和客製化的金鑰,而不是直接使用密碼。
authManager.login(key, user)
當然,App 和伺服器應該共享相同的 Salt 字串,而後端亦需要使用相同的演算法來比較相同的金鑰,以驗證使用者身分。藉著這個方式,我們能夠將安全性提升到另一個層次,讓別人很難攻擊我們 App。
總結
為我們的 App 實作安全防護是個絕不能忽略的步驟。回顧我們的文章,我們一開始討論了儲存敏感資料到 UserDefaults 帶來的危險,以及需要儲存敏感資料到 Keychain 的原因。之後我們亦討論了要先加密後來儲存敏感資料,才能提升安全等級,也提到了共享敏感資料(在我們的範例中就是使用者身分)時,與伺服器溝通的正確方式。
如果你喜歡這篇文章,或是有任何疑問或意見,歡迎留言或是電郵到 [email protected]。你亦可以追蹤我的 Medium,以閱讀更多提升 iOS 開發者技能的文章。
電郵: [email protected]。
Medium: https://medium.com/@arlindaliu.dev