Skip to content

Commit

Permalink
More
Browse files Browse the repository at this point in the history
  • Loading branch information
dfed committed Oct 11, 2024
1 parent 9919686 commit 99990d2
Show file tree
Hide file tree
Showing 17 changed files with 190 additions and 87 deletions.
2 changes: 1 addition & 1 deletion Sources/LegacyValet/Public/VALLegacySecureEnclaveValet.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// limitations under the License.
//

#import "VALLegacyValet.h"
#import <LegacyValet/VALLegacyValet.h>


/// Compiler flag for building against an SDK where Secure Enclave is available.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// limitations under the License.
//

#import "VALLegacySecureEnclaveValet.h"
#import <LegacyValet/VALLegacySecureEnclaveValet.h>


/// Reads and writes keychain elements that are stored on the Secure Enclave (available on iOS 8.0 and later and macOS 10.11 and later) using accessibility attribute VALLegacyAccessibilityWhenPasscodeSetThisDeviceOnly. The first access of these keychain elements will require the user to confirm their presence via Touch ID or passcode entry.
Expand Down
2 changes: 1 addition & 1 deletion Sources/LegacyValet/Public/VALSynchronizableValet.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// limitations under the License.
//

#import "VALLegacyValet.h"
#import <LegacyValet/VALLegacyValet.h>


/// Reads and writes keychain elements that are synchronized with iCloud (supported on devices on iOS 7.0.3 and later). Accessibility must not be scoped to this device.
Expand Down
12 changes: 6 additions & 6 deletions Sources/Valet/Internal/Keychain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ internal final class Keychain {

// MARK: Getters

internal static func string(forKey key: String, options: [String : AnyHashable]) throws -> String {
internal static func string(forKey key: String, options: [String : AnyHashable]) throws(KeychainError) -> String {
let data = try object(forKey: key, options: options)
if let string = String(data: data, encoding: .utf8) {
return string
Expand All @@ -61,7 +61,7 @@ internal final class Keychain {
}
}

internal static func object(forKey key: String, options: [String : AnyHashable]) throws -> Data {
internal static func object(forKey key: String, options: [String : AnyHashable]) throws(KeychainError) -> Data {
guard !key.isEmpty else {
throw KeychainError.emptyKey
}
Expand All @@ -76,7 +76,7 @@ internal final class Keychain {

// MARK: Setters

internal static func setString(_ string: String, forKey key: String, options: [String: AnyHashable]) throws {
internal static func setString(_ string: String, forKey key: String, options: [String: AnyHashable]) throws(KeychainError) {
let data = Data(string.utf8)
try setObject(data, forKey: key, options: options)
}
Expand Down Expand Up @@ -110,7 +110,7 @@ internal final class Keychain {

// MARK: Removal

internal static func removeObject(forKey key: String, options: [String : AnyHashable]) throws {
internal static func removeObject(forKey key: String, options: [String : AnyHashable]) throws(KeychainError) {
guard !key.isEmpty else {
throw KeychainError.emptyKey
}
Expand All @@ -121,7 +121,7 @@ internal final class Keychain {
try SecItem.deleteItems(matching: secItemQuery)
}

internal static func removeAllObjects(matching options: [String : AnyHashable]) throws {
internal static func removeAllObjects(matching options: [String : AnyHashable]) throws(KeychainError) {
try SecItem.deleteItems(matching: options)
}

Expand All @@ -140,7 +140,7 @@ internal final class Keychain {

// MARK: AllObjects

internal static func allKeys(options: [String: AnyHashable]) throws -> Set<String> {
internal static func allKeys(options: [String: AnyHashable]) throws(KeychainError) -> Set<String> {
var secItemQuery = options
secItemQuery[kSecMatchLimit as String] = kSecMatchLimitAll
secItemQuery[kSecReturnAttributes as String] = true
Expand Down
33 changes: 24 additions & 9 deletions Sources/Valet/SecureEnclave.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
// limitations under the License.
//

#if !os(tvOS) && canImport(LocalAuthentication)
import LocalAuthentication
#endif
import Foundation


Expand Down Expand Up @@ -50,7 +53,7 @@ public final class SecureEnclave: Sendable {
/// - key: A key that can be used to retrieve the `object` from the keychain.
/// - options: A base query used to scope the calls in the keychain.
/// - Throws: An error of type `KeychainError`.
internal static func setObject(_ object: Data, forKey key: String, options: [String : AnyHashable]) throws {
internal static func setObject(_ object: Data, forKey key: String, options: [String : AnyHashable]) throws(KeychainError) {
// Remove the key before trying to set it. This will prevent us from calling SecItemUpdate on an item stored on the Secure Enclave, which would cause iOS to prompt the user for authentication.
try Keychain.removeObject(forKey: key, options: options)

Expand All @@ -63,23 +66,30 @@ public final class SecureEnclave: Sendable {
/// - options: A base query used to scope the calls in the keychain.
/// - Returns: The data currently stored in the keychain for the provided key.
/// - Throws: An error of type `KeychainError`.
internal static func object(forKey key: String, withPrompt userPrompt: String, options: [String : AnyHashable]) throws -> Data {
internal static func object(forKey key: String, withPrompt userPrompt: String, options: [String : AnyHashable]) throws(KeychainError) -> Data {
var secItemQuery = options
#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication)
if !userPrompt.isEmpty {
secItemQuery[kSecUseOperationPrompt as String] = userPrompt
let context = LAContext()
context.localizedReason = userPrompt
secItemQuery[kSecUseAuthenticationContext as String] = context
}

#endif

return try Keychain.object(forKey: key, options: secItemQuery)
}

#if !os(tvOS) && canImport(LocalAuthentication)
/// - Parameters:
/// - key: The key to look up in the keychain.
/// - options: A base query used to scope the calls in the keychain.
/// - Returns: `true` if a value has been set for the given key, `false` otherwise.
/// - Throws: An error of type `KeychainError`.
internal static func containsObject(forKey key: String, options: [String : AnyHashable]) throws -> Bool {
internal static func containsObject(forKey key: String, options: [String : AnyHashable]) throws(KeychainError) -> Bool {
var secItemQuery = options
secItemQuery[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
let context = LAContext()
context.interactionNotAllowed = true
secItemQuery[kSecUseAuthenticationContext as String] = context

let status = Keychain.performCopy(forKey: key, options: secItemQuery)
switch status {
Expand All @@ -93,13 +103,14 @@ public final class SecureEnclave: Sendable {
throw KeychainError(status: status)
}
}
#endif

/// - Parameters:
/// - string: A String value to be inserted into the keychain.
/// - key: A key that can be used to retrieve the `string` from the keychain.
/// - options: A base query used to scope the calls in the keychain.
/// - Throws: An error of type `KeychainError`.
internal static func setString(_ string: String, forKey key: String, options: [String : AnyHashable]) throws {
internal static func setString(_ string: String, forKey key: String, options: [String : AnyHashable]) throws(KeychainError) {
// Remove the key before trying to set it. This will prevent us from calling SecItemUpdate on an item stored on the Secure Enclave, which would cause iOS to prompt the user for authentication.
try Keychain.removeObject(forKey: key, options: options)

Expand All @@ -112,11 +123,15 @@ public final class SecureEnclave: Sendable {
/// - options: A base query used to scope the calls in the keychain.
/// - Returns: The string currently stored in the keychain for the provided key.
/// - Throws: An error of type `KeychainError`.
internal static func string(forKey key: String, withPrompt userPrompt: String, options: [String : AnyHashable]) throws -> String {
internal static func string(forKey key: String, withPrompt userPrompt: String, options: [String : AnyHashable]) throws(KeychainError) -> String {
var secItemQuery = options
#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication)
if !userPrompt.isEmpty {
secItemQuery[kSecUseOperationPrompt as String] = userPrompt
let context = LAContext()
context.localizedReason = userPrompt
secItemQuery[kSecUseAuthenticationContext as String] = context
}
#endif

return try Keychain.string(forKey: key, options: secItemQuery)
}
Expand Down
90 changes: 68 additions & 22 deletions Sources/Valet/SecureEnclaveValet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,75 +121,119 @@ public final class SecureEnclaveValet: NSObject, Sendable {
/// - Throws: An error of type `KeychainError`.
/// - Important: Inserted data should be no larger than 4kb.
@objc
public func setObject(_ object: Data, forKey key: String) throws {
try execute(in: lock) {
try SecureEnclave.setObject(object, forKey: key, options: baseKeychainQuery)
public func setObject(_ object: Data, forKey key: String) throws(KeychainError) {
lock.lock()
defer {
lock.unlock()
}
return try SecureEnclave.setObject(object, forKey: key, options: baseKeychainQuery)
}

#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication)
/// - Parameters:
/// - key: A key used to retrieve the desired object from the keychain.
/// - userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI.
/// - Returns: The data currently stored in the keychain for the provided key.
/// - Throws: An error of type `KeychainError`.
@objc
public func object(forKey key: String, withPrompt userPrompt: String) throws -> Data {
try execute(in: lock) {
try SecureEnclave.object(forKey: key, withPrompt: userPrompt, options: baseKeychainQuery)
public func object(forKey key: String, withPrompt userPrompt: String) throws(KeychainError) -> Data {
lock.lock()
defer {
lock.unlock()
}
return try SecureEnclave.object(forKey: key, withPrompt: userPrompt, options: baseKeychainQuery)
}
#else
/// - Parameter key: A key used to retrieve the desired object from the keychain.
/// - Returns: The data currently stored in the keychain for the provided key.
/// - Throws: An error of type `KeychainError`.
@objc
public func object(forKey key: String) throws(KeychainError) -> Data {
lock.lock()
defer {
lock.unlock()
}
return try SecureEnclave.object(forKey: key, withPrompt: "", options: baseKeychainQuery)
}
#endif

#if !os(tvOS) && canImport(LocalAuthentication)
/// - Parameter key: The key to look up in the keychain.
/// - Returns: `true` if a value has been set for the given key, `false` otherwise.
/// - Throws: An error of type `KeychainError`.
/// - Note: Will never prompt the user for Face ID, Touch ID, or password.
public func containsObject(forKey key: String) throws -> Bool {
try execute(in: lock) {
try SecureEnclave.containsObject(forKey: key, options: baseKeychainQuery)
public func containsObject(forKey key: String) throws(KeychainError) -> Bool {
lock.lock()
defer {
lock.unlock()
}
return try SecureEnclave.containsObject(forKey: key, options: baseKeychainQuery)
}
#endif

/// - Parameters:
/// - string: A String value to be inserted into the keychain.
/// - key: A key that can be used to retrieve the `string` from the keychain.
/// - Throws: An error of type `KeychainError`.
/// - Important: Inserted data should be no larger than 4kb.
@objc
public func setString(_ string: String, forKey key: String) throws {
try execute(in: lock) {
try SecureEnclave.setString(string, forKey: key, options: baseKeychainQuery)
public func setString(_ string: String, forKey key: String) throws(KeychainError) {
lock.lock()
defer {
lock.unlock()
}
return try SecureEnclave.setString(string, forKey: key, options: baseKeychainQuery)
}

#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication)
/// - Parameters:
/// - key: A key used to retrieve the desired object from the keychain.
/// - 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.
/// - Throws: An error of type `KeychainError`.
@objc
public func string(forKey key: String, withPrompt userPrompt: String) throws -> String {
try execute(in: lock) {
try SecureEnclave.string(forKey: key, withPrompt: userPrompt, options: baseKeychainQuery)
public func string(forKey key: String, withPrompt userPrompt: String) throws(KeychainError) -> String {
lock.lock()
defer {
lock.unlock()
}
return try SecureEnclave.string(forKey: key, withPrompt: userPrompt, options: baseKeychainQuery)
}

#else
/// - Parameter key: A key used to retrieve the desired object from the keychain.
/// - Returns: The string currently stored in the keychain for the provided key.
/// - Throws: An error of type `KeychainError`.
@objc
public func string(forKey key: String) throws(KeychainError) -> String {
lock.lock()
defer {
lock.unlock()
}
return try SecureEnclave.string(forKey: key, withPrompt: "", options: baseKeychainQuery)
}
#endif

/// Removes a key/object pair from the keychain.
/// - Parameter key: A key used to remove the desired object from the keychain.
/// - Throws: An error of type `KeychainError`.
@objc
public func removeObject(forKey key: String) throws {
try execute(in: lock) {
try Keychain.removeObject(forKey: key, options: baseKeychainQuery)
public func removeObject(forKey key: String) throws(KeychainError) {
lock.lock()
defer {
lock.unlock()
}
return try Keychain.removeObject(forKey: key, options: baseKeychainQuery)
}

/// Removes all key/object pairs accessible by this Valet instance from the keychain.
/// - Throws: An error of type `KeychainError`.
@objc
public func removeAllObjects() throws {
try execute(in: lock) {
try Keychain.removeAllObjects(matching: baseKeychainQuery)
public func removeAllObjects() throws(KeychainError) {
lock.lock()
defer {
lock.unlock()
}
return try Keychain.removeAllObjects(matching: baseKeychainQuery)
}

/// Migrates objects matching the input query into the receiving SecureEnclaveValet instance.
Expand Down Expand Up @@ -284,6 +328,7 @@ extension SecureEnclaveValet {
return sharedGroupValet(with: identifier, accessControl: accessControl)
}

#if !os(tvOS) && canImport(LocalAuthentication)
/// - Parameter key: The key to look up in the keychain.
/// - 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.
Expand All @@ -295,5 +340,6 @@ extension SecureEnclaveValet {
}
return containsObject
}
#endif

}
8 changes: 4 additions & 4 deletions Sources/Valet/SinglePromptSecureEnclaveValet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@
// limitations under the License.
//

// Xcode 13 and prior incorrectly say that LocalAuthentication is available on tvOS, so we have to check both as long as Xcode 13 and prior are supported.
// Xcode 14 moved the LAContext availability to watchOS 3, so only that version is explicitly annotated.
#if !os(tvOS) && canImport(LocalAuthentication)
#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication)

import LocalAuthentication
import Foundation
Expand Down Expand Up @@ -196,7 +194,9 @@ public final class SinglePromptSecureEnclaveValet: NSObject, @unchecked Sendable
try execute(in: lock) {
var secItemQuery = try continuedAuthenticationKeychainQuery()
if !userPrompt.isEmpty {
secItemQuery[kSecUseOperationPrompt as String] = userPrompt
let context = LAContext()
context.localizedReason = userPrompt
secItemQuery[kSecUseAuthenticationContext as String] = context
}

return try Keychain.allKeys(options: secItemQuery)
Expand Down
Loading

0 comments on commit 99990d2

Please sign in to comment.