-
Notifications
You must be signed in to change notification settings - Fork 217
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
[Valet 4.0] Convert APIs to throw rather than return booleans or enums #198
Conversation
fb98f75
to
f44c6e0
Compare
/// Migration failed because the keychain query was not valid. | ||
case invalidQuery | ||
/// Migration failed because no items to migrate were found. | ||
case noItemsToMigrateFound |
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.
Instead we'll throw KeychainError.itemNotFound
.
/// Migration failed because no items to migrate were found. | ||
case noItemsToMigrateFound | ||
/// Migration failed because the keychain could not be read. | ||
case couldNotReadKeychain |
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.
Instead we'll throw KeychainError.couldNotAccessKeychain
.
@@ -38,8 +31,6 @@ public enum MigrationResult: Int, Equatable { | |||
case duplicateKeyInQueryResult | |||
/// Migration failed because a key in the keychain duplicates a key already managed by Valet. | |||
case keyInQueryResultAlreadyExistsInValet | |||
/// Migration failed because writing to the keychain failed. | |||
case couldNotWriteToKeychain |
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 error was never used.
README.md
Outdated
NSString *const myLuggageCombination = [myValet stringForKey:username]; | ||
``` | ||
|
||
In addition to allowing the storage of strings, Valet allows the storage of `Data` objects via `set(object: Data, forKey key: Key)` and `-objectForKey:`. Valets created with a different class type, via a different initializer, or with a different accessibility attribute will not be able to read or modify values in `myValet`. |
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.
import Foundation | ||
|
||
|
||
public final class ErrorHandler { |
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.
We no longer needed a hot-swappable ErrorHandler
. We now throw
when an error occurs. We also have assertionFailure
in a few internal
methods where the error would very much be a programmer error within Valet.
|
||
case let .error(status): | ||
switch status { | ||
case errSecInteractionNotAllowed, errSecMissingEntitlement: |
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 error handling got consolidated in SecItem.deleteItems
} | ||
|
||
|
||
internal final class SecItem { | ||
|
||
// MARK: Internal Enum | ||
|
||
internal enum DataResult<SuccessType> { |
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.
Excited that we can get rid of these types.
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.
(Note we could still get rid of these even if we didn’t move to a throws
API, since Swift 5 has a built-in Result
type)
return .error(errSecParam) | ||
internal static func copy<DesiredType>(matching query: [String : AnyHashable]) throws -> DesiredType { | ||
if query.isEmpty { | ||
assertionFailure("Must provide a query with at least one item") |
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.
We assert, and then continue here. We'll end up throwing a couldNotAccessKeychain
later in this method. But if we hit this assertion, we have problems within Valet.
/// - returns: The data currently stored in the keychain for the provided key. Returns `nil` if no object exists in the keychain for the specified key, or if the keychain is inaccessible. | ||
@available(swift, obsoleted: 1.0) | ||
@objc(objectForKey:userPrompt:userCancelled:) | ||
public func 🚫swift_object(forKey key: String, withPrompt userPrompt: String, userCancelled: UnsafeMutablePointer<ObjCBool>?) -> 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.
these methods are no longer necessary, because the error
parameter will have the information to determine if a user cancelled.
@@ -101,17 +101,17 @@ The Accessibility enum is used to determine when your secrets can be accessed. I | |||
|
|||
```swift | |||
let username = "Skroob" | |||
myValet.set(string: "12345", forKey: username) | |||
try? myValet.setString("12345", forKey: username) |
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.
Thoughts on whether we should show the error being ignored? Both here and in the Objective-C code below. Previously we were discarding the return type, so the current sample-code maintains the prior behavior.
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.
Seems reasonable to keep the example simple. The presence of try?
implies that there is an error case that could (should) be handled in real usage.
setQueue.async { XCTAssertTrue(self.valet.set(string: self.passcode, forKey: self.key)) } | ||
removeQueue.async { XCTAssertTrue(self.valet.removeObject(forKey: self.key)) } | ||
setQueue.async { | ||
do { |
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.
we need to use a do/catch
block because we're within an async
() -> Void
block that can't throw
.
Thanks @dfed |
…s yet. Documentation missing.
I believe Nick's feedback has been fully addressed. This PR should be ready for review again. |
} | ||
|
||
// MARK: Contains | ||
|
||
internal static func containsObject(forKey key: String, options: [String : AnyHashable]) -> SecItem.Result { | ||
internal static func containsObject(forKey key: String, options: [String : AnyHashable]) -> OSStatus { |
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 be exposing OSStatus
in the interface here? Seems like Keychain
abstracts away the underlying error codes everywhere else except 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.
This is an internal
method. The call-sites need to inspect the status code in order to make informed decisions. Specifically, the SecureEnclaveValet
and SinglePromptSecureEnclaveValet
need to test if an authentication failure error occured, whereas the regular Valet
does not.
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.
Previously, we included the necessary information in SecItem.Result
. I could theoretically create a new intermediary type (we've deleted SecItem.Result
in this PR), but since we're not exposing anything publicly that seemed like overkill? I agree that this is a bit of a sharp edge in an otherwise very simple system.
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.
Yeah, that's fair. Given it's all internal, it seems alright.
if Keychain.containsObject(forKey: key, options: destinationAttributes) == errSecItemNotFound { | ||
keysToMigrate.insert(key) | ||
} else { | ||
throw MigrationError.keyInQueryResultAlreadyExistsInValet |
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 this throw a different error depending on what containsObject(...)
returns?
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 don't think so? Note that we're currently preserving existing behavior here (we previously returned a keyInQueryResultAlreadyExistsInValet
, but now we're throwing). If we want to update this logic, let's do that in a separate PR 🙂
throw KeychainError.missingEntitlement | ||
|
||
default: | ||
// We succeeded as long as we can confirm that the item is not in the keychain. |
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.
Would all of the other errors mean that this succeeded? Seems like errSecSuccess
is the only one that means it actually got deleted.
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 preserves existing behavior that was previously in Keychain.swift. Let's consider improving this code in a separate pass to reduce churn in this PR.
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.
Ahh okay, I thought this was a change in behavior given that it previously returned .error(status)
. If this was existing, then LGTM.
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.
Let's add a comment here. This does indeed look weird when we know KeychainError
has more errors that it could handle. Also, should we make this explicit by handling all cases instead of using default
?
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 like making it explicit. I think that makes it more clear, and removes the need for a comment. Good thinking!
Addressed in 4e9eb8b
This reverts commit cadeb20.
throw KeychainError.missingEntitlement | ||
|
||
default: | ||
// We succeeded as long as we can confirm that the item is not in the keychain. |
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.
Ahh okay, I thought this was a change in behavior given that it previously returned .error(status)
. If this was existing, then LGTM.
40e8082
to
efeced6
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.
Mostly nits, this looks great!
@@ -48,216 +48,173 @@ internal final class Keychain { | |||
var secItemQuery = attributes | |||
secItemQuery[kSecAttrAccount as String] = canaryKey | |||
secItemQuery[kSecValueData as String] = Data(canaryValue.utf8) | |||
_ = SecItem.add(attributes: secItemQuery) | |||
try? SecItem.add(attributes: secItemQuery) |
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.
Unrelated to this PR but it feels like a method called canAccess
wouldn't have side-effects.
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 side effect is invisible to the customer. I have a test to prove that the sentinel I'm writing doesn't appear in allKeys
.
You're right that this is weird, but I think it's okay because the customer never has to know it happened.
} | ||
|
||
return setObject(data, forKey: key, options: options) | ||
try setObject(data, forKey: key, options: options) |
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.
Much nicer 👍
} | ||
|
||
// MARK: Contains | ||
|
||
internal static func containsObject(forKey key: String, options: [String : AnyHashable]) -> SecItem.Result { | ||
internal static func containsObject(forKey key: String, options: [String : AnyHashable]) -> OSStatus { |
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.
nit: osStatus(forKey
?
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.
Or alternative, delete the OSStatus
return type, making this throw and replace the return type for a Bool by changing:
return SecItem.containsObject(matching: secItemQuery)
to
let status = SecItem.containsObject(matching: secItemQuery)
guard status == errSecSuccess {
throw KeychainError(status: status)
}
return status == errSecSuccess
and making this method throw an KeychainError?
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.
Nevermind, I saw below your reply to @NickEntin and also I saw how you're using this and I think it makes sense to keep this not throwing. Maybe we do want to change the method name though since containsObject
sounds like something that will return a Bool?
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.
Fair that the name doesn't match the return type. Thoughts on:
internal static func containsObject(forKey key: String, options: [String : AnyHashable]) -> OSStatus { | |
internal static func performCopy(matchingKey key: String, options: [String : AnyHashable]) -> OSStatus { |
I'm honestly struggling with this one. What this method does under the hood is perform a SecItemCopyMatching
, but doesn't retrieve the data. It just needs the OSStatus
return type from the copy action.
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.
Or maybe
internal static func containsObject(forKey key: String, options: [String : AnyHashable]) -> OSStatus { | |
internal static func copyItemStatus(forKey key: String, options: [String : AnyHashable]) -> OSStatus { |
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 vote is for performCopy(matching:)
. The fact is returns a status is implied by the return 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.
Addressed in fd5feba
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.
Works for me! Thanks for addressing.
Sources/Valet/Internal/SecItem.swift
Outdated
} | ||
} | ||
|
||
internal static func containsObject(matching query: [String : AnyHashable]) -> Result { | ||
internal static func containsObject(matching query: [String : AnyHashable]) -> OSStatus { | ||
guard query.count > 0 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.
nit: Let's use isEmpty
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.
addressed in 249188f
throw KeychainError.missingEntitlement | ||
|
||
default: | ||
// We succeeded as long as we can confirm that the item is not in the keychain. |
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.
Let's add a comment here. This does indeed look weird when we know KeychainError
has more errors that it could handle. Also, should we make this explicit by handling all cases instead of using default
?
} | ||
return SecureEnclave.containsObject(forKey: key, options: keychainQuery) | ||
/// - Returns: `true` if a value has been set for the given key, `false` otherwise. | ||
/// - Note: Will never prompt the user for Face ID, Touch ID, or password. Method will throw a `KeychainError`. |
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.
Let's use Throws
instead of Note
for the KeychainError comment.
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.
Ah! Missed one! Nice catch 🙂.
Resolved in e7023c9
@@ -283,7 +257,7 @@ extension SecureEnclaveValet { | |||
} | |||
return valet(with: identifier, accessControl: accessControl) | |||
} | |||
|
|||
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.
nit: Unnecessary empty space
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.
Resolved in d1b2d80
@@ -20,16 +20,11 @@ | |||
|
|||
import Foundation | |||
|
|||
|
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.
nit: Empty extra space?
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.
Welcome to Square's style guide. Two empty newlines between imports and code 😉
This PR tries to make Valet more semantic Swift by relying on
throws
rather thanBool
orenum
return types. It's a bit of a massive change, but IMO this API feels a whole lot nicer to use.