Valet
Valet 允许您在没有了解Keychain工作原理的情况下安全地在iOS、tvOS、watchOS或macOS Keychain中存储数据。很简单。我们保证。
开始使用
CocoaPods
通过将以下内容添加到您的 Podfile
来使用 CocoaPods 进行安装
在iOS上
platform :ios, '9.0'
use_frameworks!
pod 'Valet'
在tvOS上
platform :tvos, '9.0'
use_frameworks!
pod 'Valet'
在watchOS上
platform :watchos, '2.0'
use_frameworks!
pod 'Valet'
在macOS上
platform :osx, '10.11'
use_frameworks!
pod 'Valet'
Carthage
使用Carthage进行安装,将以下内容添加到您的Cartfile
:
github "Square/Valet"
运行carthage
来构建框架,并将构建好的Valet.framework
拖动到您的Xcode项目中。
Swift Package Manager
使用Swift Package Manager进行安装,将以下内容添加到您的Package.swift
:
dependencies: [
.package(url: "https://github.com/Square/Valet", from: "4.0.0"),
],
子模块
或者,通过执行以下命令手动检出子模块:git submodule add [email protected]:Square/Valet.git
,将Valet.xcodeproj拖动到您的项目中,并将Valet添加为构建依赖项。
使用方法
喜欢通过观看视频学习吗?请查看这个视频教程。
基本初始化
let myValet = Valet.valet(with: Identifier(nonEmpty: "Druidia")!, accessibility: .whenUnlocked)
VALValet *const myValet = [VALValet valetWithIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];
要开始使用Valet安全地存储数据,您需要使用以下步骤创建一个Valet实例:
- 一个标识符 - 一个非空字符串,用于标识这个Valet。Swift API使用一个
Identifier
包装类来强制执行非空约束。 - 一个可访问性值 - 一个枚举(Accessibility),用于定义您将何时能够持久化和检索数据。
此myValet
实例可以用来在此设备上安全地存储和检索数据,但仅限设备解锁时。
选择最佳标识符
您为Valet选择的标识符用于创建一个沙盒,用于Valet写入钥匙串的数据。通过同一个初始化器、可访问性值和标识符创建的同类型两个Valet将能够读取和写入相同的key:value对;具有不同标识符的Valet各自拥有自己的沙盒。选择一个标识符,以描述您的Valet将保护的数据类型。您不需要在Valet的标识符中包含您的应用程序名称或包标识符。
在macOS中选择用户友好的标识符
let myValet = Valet.valet(withExplicitlySet: Identifier(nonEmpty: "Druidia")!, accessibility: .whenUnlocked)
VALValet *const myValet = [VALValet valetWithExplicitlySetIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];
开发者ID签名的Mac应用程序可能会在用户界面中向用户显示其Valet的标识符显示。
选择最佳可访问性值
Accessibility 枚举用于确定您的密钥何时可以被访问。使用可能的最高访问级别让您的应用正常运行是一个好主意。例如,如果您的应用不在后台运行,您将想确保通过使用 .whenUnlocked
或 .whenUnlockedThisDeviceOnly
,只有当手机解锁时才能读取密钥。
在持久化数据后更改访问性值
let myOldValet = Valet.valet(withExplicitlySet: Identifier(nonEmpty: "Druidia")!, accessibility: .whenUnlocked)
let myNewValet = Valet.valet(withExplicitlySet: Identifier(nonEmpty: "Druidia")!, accessibility: .afterFirstUnlock)
try? myNewValet.migrateObjects(from: myOldValet, removeOnCompletion: true)
VALValet *const myOldValet = [VALValet valetWithExplicitlySetIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];
VALValet *const myNewValet = [VALValet valetWithExplicitlySetIdentifier:@"Druidia" accessibility:VALAccessibilityAfterFirstUnlock];
[myNewValet migrateObjectsFrom:myOldValet removeOnCompletion:true error:nil];
Valet 类型、标识符、访问性值以及用来创建 Valet 的初始化器组合在一起,在密钥链中创建一个沙盒。这种行为确保不同的 Valet 无法读取或写入彼此的 key:value 对。如果您在持久化 key:value 对之后更改 Valet 的访问性,为了避免数据丢失,必须从具有不再需要的访问性的 Valet 将 key:value 对迁移到具有期望访问性的 Valet。
读取和写入
let username = "Skroob"
try? myValet.setString("12345", forKey: username)
let myLuggageCombination = myValet.string(forKey: username)
NSString *const username = @"Skroob";
[myValet setString:@"12345" forKey:username error:nil];
NSString *const myLuggageCombination = [myValet stringForKey:username error:nil];
除了允许存储字符串外,Valet 还允许通过 setObject(_ object: Data, forKey key: Key)
和 object(forKey key: String)
存储通过 Data
对象。通过不同的类类型、不同的初始化器或不同的访问性属性创建的 Valet 将无法读取或修改 myValet
中的值。
使用密钥链共享权限在多个应用程序之间共享秘密
let mySharedValet = Valet.sharedGroupValet(with: SharedGroupIdentifier(appIDPrefix: "AppID12345", nonEmptyGroup: "Druidia")!, accessibility: .whenUnlocked)
VALValet *const mySharedValet = [VALValet sharedGroupValetWithAppIDPrefix:@"AppID12345" sharedGroupIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];
此实例可用于在不同的应用中安全地存储和检索数据,这些应用由具有 AppID12345.Druidia
(或 $(AppIdentifierPrefix)Druidia
)作为 Entitlements
中 keychain-access-groups
键值的开发人员编写,其中 AppID12345
是应用程序的 App ID 前缀。当设备解锁时,可访问此Valet服务。请注意,由于两个Valet使用的是不同的初始化器,因此 myValet
和 mySharedValet
不能读取或修改另一个的值。所有Valet类型都可以通过使用 sharedGroupValet
初始化器在相同开发人员编写的应用之间共享机密。
使用应用组权限在多个应用间共享机密
let mySharedValet = Valet.sharedGroupValet(with: SharedGroupIdentifier(groupPrefix: "group", nonEmptyGroup: "Druidia")!, accessibility: .whenUnlocked)
VALValet *const mySharedValet = [VALValet sharedGroupValetWithGroupPrefix:@"group" sharedGroupIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];
此实例可用于在不同的应用中安全地存储和检索数据,这些应用由具有 group.Druidia
作为 Entitlements
中 com.apple.security.application-groups
键值的开发人员编写。当设备解锁时,可访问此Valet服务。请注意,由于两个Valet使用的是不同的初始化器,因此 myValet
和 mySharedValet
不能读取或修改另一个的值。所有Valet类型都可以通过使用 sharedGroupValet
初始化器在相同开发人员编写的应用之间共享机密。注意,在macOS上,groupPrefix
必须是 App ID 前缀 。
与Valet类似,共享iCloud Valet可以通过额外的标识符创建,允许在同一个共享组内存在多个独立沙盒化的keychain。
通过iCloud在多个设备间共享机密
let myCloudValet = Valet.iCloudValet(with: Identifier(nonEmpty: "Druidia")!, accessibility: .whenUnlocked)
VALValet *const myCloudValet = [VALValet iCloudValetWithIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];
此实例可用于存储和检索数据,这些数据可以通过在已启用电云钥匙串的同一iCloud账户的其他设备上登录此应用来检索。如果在此设备上未启用iCloud Keychain,机密仍然可以读取和写入,但不会同步到其他设备。请注意,由于 myCloudValet
使用的是不同的初始化器,因此无法通过 myCloudValet
读取或修改 myValet
或 mySharedValet
中的值。
可以用额外的标识符创建共享iCloud Valet,允许在同一个iCloud共享组内存在多个独立沙盒化的keychain。
使用人脸识别、触控ID或设备密码保护秘密
let mySecureEnclaveValet = SecureEnclaveValet.valet(with: Identifier(nonEmpty: "Druidia")!, accessControl: .userPresence)
VALSecureEnclaveValet *const mySecureEnclaveValet = [VALSecureEnclaveValet valetWithIdentifier:@"Druidia" accessControl:VALAccessControlUserPresence];
此实例可用于在Secure Enclave中存储和检索数据。每次从该临时存储检索数据时,都会提示用户通过人脸识别、触控ID或输入设备密码来确认其存在。 如果设备上没有设置密码,则此实例将无法访问或存储数据。 当用户从设备中移除密码时,将删除Secure Enclave中的数据。使用SecureEnclaveValet
存储数据是iOS、tvOS、watchOS和macOS上存储数据的最高安全方式。
let mySecureEnclaveValet = SinglePromptSecureEnclaveValet.valet(with: Identifier(nonEmpty: "Druidia")!, accessControl: .userPresence)
VALSinglePromptSecureEnclaveValet *const mySecureEnclaveValet = [VALSinglePromptSecureEnclaveValet valetWithIdentifier:@"Druidia" accessControl:VALAccessControlUserPresence];
此实例也存储和检索Secure Enclave中的数据,但在每次检索数据时不需要用户确认其存在。相反,用户只有在首次检索数据时才会被提示确认其存在。可以通过调用实例方法requirePromptOnNextAccess()
来强制一个SinglePromptSecureEnclaveValet
实例在下次数据检索时提示用户。
为了防止您的客户收到您的应用程序尚未支持人脸识别的提示,必须在您的应用程序的Info.plist中为Privacy - Face ID Usage Description (NSFaceIDUsageDescription)键设置一个值。
线程安全
临时存储构建为线程安全:可以在任何队列或线程上使用临时存储实例。临时存储实例确保与密钥链通信的代码是原子的 - 不可能通过同时在多个队列上读写而损坏临时存储中的数据。
然而,因为密钥链实际上是磁盘存储,所以不能保证读写项的速度快 - 从主队列访问临时存储实例可能会导致动画中断或界面阻塞。因此,我们建议在后台队列上使用您的临时存储实例;将临时存储视为您处理其他从磁盘读取和写入的代码。
将现有Keychain值迁移到Valet
已经在使用Keychain并且不再想维护自己的Keychain代码?我们理解你的感受。这就是我们编写了migrateObjects(matching query: [String : AnyHashable], removeOnCompletion: Bool)
方法的原因。此方法允许您在一行中将所有现有的Keychain条目迁移到Valet实例。只需传入包含kSecClass
,kSecAttrService
以及您使用的任何其他kSecAttr*
属性的字典——我们会为您迁移数据。如果您需要更多控制迁移数据的方式,请使用migrateObjects(matching query: [String : AnyHashable], compactMap: (MigratableKeyValuePair<AnyHashable>) throws -> MigratableKeyValuePair<String>?)
来过滤或重映射键值对,作为迁移过程的一部分。
将Valet集成到macOS应用程序中
您的macOS应用程序必须拥有Keychain Sharing授权才能使用Valet,即使您的应用程序并不打算在应用程序之间共享keychain数据。有关如何将Keychain Sharing授权添加到应用程序的说明,请阅读苹果关于该主题的文档。有关存在该要求的原因的更多信息,请参阅问题#213。
如果您的macOS应用程序支持macOS 10.14或更低版本,您必须在从Valet读取值之前运行myValet.migrateObjectsFromPreCatalina()
。macOS Catalina对该macOS keychain进行了破坏性更改,要求使用kSecAttrAccessible
或kSecAttrAccessGroup
的macOS keychain项在写入或访问这些项时必须将kSecUseDataProtectionKeychain
设置为true
(当写入或访问时)。Valet的migrateObjectsFromPreCatalina()
将升级在旧版macOS设备或其他操作系统上输入的keychain项,包括键值对kSecUseDataProtectionKeychain:true
。请注意,与iCloud共享keychain项的Valet免于此要求。同样,SecureEnclaveValet
和SinglePromptSecureEnclaveValet
也免于此要求。
调试
Valert确保只要写入的数据有效并且canAccessKeychain()
返回true
,读写操作就会成功。只有少数情况会导致密钥链不可访问
- 使用与您用例不匹配的
Accessibility
。不正确的使用示例包括在设备上未设置密码时使用.whenPasscodeSetThisDeviceOnly
,或在后台运行时使用.whenUnlocked
。 - 使用共享访问组Valert初始化Valet,而共享访问组标识符不在您的权限文件中。
- 在未配备Secure Enclave的iOS设备上使用
SecureEnclaveValet
。Secure Enclave是在A7芯片上引入的,该芯片首次出现在iPhone 5S、iPad Air和iPad Mini 2上。 - 从Xcode中在DEBUG模式下运行您的应用程序。Xcode有时无法正确签 nome seu应用,导致由于权限问题而无法访问密钥链。如果您遇到此问题,只需再次在Xcode中执行Run。此签名问题不会在正确签名的(非DEBUG)构建中出现。
- 在设备或模拟器上,带有调试器的应用程序运行可能会导致从密钥链读取或写入时返回权限错误。为了解决设备上的此问题,不附加调试器运行应用程序。在不附加调试器运行一次后,密钥链通常会在附加调试器运行几次后(在需要重复此过程之前)行为正确。
- 在没有应用程序-标识符权限的情况下运行应用程序或单元测试。Xcode 8引入了所有方案都必须使用应用程序-标识符权限进行签名的需求,以便访问密钥链。为了在运行单元测试时满足此要求,您必须在宿主应用程序内部运行单元测试。
- 尝试写入大于4kb的数据。密钥链旨在安全地存储小秘密-不支持苹果安全守护程序的写入大型数据块。
要求
- Xcode 11.0或更高版本。Xcode 10和Xcode 9需要
。Xcode的早期版本需要
。
- iOS 9或更高版本。
- tvOS 9或更高版本。
- watchOS 2或更高版本。
- macOS 10.11或更高版本。
从早期Valet版本迁移
好消息:大多数Valet配置在从旧版本升级时不需要迁移密钥链数据。所有Valet对象都与其早期版本兼容。我们进行了全面的单元测试以证明这一点(搜索test_backwardsCompatibility
)。对于那些被Apple弃用的配置的Valet,需要迁移存储的数据。
坏消息:先前版本存在多个破坏源API变化。
以下两个指南都解释了升级到Valet 4所需的变化。
从Valet 2迁移
- 初始化器在Swift和Objective-C中都已更改 - 这两种语言现在都使用类方法,感觉更符合语义(大多数情况下,你并不是实例化一个新的Valet,而是重新访问已经创建的一个)。 请参见上面的示例使用。
VALSynchronizableValet
(允许密钥链同步到iCloud)已被替换为Valet.iCloudValet(with:accessibility:)
(或Objective-C中的+[VALValet iCloudValetWithIdentifier:accessibility:]
)。 请参见上面的示例。VALAccessControl
已被重命名为SecureEnclaveAccessControl
(Objective-C中的VALSecureEnclaveAccessControl
)。此枚举不再引用TouchID
;相反,它引用使用生物识别
解锁,这是由于Face ID的引入。Valet
、SecureEnclaveValet
和SinglePromptSecureEnclaveValet
不再处于相同的继承树中。现在,所有三个都直接从NSObject
继承,并使用组合来共享代码。如果你以前依赖于子类层次结构,1)那可能是一个代码 smells 2)考虑声明一个协议,用于你期望的共享行为,以便使迁移到Valet 3更容易。
你还需要继续阅读下面的从Valet 3迁移部分。
从Valet 3迁移
- 已从Valet中移除“总是”(
always
)和“总是此设备仅”(alwaysThisDeviceOnly
)可访问性值,因为Apple已弃用其对应项(请参阅有关kSecAttrAccessibleAlways和kSecAttrAccessibleAlwaysThisDeviceOnly)的文档)。要迁移使用always
可访问性存储的值,请在具有您新的首选可访问性的Valet上使用方法migrateObjectsFromAlwaysAccessibleValet(removeOnCompletion:)
。要迁移使用alwaysThisDeviceOnly
可访问性存储的值,请在具有您新的首选可访问性的Valet上使用方法migrateObjectsFromAlwaysAccessibleThisDeviceOnlyValet(removeOnCompletion:)
。 - 大多数返回可选或
Bool
值的API已迁移到在遇到错误时返回非可选内容并抛出错误。忽略可以由每个API抛出的错误将保持您的代码流程行为与之前相同。以下是一个示例:在Swift中,let secret: String? = myValet.string(forKey: myKey)
变为let secret: String? = try? myValet.string(forKey: myKey)
。在Objective-C中,NSString *const secret = [myValet stringForKey:myKey];
变为NSString *const secret = [myValet stringForKey:myKey error:nil];
。如果您对未返回数据的原因感兴趣,请在Swift中使用do-catch语句,或者在Objective-C中为每个API调用传递一个NSError
并检查输出。每个方法明确记录它能throw
的Error
类型。请参阅上面的示例。 - 用于创建可以在应用程序之间通过密钥链共享访问组共享秘密的Valet的类方法已更改。为了防止在罕见情况下错误地检测到App ID前缀(请参阅相关pull request),现在必须将这些方法显式传入App ID前缀。要创建共享访问组Valet,您需要创建一个
SharedGroupIdentifier(appIDPrefix:nonEmptyGroup:)
。请参阅上面的示例。
贡献
我们很高兴你对Valet感兴趣,并期待看到你将它带向何方。在提交Pull Request之前,请阅读我们的贡献指南。
感谢,请尽情使用它吧!