kishikawakatsumi / KeychainAccess

Simple Swift wrapper for Keychain that works on iOS, watchOS, tvOS and macOS.
MIT License
7.95k stars 788 forks source link

Touch/Face ID prompt does not show on the Simulator. #486

Open kizitonwose opened 4 years ago

kizitonwose commented 4 years ago

Hello, the prompt for Touch/Face ID only works on a real device. On the simulator, the secure value is just directly read or written to without a prompt.

Googling, I found another user of this framework with the same issue here. So, my question is, how do I get the Touch/Face ID prompt to show on the simulator?

liltimtim commented 4 years ago

This may be unrelated response however, in the documentation for AuthenticationPolicy when interacting with TouchID / FaceID following the example here:

let keychain = Keychain(service: "com.example.github-token")

DispatchQueue.global().async {
    do {
        // Should be the secret invalidated when passcode is removed? If not then use `.WhenUnlocked`
        try keychain
            .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: .userPresence)
            .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")
    } catch let error {
        // Error handling if needed...
    }
}

If you follow the .userPresence AuthenticationPolicy it says above this variable...

    /**
     User presence policy using Touch ID or Passcode. Touch ID does not
     have to be available or enrolled. Item is still accessible by Touch ID
     even if fingers are added or removed.
     */

Yes it uses Touch ID or Passcode but then goes on to say does not have to be available or enrolled.

Based on this, I have to ask the following:

  1. Which policy are you using to add or remove an item?
    • perhaps the policy you are setting doesn't have the constraint of must have enrolled finger/face.

If you look at this policy however,

    /**
     Constraint: Touch ID (any finger) or Face ID. Touch ID or Face ID must be available. With Touch ID
     at least one finger must be enrolled. With Face ID user has to be enrolled. Item is still accessible by Touch ID even
     if fingers are added or removed. Item is still accessible by Face ID if user is re-enrolled.
     */
    @available(iOS 11.3, OSX 10.13.4, watchOS 4.3, tvOS 11.3, *)
    public static let biometryAny = AuthenticationPolicy(rawValue: 1 << 1)

It requires at least 1 enrolled item and enforces this policy

liltimtim commented 4 years ago

After making a toy app that integrates with KeychainAccess,

I've been able to reproduce the issue where the simulator does not show any Touch / Face ID prompts even though the policy is set explicitly to biometryAny

The following is my setup

  1. Simulator running iOS 13.6
  2. KeychainAccess running v. 4.2.0 installed via Swift Package Manager
  3. MacOS running 10.15.6
  4. XCode version : 11.6 (11E708)

The project is a SwiftUI 1.0 project with iOS target 13.6

Here's a snippet of code that sets and gets the item

import KeychainAccess

class BiometricsViewModel: ObservableObject {
    private let keychain = Keychain(service: "com.example.backgrounddownload")

    @Published var hasSetSecureItem: Bool = false

    @Published var retrievedSecureItem: String? = nil

    func setSecureItem(with value: String) {
        DispatchQueue.global().async {
            do {
                try self.keychain
                    .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: .biometryCurrentSet)
                    .authenticationPrompt("Authenticate to set secure item.")
                    .set(value, key: "test_key_2")
            } catch let error {
                print(error)
            }
        }
    }

    func retrieveSecureItem() {
        DispatchQueue.global().async {
            do {
                let value = try self.keychain
                .authenticationPrompt("Authenticate to access secure item")
                .get("test_key_2")
                DispatchQueue.main.async { [weak self] in
                    self?.retrievedSecureItem = value
                }
            } catch let error {
                print(error)
            }
        }
    }
}