Valet

CI Status Swift Package Manager compatible Carthage Compatibility codecov Swift Version Compatibility License Platform Version

Valet 让您安全地将数据存储在 iOS、tvOS、watchOS 或 macOS 钥匙串中,而无需了解钥匙串的工作原理。 这很简单。 我们保证。

入门指南

Swift Package Manager

通过将以下内容添加到您的 Package.swift,使用 Swift Package Manager 安装

dependencies: [
    .package(url: "https://github.com/Square/Valet", from: "5.0.0"),
],

CocoaPods

通过将以下内容添加到您的 Podfile,使用 CocoaPods 安装

pod 'Valet', '~> 5.0.0'

Carthage

通过将以下内容添加到您的 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 实例,并提供

myValet 实例可用于在此设备上安全地存储和检索数据,但仅当设备解锁时才可使用。

选择最佳标识符

您为 Valet 选择的标识符用于为 Valet 写入钥匙串的数据创建沙箱。 通过相同的初始化程序、访问权限值和标识符创建的两个相同类型的 Valet 将能够读取和写入相同的键值对; 具有不同标识符的 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 的标识符向其用户显示⚠️ ⚠️虽然可以显式设置用户友好的标识符,但请注意,这样做会绕过本项目保证的一种 Valet 类型将无法访问另一种类型的键值对的保证⚠️ ⚠️。 为了保持此保证,请确保每个 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)设置为应用程序 Entitlementskeychain-access-groups 键的值,其中 AppID12345 是应用程序的App ID 前缀。 当设备解锁时,可以访问此 Valet。 请注意,myValetmySharedValet 无法读取或修改彼此的值,因为这两个 Valet 是使用不同的初始化程序创建的。 所有 Valet 类型都可以通过使用 sharedGroupValet 初始化程序在同一开发者编写的应用程序之间共享密钥。

使用 App Groups 权利在多个应用程序之间共享密钥

let mySharedValet = Valet.sharedGroupValet(with: SharedGroupIdentifier(groupPrefix: "group", nonEmptyGroup: "Druidia")!, accessibility: .whenUnlocked)
VALValet *const mySharedValet = [VALValet sharedGroupValetWithGroupPrefix:@"group" sharedGroupIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];

此实例可用于在同一开发者编写的任何应用程序中安全地存储和检索数据,这些应用程序已将 group.Druidia 设置为应用程序 Entitlementscom.apple.security.application-groups 键的值。 当设备解锁时,可以访问此 Valet。 请注意,myValetmySharedValet 无法读取或修改彼此的值,因为这两个 Valet 是使用不同的初始化程序创建的。 所有 Valet 类型都可以通过使用 sharedGroupValet 初始化程序在同一开发者编写的应用程序之间共享密钥。 请注意,在 macOS 上,groupPrefix 必须是 App ID 前缀

与 Valet 一样,共享 iCloud Valet 可以使用额外的标识符创建,从而允许在同一共享组中存在多个独立沙箱化的钥匙串。

通过 iCloud 在设备之间共享密钥

let myCloudValet = Valet.iCloudValet(with: Identifier(nonEmpty: "Druidia")!, accessibility: .whenUnlocked)
VALValet *const myCloudValet = [VALValet iCloudValetWithIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];

此实例可用于存储和检索数据,该数据可以由登录到同一 iCloud 帐户并启用 iCloud 钥匙串的其他设备上的此应用检索。 如果此设备上未启用 iCloud 钥匙串,则仍可以读取和写入密钥,但不会同步到其他设备。 请注意,myCloudValet 无法读取或修改 myValetmySharedValet 中的值,因为 myCloudValet 是使用不同的初始化程序创建的。

共享 iCloud Valet 可以使用额外的标识符创建,从而允许在同一 iCloud 共享组中存在多个独立沙箱化的钥匙串。

使用 Face ID、Touch ID 或设备密码保护密钥

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。

将现有钥匙串值迁移到 Valet 中

已经在使用钥匙串并且不想再维护自己的钥匙串代码? 我们理解您。 这就是我们编写 migrateObjects(matching query: [String : AnyHashable], removeOnCompletion: Bool) 的原因。 此方法允许您在一行中将所有现有钥匙串条目迁移到 Valet 实例。 只需传入一个包含 kSecClasskSecAttrService 和您使用的任何其他 kSecAttr* 属性的字典 – 我们将为您迁移数据。 如果您需要更多地控制数据的迁移方式,请使用 migrateObjects(matching query: [String : AnyHashable], compactMap: (MigratableKeyValuePair<AnyHashable>) throws -> MigratableKeyValuePair<String>?) 以在迁移过程中过滤或重新映射键值对。

将 Valet 集成到 macOS 应用程序中

您的 macOS 应用程序必须具有钥匙串共享权利才能使用 Valet,即使您的应用程序不打算在应用程序之间共享钥匙串数据也是如此。 有关如何将钥匙串共享权利添加到您的应用程序的说明,请阅读 Apple 关于此主题的文档。 有关为什么存在此要求的更多信息,请参阅 issue #213

如果您的 macOS 应用程序支持 macOS 10.14 或更早版本,则必须在从 Valet 读取值之前运行 myValet.migrateObjectsFromPreCatalina()。 macOS Catalina 对 macOS 钥匙串引入了重大更改,要求使用 kSecAttrAccessiblekSecAttrAccessGroup 的 macOS 钥匙串项在 写入或访问 这些项时将 kSecUseDataProtectionKeychain 设置为 true。 Valet 的 migrateObjectsFromPreCatalina() 升级在较旧的 macOS 设备或其他操作系统上输入到钥匙串中的项目,以包括键值对 kSecUseDataProtectionKeychain:true。 请注意,在具有 iCloud 的设备之间共享钥匙串项的 Valet 免除此要求。 同样,SecureEnclaveValetSinglePromptSecureEnclaveValet 也免除此要求。

调试

Valet 保证,只要写入的数据有效且 canAccessKeychain() 返回 true,读取和写入操作就会成功。 只有少数情况可能导致钥匙串无法访问

  1. 为您的用例使用错误的 Accessibility。 不当使用的示例包括在设备上未设置密码时使用 .whenPasscodeSetThisDeviceOnly,或在后台运行时使用 .whenUnlocked
  2. 当共享访问组标识符不在您的权利文件中时,使用共享访问组 Valet 初始化 Valet。
  3. 在没有安全 Enclave 的 iOS 设备上使用 SecureEnclaveValet。 安全 Enclave 是随 A7 芯片 引入的,该芯片首次出现在 iPhone 5S、iPad Air 和 iPad Mini 2 中。
  4. 从 Xcode 在 DEBUG 中运行您的应用。 Xcode 有时无法正确签名您的应用,这会导致由于权利而无法访问钥匙串。 如果您遇到此问题,只需再次点击 Xcode 中的“运行”。 此签名问题不会在正确签名(非 DEBUG)的构建中发生。
  5. 在设备上或模拟器中运行您的应用,并附加调试器也可能在从钥匙串读取或写入时导致返回权利错误。 要解决设备上的此问题,请在不附加调试器的情况下运行应用。 在不附加调试器的情况下运行一次后,钥匙串通常会在附加调试器的情况下正常运行几次,然后再需要重复此过程。
  6. 在没有 application-identifier 权利的情况下运行您的应用或单元测试。 Xcode 8 引入了一项要求,即所有 scheme 必须使用 application-identifier 权利签名才能访问钥匙串。 为了在运行单元测试时满足此要求,您的单元测试必须在宿主应用程序内部运行。
  7. 尝试写入大于 4kb 的数据。 钥匙串旨在安全地存储小型密钥 – Apple 的安全守护程序不支持写入大型 blob。

要求

从以前的 Valet 版本迁移

好消息是:从旧版本的 Valet 升级时,大多数 Valet 配置都需要迁移钥匙串数据。 所有 Valet 对象都与其先前版本的对应对象向后兼容。 配置已被 Apple 弃用的 Valet 将需要迁移存储的数据。

坏消息是:与以前的版本相比,有多个源代码破坏性 API 更改。

以下两个指南都解释了升级到 Valet 4 所需的更改。

从 Valet 2 迁移

  1. Swift 和 Objective-C 中的初始化程序都已更改 - 两种语言现在都使用类方法,这感觉在语义上更诚实(很多时候您不是实例化新的 Valet,而是在重新访问您已创建的 Valet)。 请参阅上面的用法示例
  2. VALSynchronizableValet(允许将钥匙串同步到 iCloud)已被 Valet.iCloudValet(with:accessibility:)(或 Objective-C 中的 +[VALValet iCloudValetWithIdentifier:accessibility:])取代。 请参阅上面的示例
  3. VALAccessControl 已重命名为 SecureEnclaveAccessControl(Objective-C 中的 VALSecureEnclaveAccessControl)。 由于 Face ID 的引入,此枚举不再引用 TouchID; 而是引用使用 biometric 进行解锁。
  4. ValetSecureEnclaveValetSinglePromptSecureEnclaveValet 不再位于同一继承树中。 现在,所有三个都直接从 NSObject 继承,并使用组合来共享代码。 如果您以前依赖于子类层次结构,1) 这可能是一种代码异味 2) 考虑为您期望的共享行为声明一个协议,以使您更容易迁移到 Valet 3。

您还需要继续阅读下面的从 Valet 3 迁移部分。

从 Valet 3 迁移

  1. 访问权限值 alwaysalwaysThisDeviceOnly 已从 Valet 中删除,因为 Apple 已弃用其对应项(请参阅 kSecAttrAccessibleAlwayskSecAttrAccessibleAlwaysThisDeviceOnly 的文档)。 要迁移使用 always 访问权限存储的值,请在新首选访问权限的 Valet 上使用方法 migrateObjectsFromAlwaysAccessibleValet(removeOnCompletion:)。 要迁移使用 alwaysThisDeviceOnly 访问权限存储的值,请在新首选访问权限的 Valet 上使用方法 migrateObjectsFromAlwaysAccessibleThisDeviceOnlyValet(removeOnCompletion:)
  2. 大多数返回可选值或 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 中检查输出。 每个方法都清楚地记录了它可以 throwError 类型。 请参阅上面的示例
  3. 用于创建可以使用钥匙串共享访问组在应用程序之间共享密钥的 Valet 的类方法已更改。 为了防止在罕见情况下错误检测到 App ID 前缀,现在必须将 App ID 前缀显式传递到这些方法中。 要创建共享访问组 Valet,您需要创建一个 SharedGroupIdentifier(appIDPrefix:nonEmptyGroup:)请参阅上面的示例

您还需要继续阅读下面的从 Valet 4 迁移部分。

从 Valet 4 迁移

  1. 大多数 throw 方法现在使用类型化 throws,这可能会使某些 catch 语句过时。
  2. SecureEnclaveValetwithPrompt API 在 tvOS 和 watchOS 上被删除,因为最近的 API 更新表明此 API 从未真正在设备上显示提示。 添加了新的 API 来执行相同的操作,而无需自定义提示。
  3. SinglePromptSecureEnclaveValet 已从 watchOS 中删除,因为最近的 API 更新表明此 API 在 watchOS 上无法按预期工作。 如果您之前在 watchOS 上部署了 SinglePromptSecureEnclaveValet,请在具有相同标识符和访问控制的 SecureEnclaveValet 上使用方法 migrateObjectsFromSinglePromptSecureEnclaveValet(removeOnCompletion:) 来迁移您现有的键值对。

贡献

我们很高兴您对 Valet 感兴趣,并且我们很乐意看到您将其发展到何处。 请在提交拉取请求之前阅读我们的贡献指南

谢谢,请务必尽情体验一下!