2

Hello I am encountering a very odd issue with iOS keychain in terms of updating log in information stored in keychain. So if there are no saved credentials then running the save function correctly saves the log in info. If log in info already exists and the user updates their password then the update function correctly updates just the password. But if log in info exists and I try to change the email (while keeping or changing password) then the first update is unsuccessful. I have to manually click the update log in twice for the log in info to update. I tried the below code by just forcing the delete and save funcs to run twice while adding a delay between but this didn't work. The only thing that works is pressing "update" twice. Id appreciate any help. Thanks.

 delete(email: result.0)
 save(email: email, password: password)
 Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in
      delete(email: result.0)
      save(email: email, password: password)
 }
    func save(email: String, password: String) {
        let passwordData = password.data(using: .utf8)!
        
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            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) {
        if let result = read(service: "https://hustle.page"){
            if result.0 == email {
                let query: [String: Any] = [
                    kSecClass as String: kSecClassGenericPassword,
                    kSecAttrService as String: "https://hustle.page",
                    kSecAttrAccount as String: email
                ]
                let passwordData = password.data(using: .utf8)!
                let updatedData: [String: Any] = [
                    kSecValueData as String: passwordData
                ]
                
                SecItemUpdate(query as CFDictionary, updatedData as CFDictionary)
            } else {
                delete(email: result.0)
                save(email: email, password: password)
                Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in
                    delete(email: result.0)
                    save(email: email, password: password)
                }
            }
        }
    }
    func delete(email: String) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: "https://hustle.page",
            kSecAttrAccount as String: email
        ]
        SecItemDelete(query as CFDictionary)
    }
    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
    }  

in the view: 
     Button {
       save(email: email, password: password)
     } label: {
       Text("Update")
     }
ahmed
  • 341
  • 2
  • 9

1 Answers1

2

The iOS Keychain is a secured data storage, and its behavior is synchronous in that it will execute and complete one request before executing another. Therefore, rapid successive requests may not always work as expected, especially without checking for errors (and I did recommend checking errors in my previous answer).

If you want to update an existing Keychain entry, you will need to delete the old entrance and then add a new entry with the updated data.
You can also see an illustration of common operations with "Enhancing User Data Security on iOS: A Closer Look at Keychain" from Kamil Makowski

As I mentioned above, always check the return values from the Keychain functions (SecItemAdd, SecItemUpdate, and SecItemDelete). They will inform you whether the operation succeeded or what error might have occurred.

You could rewrite the save function in order to check if an entry with the provided email already exists.

  • If it does, delete the old entry and add the new one.
  • If it does not, just add the new entry.

For instance:

func save(email: String, password: String) {
    // Check if there's an existing entry with the provided email
    if let existing = read(service: "https://hustle.page"), existing.0 == email {
        // Delete the old entry
        let deleteStatus = delete(email: email)
        if deleteStatus != errSecSuccess {
            // Handle the delete error
            print("Error deleting data: \(deleteStatus)")
            return
        }
    }
    
    // Now, save the new data
    let passwordData = password.data(using: .utf8)!
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: "https://hustle.page",
        kSecAttrAccount as String: email,
        kSecValueData as String: passwordData
    ]
    
    let saveStatus = SecItemAdd(query as CFDictionary, nil)
    if saveStatus != errSecSuccess {
        // Handle the save error
        print("Error saving data: \(saveStatus)")
    }
}

func delete(email: String) -> OSStatus {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: "https://hustle.page",
        kSecAttrAccount as String: email
    ]
    
    let status = SecItemDelete(query as CFDictionary)
    return status
}

And in your view:

Button {
   save(email: email, password: password)
} label: {
   Text("Update")
}

That way, every time the save function is called, it either updates an existing entry or adds a new one, ensuring that the Keychain is always in sync with the provided data.

Karma
  • 1
  • 1
  • 3
  • 9
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • Thanks maybe you can help with this one: https://stackoverflow.com/questions/76934224/remove-more-button-from-navigation-view-for-custom-tab-bar-with-5-items. It has been bugging me a lot. – ahmed Aug 19 '23 at 17:41