web-dev-qa-db-fra.com

Enregistrer les données dans le trousseau uniquement accessible avec Touch ID dans Swift 3

Je travaille sur une paix de code qui devrait faire ce qui suit:

  • Stockez des données dans le trousseau.
  • Obtenez les données uniquement si un utilisateur s'authentifie avec Touch ID ou Code d'accès.

J'ai regardé la présentation Porte-clés et authentification avec Touch ID et compris ce qui suit:

Si vous définissez le bon paramètre tout en ajoutant une nouvelle valeur au trousseau, la prochaine fois que vous essayerez de le sortir, le système affichera automatiquement la fenêtre contextuelle Touch ID.

J'ai écrit du code et mon hypothèse ne fonctionne pas. Voici ce que j'ai écrit:

    //
    //  Secret value to store
    //
    let valueData = "The Top Secret Message V1".data(using: .utf8)!;

    //
    //  Create the Access Controll object telling how the new value
    //  should be stored. Force Touch ID by the system on Read.
    //
    let sacObject =
        SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                            kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
                            .userPresence,
                            nil);

    //
    //  Create the Key Value array, that holds the query to store 
    //  our data
    //
    let insert_query: NSDictionary = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccessControl: sacObject!,
        kSecValueData: valueData,
        kSecUseAuthenticationUI: kSecUseAuthenticationUIAllow,
        //  This two valuse ideifieis the entry, together they become the
        //  primary key in the Database
        kSecAttrService: "app_name",
        kSecAttrAccount: "first_name"
    ];

    //
    //  Execute the query to add our data to Keychain
    //
    let resultCode = SecItemAdd(insert_query as CFDictionary, nil);

Au début, je pensais que l'émulateur avait un problème, mais non, j'ai pu vérifier si Touch ID est présent ou non avec le code suivant:

    //
    //  Check if the device the code is running on is capapble of 
    //  finger printing.
    //
    let dose_it_can = LAContext()
        .canEvaluatePolicy(
            .deviceOwnerAuthenticationWithBiometrics, error: nil);

    if(dose_it_can)
    {
        print("Yes it can");
    }
    else
    {
        print("No it can't");
    }

Et j'ai également pu afficher par programme la fenêtre contextuelle Touch ID avec le code suivant:

    //
    //  Show the Touch ID dialog to check if we can get a print from 
    //  the user
    //
    LAContext().evaluatePolicy(
        LAPolicy.deviceOwnerAuthenticationWithBiometrics,
        localizedReason: "Such important reason ;)",
        reply: {
            (status: Bool, evaluationError: Error?) -> Void in

            if(status)
            {
                print("OK");
            }
            else
            {
                print("Not OK");
            }

    });

Pour tout résumer

Touch ID fonctionne, mais enregistrer une valeur dans le trousseau avec le drapeau pour forcer Touch ID par le système lui-même ne fonctionne pas - que me manque-t-il?

Exemple de pommes

L'exemple que Apple fournit appelé KeychainTouchID: Utilisation de Touch ID avec Keychain et LocalAuthentication montre également un résultat incohérent et Touch ID n'est pas appliqué par le système.

Spécifications techniques

  • Xcode 8.1
  • Swift 3
29
David Gatti

La fenêtre contextuelle Touch ID apparaît uniquement si vous appelez SecItemCopyMatching() dans une file d'attente en arrière-plan. Ceci est indiqué à la page 118 de la présentation PDF de Porte-clés et authentification avec Touch ID :

Lire un secret
...

dispatch_async(dispatch_get_global_queue(...), ^(void){
    CFTypeRef dataTypeRef = NULL;
    OSStatus status = SecItemCopyMatching((CFDictionaryRef)query,
                                     &dataTypeRef);
});

Sinon, vous bloquez le thread principal et la fenêtre contextuelle n'apparaît pas. SecItemCopyMatching() échoue (après un timeout) avec le code d'erreur -25293 = errSecAuthFailed.

L'échec n'est pas immédiatement apparent dans votre exemple de projet car il imprime la mauvaise variable dans le cas d'erreur, par exemple

if(status != noErr)
{
    print("SELECT Error: \(resultCode)."); // <-- Should be `status`
}

et de même pour la mise à jour et la suppression.

Voici une version composée de votre exemple de code avec la répartition nécessaire dans une file d'attente en arrière-plan pour récupérer l'élément de trousseau. (Bien sûr, les mises à jour de l'interface utilisateur doivent être renvoyées dans la file d'attente principale.)

Cela a fonctionné comme prévu dans mon test sur un iPhone avec Touch ID: la fenêtre contextuelle Touch ID apparaît, et l'élément de trousseau n'est récupéré qu'après une authentification réussie.

L'authentification Touch ID ne fonctionne pas pas sur le simulateur iOS.

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    //  This two values identify the entry, together they become the
    //  primary key in the database
    let myAttrService = "app_name"
    let myAttrAccount = "first_name"

    // DELETE keychain item (if present from previous run)

    let delete_query: NSDictionary = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrService: myAttrService,
        kSecAttrAccount: myAttrAccount,
        kSecReturnData: false
    ]
    let delete_status = SecItemDelete(delete_query)
    if delete_status == errSecSuccess {
        print("Deleted successfully.")
    } else if delete_status == errSecItemNotFound {
        print("Nothing to delete.")
    } else {
        print("DELETE Error: \(delete_status).")
    }

    // INSERT keychain item

    let valueData = "The Top Secret Message V1".data(using: .utf8)!
    let sacObject =
        SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                        kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                                        .userPresence,
                                        nil)!

    let insert_query: NSDictionary = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccessControl: sacObject,
        kSecValueData: valueData,
        kSecUseAuthenticationUI: kSecUseAuthenticationUIAllow,
        kSecAttrService: myAttrService,
        kSecAttrAccount: myAttrAccount
    ]
    let insert_status = SecItemAdd(insert_query as CFDictionary, nil)
    if insert_status == errSecSuccess {
        print("Inserted successfully.")
    } else {
        print("INSERT Error: \(insert_status).")
    }

    DispatchQueue.global().async {
        // RETRIEVE keychain item

        let select_query: NSDictionary = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrService: myAttrService,
            kSecAttrAccount: myAttrAccount,
            kSecReturnData: true,
            kSecUseOperationPrompt: "Authenticate to access secret message"
        ]
        var extractedData: CFTypeRef?
        let select_status = SecItemCopyMatching(select_query, &extractedData)
        if select_status == errSecSuccess {
            if let retrievedData = extractedData as? Data,
                let secretMessage = String(data: retrievedData, encoding: .utf8) {

                print("Secret message: \(secretMessage)")

                // UI updates must be dispatched back to the main thread.

                DispatchQueue.main.async {
                    self.messageLabel.text = secretMessage
                }

            } else {
                print("Invalid data")
            }
        } else if select_status == errSecUserCanceled {
            print("User canceled the operation.")
        } else {
            print("SELECT Error: \(select_status).")
        }
    }
}
23
Martin R