SwiftSecurity 是一个现代 Swift API,用于 Apple 的 Security 框架(钥匙串 API、SharedWebCredentials API、密码学等)。 通过编译时检查,以更简单的方式保护您的应用程序管理的数据。
SwiftSecurity 与其他流行的框架有何不同?
要使用 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"))
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
}
}
}
}
网站或服务器区域的密码,需要身份验证。
// 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
,您可能需要访问 password
或 identity
的持久引用。
// 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
}
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)
)
其他密钥类型,如 SymmetricKey
、Curve25519
、SecureEnclave.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 Curve
和 RSA
密钥。 有关更多详细信息,请参阅 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-certificates 的 X509
包。 如果出现 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
系统将 钥匙串访问组列表中的第一个项目视为应用程序的默认访问组,并按以下顺序评估
J42EP42PB2.com.example.app
。如果未启用钥匙串共享功能,则默认访问组为 app ID
。
注意
要启用 macOS 支持,请确保包含 钥匙串共享 (macOS) 功能并创建一个组 ${TeamIdentifierPrefix}com.example.app
,以防止操作中出现错误。 此共享组是为其他平台自动生成的,无需功能即可访问。 您可以参考 TestHost,以获取有关项目配置的信息。
如果您不想依赖默认存储选择的自动行为,您可以选择显式指定钥匙串共享组。
let keychain = Keychain(accessGroup: .keychainGroup(teamID: "J42EP42PB2", nameID: "com.example.app"))
也可以使用 App Groups 功能来实现共享。 与钥匙串共享组不同,App Group 无法自动成为钥匙串项目的默认存储。 您可能已经在使用 App Group,因此这可能是最方便的选择。
let keychain = Keychain(accessGroup: .appGroupID("group.com.example.app"))
注意
使用在钥匙串组内共享
来在 macOS 上共享,因为所描述的行为在此平台上不存在。 在一个平台上使用一个共享解决方案,而在另一个平台上使用另一个共享解决方案没有问题。
// 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 密钥 文章中的示例代码。
提示
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()
该框架的默认行为在便利性和可访问性之间提供了合理的平衡。
kSecUseDataProtectionKeychain: true
有助于实现在各个平台上保持一致的行为,因此不应也不能更改它。kSecAttrAccessibleAfterFirstUnlock
使钥匙串项目可以从后台状态访问,但可以通过使用 AccessPolicy
来更改。Dmitriy Zharov, contact@zharov.dev
SwiftSecurity 在 MIT 许可下可用。 有关更多信息,请参阅 LICENSE 文件。