SwiftSecurity

Platforms SPM supported License

SwiftSecurity 是一个现代 Swift API,用于 Apple 的 Security 框架(钥匙串 API、SharedWebCredentials API、密码学等)。 通过编译时检查,以更简单的方式保护您的应用程序管理的数据。

特性

SwiftSecurity 与其他流行的框架有何不同?

安装

要求

Swift Package Manager

要使用 SwiftSecurity,请在您的 Package.swift 中添加以下依赖项

.package(url: "https://github.com/dm-zharov/swift-security.git", from: "2.0.0")

最后,将 import SwiftSecurity 添加到您的源代码中。

快速开始

基本

// Choose Keychain
let keychain = Keychain.default

// Store secret
try keychain.store("8e9c0a7f", query: .credential(for: "OpenAI"))

// Retrieve secret
let token: String? = try keychain.retrieve(.credential(for: "OpenAI"))

// Remove secret
try keychain.remove(.credential(for: "OpenAI"))

基本 (SwiftUI)

struct AuthView: View {
    @Credential("OpenAI") private var token: String?

    var body: some View {
        VStack {
            Button("Save") {
                // Store secret
                try? _token.store("8e9c0a7f")
            }
            Button("Delete") {
                // Remove secret
                try? _token.remove()
            }
        }
        .onChange(of: token) {
            if let token {
                // Use secret
            }
        }
    }
} 

Web 凭据

网站或服务器区域的密码,需要身份验证。

// Store password for a website
try keychain.store(
    password, query: .credential(for: "username", space: .website("https://example.com"))
)

// Retrieve password for a website
let password: String? = try keychain.retrieve(
    .credential(for: "username", space: .website("https://example.com"))
)

例如,如果您需要为在同一服务器上工作的同一用户存储不同的端口凭据,您可以通过指定保护空间来进一步描述查询。

let space1 = WebProtectionSpace(host: "example.com", port: 443)
try keychain.store(password1, query: .credential(for: user, space: space1))

let space2 = WebProtectionSpace(host: "example.com", port: 8443)
try keychain.store(password2, query: .credential(for: user, space: space2))

获取属性

if let info = try keychain.info(for: .credential(for: "OpenAI")) {
    // Creation date
    print(info.creationDate)
    // Comment
    print(info.comment)
    ...
}

错误处理

SwiftSecurityError 为最常见的问题提供值。

do {
    try keychain.store("8e9c0a7f", query: .credential(for: "OpenAI"))
} catch {
    switch error as? SwiftSecurityError {
    case .duplicateItem:
        // handle duplicate
    default:
        // unhandled
    }
}

如果出现罕见问题,您将收到 .underlyingSecurityError(error:),其中包含一个 OSStatus 代码,该代码可以与底层 Security Framework Result Codes 匹配。

移除所有

// Removes everything from a keychain
try keychain.removeAll()

// Removes everything from a keychain, including distributed to other devices credentials through iCloud
try keychain.removeAll(includingSynchronizableCredentials: true)

🛠️ 用法

获取数据 & 持久引用

如果您正在使用 NEVPNProtocol,您可能需要访问 passwordidentity 的持久引用。

// Retrieve multiple values at once
if case let .dictionary(info) = try keychain.retrieve([.data, .persistentReference], query: .credential(for: "OpenAI")) {
    // Data
    info.data
    // Persistent Reference
    info.persistentReference
}

// Retrieve persistent reference right after storing the secret
if case let .persistentReference(data) = try keychain.store(
    "8e9c0a7f",
    returning: .persistentReference, /* OptionSet */
    query: .credential(for: "OpenAI")
) {
    // Persistent Reference
    data
}

CryptoKit

SwiftSecurity 允许您以原生 SecKey 实例的形式原生存储 CryptoKit 密钥。 支持此类转换的密钥类型,如 P256/P384/P521,符合 SecKeyConvertible 协议。

// Store private key
let privateKey = P256.KeyAgreement.PrivateKey()
try keychain.store(privateKey, query: .key(for: "Alice"))

// Retrieve private key (+ public key)
let privateKey: P256.KeyAgreement.PrivateKey? = try keychain.retrieve(.key(for: "Alice"))
let publicKey = privateKey.publicKey /* Recommended */

// Store public key. Not recommended, as you can generate it
try keychain.store(
    publicKey,
    query: .key(for: "Alice", descriptor: .ecPublicKey)
)

其他密钥类型,如 SymmetricKeyCurve25519SecureEnclave.P256,没有直接的钥匙串推论。 特别是,SecureEnclave.P256.PrivateKey 是一个加密块,只有同一个 Secure Enclave 才能在以后用来恢复密钥。 这些类型符合 SecDataConvertible,因此按如下方式存储它们

// Store symmetric key
let symmetricKey = SymmetricKey(size: .bits256)
try keychain.store(symmetricKey, query: .credential(for: "Chat"))

注意

SecKey 仅支持 P-256, P-384, P-521 Elliptic CurveRSA 密钥。 有关更多详细信息,请参阅 On Cryptographic Key Formats

证书

DER 编码的 X.509 证书。

// Prepare certificate
let certificateData: Data // Content of file, often with `cer`/`der` extension 
certificate = try Certificate(derRepresentation: certificateData)

// Store certificate
try keychain.store(certificate, query: .certificate(for: "Root CA"))

您可以同时使用 SwiftSecurity 和来自 apple/swift-certificatesX509 包。 如果出现 Swift Package Manager 依赖关系解析问题,请将 SecCertificateConvertible 一致性直接复制到您的项目中。

数字身份

数字身份是证书和与证书中公钥匹配的私钥的组合。

// Import digital identity from `PKCS #12` data
let pkcs12Data: Data /* Contents of PKCS #12 file (also known as PKCS12, PFX, .p12, and .pfx) */
for importItem in try PKCS12.import(pkcs12Data, passphrase: "8e9c0a7f") {
    if let identity = importItem.identity {
        // Store digital identity
        try keychain.store(identity, query: .identity(for: "Apple Development"))
    }
}

// Retrieve digital identity
if let identity = try keychain.retrieve(.identity(for: "Apple Development")) {
    // Certificate
    identity.certificate
    // Private Key Data
    identity.privateKey
    // Underlying SecIdentity
    identity.secIdentity
}

系统分别存储证书和私钥。

自定义查询

// Create query
var query = SecItemQuery<GenericPassword>()

// Customize query
query.synchronizable = true
query.service = "OpenAI"
query.label = "OpenAI Access Token"

// Perform query
try keychain.store(secret, query: query, accessPolicy: AccessPolicy(.whenUnlocked, options: .biometryAny))
_ = try keychain.retrieve(query, authenticationContext: LAContext())
try keychain.remove(query)

查询可防止为项目创建不正确的属性集

var query = SecItemQuery<InternetPassword>()
query.synchronizable = true  // ✅ Common
query.server = "example.com" // ✅ Only for `InternetPassword`
query.service = "OpenAI"     // ❌ Only for `GenericPassword`, so not accessible
query.keySizeInBits = 2048   // ❌ Only for `SecKey`, so not accessible

可能的查询

SecItemQuery<GenericPassword>  // kSecClassGenericPassword
SecItemQuery<InternetPassword> // kSecClassInternetPassword
SecItemQuery<SecKey>           // kSecClassSecKey
SecItemQuery<SecCertificate>   // kSecClassSecCertificate
SecItemQuery<SecIdentity>      // kSecClassSecIdentity

调试

// Print Keychain (or use LLDB `po` command)
print(keychain.debugDescription)

// Print Query
print(query.debugDescription)

// Output -> ["Class: GenericPassword", ..., "Service: OpenAI"]

🔑 如何选择钥匙串

默认

let keychain = Keychain.default

系统将 钥匙串访问组列表中的第一个项目视为应用程序的默认访问组,并按以下顺序评估

如果未启用钥匙串共享功能,则默认访问组为 app ID

注意

要启用 macOS 支持,请确保包含 钥匙串共享 (macOS) 功能并创建一个组 ${TeamIdentifierPrefix}com.example.app,以防止操作中出现错误。 此共享组是为其他平台自动生成的,无需功能即可访问。 您可以参考 TestHost,以获取有关项目配置的信息。

在钥匙串组内共享

如果您不想依赖默认存储选择的自动行为,您可以选择显式指定钥匙串共享组。

let keychain = Keychain(accessGroup: .keychainGroup(teamID: "J42EP42PB2", nameID: "com.example.app"))

在 App Group 内共享

也可以使用 App Groups 功能来实现共享。 与钥匙串共享组不同,App Group 无法自动成为钥匙串项目的默认存储。 您可能已经在使用 App Group,因此这可能是最方便的选择。

let keychain = Keychain(accessGroup: .appGroupID("group.com.example.app"))

注意

使用在钥匙串组内共享来在 macOS 上共享,因为所描述的行为在此平台上不存在。 在一个平台上使用一个共享解决方案,而在另一个平台上使用另一个共享解决方案没有问题。

🔓 使用 Face ID (Touch ID) 和密码保护

存储受保护的项目

// Store with specified `AccessPolicy`
try keychain.store(
    secret,
    query: .credential(for: "FBI"),
    accessPolicy: AccessPolicy(.whenUnlocked, options: .userPresence) // Requires biometry/passcode authentication
)

检索受保护的项目

如果您请求受保护的项目,将自动出现身份验证屏幕。

// Retrieve value
try keychain.retrieve(.credential(for: "FBI"))

如果您想在发出请求之前手动进行身份验证或自定义身份验证屏幕,请将 LAContext 提供给检索方法。

// Create an LAContext
var context = LAContext()

// Authenticate
do {
    let success = try await context.evaluatePolicy(
        .deviceOwnerAuthentication,
        localizedReason: "Authenticate to proceed." // Authentication prompt
    )
} else {
    // Handle LAError error
}

// Check authentication result 
if success {
    // Retrieve value
    try keychain.retrieve(.credential(for: "FBI"), authenticationContext: context)
}

警告

在您应用程序的 Info.plist 文件中包含 NSFaceIDUsageDescription 键。 否则,身份验证请求可能会失败。

ℹ️ 数据类型

您可以存储、检索和移除各种类型的值。

Foundation:
    - Data /* GenericPassword, InternetPassword */
    - String /* GenericPassword, InternetPassword */
CryptoKit:
    - SymmetricKey /* GenericPassword */
    - Curve25519 -> PrivateKey /* GenericPassword */
    - SecureEnclave.P256 -> PrivateKey /* GenericPassword (SE's Key Data is Persistent Reference) */
    - P256, P384, P521 -> PrivateKey /* SecKey (ANSI x9.63 Elliptic Curves) */
X509 (external package `apple/swift-certificates`):
    - Certificate /* SecCertificate */
SwiftSecurity:
    - Certificate /* SecCertificate */
    - DigitalIdentity /* SecIdentity (The Pair of SecCertificate and SecKey) */

要添加对自定义类型的支持,您可以通过符合以下协议来扩展它们。

// Store as Data (GenericPassword, InternetPassword)
extension CustomType: SecDataConvertible {}

// Store as Key (ANSI x9.63 Elliptic Curves or RSA Keys)
extension CustomType: SecKeyConvertible {}

// Store as Certificate (X.509)
extension CustomType: SecCertificateConvertible {}

// Store as Identity (The Pair of Certificate and Private Key)
extension CustomType: SecIdentityConvertible {}

这些协议的灵感来自 Apple 在 在钥匙串中存储 CryptoKit 密钥 文章中的示例代码。

🔑 共享 Web 凭据

提示

SharedWebCredentials API 使得与网站对应方共享凭据成为可能。 例如,用户可以使用 Safari 登录网站并将凭据保存到 iCloud 钥匙串。 之后,用户可以运行来自同一开发者的应用程序,而无需用户重新输入用户名和密码,它可以访问现有凭据。 用户可以创建新帐户、更新密码或从应用程序中删除帐户。 这些更改应从应用程序保存,以供 Safari 使用。

// Store
SharedWebCredential.store("https://example.com", account: "username", password: "secret") { result in
    switch result {
    case .failure(let error):
        // Handle error
    case .success:
        // Handle success
    }
}

// Remove
SharedWebCredential.remove("https://example.com", account: "username") { result in
    switch result {
    case .failure(let error):
        // Handle error
    case .success:
        // Handle success
    }
}

// Retrieve
// - Use `ASAuthorizationController` to make an `ASAuthorizationPasswordRequest`.

🔒 安全数据生成器

// Data with 20 uniformly distributed random bytes
let randomData = try SecureRandomDataGenerator(count: 20).next()

安全性

该框架的默认行为在便利性和可访问性之间提供了合理的平衡。

沟通

知识

作者

Dmitriy Zharov, contact@zharov.dev

许可证

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