From 96fd28ba90b7f6fd6ea5e9c99e9586964b12070c Mon Sep 17 00:00:00 2001 From: Anton Sotkov Date: Wed, 24 Sep 2025 14:52:10 +0300 Subject: [PATCH 1/3] Add support for multiple Valets within explicitly set shared groups --- Sources/Valet/Internal/Service.swift | 16 ++--- Sources/Valet/SecureEnclave.swift | 4 +- Sources/Valet/SecureEnclaveValet.swift | 4 +- Sources/Valet/Valet.swift | 92 ++++++++++++++++++-------- 4 files changed, 75 insertions(+), 41 deletions(-) diff --git a/Sources/Valet/Internal/Service.swift b/Sources/Valet/Internal/Service.swift index 6e8a047e..2ffc2e6e 100644 --- a/Sources/Valet/Internal/Service.swift +++ b/Sources/Valet/Internal/Service.swift @@ -20,8 +20,8 @@ import Foundation enum Service: CustomStringConvertible, Equatable, Sendable { case standard(Identifier, Configuration) case sharedGroup(SharedGroupIdentifier, Identifier?, Configuration) - case standardOverride(service: Identifier, Configuration) - case sharedGroupOverride(service: SharedGroupIdentifier, Configuration) + case standardOverride(Identifier, Configuration) + case sharedGroupOverride(SharedGroupIdentifier, Identifier?, Configuration) // MARK: Equatable @@ -49,10 +49,6 @@ enum Service: CustomStringConvertible, Equatable, Sendable { } } - static func sharedGroup(with configuration: Configuration, explicitlySetIdentifier identifier: Identifier, accessibilityDescription: String) -> String { - "VAL_\(configuration.description)_initWithSharedAccessGroupIdentifier:accessibility:_\(identifier)_\(accessibilityDescription)" - } - // MARK: Internal Methods func generateBaseQuery() -> [String : AnyHashable] { @@ -77,8 +73,8 @@ enum Service: CustomStringConvertible, Equatable, Sendable { case let .standardOverride(_, desiredConfiguration): configuration = desiredConfiguration - case let .sharedGroupOverride(identifier, desiredConfiguration): - baseQuery[kSecAttrAccessGroup as String] = identifier.description + case let .sharedGroupOverride(groupIdentifier, _, desiredConfiguration): + baseQuery[kSecAttrAccessGroup as String] = groupIdentifier.description configuration = desiredConfiguration } @@ -110,8 +106,8 @@ enum Service: CustomStringConvertible, Equatable, Sendable { service = Service.sharedGroup(with: configuration, groupIdentifier: groupIdentifier, identifier: identifier, accessibilityDescription: configuration.accessibility.description) case let .standardOverride(identifier, _): service = identifier.description - case let .sharedGroupOverride(identifier, _): - service = identifier.groupIdentifier + case let .sharedGroupOverride(groupIdentifier, identifier, _): + service = identifier?.description ?? groupIdentifier.groupIdentifier } switch self { diff --git a/Sources/Valet/SecureEnclave.swift b/Sources/Valet/SecureEnclave.swift index 4e03106d..9dcb2735 100644 --- a/Sources/Valet/SecureEnclave.swift +++ b/Sources/Valet/SecureEnclave.swift @@ -35,8 +35,8 @@ public final class SecureEnclave: Sendable { noPromptValet = .valet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) case let .standard(identifier, _): noPromptValet = .valet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) - case let .sharedGroupOverride(identifier, _): - noPromptValet = .sharedGroupValet(withExplicitlySet: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) + case let .sharedGroupOverride(groupIdentifier, identifier, _): + noPromptValet = .sharedGroupValet(withExplicitlySet: groupIdentifier, identifier: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) case let .sharedGroup(groupIdentifier, identifier, _): noPromptValet = .sharedGroupValet(with: groupIdentifier, identifier: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) } diff --git a/Sources/Valet/SecureEnclaveValet.swift b/Sources/Valet/SecureEnclaveValet.swift index 734a7a4a..4c093a75 100644 --- a/Sources/Valet/SecureEnclaveValet.swift +++ b/Sources/Valet/SecureEnclaveValet.swift @@ -433,8 +433,8 @@ extension Service { .sharedGroup(sharedGroupIdentifier, identifier, .singlePromptSecureEnclave(accessControl)) case let .standardOverride(identifier, _): .standard(identifier, .singlePromptSecureEnclave(accessControl)) - case let .sharedGroupOverride(sharedGroupIdentifier, _): - .sharedGroupOverride(service: sharedGroupIdentifier, .singlePromptSecureEnclave(accessControl)) + case let .sharedGroupOverride(sharedGroupIdentifier, identifier, _): + .sharedGroupOverride(sharedGroupIdentifier, identifier, .singlePromptSecureEnclave(accessControl)) } } } diff --git a/Sources/Valet/Valet.swift b/Sources/Valet/Valet.swift index 674e2e57..efff7a4b 100644 --- a/Sources/Valet/Valet.swift +++ b/Sources/Valet/Valet.swift @@ -87,8 +87,8 @@ public final class Valet: NSObject, Sendable { /// - Returns: A Valet that reads/writes keychain elements that can be shared across applications written by the same development team. /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. /// - SeeAlso: https://github.com/square/Valet/issues/140 - public class func sharedGroupValet(withExplicitlySet identifier: SharedGroupIdentifier, accessibility: Accessibility) -> Valet { - findOrCreate(explicitlySet: identifier, configuration: .valet(accessibility)) + public class func sharedGroupValet(withExplicitlySet groupIdentifier: SharedGroupIdentifier, identifier: Identifier? = nil, accessibility: Accessibility) -> Valet { + findOrCreate(explicitlySet: groupIdentifier, identifier: identifier, configuration: .valet(accessibility)) } /// Creates an iCloud-shared-access-group Valet with an explicitly set kSecAttrService. This API is intended for use with macOS applications where service identifiers can be user-facing. @@ -98,8 +98,8 @@ public final class Valet: NSObject, Sendable { /// - Returns: A Valet (synchronized with iCloud) that reads/writes keychain elements that can be shared across applications written by the same development team. /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. /// - SeeAlso: https://github.com/square/Valet/issues/140 - public class func iCloudSharedGroupValet(withExplicitlySet identifier: SharedGroupIdentifier, accessibility: CloudAccessibility) -> Valet { - findOrCreate(explicitlySet: identifier, configuration: .iCloud(accessibility)) + public class func iCloudSharedGroupValet(withExplicitlySet groupIdentifier: SharedGroupIdentifier, identifier: Identifier? = nil, accessibility: CloudAccessibility) -> Valet { + findOrCreate(explicitlySet: groupIdentifier, identifier: identifier, configuration: .iCloud(accessibility)) } // MARK: Equatable @@ -143,7 +143,7 @@ public final class Valet: NSObject, Sendable { private class func findOrCreate(explicitlySet identifier: Identifier, configuration: Configuration) -> Valet { - let service: Service = .standardOverride(service: identifier, configuration) + let service: Service = .standardOverride(identifier, configuration) let key = service.description + configuration.description + configuration.accessibility.description + identifier.description if let existingValet = identifierToValetMap[key] { return existingValet @@ -155,14 +155,14 @@ public final class Valet: NSObject, Sendable { } } - private class func findOrCreate(explicitlySet identifier: SharedGroupIdentifier, configuration: Configuration) -> Valet { - let service: Service = .sharedGroupOverride(service: identifier, configuration) - let key = service.description + configuration.description + configuration.accessibility.description + identifier.description + private class func findOrCreate(explicitlySet groupIdentifier: SharedGroupIdentifier, identifier: Identifier?, configuration: Configuration) -> Valet { + let service: Service = .sharedGroupOverride(groupIdentifier, identifier, configuration) + let key = service.description + configuration.description + configuration.accessibility.description + groupIdentifier.description if let existingValet = identifierToValetMap[key] { return existingValet } else { - let valet = Valet(overrideSharedAccess: identifier, configuration: configuration) + let valet = Valet(overrideSharedAccess: groupIdentifier, identifier: identifier, configuration: configuration) identifierToValetMap[key] = valet return valet } @@ -200,14 +200,14 @@ public final class Valet: NSObject, Sendable { private init(overrideIdentifier: Identifier, configuration: Configuration) { self.identifier = overrideIdentifier self.configuration = configuration - service = .standardOverride(service: identifier, configuration) + service = .standardOverride(identifier, configuration) accessibility = configuration.accessibility } - private init(overrideSharedAccess identifier: SharedGroupIdentifier, configuration: Configuration) { - self.identifier = identifier.asIdentifier + private init(overrideSharedAccess groupIdentifier: SharedGroupIdentifier, identifier: Identifier?, configuration: Configuration) { + self.identifier = identifier ?? groupIdentifier.asIdentifier self.configuration = configuration - service = .sharedGroupOverride(service: identifier, configuration) + service = .sharedGroupOverride(groupIdentifier, identifier, configuration) accessibility = configuration.accessibility } @@ -476,8 +476,8 @@ public final class Valet: NSObject, Sendable { serviceAttribute = Service.sharedGroup(with: configuration, groupIdentifier: sharedGroupIdentifier, identifier: identifier, accessibilityDescription: accessibilityDescription) case .standard: serviceAttribute = Service.standard(with: configuration, identifier: identifier, accessibilityDescription: accessibilityDescription) - case let .sharedGroupOverride(sharedGroupIdentifier, _): - serviceAttribute = sharedGroupIdentifier.description + case let .sharedGroupOverride(groupIdentifier, identifier, _): + serviceAttribute = identifier?.description ?? groupIdentifier.description case .standardOverride: serviceAttribute = identifier.description } @@ -509,8 +509,8 @@ public final class Valet: NSObject, Sendable { serviceAttribute = Service.sharedGroup(with: configuration, groupIdentifier: groupIdentifier, identifier: identifier, accessibilityDescription: accessibilityDescription) case .standard: serviceAttribute = Service.standard(with: configuration, identifier: identifier, accessibilityDescription: accessibilityDescription) - case .sharedGroupOverride: - serviceAttribute = Service.sharedGroup(with: configuration, explicitlySetIdentifier: identifier, accessibilityDescription: accessibilityDescription) + case let .sharedGroupOverride(groupIdentifier, identifier, _): + serviceAttribute = Service.sharedGroup(with: configuration, groupIdentifier: groupIdentifier, identifier: identifier, accessibilityDescription: accessibilityDescription) case .standardOverride: serviceAttribute = Service.standard(with: configuration, identifier: identifier, accessibilityDescription: accessibilityDescription) } @@ -673,7 +673,7 @@ extension Valet { /// Creates a shared-access-group Valet with an explicitly set kSecAttrService. /// - Parameters: /// - appIDPrefix: The application's App ID prefix. This string can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty. - /// - identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. Must be unique relative to other Valet identifiers. + /// - groupIdentifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. Must be unique relative to other Valet identifiers. /// - accessibility: The desired accessibility for the Valet. /// - Returns: A Valet that reads/writes keychain elements that can be shared across applications written by the same development team. /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. @@ -681,17 +681,36 @@ extension Valet { /// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps @available(swift, obsoleted: 1.0) @objc(valetWithAppIDPrefix:explicitlySetSharedGroupIdentifier:accessibility:) - public class func 🚫swift_sharedGroupValet(appIDPrefix: String, withExplicitlySet identifier: String, accessibility: Accessibility) -> Valet? { - guard let identifier = SharedGroupIdentifier(appIDPrefix: appIDPrefix, nonEmptyGroup: identifier) else { + public class func 🚫swift_sharedGroupValet(appIDPrefix: String, withExplicitlySet groupIdentifier: String?, accessibility: Accessibility) -> Valet? { + guard let groupIdentifier = SharedGroupIdentifier(appIDPrefix: appIDPrefix, nonEmptyGroup: groupIdentifier) else { return nil } - return findOrCreate(explicitlySet: identifier, configuration: .valet(accessibility)) + return findOrCreate(explicitlySet: groupIdentifier, identifier: nil, configuration: .valet(accessibility)) + } + + /// Creates a shared-access-group Valet with an explicitly set kSecAttrService. + /// - Parameters: + /// - appIDPrefix: The application's App ID prefix. This string can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty. + /// - groupIdentifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. Must be unique relative to other Valet identifiers. + /// - identifier: A non-empty string that uniquely identifies a Valet. Must be unique relative to other Valet identifiers. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet that reads/writes keychain elements that can be shared across applications written by the same development team. + /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. + /// - SeeAlso: https://github.com/square/Valet/issues/140 + /// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps + @available(swift, obsoleted: 1.0) + @objc(valetWithAppIDPrefix:explicitlySetSharedGroupIdentifier:identifier:accessibility:) + public class func 🚫swift_sharedGroupValet(appIDPrefix: String, withExplicitlySet groupIdentifier: String?, identifier: String?, accessibility: Accessibility) -> Valet? { + guard let groupIdentifier = SharedGroupIdentifier(appIDPrefix: appIDPrefix, nonEmptyGroup: groupIdentifier) else { + return nil + } + return findOrCreate(explicitlySet: groupIdentifier, identifier: Identifier(nonEmpty: identifier), configuration: .valet(accessibility)) } /// Creates an iCloud-shared-access-group Valet with an explicitly set kSecAttrService. /// - Parameters: /// - appIDPrefix: The application's App ID prefix. This string can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty. - /// - identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. Must be unique relative to other Valet identifiers. + /// - groupIdentifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. Must be unique relative to other Valet identifiers. /// - accessibility: The desired accessibility for the Valet. /// - Returns: A Valet (synchronized with iCloud) that reads/writes keychain elements that can be shared across applications written by the same development team. /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. @@ -699,11 +718,30 @@ extension Valet { /// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps @available(swift, obsoleted: 1.0) @objc(iCloudValetWithAppIDPrefix:explicitlySetSharedGroupIdentifier:accessibility:) - public class func 🚫swift_iCloudSharedGroupValet(appIDPrefix: String, withExplicitlySet identifier: String, accessibility: CloudAccessibility) -> Valet? { - guard let identifier = SharedGroupIdentifier(appIDPrefix: appIDPrefix, nonEmptyGroup: identifier) else { + public class func 🚫swift_iCloudSharedGroupValet(appIDPrefix: String, withExplicitlySet groupIdentifier: String, accessibility: CloudAccessibility) -> Valet? { + guard let groupIdentifier = SharedGroupIdentifier(appIDPrefix: appIDPrefix, nonEmptyGroup: groupIdentifier) else { return nil } - return findOrCreate(explicitlySet: identifier, configuration: .iCloud(accessibility)) + return findOrCreate(explicitlySet: groupIdentifier, identifier: nil, configuration: .iCloud(accessibility)) + } + + /// Creates an iCloud-shared-access-group Valet with an explicitly set kSecAttrService. + /// - Parameters: + /// - appIDPrefix: The application's App ID prefix. This string can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty. + /// - groupIdentifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. Must be unique relative to other Valet identifiers. + /// - identifier: A non-empty string that uniquely identifies a Valet. Must be unique relative to other Valet identifiers. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet (synchronized with iCloud) that reads/writes keychain elements that can be shared across applications written by the same development team. + /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. + /// - SeeAlso: https://github.com/square/Valet/issues/140 + /// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps + @available(swift, obsoleted: 1.0) + @objc(iCloudValetWithAppIDPrefix:explicitlySetSharedGroupIdentifier:identifier:accessibility:) + public class func 🚫swift_iCloudSharedGroupValet(appIDPrefix: String, withExplicitlySet groupIdentifier: String, identifier: String?, accessibility: CloudAccessibility) -> Valet? { + guard let groupIdentifier = SharedGroupIdentifier(appIDPrefix: appIDPrefix, nonEmptyGroup: groupIdentifier) else { + return nil + } + return findOrCreate(explicitlySet: groupIdentifier, identifier: Identifier(nonEmpty: identifier), configuration: .iCloud(accessibility)) } // MARK: Public Methods @@ -805,7 +843,7 @@ extension Valet { class func permutations(withExplictlySet identifier: SharedGroupIdentifier) -> [Valet] { Accessibility.allCases.map { accessibility in - .sharedGroupValet(withExplicitlySet: identifier, accessibility: accessibility) + .sharedGroupValet(withExplicitlySet: identifier, identifier: nil, accessibility: accessibility) } } @@ -817,7 +855,7 @@ extension Valet { class func iCloudPermutations(withExplictlySet identifier: SharedGroupIdentifier) -> [Valet] { CloudAccessibility.allCases.map { cloudAccessibility in - .iCloudSharedGroupValet(withExplicitlySet: identifier, accessibility: cloudAccessibility) + .iCloudSharedGroupValet(withExplicitlySet: identifier, identifier: nil, accessibility: cloudAccessibility) } } From 896ab06feb59bb2248dd7a9d310613bd76af442b Mon Sep 17 00:00:00 2001 From: Anton Sotkov Date: Thu, 26 Mar 2026 06:06:57 +0200 Subject: [PATCH 2/3] Document explicit shared-group identifiers --- Sources/Valet/Valet.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/Valet/Valet.swift b/Sources/Valet/Valet.swift index efff7a4b..cda0fd63 100644 --- a/Sources/Valet/Valet.swift +++ b/Sources/Valet/Valet.swift @@ -82,7 +82,8 @@ public final class Valet: NSObject, Sendable { /// Creates a shared-access-group Valet with an explicitly set kSecAttrService. This API is intended for use with macOS applications where service identifiers can be user-facing. /// - Parameters: - /// - identifier: The identifier for the Valet's shared access group. Must correspond with the value for keychain-access-groups in your Entitlements file. Must be unique relative to other Valet identifiers. + /// - groupIdentifier: The identifier for the Valet's shared access group. Must correspond with the value for keychain-access-groups in your Entitlements file. + /// - identifier: An optional non-empty string that uniquely identifies a Valet. Must be unique relative to other Valet identifiers. /// - accessibility: The desired accessibility for the Valet. /// - Returns: A Valet that reads/writes keychain elements that can be shared across applications written by the same development team. /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. @@ -93,7 +94,8 @@ public final class Valet: NSObject, Sendable { /// Creates an iCloud-shared-access-group Valet with an explicitly set kSecAttrService. This API is intended for use with macOS applications where service identifiers can be user-facing. /// - Parameters: - /// - identifier: The identifier for the Valet's shared access group. Must correspond with the value for keychain-access-groups in your Entitlements file. Must be unique relative to other Valet identifiers. + /// - groupIdentifier: The identifier for the Valet's shared access group. Must correspond with the value for keychain-access-groups in your Entitlements file. + /// - identifier: An optional non-empty string that uniquely identifies a Valet. Must be unique relative to other Valet identifiers. /// - accessibility: The desired accessibility for the Valet. /// - Returns: A Valet (synchronized with iCloud) that reads/writes keychain elements that can be shared across applications written by the same development team. /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. From 2245dc3f6ca5c42dbcea6ddd149fe565c49d35a9 Mon Sep 17 00:00:00 2001 From: Anton Sotkov Date: Thu, 26 Mar 2026 06:09:40 +0200 Subject: [PATCH 3/3] Revert whitespace change --- Sources/Valet/Valet.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Valet/Valet.swift b/Sources/Valet/Valet.swift index cda0fd63..40adf4bc 100644 --- a/Sources/Valet/Valet.swift +++ b/Sources/Valet/Valet.swift @@ -845,7 +845,7 @@ extension Valet { class func permutations(withExplictlySet identifier: SharedGroupIdentifier) -> [Valet] { Accessibility.allCases.map { accessibility in - .sharedGroupValet(withExplicitlySet: identifier, identifier: nil, accessibility: accessibility) + .sharedGroupValet(withExplicitlySet: identifier, identifier: nil, accessibility: accessibility) } }