KeychainAccess

Build Status Carthage compatible SPM supported Version Platform

KeychainAccess 是一个简单的 Swift Keychain 封装,可在 iOS 和 macOS 上使用。 使 Keychain API 的使用变得非常容易,并且更适合在 Swift 中使用。

💡 特性

📖 用法

👀 另请参阅

🔑 基础知识

保存应用程序密码

let keychain = Keychain(service: "com.example.github-token")
keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"

保存互联网密码

let keychain = Keychain(server: "https://github.com", protocolType: .https)
keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"

🔑 实例化

创建用于应用程序密码的 Keychain

let keychain = Keychain(service: "com.example.github-token")
let keychain = Keychain(service: "com.example.github-token", accessGroup: "12ABCD3E4F.shared")

创建用于互联网密码的 Keychain

let keychain = Keychain(server: "https://github.com", protocolType: .https)
let keychain = Keychain(server: "https://github.com", protocolType: .https, authenticationType: .htmlForm)

🔑 添加项目

下标 (subscripting)

用于 String
keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
keychain[string: "kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
用于 NSData
keychain[data: "secret"] = NSData(contentsOfFile: "secret.bin")

set 方法

keychain.set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")

错误处理

do {
    try keychain.set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
}
catch let error {
    print(error)
}

🔑 获取项目

下标 (subscripting)

用于 String (如果该值为 NSData,则尝试转换为 String)
let token = keychain["kishikawakatsumi"]
let token = keychain[string: "kishikawakatsumi"]
用于 NSData
let secretData = keychain[data: "secret"]

get 方法

作为 String
let token = try? keychain.get("kishikawakatsumi")
let token = try? keychain.getString("kishikawakatsumi")
作为 NSData
let data = try? keychain.getData("kishikawakatsumi")

🔑 删除项目

下标 (subscripting)

keychain["kishikawakatsumi"] = nil

remove 方法

do {
    try keychain.remove("kishikawakatsumi")
} catch let error {
    print("error: \(error)")
}

🔑 设置标签和注释

let keychain = Keychain(server: "https://github.com", protocolType: .https)
do {
    try keychain
        .label("github.com (kishikawakatsumi)")
        .comment("github access token")
        .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
} catch let error {
    print("error: \(error)")
}

🔑 获取其他属性

PersistentRef

let keychain = Keychain()
let persistentRef = keychain[attributes: "kishikawakatsumi"]?.persistentRef
...

创建日期

let keychain = Keychain()
let creationDate = keychain[attributes: "kishikawakatsumi"]?.creationDate
...

所有属性

let keychain = Keychain()
do {
    let attributes = try keychain.get("kishikawakatsumi") { $0 }
    print(attributes?.comment)
    print(attributes?.label)
    print(attributes?.creator)
    ...
} catch let error {
    print("error: \(error)")
}
下标 (subscripting)
let keychain = Keychain()
if let attributes = keychain[attributes: "kishikawakatsumi"] {
    print(attributes.comment)
    print(attributes.label)
    print(attributes.creator)
}

🔑 配置(辅助功能、共享、iCloud 同步)

提供流畅的接口

let keychain = Keychain(service: "com.example.github-token")
    .label("github.com (kishikawakatsumi)")
    .synchronizable(true)
    .accessibility(.afterFirstUnlock)

辅助功能

默认辅助功能匹配后台应用程序 (=kSecAttrAccessibleAfterFirstUnlock)
let keychain = Keychain(service: "com.example.github-token")
对于后台应用程序
创建实例
let keychain = Keychain(service: "com.example.github-token")
    .accessibility(.afterFirstUnlock)

keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
一次性
let keychain = Keychain(service: "com.example.github-token")

do {
    try keychain
        .accessibility(.afterFirstUnlock)
        .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
} catch let error {
    print("error: \(error)")
}
对于前台应用程序
创建实例
let keychain = Keychain(service: "com.example.github-token")
    .accessibility(.whenUnlocked)

keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
一次性
let keychain = Keychain(service: "com.example.github-token")

do {
    try keychain
        .accessibility(.whenUnlocked)
        .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
} catch let error {
    print("error: \(error)")
}

👫 共享 Keychain 项目

let keychain = Keychain(service: "com.example.github-token", accessGroup: "12ABCD3E4F.shared")

🔄 使用 iCloud 同步 Keychain 项目

创建实例
let keychain = Keychain(service: "com.example.github-token")
    .synchronizable(true)

keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
一次性
let keychain = Keychain(service: "com.example.github-token")

do {
    try keychain
        .synchronizable(true)
        .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
} catch let error {
    print("error: \(error)")
}

🌀 Touch ID (Face ID) 集成

任何需要身份验证的操作都必须在后台线程中运行。
如果在主线程中运行,UI 线程将锁定,以便系统尝试显示身份验证对话框。

要使用 Face ID,请将 NSFaceIDUsageDescription 键添加到您的 Info.plist 文件中

🔐 添加受 Touch ID (Face ID) 保护的项目

如果要存储受 Touch ID 保护的 Keychain 项目,请指定 accessibilityauthenticationPolicy 属性。

let keychain = Keychain(service: "com.example.github-token")

DispatchQueue.global().async {
    do {
        // Should be the secret invalidated when passcode is removed? If not then use `.WhenUnlocked`
        try keychain
            .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: [.biometryAny])
            .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
    } catch let error {
        // Error handling if needed...
    }
}

🔐 更新受 Touch ID (Face ID) 保护的项目

与添加时相同。

如果尝试添加的项目可能已存在并受到保护,请勿在主线程中运行。 因为更新受保护的项目需要身份验证。

此外,如果要更新时显示自定义身份验证提示消息,请指定 authenticationPrompt 属性。 如果该项目未受保护,则 authenticationPrompt 参数将被忽略。

let keychain = Keychain(service: "com.example.github-token")

DispatchQueue.global().async {
    do {
        // Should be the secret invalidated when passcode is removed? If not then use `.WhenUnlocked`
        try keychain
            .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: [.biometryAny])
            .authenticationPrompt("Authenticate to update your access token")
            .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
    } catch let error {
        // Error handling if needed...
    }
}

🔐 获取受 Touch ID (Face ID) 保护的项目

与获取普通项目的方式相同。 如果您尝试获取的项目受到保护,它将自动显示 Touch ID 或密码身份验证。
如果要显示自定义身份验证提示消息,请指定 authenticationPrompt 属性。 如果该项目未受保护,则 authenticationPrompt 参数将被忽略。

let keychain = Keychain(service: "com.example.github-token")

DispatchQueue.global().async {
    do {
        let password = try keychain
            .authenticationPrompt("Authenticate to login to server")
            .get("kishikawakatsumi")

        print("password: \(password)")
    } catch let error {
        // Error handling if needed...
    }
}

🔐 删除受 Touch ID (Face ID) 保护的项目

与删除普通项目的方式相同。 删除 Keychain 项目时,无法显示 Touch ID 或密码身份验证。

let keychain = Keychain(service: "com.example.github-token")

do {
    try keychain.remove("kishikawakatsumi")
} catch let error {
    // Error handling if needed...
}

🔑 共享 Web 凭据

共享 Web 凭据是一种编程接口,使本机 iOS 应用程序能够与其网站对应部分共享凭据。 例如,用户可以在 Safari 中登录网站,输入用户名和密码,并使用 iCloud Keychain 保存这些凭据。 稍后,用户可能会运行来自同一开发人员的本机应用程序,而不是要求用户重新输入用户名和密码,共享 Web 凭据使其可以访问先前在 Safari 中输入的凭据。 用户还可以创建新帐户、更新密码或从应用程序中删除她的帐户。 这些更改随后会被保存并被 Safari 使用。
https://developer.apple.com/library/ios/documentation/Security/Reference/SharedWebCredentialsRef/

let keychain = Keychain(server: "https://www.kishikawakatsumi.com", protocolType: .HTTPS)

let username = "kishikawakatsumi@mac.com"

// First, check the credential in the app's Keychain
if let password = try? keychain.get(username) {
    // If found password in the Keychain,
    // then log into the server
} else {
    // If not found password in the Keychain,
    // try to read from Shared Web Credentials
    keychain.getSharedPassword(username) { (password, error) -> () in
        if password != nil {
            // If found password in the Shared Web Credentials,
            // then log into the server
            // and save the password to the Keychain

            keychain[username] = password
        } else {
            // If not found password either in the Keychain also Shared Web Credentials,
            // prompt for username and password

            // Log into server

            // If the login is successful,
            // save the credentials to both the Keychain and the Shared Web Credentials.

            keychain[username] = inputPassword
            keychain.setSharedPassword(inputPassword, account: username)
        }
    }
}

请求所有关联域的凭据

Keychain.requestSharedWebCredential { (credentials, error) -> () in

}

生成强随机密码

生成 Safari 自动填充使用的相同格式的强随机密码 (xxx-xxx-xxx-xxx)。

let password = Keychain.generatePassword() // => Nhu-GKm-s3n-pMx

如何设置共享 Web 凭据

  1. 将 com.apple.developer.associated-domains 授权添加到您的应用程序。 此授权必须包括您要与之共享凭据的所有域。

  2. 将 apple-app-site-association 文件添加到您的网站。 此文件必须包含站点要与之共享凭据的所有应用程序的应用程序标识符,并且必须正确签名。

  3. 安装应用程序后,系统会下载并验证其每个关联域的站点关联文件。 如果验证成功,则该应用程序将与该域关联。

更多细节
https://developer.apple.com/library/ios/documentation/Security/Reference/SharedWebCredentialsRef/

🔍 调试

如果打印 keychain 对象,则显示所有存储的项目

let keychain = Keychain(server: "https://github.com", protocolType: .https)
print("\(keychain)")
=>
[
  [authenticationType: default, key: kishikawakatsumi, server: github.com, class: internetPassword, protocol: https]
  [authenticationType: default, key: hirohamada, server: github.com, class: internetPassword, protocol: https]
  [authenticationType: default, key: honeylemon, server: github.com, class: internetPassword, protocol: https]
]

获取所有存储的密钥

let keychain = Keychain(server: "https://github.com", protocolType: .https)

let keys = keychain.allKeys()
for key in keys {
  print("key: \(key)")
}
=>
key: kishikawakatsumi
key: hirohamada
key: honeylemon

获取所有存储的项目

let keychain = Keychain(server: "https://github.com", protocolType: .https)

let items = keychain.allItems()
for item in items {
  print("item: \(item)")
}
=>
item: [authenticationType: Default, key: kishikawakatsumi, server: github.com, class: InternetPassword, protocol: https]
item: [authenticationType: Default, key: hirohamada, server: github.com, class: InternetPassword, protocol: https]
item: [authenticationType: Default, key: honeylemon, server: github.com, class: InternetPassword, protocol: https]

Keychain 共享功能

如果遇到以下错误,则需要添加 Keychain.entitlements

OSStatus error:[-34018] Internal error when a required entitlement isn't present, client has neither application-identifier nor keychain-access-groups entitlements.

Screen Shot 2019-10-27 at 8 08 50

要求

操作系统 Swift
v1.1.x iOS 7+, macOS 10.9+ 1.1
v1.2.x iOS 7+, macOS 10.9+ 1.2
v2.0.x iOS 7+, macOS 10.9+, watchOS 2+ 2.0
v2.1.x iOS 7+, macOS 10.9+, watchOS 2+ 2.0
v2.2.x iOS 8+, macOS 10.9+, watchOS 2+, tvOS 9+ 2.0, 2.1
v2.3.x iOS 8+, macOS 10.9+, watchOS 2+, tvOS 9+ 2.0, 2.1, 2.2
v2.4.x iOS 8+, macOS 10.9+, watchOS 2+, tvOS 9+ 2.2, 2.3
v3.0.x iOS 8+, macOS 10.9+, watchOS 2+, tvOS 9+ 3.x
v3.1.x iOS 8+, macOS 10.9+, watchOS 2+, tvOS 9+ 4.0, 4.1, 4.2
v3.2.x iOS 8+, macOS 10.9+, watchOS 2+, tvOS 9+ 4.0, 4.1, 4.2, 5.0
v4.0.x iOS 8+, macOS 10.9+, watchOS 2+, tvOS 9+ 4.0, 4.1, 4.2, 5.1
v4.1.x iOS 8+, macOS 10.9+, watchOS 3+, tvOS 9+, Mac Catalyst 13+ 4.0, 4.1, 4.2, 5.1

安装

CocoaPods

KeychainAccess 可通过 CocoaPods 获得。 要安装它,只需将以下行添加到您的 Podfile

use_frameworks!
pod 'KeychainAccess'

Carthage

KeychainAccess 可通过 Carthage 获得。 要安装它,只需将以下行添加到您的 Cartfile

github "kishikawakatsumi/KeychainAccess"

Swift 包管理器

KeychainAccess 也可通过 Swift Package Manager 获得。

Xcode

选择 File > Add Packages... > Add Package Dependency...,

CLI

首先,创建 Package.swift,其包声明包括

// swift-tools-version:5.0
import PackageDescription

let package = Package(
    name: "MyLibrary",
    products: [
        .library(name: "MyLibrary", targets: ["MyLibrary"]),
    ],
    dependencies: [
        .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "3.0.0"),
    ],
    targets: [
        .target(name: "MyLibrary", dependencies: ["KeychainAccess"]),
    ]
)

然后,输入

$ swift build

手动添加到您的项目

  1. Lib/KeychainAccess.xcodeproj 添加到您的项目
  2. KeychainAccess.framework 与您的目标链接
  3. 添加 Copy Files Build Phase 以将框架包含到您的应用程序捆绑包中

请参阅 iOS 示例项目 作为参考。

作者

kishikawa katsumi, kishikawakatsumi@mac.com

许可

KeychainAccess 在 MIT 许可下可用。 有关更多信息,请参见 LICENSE 文件。