版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2019.01.01 星期二 |
前言
在這個信息爆炸的年代,特別是一些敏感的行業,比如金融業和銀行卡相關等等,這都對
app
的安全機制有更高的需求,很多大公司都有安全 部門,用于檢測自己產品的安全性,但是及時是這樣,安全問題仍然被不斷曝出,接下來幾篇我們主要說一下app
的安全機制。感興趣的看我上面幾篇。
1. APP安全機制(一)—— 幾種和安全性有關的情況
2. APP安全機制(二)—— 使用Reveal查看任意APP的UI
3. APP安全機制(三)—— Base64加密
4. APP安全機制(四)—— MD5加密
5. APP安全機制(五)—— 對稱加密
6. APP安全機制(六)—— 非對稱加密
7. APP安全機制(七)—— SHA加密
8. APP安全機制(八)—— 偏好設置的加密存儲
9. APP安全機制(九)—— 基本iOS安全之鑰匙鏈和哈希(一)
10. APP安全機制(十)—— 基本iOS安全之鑰匙鏈和哈希(二)
11. APP安全機制(十一)—— 密碼工具:提高用戶安全性和體驗(一)
12. APP安全機制(十二)—— 密碼工具:提高用戶安全性和體驗(二)
13. APP安全機制(十三)—— 密碼工具:提高用戶安全性和體驗(三)
開始
首先看下寫作環境
Swift 4.2, iOS 12, Xcode 10
在iOS的這篇教程中,您將學習如何與C語言API進行交互,以便在iOS Keychain
中安全地存儲密碼。
Apple Keychain
是Apple開發人員最重要的安全元素之一,它是一個用于存儲元數據和敏感信息的專用數據庫。使用Keychain是存儲對您的應用至關重要的小塊數據的最佳方式,例如秘密和密碼。
直接與Keychain
交互很復雜,特別是在Swift中。您必須使用Security框架,該框架主要使用C語言編寫。
有不同的Swift包裝器,允許您與Keychain進行交互。 Apple甚至提供了一款名為GenericKeychain的產品,讓您的生活更輕松。
雖然您可以輕松地使用第三方包裝器與Apple提供的不友好的API進行交互,但了解Keychain Services
可為您的開發人員工具帶添加一個有價值的工具。
在本教程中,您將深入研究Keychain Services API
并學習如何創建自己的包裝器,將其開發為iOS框架。
特別是,您將學習如何添加,修改,刪除和搜索通用和Internet密碼。此外,您將提供單元測試以驗證您的代碼是否按預期工作。
對于本教程,您將使用SecureStore
,這是一個樣板iOS框架,您可以在其中實現Keychain Services API
。
首先在Xcode中打開準備好的起始項目SecureStore.xcodeproj
。
為了讓您專注,初學者項目具有與實現已為您設置的包裝器相關的所有內容。
項目的結構應如下所示:
包裝器的代碼位于SecureStore
組文件夾中:
-
SecureStoreError.swift:包含一個枚舉,它表示您的包裝器可以處理的所有可能的錯誤。
SecureStoreError
遵循LocalizedError
協議,提供描述錯誤及其發生原因的本地化消息。 -
SecureStoreQueryable.swift:定義與文件同名的協議。
SecureStoreQueryable
強制實現者提供定義為類型為[String:Any]
的字典的query
屬性。在內部,您的API僅處理這些類型的對象。稍后會詳細介紹。 -
SecureStore.swift:定義您將在本教程中實現的包裝器。它提供了一個初始化程序和一組存根方法,用于從Keychain添加,更新,刪除和檢索您的密碼。使用者可以通過注入一些符合
SecureStoreQueryable
的類型來創建包裝器的實例。 -
InternetProtocol.swift:表示您可以處理的所有可能的
Internet
協議值。 - InternetAuthenticationType.swift:描述包裝器提供的身份驗證機制。
注意:依賴注入
(Dependency injection)
允許您編寫擴展和隔離功能的類。 對于一個非常簡單的概念來說,這是一個可怕的詞。 在本教程中,您將看到“ inject”
一詞,它指的是將整個對象傳遞給初始化程序。
除了框架代碼,您還應該有兩個其他文件夾:SecureStoreTests
和TestHost
。 前者包含您將隨框架一起提供的單元測試。 后者包含一個空的app,您將用它來測試您的框架API。
注意:通常,要測試您在教程中編寫的代碼,請在模擬器中運行應用程序。 如果不是這樣做,您將通過運行單元測試驗證您的代碼是否正常工作。 因此項目中的測試主機應用程序將無法在模擬器中運行;相反,它充當它為框架執行單元測試的容器。
在深入研究代碼之前,先看看一些理論!
An Overview of Keychain Services
為什么使用Keychain
而不是更簡單的解決方案?在UserDefaults
中存儲用戶的base-64
編碼密碼難道還不夠嗎?
當然不!攻擊者恢復以這種方式存儲的密碼是微不足道的。
Keychain Services
可幫助您代表用戶將項目或小塊數據安全地存儲到加密數據庫中。
從Apple的文檔中,SecKeychain類表示數據庫,而SecKeychainItem類表示項目。
根據您運行的操作系統,Keychain Services
的運行方式不同。
在iOS中,應用程序可以訪問包含iCloud Keychain
的單個Keychain
。鎖定和解鎖設備會自動鎖定和解鎖鑰匙串。這可以防止不必要的訪問。此外,應用程序只能訪問自己的項目或與其所屬的組共享的項目。
另一方面,macOS支持多個密鑰鏈。您通常依靠用戶使用Keychain Access
應用程序管理這些內容,并使用默認鑰匙串隱式工作。此外,您可以直接操作鑰匙串;例如,創建和管理嚴格專用于您的應用的鑰匙串。
如果要存儲密碼等密鑰,請將其打包為keychain item
。這是一種不透明類型,由兩部分組成:數據和一組屬性。在它插入新項目之前,Keychain Services
會對數據進行加密,然后將其與其屬性一起包裝。
使用屬性標識和存儲元數據或控制對存儲項的訪問。 將屬性指定為表示為CFDictionary
的字典的鍵和值。 您可以在Item Attribute Keys and Values中找到可用鍵的列表。 相應的值可以是字符串,數字,一些其他基本類型或與Security框架打包在一起的常量。
Keychain Services
提供特殊類型的屬性,允許您識別特定項目的類。 在本教程中,您將使用kSecClassGenericPassword和kSecClassInternetPassword來處理通用和Internet密碼。
每個類僅支持一組特殊的屬性。 換句話說,并非所有屬性都適用于特定的項目類。 您可以在相關的 item class value documentation中驗證它們。
注意:除了操縱密碼外,Apple還提供與其他類型項目(如證書,加密密鑰和身份)進行交互的機會。 它們分別由kSecClassCertificate,kSecClassKey和kSecClassIdentity類表示。
Diving Into Keychain Services API
由于代碼隱藏了來自惡意用戶的項目,因此Keychain Services
提供了一組與之交互的C函數。以下是您將用于操縱通用和互聯網密碼的API:
- SecItemAdd(::):使用此功能將一個或多個項目添加到鑰匙串。
- SecItemCopyMatching(::):此函數返回與搜索查詢匹配的一個或多個鑰匙串項。此外,它還可以復制特定鑰匙串項的屬性。
- SecItemUpdate(::):此函數允許您修改與搜索查詢匹配的項目。
- SecItemDelete(_:):此函數刪除與搜索查詢匹配的項目。
雖然上述函數使用不同的參數進行操作,但它們都返回表示為OSStatus
的結果代碼。這是一個32位有符號整數,它可以采用Item Return Result Keys中列出的值之一。
由于OSStatus
可能很難理解,因此Apple提供了一個名為SecCopyErrorMessageString(_:_ :)
的附加API,以獲取與這些狀態代碼對應的人類可讀字符串。
注意:除了添加,修改,刪除或搜索特定的鑰匙串項目外,Apple還提供導出和導入證書,密鑰和身份,甚至修改項目訪問控制的功能。 如果您想了解更多信息,請查看Keychain Items的文檔。
現在您已經掌握了Keychain Services
,在下一節中,您將學習如何刪除包裝器提供的存根方法。
Implementing Wrapper’s API
打開SecureStore.swift
并在setValue(_:for :)
中添加以下實現:
// 1
guard let encodedPassword = value.data(using: .utf8) else {
throw SecureStoreError.string2DataConversionError
}
// 2
var query = secureStoreQueryable.query
query[String(kSecAttrAccount)] = userAccount
// 3
var status = SecItemCopyMatching(query as CFDictionary, nil)
switch status {
// 4
case errSecSuccess:
var attributesToUpdate: [String: Any] = [:]
attributesToUpdate[String(kSecValueData)] = encodedPassword
status = SecItemUpdate(query as CFDictionary,
attributesToUpdate as CFDictionary)
if status != errSecSuccess {
throw error(from: status)
}
// 5
case errSecItemNotFound:
query[String(kSecValueData)] = encodedPassword
status = SecItemAdd(query as CFDictionary, nil)
if status != errSecSuccess {
throw error(from: status)
}
default:
throw error(from: status)
}
顧名思義,此方法允許為特定帳戶存儲新密碼。如果它無法更新或添加密碼,則會拋出SecureStoreError.unhandledError
,它為其指定本地化描述。
這是你的代碼所做的:
- 1) 檢查它是否可以將值編碼存儲為
Data
類型。如果這不可能,則會引發轉換錯誤。 - 2) 請求
secureStoreQueryable
實例執行查詢并附加您正在查找的帳戶。 - 3) 返回與查詢匹配的
keychain
項。 - 4) 如果查詢成功,則表示該帳戶的密碼已存在。在這種情況下,您使用
SecItemUpdate(_:_ :)
替換現有密碼的值。 - 5) 如果找不到項目,則該帳戶的密碼尚不存在。您可以通過調用
SecItemAdd(_:_ :)
來添加項目。
Keychain Services API
使用Core Foundation
類型。要使編譯器工作,必須從Core Foundation
類型轉換為Swift
類型,反之亦然。
在第一種情況下,由于每個鍵的屬性都是CFString
類型,因此它在查詢字典中作為鍵的用法需要強制轉換為String
。但是,從[String:Any]
到CFDictionary的轉換使您可以調用C函數。
現在是時候檢索你的密碼了。滾動下面剛剛實現的方法,并使用以下內容替換getValue(for :)
的實現:
// 1
var query = secureStoreQueryable.query
query[String(kSecMatchLimit)] = kSecMatchLimitOne
query[String(kSecReturnAttributes)] = kCFBooleanTrue
query[String(kSecReturnData)] = kCFBooleanTrue
query[String(kSecAttrAccount)] = userAccount
// 2
var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
SecItemCopyMatching(query as CFDictionary, $0)
}
switch status {
// 3
case errSecSuccess:
guard
let queriedItem = queryResult as? [String: Any],
let passwordData = queriedItem[String(kSecValueData)] as? Data,
let password = String(data: passwordData, encoding: .utf8)
else {
throw SecureStoreError.data2StringConversionError
}
return password
// 4
case errSecItemNotFound:
return nil
default:
throw error(from: status)
}
給定特定帳戶,此方法將檢索與其關聯的密碼。同樣,如果請求出現問題,代碼將拋出SecureStoreError.unhandledError
。
以下是您剛剛添加的代碼所發生的情況:
- 1) 請求
secureStoreQueryable
以執行查詢。除了添加您感興趣的帳戶外,還可以使用其他屬性及其相關值來豐富查詢。特別是,您要求它返回單個結果,以返回與該特定項關聯的所有屬性,并返回未加密的數據作為結果。 - 2) 使用
SecItemCopyMatching(_:_ :)
執行搜索。完成后,queryResult
將包含對找到的項的引用(如果可用)。withUnsafeMutablePointer(to:_ :)
使您可以訪問UnsafeMutablePointer
,您可以在閉包內使用和修改以存儲結果。 - 3) 如果查詢成功,則表示它找到了一個項目。由于結果由包含您要求的所有屬性的字典表示,因此您需要先提取數據,然后將其解碼為
Data
類型。 - 4) 如果找不到項目,則返回nil。
添加或檢索帳戶的密碼是不夠的。您還需要集成一種刪除密碼的方法。
找到removeValue(for :)
并添加此實現:
var query = secureStoreQueryable.query
query[String(kSecAttrAccount)] = userAccount
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw error(from: status)
}
要刪除密碼,請執行SecItemDelete(_ :)
,指定您要查找的帳戶。 如果您成功刪除了密碼,或者沒有找到任何項目,那么您的工作就完成了。 否則,您將拋出未處理的錯誤,以便讓用戶知道出錯的地方。
但是,如果要刪除與特定服務關聯的所有密碼,該怎么辦? 您的下一步是實現最終代碼以實現此目的。
找到removeAllValues()
并在其括號內添加以下代碼:
let query = secureStoreQueryable.query
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw error(from: status)
}
正如您將注意到的,除了傳遞給SecItemDelete(_ :)
函數的查詢之外,此方法與前一個方法類似。 在這種情況下,您可以獨立于用戶帳戶刪除密碼。
最后,構建框架以驗證所有內容是否正確編譯。
Connecting the Dots
到目前為止,您所做的所有工作都豐富了包裝器的添加,更新,刪除和檢索功能。 因此,您必須使用符合SecureStoreQueryable
的某種類型的實例創建包裝器。
由于您的第一個目標是同時處理通用密碼和互聯網密碼,因此您的下一步是創建兩個不同的配置,供消費者創建并注入您的包裝器。
首先,研究如何撰寫通用密碼的查詢。
打開SecureStoreQueryable.swift
并在SecureStoreQueryable
定義下面添加以下代碼:
public struct GenericPasswordQueryable {
let service: String
let accessGroup: String?
init(service: String, accessGroup: String? = nil) {
self.service = service
self.accessGroup = accessGroup
}
}
GenericPasswordQueryable
是一個簡單的結構,它接受服務和訪問組作為String
參數。
接下來,在GenericPasswordQueryable
定義下面添加以下擴展:
extension GenericPasswordQueryable: SecureStoreQueryable {
public var query: [String: Any] {
var query: [String: Any] = [:]
query[String(kSecClass)] = kSecClassGenericPassword
query[String(kSecAttrService)] = service
// Access group if target environment is not simulator
#if !targetEnvironment(simulator)
if let accessGroup = accessGroup {
query[String(kSecAttrAccessGroup)] = accessGroup
}
#endif
return query
}
}
要符合SecureStoreQueryable
協議,必須將query
實現為屬性。 該查詢表示您的包裝器能夠執行所選功能的方式。
查詢具有特定的鍵和值:
- 由鍵
kSecClass
表示的item
類具有值kSecClassGenericPassword
,因為您正在處理通用密碼。 這就是鑰匙串推斷數據是秘密的并且需要加密的方式。 - kSecAttrService設置為使用
GenericPasswordQueryable
的新實例注入的service
參數值。 - 最后,如果您的代碼沒有在模擬器上運行,您還可以將
kSecAttrAccessGroup
密鑰設置為提供的accessGroup
值。 這使您可以使用相同的訪問組在不同的應用程序之間共享項目。
接下來,構建框架以確保一切正常。
注意:對于類
kSecClassGenericPassword
的keychain
項,主鍵是kSecAttrAccount
和kSecAttrService
的組合。 換句話說,元組允許您在Keychain
中唯一標識通用密碼。
你閃亮的新包裝還沒有完成! 下一步是整合函數,允許使用者與互聯網密碼進行交互。
滾動到SecureStoreQueryable.swift
的末尾并添加以下內容:
public struct InternetPasswordQueryable {
let server: String
let port: Int
let path: String
let securityDomain: String
let internetProtocol: InternetProtocol
let internetAuthenticationType: InternetAuthenticationType
}
InternetPasswordQueryable
是一個結構體,可以幫助您在應用程序Keychain
中操作Internet Passwords
。
在遵守SecureStoreQueryable
之前,請花點時間了解您的API在這種情況下的工作方式。
如果用戶想要處理互聯網密碼,他們會創建一個新的InternetPasswordQueryable
實例,其中internetProtocol
和internetAuthenticationType
屬性綁定到特定域。
接下來,將以下內容添加到InternetPasswordQueryable
實現的下方:
extension InternetPasswordQueryable: SecureStoreQueryable {
public var query: [String: Any] {
var query: [String: Any] = [:]
query[String(kSecClass)] = kSecClassInternetPassword
query[String(kSecAttrPort)] = port
query[String(kSecAttrServer)] = server
query[String(kSecAttrSecurityDomain)] = securityDomain
query[String(kSecAttrPath)] = path
query[String(kSecAttrProtocol)] = internetProtocol.rawValue
query[String(kSecAttrAuthenticationType)] = internetAuthenticationType.rawValue
return query
}
}
如通用密碼情況所示,查詢具有特定的鍵和值:
- 由鍵
kSecClass
表示的item類的值為-kSecClassInternetPassword
,因為您現在正在與Internet
密碼進行交互。 -
kSecAttrPort
設置為port
參數。 -
kSecAttrServer
設置為server
參數。 -
kSecAttrSecurityDomain
設置為securityDomain
參數。 -
kSecAttrPath
設置為path
參數。 -
kSecAttrProtocol
綁定到internetProtocol
參數的rawValue
。 - 最后,
kSecAttrAuthenticationType
綁定到internetAuthenticationType
參數的rawValue
。
再次,構建以查看Xcode是否正確編譯。
注意:對于類
kSecClassInternetPassword
的keychain
項,主鍵是kSecAttrAccount
,kSecAttrSecurityDomain
,kSecAttrServer
,kSecAttrProtocol
,kSecAttrAuthenticationType
,kSecAttrPort和kSecAttrPath
的組合。 換句話說,這些值允許您在Keychain
中唯一標識Internet
密碼。
現在是時候看看你辛勤工作的結果了。 可是等等! 由于您沒有創建在模擬器上運行的應用程序,您將如何驗證它?
這是單元測試有用的地方。
后記
本篇主要講述了Keychain Services API使用簡單示例,感興趣的給個贊或者關注~~~