kishikawakatsumi / KeychainAccess

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

Disable passcode entry completely #477

Closed kipropkorir closed 4 years ago

kipropkorir commented 4 years ago

I have tried disabling by using policy authenticationPolicy: .biometryCurrentSet and authenticationPolicy: .biometryAny but after three failed attempts the app asks for a passcode. How do I remove passcode completely?

kishikawakatsumi commented 4 years ago

Did you mean that you want to enable biometrics, but you do not want the passcode fallback, right? If so it's impossible. The passcode fallback option is always provided by the system. Otherwise you can completely disable biometrics and passcode.

kipropkorir commented 4 years ago

I see other apps have done it though, it possible to do it without using this library maybe?

kipropkorir commented 4 years ago

See this https://stackoverflow.com/questions/50820505/how-to-ignore-ios-device-pin-prompt-after-3-incorrect-touch-face-id-recognitions

kishikawakatsumi commented 4 years ago

This configuration can also be done via the library. Give it a try.

kipropkorir commented 4 years ago

Please guide me sir, using this library

kishikawakatsumi commented 4 years ago
let keychain = Keychain()
keychain
    .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: .touchIDAny)
keychain["..."] = ...

I haven't tested but it should work.

kipropkorir commented 4 years ago

It does not work, please add the feature to your library thanks

kishikawakatsumi commented 4 years ago

@kipropkorir

I have confirmed that the above code works. Perhaps you are using the library in the wrong way. I won't change the library because it is working properly. If my perception of your problem is wrong, please provide me more information tells me that.

kipropkorir commented 4 years ago

Hey @kishikawakatsumi , thanks for the update, my case scenario is as below.

keychain .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: .userPresence) Using the above code shows both biometric and passocode option.

But when I use:

keychain .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: .touchIDAny) or keychain .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: .biometricAny)

It doesn't show the passcode option at first but after three failed biometric auth attempts it falls back to "enter passcode", kindly confirm this 👍

kishikawakatsumi commented 4 years ago

Show full code. I believe you do not use return value or running it on the main thread.

kipropkorir commented 4 years ago

Here is the full code:

import Foundation import KeychainAccess import LocalAuthentication

public struct KeyChainKeys { public static let MASTER_MSISDN = "master-msisdn" public static let BIOMETRIC_ID_KEY = "touch-id-store" }

protocol BiometricIDService { var biometricSupported: BiometricType { get }

func checkForBiometricId(completion: @escaping ((_ pincode: String?, _ error: Error?) -> Void))
func storePinForBiometricId(_ pin: String, completion: @escaping ((_ error: Error?) -> Void))
func removeBiometricIdPin(completion: @escaping ((_ error: Error?) -> Void))

}

class BiometricIDServiceDefault: BiometricIDService {

private let bundleId = Bundle.main.bundleIdentifier!

let biometricSupported = LAContext().biometricType

func checkForBiometricId(completion: @escaping ((_ pincode: String?, _ error: Error?) -> Void)) 

{

    let keychain = Keychain(service: bundleId)
    DispatchQueue.global().async {
        do {
            let storedPin = try keychain
                .authenticationPrompt(NSLocalizedString("login", comment: "login" ))
                .get(KeyChainKeys.BIOMETRIC_ID_KEY)

            Logger.debug("pincode: \(String(describing: storedPin))")
            DispatchQueue.main.async {
                completion(storedPin, nil)
            }

        } catch let error {
            DispatchQueue.main.async {
                completion(nil, error)
            }
        }
    }

}

func storePinForBiometricId(_ pin: String, completion: @escaping ((_ error: Error?) -> Void)) {

    let keychain = Keychain(service: bundleId)
    DispatchQueue.global().async {
        do {
            try keychain
                .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: .touchIDAny) 
                .set(pin, key: KeyChainKeys.BIOMETRIC_ID_KEY)
            DispatchQueue.main.async {
                completion(nil)
            }
        } catch let error {
            DispatchQueue.main.async {
                completion(error)
            }
        }
    }

}

func removeBiometricIdPin(completion: @escaping ((Error?) -> Void)) {

    let keychain = Keychain(service: bundleId)
    do {
        try keychain.remove(KeyChainKeys.BIOMETRIC_ID_KEY)
        DispatchQueue.main.async {
            completion(nil)
        }
    } catch let error {
        DispatchQueue.main.async {
            completion(error)
        }
    }

}

}

enum BiometricType: String { case none case touchID case faceID }

private extension LAContext { var biometricType: BiometricType { var error: NSError?

    guard canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
        Logger.debug("Cannot evaluate biometrics policy \(error?.localizedDescription ?? "Error unavailable")")
        return .none
    }

    guard #available(iOS 11.0, *) else {
        return .touchID
    }
    switch biometryType {
    case .none:
        return .none
    case .touchID:
        return .touchID
    case .faceID:
        return .faceID
    @unknown default:
        assertionFailure("Unknown biometry type detected")
        return .none
    }
}

}

I use the storePinForBiometricId method to store the pin and use the checkForBiometricId to fetch the key in a different view controller

kishikawakatsumi commented 4 years ago

Thanks. I have run your code. It seems working as expected.

When the first failure, it shows retry and cancel button. Then the second failure, shows only the cancel button. There is no passcode fallback. Isn't this the behavior you want?

The code I tried is here:

BiometricIDServiceDefault().storePinForBiometricId("abcd") { (e) in
    print(e) // => nil

    DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
        BiometricIDServiceDefault().checkForBiometricId { (p, e) in
            print(p)
            print(e)
        }
    }
}
First Failure Second Failure
Screen Shot 2020-04-30 at 23 57 17 Screen Shot 2020-04-30 at 23 57 24