-
Notifications
You must be signed in to change notification settings - Fork 19
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
Conversation
a1ea1b7
to
cea880c
Compare
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
- If we save and get a duplicateItem error, we'll have to delete anyway, and then re-save. That'll result in 3 operations.
- 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.
- 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.
- 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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
?
No description provided.