Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Control biometric / keychain access control settings #39

Merged
merged 4 commits into from
Apr 27, 2022

Conversation

mikenachbaur-okta
Copy link
Contributor

No description provided.

@mikenachbaur-okta mikenachbaur-okta marked this pull request as draft April 21, 2022 17:34
Copy link

@IldarAbdullin-okta IldarAbdullin-okta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very clean and readable 👍
Left several comments to consider

}
}

guard let result = ref as? Dictionary<String, Any> else {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we wrap AnyObject into Keychain.Item rather than wrap concrete type so owner can do the upcast on its side(it can be Data for example)? Looks as strict requirement to GET item only as Dictionary<String, Any>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand. Are you referring to using AnyObject in the value property, instead of using Data explicitly?

I've done that in the past, where I did auto-encoding of data into the keychain, but encountered a lot of problems with custom coders/decoders, among other issues. So I decided this time around to stick as close to what the Keychain itself provides, and leave it as an exercise to the developer to encode the data they want (or, create a custom extension of Keychain.Item that does the right thing).

If that's not what you meant, my apologies :-D

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think I missed important setting in query kSecReturnAttributes so you basically asking keychain to return attributes along with data. So downcast to Dictionary<String, Any> a bit confused me as without kSecReturnAttributes setting keychain returns reference to Data or SecKey type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct; this current use-case for the Keychain class is limited to the Generic Password type (pretty much the majority use-case, since dealing with certificates isn't as common).

extension KeychainGettable {
var getQuery: [String: Any] {
var result = self.query
result[kSecMatchLimit as String] = kSecMatchLimitOne

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we wanna make it configurable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to keep it simple, making a get only return a single item ( it include data), while a list could return multiple items without data (meaning a list could safely be used without popping FaceID/TouchID prompts).

I also wanted to make the interface unambiguous without the need for nullability checks. So if an item is returned, it can be accessed simply.

I could theoretically make list configurable though. List is useful because a search result itself has a get method, allowing multiple items to be retrieved.

}
}

func performUpdate(_ item: Keychain.Item, authenticationContext: KeychainAuthenticationContext?) throws {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this function only updates data via remove/insert. Do we want to allow update not only data but some other attributes, let's say accessibility parameters?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is calling the underlying keychain update API.

let updateSearchQuery = self.updateQuery

var saveQuery = item.query
saveQuery.removeValue(forKey: kSecClass as String)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, why we are doing remove if our underlying call is SecItemUpdate? User may loose previous data if SecItemUpdate fails

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is removing the keychain item class, not the actual keychain item itself. This is to just format the query into something acceptable by the keychain API.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I see. We just normalizing query here


Keychain.implementation.deleteItem(cfDictionary)
Keychain.implementation.deleteItem(cfDictionary as CFDictionary)
Copy link

@IldarAbdullin-okta IldarAbdullin-okta Apr 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just create explicit error for duplicate item(errSecDuplicateItem) case instead of physically delete? There is a risk that user may loose the data

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern around saves is a bit complex.

  1. If we save and get a duplicateItem error, we'll have to delete anyway, and then re-save. That'll result in 3 operations.
  2. If we save and get a duplicateItem, we can call update, but will still need to perform a Get to retrieve the saved item, which risks popping up a FaceID prompt in the process, and still results in 3 operations.
  3. If we perform a delete and it fails for a reason other than duplicateItem, the save may or may not fail as well, which would be moot point.
  4. If the delete succeeds, but the save fails for some reason, there is a chance of data loss, but only if the newly created keychain item is invalid / poorly formed (e.g. conflicting access control policy settings).

I think the risk is low, though I'd like to get your thoughts on the matter.


delegate?.token(storage: self, replaced: id, from: oldToken, to: token)
try? oldResult.delete()
try newItem.save(authenticationContext: security?.context)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we don't use update API here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update has some limitations, in particular it doesn't let you return a reference of the newly created keychain item. So for cases such as this, a delete/remove is often safer.

I plan to use update in the setMetadata method however.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You basically already did the GET of oldResult so you have all required attributes to copy into the new Item. I still like the GET/UPDATE approach since it is only 2 operations comparing to GET/DELETE/ADD. Data safety aspect is also important

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, in this particular use-case you're totally correct, this is a prime candidate for Update. I'll make that change.

.Item(account: metadata.id,
service: KeychainTokenStorage.metadataName,
accessibility: .afterFirstUnlock,
value: try encoder.encode(metadata))
.save()

try? item.delete()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risky. Maybe just build the logic around Add/Update APIs rather than Delete/Add? I think Add/Update is not that risky, at least can't cause data loss

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mentioned this a bit up above, both the discussion around how to handle Delete/Add, as well as the use-cases around Update. For Metadata items, I'll change it to use Update, since I don't need to get the resulting item.

accessibility: .unlocked,
value: data)
.save()
let accessibility = security?.accessibility ?? .afterFirstUnlock

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we derive this setting from oldResult?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user could potentially change the accessibility settings, but you're correct in that it should default to the current value.

value: data)
.save()
let accessibility = security?.accessibility ?? .afterFirstUnlock
let accessGroup = security?.accessGroup

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we derive this setting from oldResult?

@mikenachbaur-okta mikenachbaur-okta merged commit 30170d2 into master Apr 27, 2022
@mikenachbaur-okta mikenachbaur-okta deleted the man-OKTA-467632-Biometric branch April 27, 2022 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants