Valet 让您安全地将数据存储在 iOS、tvOS、watchOS 或 macOS 钥匙串中,而无需了解钥匙串的工作原理。 这很简单。 我们保证。
通过将以下内容添加到您的 Package.swift
,使用 Swift Package Manager 安装
dependencies: [
.package(url: "https://github.com/Square/Valet", from: "5.0.0"),
],
通过将以下内容添加到您的 Podfile
,使用 CocoaPods 安装
pod 'Valet', '~> 5.0.0'
通过将以下内容添加到您的 Cartfile
,使用 Carthage 安装
github "Square/Valet"
运行 carthage
以构建框架,并将构建的 Valet.framework
拖到您的 Xcode 项目中。
或者手动检出子模块,使用 git submodule add git@github.com:Square/Valet.git
,将 Valet.xcodeproj 拖到您的项目中,并将 Valet 添加为构建依赖项。
更喜欢通过观看视频学习? 请查看此视频教程。 请注意,此视频是在 Valet 4 发布期间录制的。
let myValet = Valet.valet(with: Identifier(nonEmpty: "Druidia")!, accessibility: .whenUnlocked)
VALValet *const myValet = [VALValet valetWithIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];
要开始使用 Valet 安全地存储数据,您需要创建一个 Valet 实例,并提供
Identifier
包装器类来强制执行非空约束。此 myValet
实例可用于在此设备上安全地存储和检索数据,但仅当设备解锁时才可使用。
您为 Valet 选择的标识符用于为 Valet 写入钥匙串的数据创建沙箱。 通过相同的初始化程序、访问权限值和标识符创建的两个相同类型的 Valet 将能够读取和写入相同的键值对; 具有不同标识符的 Valet 各自拥有自己的沙箱。 选择一个描述您的 Valet 将保护的数据类型的标识符。 您无需在 Valet 的标识符中包含您的应用程序名称或捆绑包标识符。
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 无法读取或写入彼此的键值对。 如果您在持久化键值对后更改了 Valet 的访问权限,则必须将具有不再需要的访问权限的 Valet 中的键值对迁移到具有所需访问权限的 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。 请注意,myValet
和 mySharedValet
无法读取或修改彼此的值,因为这两个 Valet 是使用不同的初始化程序创建的。 所有 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。 请注意,myValet
和 mySharedValet
无法读取或修改彼此的值,因为这两个 Valet 是使用不同的初始化程序创建的。 所有 Valet 类型都可以通过使用 sharedGroupValet
初始化程序在同一开发者编写的应用程序之间共享密钥。 请注意,在 macOS 上,groupPrefix
必须是 App ID 前缀。
与 Valet 一样,共享 iCloud Valet 可以使用额外的标识符创建,从而允许在同一共享组中存在多个独立沙箱化的钥匙串。
let myCloudValet = Valet.iCloudValet(with: Identifier(nonEmpty: "Druidia")!, accessibility: .whenUnlocked)
VALValet *const myCloudValet = [VALValet iCloudValetWithIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];
此实例可用于存储和检索数据,该数据可以由登录到同一 iCloud 帐户并启用 iCloud 钥匙串的其他设备上的此应用检索。 如果此设备上未启用 iCloud 钥匙串,则仍可以读取和写入密钥,但不会同步到其他设备。 请注意,myCloudValet
无法读取或修改 myValet
或 mySharedValet
中的值,因为 myCloudValet
是使用不同的初始化程序创建的。
共享 iCloud Valet 可以使用额外的标识符创建,从而允许在同一 iCloud 共享组中存在多个独立沙箱化的钥匙串。
let mySecureEnclaveValet = SecureEnclaveValet.valet(with: Identifier(nonEmpty: "Druidia")!, accessControl: .userPresence)
VALSecureEnclaveValet *const mySecureEnclaveValet = [VALSecureEnclaveValet valetWithIdentifier:@"Druidia" accessControl:VALAccessControlUserPresence];
此实例可用于在安全 Enclave 中存储和检索数据。 每次从该 Valet 检索数据时,都会提示用户通过 Face ID、Touch ID 或输入设备密码来确认其身份。 如果设备上未设置密码,则此实例将无法访问或存储数据。 当用户从设备中删除密码时,数据将从安全 Enclave 中删除。 使用 SecureEnclaveValet
存储数据是在 iOS、tvOS、watchOS 和 macOS 上存储数据的最安全方式。
let mySecureEnclaveValet = SinglePromptSecureEnclaveValet.valet(with: Identifier(nonEmpty: "Druidia")!, accessControl: .userPresence)
VALSinglePromptSecureEnclaveValet *const mySecureEnclaveValet = [VALSinglePromptSecureEnclaveValet valetWithIdentifier:@"Druidia" accessControl:VALAccessControlUserPresence];
此实例还在安全 Enclave 中存储和检索数据,但不需要用户每次检索数据时都确认其身份。 相反,仅在首次数据检索时提示用户确认其身份。 可以通过调用实例方法 requirePromptOnNextAccess()
强制 SinglePromptSecureEnclaveValet
实例在下次数据检索时提示用户。
为了让您的客户不会收到您的应用尚不支持 Face ID 的提示,您必须在应用的 Info.plist 中为 Privacy - Face ID Usage Description (NSFaceIDUsageDescription) 键设置一个值。
Valet 构建为线程安全的:可以在任何队列或线程上使用 Valet 实例。 Valet 实例确保与钥匙串通信的代码是原子性的 – 不可能通过在多个队列上同时读取和写入来损坏 Valet 中的数据。
但是,由于钥匙串实际上是磁盘存储,因此无法保证读取和写入项的速度很快 – 从主队列访问 Valet 实例可能会导致动画卡顿或 UI 阻塞。 因此,我们建议在后台队列中使用您的 Valet 实例; 像对待其他从磁盘读取和写入的代码一样对待 Valet。
已经在使用钥匙串并且不想再维护自己的钥匙串代码? 我们理解您。 这就是我们编写 migrateObjects(matching query: [String : AnyHashable], removeOnCompletion: Bool)
的原因。 此方法允许您在一行中将所有现有钥匙串条目迁移到 Valet 实例。 只需传入一个包含 kSecClass
、kSecAttrService
和您使用的任何其他 kSecAttr*
属性的字典 – 我们将为您迁移数据。 如果您需要更多地控制数据的迁移方式,请使用 migrateObjects(matching query: [String : AnyHashable], compactMap: (MigratableKeyValuePair<AnyHashable>) throws -> MigratableKeyValuePair<String>?)
以在迁移过程中过滤或重新映射键值对。
您的 macOS 应用程序必须具有钥匙串共享权利才能使用 Valet,即使您的应用程序不打算在应用程序之间共享钥匙串数据也是如此。 有关如何将钥匙串共享权利添加到您的应用程序的说明,请阅读 Apple 关于此主题的文档。 有关为什么存在此要求的更多信息,请参阅 issue #213。
如果您的 macOS 应用程序支持 macOS 10.14 或更早版本,则必须在从 Valet 读取值之前运行 myValet.migrateObjectsFromPreCatalina()
。 macOS Catalina 对 macOS 钥匙串引入了重大更改,要求使用 kSecAttrAccessible
或 kSecAttrAccessGroup
的 macOS 钥匙串项在 写入或访问 这些项时将 kSecUseDataProtectionKeychain
设置为 true
。 Valet 的 migrateObjectsFromPreCatalina()
升级在较旧的 macOS 设备或其他操作系统上输入到钥匙串中的项目,以包括键值对 kSecUseDataProtectionKeychain:true
。 请注意,在具有 iCloud 的设备之间共享钥匙串项的 Valet 免除此要求。 同样,SecureEnclaveValet
和 SinglePromptSecureEnclaveValet
也免除此要求。
Valet 保证,只要写入的数据有效且 canAccessKeychain()
返回 true
,读取和写入操作就会成功。 只有少数情况可能导致钥匙串无法访问
Accessibility
。 不当使用的示例包括在设备上未设置密码时使用 .whenPasscodeSetThisDeviceOnly
,或在后台运行时使用 .whenUnlocked
。SecureEnclaveValet
。 安全 Enclave 是随 A7 芯片 引入的,该芯片首次出现在 iPhone 5S、iPad Air 和 iPad Mini 2 中。好消息是:从旧版本的 Valet 升级时,大多数 Valet 配置都不需要迁移钥匙串数据。 所有 Valet 对象都与其先前版本的对应对象向后兼容。 配置已被 Apple 弃用的 Valet 将需要迁移存储的数据。
坏消息是:与以前的版本相比,有多个源代码破坏性 API 更改。
以下两个指南都解释了升级到 Valet 4 所需的更改。
VALSynchronizableValet
(允许将钥匙串同步到 iCloud)已被 Valet.iCloudValet(with:accessibility:)
(或 Objective-C 中的 +[VALValet iCloudValetWithIdentifier:accessibility:]
)取代。 请参阅上面的示例。VALAccessControl
已重命名为 SecureEnclaveAccessControl
(Objective-C 中的 VALSecureEnclaveAccessControl
)。 由于 Face ID 的引入,此枚举不再引用 TouchID
; 而是引用使用 biometric
进行解锁。Valet
、SecureEnclaveValet
和 SinglePromptSecureEnclaveValet
不再位于同一继承树中。 现在,所有三个都直接从 NSObject
继承,并使用组合来共享代码。 如果您以前依赖于子类层次结构,1) 这可能是一种代码异味 2) 考虑为您期望的共享行为声明一个协议,以使您更容易迁移到 Valet 3。您还需要继续阅读下面的从 Valet 3 迁移部分。
always
和 alwaysThisDeviceOnly
已从 Valet 中删除,因为 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 语句,或将 NSError
传递给每个 API 调用并在 Objective-C 中检查输出。 每个方法都清楚地记录了它可以 throw
的 Error
类型。 请参阅上面的示例。SharedGroupIdentifier(appIDPrefix:nonEmptyGroup:)
。 请参阅上面的示例。您还需要继续阅读下面的从 Valet 4 迁移部分。
throw
方法现在使用类型化 throws,这可能会使某些 catch
语句过时。SecureEnclaveValet
的 withPrompt
API 在 tvOS 和 watchOS 上被删除,因为最近的 API 更新表明此 API 从未真正在设备上显示提示。 添加了新的 API 来执行相同的操作,而无需自定义提示。SinglePromptSecureEnclaveValet
已从 watchOS 中删除,因为最近的 API 更新表明此 API 在 watchOS 上无法按预期工作。 如果您之前在 watchOS 上部署了 SinglePromptSecureEnclaveValet
,请在具有相同标识符和访问控制的 SecureEnclaveValet
上使用方法 migrateObjectsFromSinglePromptSecureEnclaveValet(removeOnCompletion:)
来迁移您现有的键值对。我们很高兴您对 Valet 感兴趣,并且我们很乐意看到您将其发展到何处。 请在提交拉取请求之前阅读我们的贡献指南。
谢谢,请务必尽情体验一下!