Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 6 additions & 20 deletions Sources/Valet/Accessibility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,15 @@ public enum Accessibility: Int, CustomStringConvertible, Equatable {
/// Valet data can only be accessed while the device is unlocked. This attribute is recommended for data that only needs to be accessible while the application is in the foreground. Valet data with this attribute will migrate to a new device when using encrypted backups.
case whenUnlocked = 1
/// Valet data can only be accessed once the device has been unlocked after a restart. This attribute is recommended for data that needs to be accessible by background applications. Valet data with this attribute will migrate to a new device when using encrypted backups.
case afterFirstUnlock
/// Valet data can always be accessed regardless of the lock state of the device. This attribute is not recommended. Valet data with this attribute will migrate to a new device when using encrypted backups.
case always

case afterFirstUnlock = 2

/// Valet data can only be accessed while the device is unlocked. This attribute is recommended for items that only need to be accessible while the application is in the foreground. Valet data with this attribute will never migrate to a new device, so these items will be missing after a backup is restored to a new device. No items can be stored in this class on devices without a passcode. Disabling the device passcode will cause all items in this class to be deleted.
case whenPasscodeSetThisDeviceOnly
case whenPasscodeSetThisDeviceOnly = 4
/// Valet data can only be accessed while the device is unlocked. This is recommended for data that only needs to be accessible while the application is in the foreground. Valet data with this attribute will never migrate to a new device, so these items will be missing after a backup is restored to a new device.
case whenUnlockedThisDeviceOnly
case whenUnlockedThisDeviceOnly = 5
/// Valet data can only be accessed once the device has been unlocked after a restart. This is recommended for items that need to be accessible by background applications. Valet data with this attribute will never migrate to a new device, so these items will be missing after a backup is restored to a new device.
case afterFirstUnlockThisDeviceOnly
/// Valet data can always be accessed regardless of the lock state of the device. This option is not recommended. Valet data with this attribute will never migrate to a new device, so these items will be missing after a backup is restored to a new device.
case alwaysThisDeviceOnly

case afterFirstUnlockThisDeviceOnly = 6

// MARK: CustomStringConvertible

public var description: String {
Expand All @@ -47,10 +43,6 @@ public enum Accessibility: Int, CustomStringConvertible, Equatable {
return "AccessibleAfterFirstUnlock"
case .afterFirstUnlockThisDeviceOnly:
return "AccessibleAfterFirstUnlockThisDeviceOnly"
case .always:
return "AccessibleAlways"
case .alwaysThisDeviceOnly:
return "AccessibleAlwaysThisDeviceOnly"
case .whenPasscodeSetThisDeviceOnly:
return "AccessibleWhenPasscodeSetThisDeviceOnly"
case .whenUnlocked:
Expand All @@ -70,10 +62,6 @@ public enum Accessibility: Int, CustomStringConvertible, Equatable {
accessibilityAttribute = kSecAttrAccessibleAfterFirstUnlock
case .afterFirstUnlockThisDeviceOnly:
accessibilityAttribute = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
case .always:
accessibilityAttribute = kSecAttrAccessibleAlways
case .alwaysThisDeviceOnly:
accessibilityAttribute = kSecAttrAccessibleAlwaysThisDeviceOnly
case .whenPasscodeSetThisDeviceOnly:
accessibilityAttribute = kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
case .whenUnlocked:
Expand All @@ -91,11 +79,9 @@ public enum Accessibility: Int, CustomStringConvertible, Equatable {
return [
.whenUnlocked,
.afterFirstUnlock,
.always,
.whenPasscodeSetThisDeviceOnly,
.whenUnlockedThisDeviceOnly,
.afterFirstUnlockThisDeviceOnly,
.alwaysThisDeviceOnly
]
}
}
9 changes: 2 additions & 7 deletions Sources/Valet/CloudAccessibility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,8 @@ public enum CloudAccessibility: Int, CustomStringConvertible, Equatable {
/// Valet data can only be accessed while the device is unlocked. This attribute is recommended for data that only needs to be accessible while the application is in the foreground. Valet data with this attribute will migrate to a new device when using encrypted backups.
case whenUnlocked = 1
/// Valet data can only be accessed once the device has been unlocked after a restart. This attribute is recommended for data that needs to be accessible by background applications. Valet data with this attribute will migrate to a new device when using encrypted backups.
case afterFirstUnlock
/// Valet data can always be accessed regardless of the lock state of the device. This attribute is not recommended. Valet data with this attribute will migrate to a new device when using encrypted backups.
case always

case afterFirstUnlock = 2

// MARK: CustomStringConvertible

public var description: String {
Expand All @@ -44,8 +42,6 @@ public enum CloudAccessibility: Int, CustomStringConvertible, Equatable {
return .whenUnlocked
case .afterFirstUnlock:
return .afterFirstUnlock
case .always:
return .always
}
}

Expand All @@ -59,7 +55,6 @@ public enum CloudAccessibility: Int, CustomStringConvertible, Equatable {
return [
.whenUnlocked,
.afterFirstUnlock,
.always
]
}
}
17 changes: 7 additions & 10 deletions Sources/Valet/Internal/SecItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ internal final class SecItem {
// MARK: Internal Class Properties

/// Programatically grab the required prefix for the shared access group (i.e. Bundle Seed ID). The value for the kSecAttrAccessGroup key in queries for data that is shared between apps must be of the format bundleSeedID.sharedAccessGroup. For more information on the Bundle Seed ID, see https://developer.apple.com/library/ios/qa/qa1713/_index.html
internal static var sharedAccessGroupPrefix: String {
internal static var sharedAccessGroupPrefix: String? {
let query = [
kSecClass : kSecClassGenericPassword,
kSecAttrAccount : "SharedAccessGroupAlwaysAccessiblePrefixPlaceholder",
kSecAttrAccount : "SharedAccessGroupPrefixPlaceholder",
kSecReturnAttributes : true,
kSecAttrAccessible : Accessibility.alwaysThisDeviceOnly.secAccessibilityAttribute
kSecAttrAccessible : Accessibility.afterFirstUnlockThisDeviceOnly.secAccessibilityAttribute
] as CFDictionary

secItemLock.lock()
Expand All @@ -90,22 +90,19 @@ internal final class SecItem {
}

guard status == errSecSuccess, let queryResult = result as? [CFString : AnyHashable], let accessGroup = queryResult[kSecAttrAccessGroup] as? String else {
ErrorHandler.assertionFailure("Could not find shared access group prefix.")
// We should always be able to access the shared access group prefix because the accessibility of the above keychain data is set to `always`.
// In other words, we should never hit this code. This code is here as a failsafe to prevent a crash in a scenario where the keychain is entirely hosed.
// We may not be able to access the shared access group prefix because the accessibility of the above keychain data is set to `afterFirstUnlock`.
// Consumers should always check `canAccessKeychain()` after creating a Valet and before using it. Doing so will catch this error.
return "INVALID_SHARED_ACCESS_GROUP_PREFIX"
return nil
}

let components = accessGroup.components(separatedBy: ".")
if let bundleSeedIdentifier = components.first, !bundleSeedIdentifier.isEmpty {
return bundleSeedIdentifier

} else {
// We should always be able to access the shared access group prefix because the accessibility of the above keychain data is set to `always`.
// In other words, we should never hit this code. This code is here as a failsafe to prevent a crash in a scenario where the keychain is entirely hosed.
// We may not be able to access the shared access group prefix because the accessibility of the above keychain data is set to `afterFirstUnlock`.
// Consumers should always check `canAccessKeychain()` after creating a Valet and before using it. Doing so will catch this error.
return "INVALID_SHARED_ACCESS_GROUP_PREFIX"
return nil
}
}

Expand Down
11 changes: 7 additions & 4 deletions Sources/Valet/Internal/Service.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Foundation
internal enum Service: CustomStringConvertible, Equatable {
case standard(Identifier, Configuration)
case sharedAccessGroup(Identifier, Configuration)

// MARK: Equatable

internal static func ==(lhs: Service, rhs: Service) -> Bool {
Expand All @@ -39,7 +39,7 @@ internal enum Service: CustomStringConvertible, Equatable {

// MARK: Internal Methods

internal func generateBaseQuery() -> [String : AnyHashable] {
internal func generateBaseQuery() -> [String : AnyHashable]? {
var baseQuery: [String : AnyHashable] = [
kSecClass as String : kSecClassGenericPassword as String,
kSecAttrService as String : secService
Expand All @@ -51,8 +51,11 @@ internal enum Service: CustomStringConvertible, Equatable {
configuration = desiredConfiguration

case let .sharedAccessGroup(identifier, desiredConfiguration):
ErrorHandler.assert(!identifier.description.hasPrefix("\(SecItem.sharedAccessGroupPrefix)."), "Do not add the Bundle Seed ID as a prefix to your identifier. Valet prepends this value for you. Your Valet will not be able to access the keychain with the provided configuration")
baseQuery[kSecAttrAccessGroup as String] = "\(SecItem.sharedAccessGroupPrefix).\(identifier.description)"
guard let sharedAccessGroupPrefix = SecItem.sharedAccessGroupPrefix else {
return nil
}
ErrorHandler.assert(!identifier.description.hasPrefix("\(sharedAccessGroupPrefix)."), "Do not add the Bundle Seed ID as a prefix to your identifier. Valet prepends this value for you. Your Valet will not be able to access the keychain with the provided configuration")
baseQuery[kSecAttrAccessGroup as String] = "\(sharedAccessGroupPrefix).\(identifier.description)"
configuration = desiredConfiguration
}

Expand Down
29 changes: 0 additions & 29 deletions Sources/Valet/KeychainQueryConvertible.swift

This file was deleted.

73 changes: 58 additions & 15 deletions Sources/Valet/SecureEnclaveValet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,25 @@ public final class SecureEnclaveValet: NSObject {
fatalError("Use the class methods above to create usable SecureEnclaveValet objects")
}

private init(identifier: Identifier, accessControl: SecureEnclaveAccessControl) {
service = .standard(identifier, .secureEnclave(accessControl))
keychainQuery = service.generateBaseQuery()
self.identifier = identifier
self.accessControl = accessControl
private convenience init(identifier: Identifier, accessControl: SecureEnclaveAccessControl) {
self.init(
identifier: identifier,
service: .standard(identifier, .secureEnclave(accessControl)),
accessControl: accessControl)
}

private init(sharedAccess identifier: Identifier, accessControl: SecureEnclaveAccessControl) {
service = .sharedAccessGroup(identifier, .secureEnclave(accessControl))
keychainQuery = service.generateBaseQuery()
private convenience init(sharedAccess identifier: Identifier, accessControl: SecureEnclaveAccessControl) {
self.init(
identifier: identifier,
service: .sharedAccessGroup(identifier, .secureEnclave(accessControl)),
accessControl: accessControl)
}

private init(identifier: Identifier, service: Service, accessControl: SecureEnclaveAccessControl) {
self.identifier = identifier
self.service = service
self.accessControl = accessControl
_keychainQuery = service.generateBaseQuery()
}

// MARK: Hashable
Expand Down Expand Up @@ -116,6 +123,9 @@ public final class SecureEnclaveValet: NSObject {
@discardableResult
public func set(object: Data, forKey key: String) -> Bool {
return execute(in: lock) {
guard let keychainQuery = keychainQuery else {
return false
}
return SecureEnclave.set(object: object, forKey: key, options: keychainQuery)
}
}
Expand All @@ -125,16 +135,22 @@ public final class SecureEnclaveValet: NSObject {
/// - returns: The data currently stored in the keychain for the provided key. Returns `.itemNotFound` if no object exists in the keychain for the specified key, or if the keychain is inaccessible. Returns `.userCancelled` if the user cancels the user-presence prompt.
public func object(forKey key: String, withPrompt userPrompt: String) -> SecureEnclave.Result<Data> {
return execute(in: lock) {
guard let keychainQuery = keychainQuery else {
return .itemNotFound
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we add a new result case for .keychainNotAccessible? (applies throughout diff)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Per the comment above:

Returns .itemNotFound if no object exists in the keychain for the specified key, or if the keychain is inaccessible.

So we're doing the right thing per the comment. Adding a new case here would be a breaking change. The Valet 4.0 code will throw the right error (couldNotAccessKeychain ) in this case assuming #198 lands

}
return SecureEnclave.object(forKey: key, withPrompt: userPrompt, options: keychainQuery)
}
}

/// - parameter key: The key to look up in the keychain.
/// - returns: `true` if a value has been set for the given key, `false` otherwise.
/// - returns: `true` if a value has been set for the given key, `false` otherwise. Will return `false` if the keychain is not accessible.
/// - note: Will never prompt the user for Face ID, Touch ID, or password.
@objc(containsObjectForKey:)
public func containsObject(forKey key: String) -> Bool {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should note in the header doc that it will return false if the keychain isn't accessible.

return execute(in: lock) {
guard let keychainQuery = keychainQuery else {
return false
}
return SecureEnclave.containsObject(forKey: key, options: keychainQuery)
}
}
Expand All @@ -146,15 +162,21 @@ public final class SecureEnclaveValet: NSObject {
@discardableResult
public func set(string: String, forKey key: String) -> Bool {
return execute(in: lock) {
guard let keychainQuery = keychainQuery else {
return false
}
return SecureEnclave.set(string: string, forKey: key, options: keychainQuery)
}
}

/// - parameter key: A Key used to retrieve the desired object from the keychain.
/// - parameter userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI.
/// - returns: The string currently stored in the keychain for the provided key. Returns `nil` if no string exists in the keychain for the specified key, or if the keychain is inaccessible.
/// - returns: The string currently stored in the keychain for the provided key. Returns `itemNotFound` if no string exists in the keychain for the specified key, or if the keychain is inaccessible.
public func string(forKey key: String, withPrompt userPrompt: String) -> SecureEnclave.Result<String> {
return execute(in: lock) {
guard let keychainQuery = keychainQuery else {
return .itemNotFound
}
return SecureEnclave.string(forKey: key, withPrompt: userPrompt, options: keychainQuery)
}
}
Expand All @@ -165,6 +187,9 @@ public final class SecureEnclaveValet: NSObject {
@discardableResult
public func removeObject(forKey key: String) -> Bool {
return execute(in: lock) {
guard let keychainQuery = keychainQuery else {
return false
}
return Keychain.removeObject(forKey: key, options: keychainQuery).didSucceed
}
}
Expand All @@ -175,6 +200,9 @@ public final class SecureEnclaveValet: NSObject {
@discardableResult
public func removeAllObjects() -> Bool {
return execute(in: lock) {
guard let keychainQuery = keychainQuery else {
return false
}
return Keychain.removeAllObjects(matching: keychainQuery).didSucceed
}
}
Expand All @@ -187,6 +215,9 @@ public final class SecureEnclaveValet: NSObject {
@objc(migrateObjectsMatchingQuery:removeOnCompletion:)
public func migrateObjects(matching query: [String : AnyHashable], removeOnCompletion: Bool) -> MigrationResult {
return execute(in: lock) {
guard let keychainQuery = keychainQuery else {
return .couldNotReadKeychain
}
return Keychain.migrateObjects(matching: query, into: keychainQuery, removeOnCompletion: removeOnCompletion)
}
}
Expand All @@ -196,19 +227,31 @@ public final class SecureEnclaveValet: NSObject {
/// - parameter removeOnCompletion: If `true`, the migrated data will be removed from the keychfain if the migration succeeds.
/// - returns: Whether the migration succeeded or failed.
/// - note: The keychain is not modified if a failure occurs.
@objc(migrateObjectsFromKeychain:removeOnCompletion:)
public func migrateObjects(from keychain: KeychainQueryConvertible, removeOnCompletion: Bool) -> MigrationResult {
return migrateObjects(matching: keychain.keychainQuery, removeOnCompletion: removeOnCompletion)
@objc(migrateObjectsFromValet:removeOnCompletion:)
public func migrateObjects(from valet: Valet, removeOnCompletion: Bool) -> MigrationResult {
guard let keychainQuery = valet.keychainQuery else {
return .couldNotReadKeychain
}
return migrateObjects(matching: keychainQuery, removeOnCompletion: removeOnCompletion)
}

// MARK: Internal Properties

internal let service: Service

// MARK: Private Properties

private let lock = NSLock()
private let keychainQuery: [String : AnyHashable]
private var keychainQuery: [String : AnyHashable]? {
if let keychainQuery = _keychainQuery {
return keychainQuery
} else {
_keychainQuery = service.generateBaseQuery()
return _keychainQuery
}
}

private var _keychainQuery: [String : AnyHashable]?
}


Expand Down
Loading