entitlements photoHello I have a log in view that uses face recognition to authenticate the user and If the user is authenticated it reads their log in info from keychain if they have it saved. For some reason all this functionality isn't working, Ive looked at some other SO threads and was not able to get it working. I think something is wrong with both the save and read functions. I was able to get the authentication function working however. Id appreciate any help.

import SwiftUI
import LocalAuthentication
import AuthenticationServices

struct LogInView: View {
    @State private var email = ""
    @State private var password = ""
    var body: some View {
        ZStack(alignment: .top){
            GeometryReader { geometry in
                VStack {
                    HStack{
                            Button {
                                save(email: email, password: password)
                            } label: {
                                Text("Save password")
                            }
                        
                    }.padding(.top, 10)
                    VStack(spacing: 15){
                        CustomTextField(imageName: "envelope", placeHolderText: "Email", text: $email)
                        CustomTextField(imageName: "lock", placeHolderText: "Password", isSecureField: true, text: $password)
                    }
                }
            }
        }
        .onAppear(perform: authenticate)
    }
    func save(email: String, password: String) {
        let emailData = email.data(using: .utf8)!
        let passwordData = password.data(using: .utf8)!
        
        let query: [String: Any] = [
            kSecClass as String: kSecClassInternetPassword,
            kSecAttrService as String: "https://hustle.page",
            kSecAttrAccount as String: emailData,
            kSecValueData as String: passwordData
        ]
        let saveStatus = SecItemAdd(query as CFDictionary, nil)
        if saveStatus == errSecDuplicateItem {
            update(email: email, password: password)
        }
    }
    func update(email: String, password: String) {
        let emailData = email.data(using: .utf8)!
        let passwordData = password.data(using: .utf8)!

        let query: [String: Any] = [
            kSecClass as String: kSecClassInternetPassword,
            kSecAttrService as String: "https://hustle.page",
            kSecAttrAccount as String: emailData
        ]
            
        let updatedData: [String: Any] = [
            kSecValueData as String: passwordData
        ]
        
        SecItemUpdate(query as CFDictionary, updatedData as CFDictionary)
    }
    func read(service: String) -> (String, String)? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassInternetPassword,
            kSecAttrService as String: service,
            kSecReturnAttributes as String: true,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitAll
        ]
        
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        
        if status == errSecSuccess, let items = result as? [[String: Any]], let item = items.first {
            if let account = item[kSecAttrAccount as String] as? String,
               let passwordData = item[kSecValueData as String] as? Data,
               let password = String(data: passwordData, encoding: .utf8) {
                return (account, password)
            }
        }
        return nil
    }
    func authenticate() {
        let context = LAContext()
        var error: NSError?

        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
            let reason = "Secure Authentication."

            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
                if success {
                    if let loginInfo = read(service: "https://hustle.page") {
                        let (email, password) = loginInfo
                    }
                }
            }
        }
    }
}

推荐答案

对于保存在密钥链中的每一项,您通常都有一个元组(服务、帐户、密码)来唯一标识密码.当您想要读取密码时,需要提供相应的服务和帐号.

这里,service通常是您的应用程序或Web服务的URL或标识符,account通常是用户的用户名或邮箱.因此,要读取保存在Keychain中的密码,您通常需要关联的serviceaccount(在您的情况下通常是邮箱或用户名).

这就是为什么像Instagram这样的应用程序可以在面部ID身份验证通过后自动填写邮箱和密码字段.他们之前将这些值保存到 keys 链中,在成功进行Face ID身份验证后,他们获取并自动填充这些保存的值.这需要他们知道与保存的密码相关联的服务和帐户(邮箱或用户名).

因此,在您当前的实现中,在不知道关联的服务和帐户(邮箱)的情况下,您无法从KeyChain读取密码.如果您有多个帐户(邮箱),每个帐户(邮箱)都应该有自己的相应密码单独保存在 keys 链中.


在您的代码中,saveupdate函数使用Data而不是String:

func save(email: String, password: String) {
    let emailData = email.data(using: .utf8)!  // <-- Here you are converting the email into Data
    let passwordData = password.data(using: .utf8)!
    
    let query: [String: Any] = [
        kSecClass as String: kSecClassInternetPassword,
        kSecAttrService as String: "https://hustle.page",
        kSecAttrAccount as String: emailData,  // <-- Here you are saving the email Data into Keychain
        kSecValueData as String: passwordData
    ]
    let saveStatus = SecItemAdd(query as CFDictionary, nil)
    if saveStatus == errSecDuplicateItem {
        update(email: email, password: password)
    }
}

func update(email: String, password: String) {
    let emailData = email.data(using: .utf8)!  // <-- Here you are converting the email into Data again
    let passwordData = password.data(using: .utf8)!

    let query: [String: Any] = [
        kSecClass as String: kSecClassInternetPassword,
        kSecAttrService as String: "https://hustle.page",
        kSecAttrAccount as String: emailData  // <-- And here you are saving the email Data into Keychain again
    ]
        
    let updatedData: [String: Any] = [
        kSecValueData as String: passwordData
    ]
    
    SecItemUpdate(query as CFDictionary, updatedData as CFDictionary)
}

saveupdate功能中,您都要将邮箱转换为Data,然后将Data存储到帐户(kSecAttrAccount)的KeyChain中.然而,kSecAttrAccount预计是String种,而不是Data种.这可能会导致您在保存和读取登录信息时出现问题.

read方法中发现了相同的转换问题,在该方法中,您try 将邮箱(帐户)读取为String:

if let account = item[kSecAttrAccount as String] as? String,  // <-- Here you are trying to read the email as a String
   let passwordData = item[kSecValueData as String] as? Data,
   let password = String(data: passwordData, encoding: .utf8) {
    return (account, password)
}

这不会正常工作,因为您最初将邮箱存储为Data,而不是String.


这应该会更好地发挥作用:

func save(email: String, password: String) {
    let passwordData = password.data(using: .utf8)!
    
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword, // <-- Change this
        kSecAttrService as String: "https://hustle.page",
        kSecAttrAccount as String: email,
        kSecValueData as String: passwordData
    ]
    let saveStatus = SecItemAdd(query as CFDictionary, nil)
    if saveStatus == errSecDuplicateItem {
        update(email: email, password: password)
    }
}

func update(email: String, password: String) {
    let passwordData = password.data(using: .utf8)!
    
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword, // <-- Change this
        kSecAttrService as String: "https://hustle.page",
        kSecAttrAccount as String: email
    ]
    
    let updatedData: [String: Any] = [
        kSecValueData as String: passwordData
    ]
    
    SecItemUpdate(query as CFDictionary, updatedData as CFDictionary)
}

the comments中的Rob Napier所示,用于存储与互联网服务器相关联的互联网密码的kSecClassInternetPassword不支持kSecAttrService,因此我们需要kSecClassGenericPassword作为通用登录存储(用户名和密码).

read方法中,您应该将kSecMatchLimit as String: kSecMatchLimitAll更改为kSecMatchLimit as String: kSecMatchLimitOne,因为您 for each 服务存储一个帐户.

func read(service: String) -> (String, String)? {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword, // <-- Change this
        kSecAttrService as String: service,
        kSecReturnAttributes as String: true,
        kSecReturnData as String: true,
        kSecMatchLimit as String: kSecMatchLimitOne
    ]
        
    var result: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &result)
        
    if status == errSecSuccess, let items = result as? [[String: Any]], let item = items.first {
        if let account = item[kSecAttrAccount as String] as? String,
           let passwordData = item[kSecValueData as String] as? Data,
           let password = String(data: passwordData, encoding: .utf8) {
            return (account, password)
        }
    }
    return nil
}

关于authenticate()函数中的完成块,重要的是确保在主线程上进行与UI相关的更改:

func authenticate() {
    let context = LAContext()
    var error: NSError?

    if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
        let reason = "Secure Authentication."

        context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { [weak self] success, authenticationError in
            DispatchQueue.main.async {
                if success {
                    if let loginInfo = self?.read(service: "https://hustle.page") {
                        let (email, password) = loginInfo
                        self?.email = email
                        self?.password = password
                    }
                }
            }
        }
    }
}

这应该会让您在成功进行人脸识别身份验证后,填写邮箱和密码字段获得所需的效果.只需确保您已保存凭据,然后再try 读取它们.

确保判断SecItemAddSecItemUpdateSecItemCopyMatching函数的结果并正确处理错误.


我try 通过调用Read函数在SAVE函数的末尾添加一些调试.当我这样做时,Read函数的结果似乎是空的.我还打印出了Read函数中的"Status"变量,它也是0,表示没有错误

仅当找到与查询匹配的项时,SecItemCopyMatching函数才返回状态errSecSuccess(为0)和结果.如果未找到项目,则状态仍为errSecSuccess,但结果将为nil.

您在read函数中的查询需要一个项目列表([[String: Any]]),但您在查询中使用的是kSecMatchLimitOne,这意味着该函数将只返回单个项目(不在列表中).因此,结果应该是单个项目([String: Any]),而不是项目列表.

下面是修改read函数的方法:

func read(service: String) -> (String, String)? {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: service,
        kSecReturnAttributes as String: true,
        kSecReturnData as String: true,
        kSecMatchLimit as String: kSecMatchLimitOne
    ]
    
    var result: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &result)
    
    if status == errSecSuccess, let item = result as? [String: Any] {
        if let account = item[kSecAttrAccount as String] as? String,
           let passwordData = item[kSecValueData as String] as? Data,
           let password = String(data: passwordData, encoding: .utf8) {
            return (account, password)
        }
    }
    return nil
}

有了这一更改,您应该能够正确地从Keychain中获取单个项.

Ios相关问答推荐

在Android和iOS上从后台恢复应用程序后,inappwebview上出现白屏?

在iOS中禁用URLSession自动重试机制

如何在iOS中以编程方式推送另一个页面?

如何将模糊输入/模糊输出添加到SwiftUI中的非对称过渡?

SwiftData共享扩展阻止应用程序打开SQLite文件

Xcode 15中的版本控制发生了什么?

使用 SwiftUI 显示多个 VNRecognizedObjectObservation 边界框时偏移量错误

@MainActor 类的扩展是主要演员吗?

如何在应用程序中显示我的小部件的快照?

SwiftUI 文本字段代理绑定代码忽略空格不起作用?

async/await、Task 和 [weak self]

如何在不同设备的segment控制中管理文本大小(text size)?

如何快速设置条形按钮的图像?

警告:iPad:Icon-72.png:图标尺寸(0 x 0)

不变违规:应用程序 AwesomeProject 尚未注册使用静态 jsbundle 为 iOS 设备构建时

iOS SDK - 以编程方式生成 PDF 文件

AVAudioRecorder 抛出错误

Objective-C 将 NSDate 设置为当前 UTC

如何检测快速touch 或单击的tableView单元格

如何在 Swift 中更改按钮标题对齐方式?