4 Replies
      Latest reply on Nov 6, 2019 8:31 PM by Bo98
      Bo98 Level 1 Level 1 (0 points)

        To lay the scene, here's what the creation, copy and updating looks like:

         

        let attrs: [String: Any] = [kSecClass as String: kSecClassInternetPassword
                                    kSecAttrServer as String: self.hostname,
                                    kSecAttrPort as String: self.port,
                                    kSecAttrAccount as String: self.username,
                                    kSecValueData as String: self.password.data(using: .utf8)!,
                                    kSecAttrPath as String: self.database,
                                    kSecAttrLabel as String: "Some label"
                                    kSecReturnPersistentRef as String: true]
        var result: CFTypeRef?
        let status = SecItemAdd(attrs as CFDictionary, &result)
        guard status == errSecSuccess else {
            debugPrint("SecItemAdd returned \(status)")
            return
        }
        self.keychainRef = result as? Data

         

        var result: CFTypeRef?
        let status = SecItemCopyMatching([
            kSecValuePersistentRef as String: keychainRef,
            kSecMatchLimit as String: kSecMatchLimitOne,
            kSecReturnAttributes as String: true,
            kSecReturnData as String: true
        ] as CFDictionary, &result)
        guard status == errSecSuccess else {
            debugPrint("SecItemCopyMatching returned \(status)")
            // ...
            return
        }
        // ...

         

        let attrs: [String: Any] = [kSecAttrServer as String: self.hostname,
                                    kSecAttrPort as String: self.port,
                                    kSecAttrAccount as String: self.username,
                                    kSecValueData as String: self.password.data(using: .utf8)!,
                                    kSecAttrPath as String: self.database]
        let status = SecItemUpdate([
            kSecValuePersistentRef as String: keychainRef
        ] as CFDictionary, attrs as CFDictionary)
        guard status == errSecSuccess else {
            debugPrint("SecItemUpdate returned \(status)")
            return
        }

         

        This is being run on macOS.

         

        Now the scenario:

         

        If the password field (kSecValueData) alone is updated, all is fine. I can continue to use SecItemCopyMatching without issue.

         

        However if I was to update another attribute, say the username (kSecAttrAccount) then it seems that the persistent reference is no longer valid as SecItemCopyMatching now returns errSecItemNotFound. But there seems to be no way to get a new reference (if indeed that is the issue) from SecItemUpdate.

         

        Is the recommendation to just delete and add every time?

        • Re: Persistent references after updating attributes in SecItemUpdate
          eskimo Apple Staff Apple Staff (12,265 points)

          This is being run on macOS.

          Does this fail similarly if you target the iOS-style keychain?  You can do that either by running the code on iOS, or running an iOS app on macOS using Mac Catalyst, or by setting kSecUseDataProtectionKeychain (using kSecAttrSynchronizable if you’re on a pre-10.15 system).

          Share and Enjoy

          Quinn “The Eskimo!”
          Apple Developer Relations, Developer Technical Support, Core OS/Hardware
          let myEmail = "eskimo" + "1" + "@apple.com"

            • Re: Persistent references after updating attributes in SecItemUpdate
              Bo98 Level 1 Level 1 (0 points)

              It seems not. After setting kSecAttrSynchronizable, the problem does not happen.

               

              As much as I'd like to use kSecUseDataProtectionKeychain, unfortunately I still need to support 10.14 and syncing with iCloud is less than ideal.

                • Re: Persistent references after updating attributes in SecItemUpdate
                  eskimo Apple Staff Apple Staff (12,265 points)

                  After setting kSecAttrSynchronizable, the problem does not happen.

                  OK.  That’s actually good news, because it means that the mainstream (iOS-style) keychain is behaving itself, and your problem lies with the shim that connects the SecItem API to the traditional file-based keychain.  More on that below.

                  As much as I'd like to use kSecUseDataProtectionKeychain, unfortunately I still need to support 10.14 and syncing with iCloud is less than ideal.

                  Fair enough.  That means you need a workaround, but the workaround only needs to apply to the above-mentioned shim.  That’s much easier to craft than one that needs to handle both styles of keychain.

                  IMPORTANT For those reading along at home, everything after this is a workaround intended for folks that are stuck on the file-based keychain.  Do not apply it if you’re using the iOS-style keychain.

                  Having you thought about dropping persistent references entirely.  Internet password keychain items are uniquely identified by a set of attributes (that list is documented on the errSecDuplicateItem page) and you could just store those rather than a persistent reference.

                  Alternatively, you could use those attributes to refetch the persistent reference after the update.

                  Oh, and you should definitely file a bug against the shim.  Historically it’s had various problems implementing the iOS-style keychain semantics and, while it’s got better over the years, largely as the result of developer bugs I might add, it’s clear that this issue has slipped through the cracks.

                  Please post your bug number, just for the record.

                  Share and Enjoy

                  Quinn “The Eskimo!”
                  Apple Developer Relations, Developer Technical Support, Core OS/Hardware
                  let myEmail = "eskimo" + "1" + "@apple.com"

                    • Re: Persistent references after updating attributes in SecItemUpdate
                      Bo98 Level 1 Level 1 (0 points)
                      Having you thought about dropping persistent references entirely.  Internet password keychain items are uniquely identified by a set of attributes

                      I don't think this particular approach will work for me without some sort of data duplication. The scenario is database login details. A user may well change every attribute if they were to move to another server with a new username, password, port and database name. There isn't any constant attribute besides the label.

                       

                      The idea was I would store the persistent reference in UserDefaults and retrieve that and get the details from the keychain when the application is launched. To do that without a persistent reference, I would have to copy everything (except the password) and store those. I suppose not too much of an issue. There's other issues that follow however: I would also have to store the old set of attributes when performing an update since any or all of the attributes could be changed in this update operation. So I'd search on the old values and update with the new attributes. It's all doable, but not very clean which I thought persistent references would have been a good answer for.

                       

                      I suppose I could abuse an attribute and make kSecAttrSecurityDomain a UUID or something - but that's a bit of a hack it feels.

                       

                      Alternatively, you could use those attributes to refetch the persistent reference after the update.

                      This approach will be cleaner than the above since I don't need to store an extra data in the application to do this. I think this is the workaround I'll try out. I can just re-use the array I'm putting into SecItemUpdate in the follow up query to get the new persistent reference.

                       

                      Oh, and you should definitely file a bug against the shim.

                      Done: FB7431827

                       

                      Ultimately though, if fixed it will probably not be backported to earlier macOS versions at this stage. And when I'm in the position to drop support for older macOS versions, I'll probably be moving to use kSecUseDataProtectionKeychain anyway, as per the official guidance.