版本记录
版本号 | 时间 |
---|---|
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使用简单示例,感兴趣的给个赞或者关注~~~