Search code examples
iosswiftkeychain

Keychain SecItemAdd fails when using SecAccessControl with Passcode & Biometry


I am currently able to reproduce an error, where SecItemAdd fails when testing on devices which have a Passcode set but Biometry is not yet setup on the device (but generally available).

The error: OSStatus gives -25293, which seems to be errSecAuthFailed.

What I want to achieve: I want to store an Item to the Keychain with Device Passcode as the minimal security requirement. Additionally, if Biometry is enabled, I want to allow users to use it to protect and access the item. In contrast to .userPresence I want to prevent the item to be accessible when Biometry changes, so .biometryCurrentSet in the flags seems to the be right option.

So the failing combination is kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly for the protection level with SecAccessControlCreateFlags being [.biometryCurrentSet, .or, .devicePasscode].

The example code (minimal demo to reproduce):

import SwiftUI

struct ContentView: View {

    func keychainTest() {
        var attributes: [String: Any] = [kSecValueData as String: "abc".data(using: .utf8)!]
        var error: Unmanaged<CFError>?

        defer {
            error?.release()
        }

        guard let accessControl = SecAccessControlCreateWithFlags(
            nil,
            kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly as CFString,
            [.biometryCurrentSet, .or, .devicePasscode], // this fails when Device has Biometry deactivated
            &error
        ) else {
            debugPrint("Error creating Access Control")
            return
        }

        attributes[kSecAttrAccessControl as String] = accessControl

        // try adding new keychain item
        attributes[kSecClass as String] = kSecClassGenericPassword
        attributes[kSecAttrAccount as String] = "keychaintest.mybundle.com.keyid1234"

        let saveStatus = SecItemAdd(attributes as CFDictionary, nil)

        if saveStatus != errSecSuccess {
            debugPrint("Error Saving")
        }
    }

    var body: some View {
        Text("Hello, world!")
            .padding()
            .onAppear {
                self.keychainTest()
            }
    }

}

Note: The minimal demo is created in SwiftUI but the original failing source is from a UIKit Project, which does not matter here. The FaceID privacy string is set in the Info.plist.

As soon as I use [.userPresence] (which is an equivalent of [.biometryAny, .or, .devicePasscode]) instead of the wanted flags, the SecItemAdd succeeds.

I am missing something?

Edit: Of course the previous item with the same key exists. I'm altering it with each try and use a fresh test device (iOS 14.4).

Edit 2: This seems related. IOS not able to create privatekey if only devicecode is set on device?. Not the same (here SecItemAdd), but Access Control also creates a valid reference but later the Key method fails.

Additionally a dump of the attributes in the saveStatus failed branch:

(lldb) po attributes
▿ 4 elements
  ▿ 0 : 2 elements
    - key : "v_Data"
    ▿ value : 3 bytes
      - count : 3
      ▿ pointer : 0x000000016b282d50
        - pointerValue : 6092762448
      ▿ bytes : 3 elements
        - 0 : 97
        - 1 : 98
        - 2 : 99
  ▿ 1 : 2 elements
    - key : "accc"
    - value : <SecAccessControlRef: akpu;od(pkofn(1)cup(true)cbio(pbioc()pbioh()));odel(true);oe(true)>
  ▿ 2 : 2 elements
    - key : "acct"
    - value : "keychaintest.mybundle.com.keyid1234.13"
  ▿ 3 : 2 elements
    - key : "class"
    - value : genp

Solution

  • Filed as Radar FB9039075.

    A friend (https://stackoverflow.com/users/2451589/julien-klindt, thanks for this!) received feedback from Apple (using DTS) afterwards:

    The .or operator here refers to the read operation. That is, to read the item the system must be able to satisfy one or more of the specified constraints. However, things are failing in your case at the add operation. Here the presence of .biometryCurrentSet indicates that the keychain must tag the item with the current biometric set, and that can’t possibly work because no biometrics are currently configured.

    So this is as designed. Apple suggest creating a feature request for the described behavior, if wanted.

    In terms of workarounds, it seems reasonable to catch this error and then ask Local Authentication whether biometry is enabled. If it isn’t, you can fall back to .devicePasscode.

    If it's not needed to have changes in Biometry invalidate the item, I would suggest to use .userPresence instead.