Ricky
Ricky

Reputation: 3171

How to check for existing Keychain item without invoking biometrics

I have an app that stores a user's password in the device Keychain, and it is accessed using the device biometrics (Face ID or Touch ID).

I do this successfully with the following:

const SecAccessControlRef accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlUserPresence, &accessControlError);
LAContext * const localAuthContext = [[LAContext alloc] init];
NSDictionary * const addQuery = @{
   (__bridge NSString *)kSecClass: (__bridge NSString *)kSecClassGenericPassword,
   (__bridge NSString *)kSecAttrAccount: username,
   (__bridge NSString *)kSecAttrAccessControl: (__bridge id)accessControl,
   (__bridge NSString *)kSecUseAuthenticationContext: localAuthContext,
   (__bridge NSString *)kSecValueData: passwordData
};
const OSStatus addStatus = SecItemAdd((__bridge CFDictionaryRef)addQuery, nil);

The problem arises when I want to update the password. I need to use the SecItemUpdate(...) function. So I implemented a check to see if the item already exists for the given username, but because of how the item was stored, the Face ID prompt on my iPhone X comes up.

NSDictionary * const findQuery = @{
   (__bridge NSString *)kSecClass: (__bridge NSString *)kSecClassGenericPassword,
   (__bridge NSString *)kSecAttrAccount: username,
   (__bridge NSString *)kSecReturnData: @(NO)
};
const OSStatus readStatus = SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, nil);

Is there any way to do this without invoking the biometrics access? If not, how can I reliably check whether I am to add or update a Keychain item?

Upvotes: 4

Views: 2010

Answers (2)

Michael Long
Michael Long

Reputation: 1102

I'm using something like the following...

    func has(service: String, account: String, options: [Options] = []) -> Bool {
        var query = self.query(service: service, account: account, options: options)
        query[kSecUseAuthenticationContext as String] = internalContext

        var secItemResult: CFTypeRef?
        status = SecItemCopyMatching(query as CFDictionary, &secItemResult)

        return status == errSecSuccess || status == errSecInteractionNotAllowed
    }

    private var internalContext: LAContext? = {
        let context = LAContext()
        context.interactionNotAllowed = true
        return context
    }()

Formerly you could also use UseAuthenticationUIFail, but that was deprecated as of iOS 14.

Upvotes: 3

Ricky
Ricky

Reputation: 3171

I couldn't find a reliable way to use SecItemUpdate(...) without invoking biometrics, so what I have resorted to is deleting a pre-existing entry and then adding a new one (effectively updating it).

This block of code adds a new item, or replaces an existing one.

NSData * const passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
NSString * const itemClass = (__bridge NSString *)kSecClassGenericPassword;
NSString * const account = username;
NSString * const service = [[NSBundle mainBundle] bundleIdentifier];

// First, try to find an existing item
NSDictionary * const findQuery = @{
    (__bridge NSString *)kSecClass: itemClass,
    (__bridge NSString *)kSecAttrService: service,
    (__bridge NSString *)kSecAttrAccount: account,
    (__bridge NSString *)kSecUseAuthenticationUI: (__bridge NSString *)kSecUseAuthenticationUIFail,
};
const OSStatus readStatus = SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, nil);
NSLog (@"Tried to find existing secure local password. Status = %d", readStatus);
const BOOL passwordAlreadyExists = (readStatus == errSecInteractionNotAllowed);
if (passwordAlreadyExists) {
    // Delete
    NSDictionary * const deleteQuery = @{
        (__bridge NSString *)kSecClass: itemClass,
        (__bridge NSString *)kSecAttrService: service,
        (__bridge NSString *)kSecAttrAccount: account,
        (__bridge NSString *)kSecReturnData: @(NO)
    };
    const OSStatus deleteStatus = SecItemDelete((__bridge CFDictionaryRef)deleteQuery);
    NSLog (@"Deleted existing secure local password. Status = %d", deleteStatus);
}

// Create an access control instance that dictates how the item can be read later.
CFErrorRef accessControlError = nil;
const SecAccessControlRef accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlUserPresence, &accessControlError);
if (accessControlError) {
    NSError * const error = (__bridge NSError *)accessControlError;
    NSLog (@"There was an error creating the access control: %@", error.localizedDescription);
    return;
}

// Create the context
LAContext * const localAuthContext = [[LAContext alloc] init];
localAuthContext.localizedFallbackTitle = @"Use Magic Link";

// Build the query
NSDictionary * const addQuery = @{
    (__bridge NSString *)kSecClass: itemClass,
    (__bridge NSString *)kSecAttrService: service,
    (__bridge NSString *)kSecAttrAccount: account,
    (__bridge NSString *)kSecAttrAccessControl: (__bridge id)accessControl,
    (__bridge NSString *)kSecUseAuthenticationContext: localAuthContext,
    (__bridge NSString *)kSecValueData: passwordData
};

// Execute
const OSStatus addStatus = SecItemAdd((__bridge CFDictionaryRef)addQuery, nil);
NSLog (@"Created secure local password. Status = %d", addStatus);

Upvotes: 1

Related Questions