From afaf66df6e6eb4529d0456e8ca099c9e396122be Mon Sep 17 00:00:00 2001 From: efirestone Date: Thu, 11 May 2023 21:55:00 -0500 Subject: [PATCH 1/4] Fix warning about missing global source in Gemfile --- Gemfile | 6 +++--- Gemfile.lock | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 3e6122dd..a29f56d1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ -source 'https://rubygems.org' do - gem 'cocoapods', '~> 1.11.0' -end +source "https://rubygems.org" + +gem 'cocoapods', '~> 1.11.0' diff --git a/Gemfile.lock b/Gemfile.lock index 0e8fd6bc..bc5546d1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,3 @@ -GEM - specs: - GEM remote: https://rubygems.org/ specs: @@ -94,7 +91,7 @@ PLATFORMS ruby DEPENDENCIES - cocoapods (~> 1.11.0)! + cocoapods (~> 1.11.0) BUNDLED WITH 2.3.7 From b84269e8a9d69ceaad5e66288fd4dca2b97a0f78 Mon Sep 17 00:00:00 2001 From: efirestone Date: Thu, 11 May 2023 22:30:16 -0500 Subject: [PATCH 2/4] Commit Xcode-generated changes These changes happened automatically as a result of using a newer Xcode version --- ...t watchOS Test Host App Extension.xcscheme | 25 +++++-------------- .../Valet watchOS Test Host App.xcscheme | 25 +++++-------------- 2 files changed, 12 insertions(+), 38 deletions(-) diff --git a/Valet.xcodeproj/xcshareddata/xcschemes/Valet watchOS Test Host App Extension.xcscheme b/Valet.xcodeproj/xcshareddata/xcschemes/Valet watchOS Test Host App Extension.xcscheme index a658d37b..07c9e8e5 100644 --- a/Valet.xcodeproj/xcshareddata/xcschemes/Valet watchOS Test Host App Extension.xcscheme +++ b/Valet.xcodeproj/xcshareddata/xcschemes/Valet watchOS Test Host App Extension.xcscheme @@ -77,10 +77,8 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - + - + - + - - - - - + diff --git a/Valet.xcodeproj/xcshareddata/xcschemes/Valet watchOS Test Host App.xcscheme b/Valet.xcodeproj/xcshareddata/xcschemes/Valet watchOS Test Host App.xcscheme index 2da03019..2c0f9743 100644 --- a/Valet.xcodeproj/xcshareddata/xcschemes/Valet watchOS Test Host App.xcscheme +++ b/Valet.xcodeproj/xcshareddata/xcschemes/Valet watchOS Test Host App.xcscheme @@ -63,10 +63,8 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - + - + - + - - - - - + From f3a38163b4cb73311aadb45884e52e3db5f0cb45 Mon Sep 17 00:00:00 2001 From: efirestone Date: Thu, 11 May 2023 22:30:32 -0500 Subject: [PATCH 3/4] Add support for multiple Valets within the same access group Previously, the shared access group identifier was used for both the access group itself, as well as the uniqueness identifier for the given Valet. This adds an optional additional `identifier` parameter that can be specified when creating a shared access group Valet. The identifier adds an additional element of uniqueness, so two Valets with the same shared access group can exist, and their data will not overlap, so long as they have different identifiers. The default for this value is `nil`, which keeps the existing behavior for full backward compatibility. --- README.md | 4 + Sources/Valet/Internal/Service.swift | 20 ++- Sources/Valet/SecureEnclave.swift | 4 +- Sources/Valet/SecureEnclaveValet.swift | 15 +- .../SinglePromptSecureEnclaveValet.swift | 12 +- Sources/Valet/Valet.swift | 36 ++--- .../ValetBackwardsCompatibilityTests.swift | 2 +- .../SecureEnclaveIntegrationTests.swift | 18 +++ ...ePromptSecureEnclaveIntegrationTests.swift | 18 +++ .../ValetIntegrationTests.swift | 137 +++++++++++++++++- Tests/ValetTests/SecureEnclaveTests.swift | 12 +- .../SinglePromptSecureEnclaveTests.swift | 2 +- Tests/ValetTests/ValetTests.swift | 14 +- .../Valet iOS Test Host App.entitlements | 1 + .../Valet_macOS_Test_Host_App.entitlements | 1 + .../Valet tvOS Test Host App.entitlements | 1 + ...tchOS Test Host App Extension.entitlements | 1 + 17 files changed, 251 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 1c94144f..006d7010 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,8 @@ VALValet *const mySharedValet = [VALValet sharedGroupValetWithGroupPrefix:@"grou This instance can be used to store and retrieve data securely across any app written by the same developer that has `group.Druidia` set as a value for the `com.apple.security.application-groups` key in the app’s `Entitlements`. This Valet is accessible when the device is unlocked. Note that `myValet` and `mySharedValet` cannot read or modify one another’s values because the two Valets were created with different initializers. All Valet types can share secrets across applications written by the same developer by using the `sharedGroupValet` initializer. Note that on macOS, the `groupPrefix` [must be the App ID prefix](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups#discussion). +As with Valets, shared iCloud Valets can be created with an additional identifier, allowing multiple independently sandboxed keychains to exist within the same shared group. + ### Sharing Secrets Across Devices with iCloud ```swift @@ -181,6 +183,8 @@ VALValet *const myCloudValet = [VALValet iCloudValetWithIdentifier:@"Druidia" ac This instance can be used to store and retrieve data that can be retrieved by this app on other devices logged into the same iCloud account with iCloud Keychain enabled. If iCloud Keychain is not enabled on this device, secrets can still be read and written, but will not sync to other devices. Note that `myCloudValet` can not read or modify values in either `myValet` or `mySharedValet` because `myCloudValet` was created a different initializer. +Shared iCloud Valets can be created with an additional identifier, allowing multiple independently sandboxed keychains to exist within the same iCloud shared group. + ### Protecting Secrets with Face ID, Touch ID, or device Passcode ```swift diff --git a/Sources/Valet/Internal/Service.swift b/Sources/Valet/Internal/Service.swift index e9357c69..df4bd4a2 100644 --- a/Sources/Valet/Internal/Service.swift +++ b/Sources/Valet/Internal/Service.swift @@ -19,7 +19,7 @@ import Foundation internal enum Service: CustomStringConvertible, Equatable { case standard(Identifier, Configuration) - case sharedGroup(SharedGroupIdentifier, Configuration) + case sharedGroup(SharedGroupIdentifier, Identifier?, Configuration) #if os(macOS) case standardOverride(service: Identifier, Configuration) @@ -44,8 +44,12 @@ internal enum Service: CustomStringConvertible, Equatable { "VAL_\(configuration.description)_initWithIdentifier:accessibility:_\(identifier)_\(accessibilityDescription)" } - internal static func sharedGroup(with configuration: Configuration, identifier: SharedGroupIdentifier, accessibilityDescription: String) -> String { - "VAL_\(configuration.description)_initWithSharedAccessGroupIdentifier:accessibility:_\(identifier.groupIdentifier)_\(accessibilityDescription)" + internal static func sharedGroup(with configuration: Configuration, groupIdentifier: SharedGroupIdentifier, identifier: Identifier?, accessibilityDescription: String) -> String { + if let identifier = identifier { + return "VAL_\(configuration.description)_initWithSharedAccessGroupIdentifier:accessibility:_\(groupIdentifier.groupIdentifier)_\(identifier)_\(accessibilityDescription)" + } else { + return "VAL_\(configuration.description)_initWithSharedAccessGroupIdentifier:accessibility:_\(groupIdentifier.groupIdentifier)_\(accessibilityDescription)" + } } internal static func sharedGroup(with configuration: Configuration, explicitlySetIdentifier identifier: Identifier, accessibilityDescription: String) -> String { @@ -69,8 +73,8 @@ internal enum Service: CustomStringConvertible, Equatable { case let .standard(_, desiredConfiguration): configuration = desiredConfiguration - case let .sharedGroup(identifier, desiredConfiguration): - baseQuery[kSecAttrAccessGroup as String] = identifier.description + case let .sharedGroup(groupIdentifier, _, desiredConfiguration): + baseQuery[kSecAttrAccessGroup as String] = groupIdentifier.description configuration = desiredConfiguration #if os(macOS) @@ -107,8 +111,8 @@ internal enum Service: CustomStringConvertible, Equatable { switch self { case let .standard(identifier, configuration): service = Service.standard(with: configuration, identifier: identifier, accessibilityDescription: configuration.accessibility.description) - case let .sharedGroup(identifier, configuration): - service = Service.sharedGroup(with: configuration, identifier: identifier, accessibilityDescription: configuration.accessibility.description) + case let .sharedGroup(groupIdentifier, identifier, configuration): + service = Service.sharedGroup(with: configuration, groupIdentifier: groupIdentifier, identifier: identifier, accessibilityDescription: configuration.accessibility.description) #if os(macOS) case let .standardOverride(identifier, _): service = identifier.description @@ -119,7 +123,7 @@ internal enum Service: CustomStringConvertible, Equatable { switch self { case let .standard(_, configuration), - let .sharedGroup(_, configuration): + let .sharedGroup(_, _, configuration): switch configuration { case .valet, .iCloud: // Nothing to do here. diff --git a/Sources/Valet/SecureEnclave.swift b/Sources/Valet/SecureEnclave.swift index 9992c618..e87d6a7b 100644 --- a/Sources/Valet/SecureEnclave.swift +++ b/Sources/Valet/SecureEnclave.swift @@ -38,8 +38,8 @@ public final class SecureEnclave { case let .sharedGroupOverride(identifier, _): noPromptValet = .sharedGroupValet(withExplicitlySet: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) #endif - case let .sharedGroup(identifier, _): - noPromptValet = .sharedGroupValet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) + case let .sharedGroup(groupIdentifier, identifier, _): + noPromptValet = .sharedGroupValet(with: groupIdentifier, identifier: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) } return noPromptValet.canAccessKeychain() diff --git a/Sources/Valet/SecureEnclaveValet.swift b/Sources/Valet/SecureEnclaveValet.swift index fc050ab6..db91bd0b 100644 --- a/Sources/Valet/SecureEnclaveValet.swift +++ b/Sources/Valet/SecureEnclaveValet.swift @@ -43,13 +43,13 @@ public final class SecureEnclaveValet: NSObject { /// - identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. /// - accessControl: The desired access control for the SecureEnclaveValet. /// - Returns: A SecureEnclaveValet that reads/writes keychain elements that can be shared across applications written by the same development team. - public class func sharedGroupValet(with identifier: SharedGroupIdentifier, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet { - let key = Service.sharedGroup(identifier, .secureEnclave(accessControl)).description as NSString + public class func sharedGroupValet(with groupIdentifier: SharedGroupIdentifier, identifier: Identifier? = nil, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet { + let key = Service.sharedGroup(groupIdentifier, identifier, .secureEnclave(accessControl)).description as NSString if let existingValet = identifierToValetMap.object(forKey: key) { return existingValet } else { - let valet = SecureEnclaveValet(sharedAccess: identifier, accessControl: accessControl) + let valet = SecureEnclaveValet(sharedAccess: groupIdentifier, identifier: identifier, accessControl: accessControl) identifierToValetMap.setObject(valet, forKey: key) return valet } @@ -80,11 +80,12 @@ public final class SecureEnclaveValet: NSObject { accessControl: accessControl) } - private convenience init(sharedAccess groupIdentifier: SharedGroupIdentifier, accessControl: SecureEnclaveAccessControl) { + private convenience init(sharedAccess groupIdentifier: SharedGroupIdentifier, identifier: Identifier? = nil, accessControl: SecureEnclaveAccessControl) { self.init( - identifier: groupIdentifier.asIdentifier, - service: .sharedGroup(groupIdentifier, .secureEnclave(accessControl)), - accessControl: accessControl) + identifier: identifier ?? groupIdentifier.asIdentifier, + service: .sharedGroup(groupIdentifier, identifier, .secureEnclave(accessControl)), + accessControl: accessControl + ) } private init(identifier: Identifier, service: Service, accessControl: SecureEnclaveAccessControl) { diff --git a/Sources/Valet/SinglePromptSecureEnclaveValet.swift b/Sources/Valet/SinglePromptSecureEnclaveValet.swift index 5b7df38b..ebcf9b63 100644 --- a/Sources/Valet/SinglePromptSecureEnclaveValet.swift +++ b/Sources/Valet/SinglePromptSecureEnclaveValet.swift @@ -47,13 +47,13 @@ public final class SinglePromptSecureEnclaveValet: NSObject { /// - identifier: A non-empty identifier that must correspond with the value for keychain-access-groups in your Entitlements file. /// - accessControl: The desired access control for the SinglePromptSecureEnclaveValet. /// - Returns: A SinglePromptSecureEnclaveValet that reads/writes keychain elements that can be shared across applications written by the same development team. - public class func sharedGroupValet(with identifier: SharedGroupIdentifier, accessControl: SecureEnclaveAccessControl) -> SinglePromptSecureEnclaveValet { - let key = Service.sharedGroup(identifier, .singlePromptSecureEnclave(accessControl)).description as NSString + public class func sharedGroupValet(with groupIdentifier: SharedGroupIdentifier, identifier: Identifier? = nil, accessControl: SecureEnclaveAccessControl) -> SinglePromptSecureEnclaveValet { + let key = Service.sharedGroup(groupIdentifier, identifier, .singlePromptSecureEnclave(accessControl)).description as NSString if let existingValet = identifierToValetMap.object(forKey: key) { return existingValet } else { - let valet = SinglePromptSecureEnclaveValet(sharedAccess: identifier, accessControl: accessControl) + let valet = SinglePromptSecureEnclaveValet(sharedAccess: groupIdentifier, identifier: identifier, accessControl: accessControl) identifierToValetMap.setObject(valet, forKey: key) return valet } @@ -84,10 +84,10 @@ public final class SinglePromptSecureEnclaveValet: NSObject { accessControl: accessControl) } - private convenience init(sharedAccess groupIdentifier: SharedGroupIdentifier, accessControl: SecureEnclaveAccessControl) { + private convenience init(sharedAccess groupIdentifier: SharedGroupIdentifier, identifier: Identifier? = nil, accessControl: SecureEnclaveAccessControl) { self.init( - identifier: groupIdentifier.asIdentifier, - service: .sharedGroup(groupIdentifier, .singlePromptSecureEnclave(accessControl)), + identifier: identifier ?? groupIdentifier.asIdentifier, + service: .sharedGroup(groupIdentifier, identifier, .singlePromptSecureEnclave(accessControl)), accessControl: accessControl) } diff --git a/Sources/Valet/Valet.swift b/Sources/Valet/Valet.swift index f2a1fbbe..3ac2566c 100644 --- a/Sources/Valet/Valet.swift +++ b/Sources/Valet/Valet.swift @@ -40,19 +40,21 @@ public final class Valet: NSObject { } /// - Parameters: - /// - identifier: The identifier for the Valet's shared access group. Must correspond with the value for keychain-access-groups in your Entitlements file. + /// - 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 additional uniqueness identifier. Using this identifier allows for the creation of separate, sandboxed Valets within the same shared access group. /// - 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. - public class func sharedGroupValet(with identifier: SharedGroupIdentifier, accessibility: Accessibility) -> Valet { - findOrCreate(identifier, configuration: .valet(accessibility)) + public class func sharedGroupValet( + with groupIdentifier: SharedGroupIdentifier, identifier: Identifier? = nil, accessibility: Accessibility) -> Valet { + findOrCreate(groupIdentifier, identifier: identifier, configuration: .valet(accessibility)) } /// - Parameters: /// - identifier: The identifier for the Valet's shared access group. Must correspond with the value for keychain-access-groups in your Entitlements file. /// - 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. - public class func iCloudSharedGroupValet(with identifier: SharedGroupIdentifier, accessibility: CloudAccessibility) -> Valet { - findOrCreate(identifier, configuration: .iCloud(accessibility)) + public class func iCloudSharedGroupValet(with groupIdentifier: SharedGroupIdentifier, identifier: Identifier? = nil, accessibility: CloudAccessibility) -> Valet { + findOrCreate(groupIdentifier, identifier: identifier, configuration: .iCloud(accessibility)) } #if os(macOS) @@ -127,14 +129,14 @@ public final class Valet: NSObject { } } - private class func findOrCreate(_ identifier: SharedGroupIdentifier, configuration: Configuration) -> Valet { - let service: Service = .sharedGroup(identifier, configuration) + private class func findOrCreate(_ groupIdentifier: SharedGroupIdentifier, identifier: Identifier?, configuration: Configuration) -> Valet { + let service: Service = .sharedGroup(groupIdentifier, identifier, configuration) let key = service.description as NSString if let existingValet = identifierToValetMap.object(forKey: key) { return existingValet } else { - let valet = Valet(sharedAccess: identifier, configuration: configuration) + let valet = Valet(sharedAccess: groupIdentifier, identifier: identifier, configuration: configuration) identifierToValetMap.setObject(valet, forKey: key) return valet } @@ -184,10 +186,10 @@ public final class Valet: NSObject { configuration: configuration) } - private convenience init(sharedAccess groupIdentifier: SharedGroupIdentifier, configuration: Configuration) { + private convenience init(sharedAccess groupIdentifier: SharedGroupIdentifier, identifier: Identifier?, configuration: Configuration) { self.init( - identifier: groupIdentifier.asIdentifier, - service: .sharedGroup(groupIdentifier, configuration), + identifier: identifier ?? groupIdentifier.asIdentifier, + service: .sharedGroup(groupIdentifier, identifier, configuration), configuration: configuration) } @@ -404,8 +406,8 @@ public final class Valet: NSObject { let accessibilityDescription = "AccessibleAlways" let serviceAttribute: String switch service { - case let .sharedGroup(sharedGroupIdentifier, _): - serviceAttribute = Service.sharedGroup(with: configuration, identifier: sharedGroupIdentifier, accessibilityDescription: accessibilityDescription) + case let .sharedGroup(sharedGroupIdentifier, identifier, _): + serviceAttribute = Service.sharedGroup(with: configuration, groupIdentifier: sharedGroupIdentifier, identifier: identifier, accessibilityDescription: accessibilityDescription) case .standard: serviceAttribute = Service.standard(with: configuration, identifier: identifier, accessibilityDescription: accessibilityDescription) #if os(macOS) @@ -439,8 +441,8 @@ public final class Valet: NSObject { let accessibilityDescription = "AccessibleAlwaysThisDeviceOnly" let serviceAttribute: String switch service { - case let .sharedGroup(identifier, _): - serviceAttribute = Service.sharedGroup(with: configuration, identifier: identifier, accessibilityDescription: accessibilityDescription) + case let .sharedGroup(groupIdentifier, identifier, _): + serviceAttribute = Service.sharedGroup(with: configuration, groupIdentifier: groupIdentifier, identifier: identifier, accessibilityDescription: accessibilityDescription) case .standard: serviceAttribute = Service.standard(with: configuration, identifier: identifier, accessibilityDescription: accessibilityDescription) #if os(macOS) @@ -723,9 +725,9 @@ internal extension Valet { } } - class func permutations(with identifier: SharedGroupIdentifier) -> [Valet] { + class func permutations(with groupIdentifier: SharedGroupIdentifier, identifier: Identifier? = nil) -> [Valet] { Accessibility.allCases.map { accessibility in - .sharedGroupValet(with: identifier, accessibility: accessibility) + .sharedGroupValet(with: groupIdentifier, identifier: identifier, accessibility: accessibility) } } diff --git a/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/ValetBackwardsCompatibilityTests.swift b/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/ValetBackwardsCompatibilityTests.swift index 62d36357..95cf2c57 100644 --- a/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/ValetBackwardsCompatibilityTests.swift +++ b/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/ValetBackwardsCompatibilityTests.swift @@ -27,7 +27,7 @@ internal extension Valet { var legacyIdentifier: String { switch service { - case let .sharedGroup(sharedAccessGroupIdentifier, _): + case let .sharedGroup(sharedAccessGroupIdentifier, _, _): return sharedAccessGroupIdentifier.groupIdentifier case let .standard(identifier, _): return identifier.description diff --git a/Tests/ValetIntegrationTests/SecureEnclaveIntegrationTests.swift b/Tests/ValetIntegrationTests/SecureEnclaveIntegrationTests.swift index 43df0c37..edcdbc64 100644 --- a/Tests/ValetIntegrationTests/SecureEnclaveIntegrationTests.swift +++ b/Tests/ValetIntegrationTests/SecureEnclaveIntegrationTests.swift @@ -69,6 +69,24 @@ class SecureEnclaveIntegrationTests: XCTestCase XCTAssertEqual(error as? KeychainError, .itemNotFound) } } + + func test_secureEnclaveSharedGroupValetsWithDifferingIdentifiers_canNotAccessSameData() throws + { + guard testEnvironmentIsSigned() && testEnvironmentSupportsWhenPasscodeSet() else { + return + } + + let valet1 = SecureEnclaveValet.sharedGroupValet(with: Valet.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet1"), accessControl: .devicePasscode) + let valet2 = SecureEnclaveValet.sharedGroupValet(with: Valet.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet2"), accessControl: .devicePasscode) + + try valet1.setString(passcode, forKey: key) + + XCTAssertNotEqual(valet1, valet2) + XCTAssertEqual(passcode, try valet1.string(forKey: key, withPrompt: "")) + XCTAssertThrowsError(try valet2.string(forKey: key, withPrompt: "")) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + } // MARK: canAccessKeychain diff --git a/Tests/ValetIntegrationTests/SinglePromptSecureEnclaveIntegrationTests.swift b/Tests/ValetIntegrationTests/SinglePromptSecureEnclaveIntegrationTests.swift index 7c62ab01..fea2cdd1 100644 --- a/Tests/ValetIntegrationTests/SinglePromptSecureEnclaveIntegrationTests.swift +++ b/Tests/ValetIntegrationTests/SinglePromptSecureEnclaveIntegrationTests.swift @@ -74,6 +74,24 @@ class SinglePromptSecureEnclaveIntegrationTests: XCTestCase } } + func test_SinglePromptSecureEnclaveValetsWithDifferingIdentifiers_canNotAccessSameData() throws + { + guard testEnvironmentIsSigned() && testEnvironmentSupportsWhenPasscodeSet() else { + return + } + + let valet1 = SinglePromptSecureEnclaveValet.sharedGroupValet(with: Valet.sharedAppGroupIdentifier, identifier: Identifier(nonEmpty: "valet1"), accessControl: .devicePasscode) + let valet2 = SinglePromptSecureEnclaveValet.sharedGroupValet(with: Valet.sharedAppGroupIdentifier, identifier: Identifier(nonEmpty: "valet2"), accessControl: .devicePasscode) + + try valet1.setString(passcode, forKey: key) + + XCTAssertNotEqual(valet1, valet2) + XCTAssertEqual(passcode, try valet1.string(forKey: key, withPrompt: "")) + XCTAssertThrowsError(try valet2.string(forKey: key, withPrompt: "")) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + } + // MARK: allKeys func test_allKeys() throws diff --git a/Tests/ValetIntegrationTests/ValetIntegrationTests.swift b/Tests/ValetIntegrationTests/ValetIntegrationTests.swift index 06160d49..442e1c70 100644 --- a/Tests/ValetIntegrationTests/ValetIntegrationTests.swift +++ b/Tests/ValetIntegrationTests/ValetIntegrationTests.swift @@ -74,6 +74,20 @@ internal extension Valet { #endif }() + static var sharedAccessGroupIdentifier2: SharedGroupIdentifier = { + #if os(iOS) + return SharedGroupIdentifier(appIDPrefix: "9XUJ7M53NG", nonEmptyGroup: "com.squareup.Valet-iOS-Test-Host-App2")! + #elseif os(macOS) + return SharedGroupIdentifier(appIDPrefix: "9XUJ7M53NG", nonEmptyGroup: "com.squareup.Valet-macOS-Test-Host-App2")! + #elseif os(tvOS) + return SharedGroupIdentifier(appIDPrefix: "9XUJ7M53NG", nonEmptyGroup: "com.squareup.Valet-tvOS-Test-Host-App2")! + #elseif os(watchOS) + return SharedGroupIdentifier(appIDPrefix: "9XUJ7M53NG", nonEmptyGroup: "com.squareup.ValetTouchIDTestApp.watchkitapp.watchkitextension2")! + #else + XCTFail() + #endif + }() + // MARK: Shared App Group static var sharedAppGroupIdentifier: SharedGroupIdentifier = { @@ -96,9 +110,11 @@ internal extension Valet { class ValetIntegrationTests: XCTestCase { static let sharedAccessGroupIdentifier = Valet.sharedAccessGroupIdentifier + static let sharedAccessGroupIdentifier2 = Valet.sharedAccessGroupIdentifier2 static let sharedAppGroupIdentifier = Valet.sharedAppGroupIdentifier var allPermutations: [Valet] { var signedPermutations = Valet.permutations(with: ValetIntegrationTests.sharedAccessGroupIdentifier) + signedPermutations += Valet.permutations(with: ValetIntegrationTests.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "UniquenessIdentifier")) #if !os(macOS) // We can't test app groups on macOS without a paid developer account, which we don't have. signedPermutations += Valet.permutations(with: ValetIntegrationTests.sharedAppGroupIdentifier) @@ -157,7 +173,17 @@ class ValetIntegrationTests: XCTestCase Accessibility.allCases.forEach { accessibility in let backingService = Valet.sharedGroupValet(with: identifier, accessibility: accessibility).service - XCTAssertEqual(backingService, Service.sharedGroup(identifier, .valet(accessibility))) + XCTAssertEqual(backingService, Service.sharedGroup(identifier, nil, .valet(accessibility))) + } + } + + func test_init_createsCorrectBackingService_sharedAccess_withIdentifier() { + let sharedIdentifier = Valet.sharedAccessGroupIdentifier + let identifier = Identifier(nonEmpty: "id") + + Accessibility.allCases.forEach { accessibility in + let backingService = Valet.sharedGroupValet(with: sharedIdentifier, identifier: identifier, accessibility: accessibility).service + XCTAssertEqual(backingService, Service.sharedGroup(sharedIdentifier, identifier, .valet(accessibility))) } } @@ -175,7 +201,17 @@ class ValetIntegrationTests: XCTestCase CloudAccessibility.allCases.forEach { accessibility in let backingService = Valet.iCloudSharedGroupValet(with: identifier, accessibility: accessibility).service - XCTAssertEqual(backingService, Service.sharedGroup(identifier, .iCloud(accessibility))) + XCTAssertEqual(backingService, Service.sharedGroup(identifier, nil, .iCloud(accessibility))) + } + } + + func test_init_createsCorrectBackingService_cloudSharedAccess_withIdentifier() { + let groupIdentifier = Valet.sharedAccessGroupIdentifier + let identifier = Identifier(nonEmpty: "id") + + CloudAccessibility.allCases.forEach { accessibility in + let backingService = Valet.iCloudSharedGroupValet(with: groupIdentifier, identifier: identifier, accessibility: accessibility).service + XCTAssertEqual(backingService, Service.sharedGroup(groupIdentifier, identifier, .iCloud(accessibility))) } } @@ -317,6 +353,45 @@ class ValetIntegrationTests: XCTestCase } } + func test_stringForKey_withDifferingIdentifierInSameAccessGroup_throwsItemNotFound() throws + { + let valet1 = Valet.sharedGroupValet(with: Self.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet1")!, accessibility: vanillaValet.accessibility) + let valet2 = Valet.sharedGroupValet(with: Self.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet2")!, accessibility: vanillaValet.accessibility) + + try valet1.setString(passcode, forKey: key) + XCTAssertEqual(passcode, try valet1.string(forKey: key)) + + XCTAssertThrowsError(try valet2.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + } + + func test_stringForKey_withSameIdentifierInDifferentAccessGroup_throwsItemNotFound() throws + { + let valet1 = Valet.sharedGroupValet(with: Self.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet1")!, accessibility: vanillaValet.accessibility) + let valet2 = Valet.sharedGroupValet(with: Self.sharedAccessGroupIdentifier2, identifier: Identifier(nonEmpty: "valet1")!, accessibility: vanillaValet.accessibility) + + try valet1.setString(passcode, forKey: key) + XCTAssertEqual(passcode, try valet1.string(forKey: key)) + + XCTAssertThrowsError(try valet2.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + } + + func test_stringForKey_withDifferingIdentifierInSameiCloudGroup_throwsItemNotFound() throws + { + let valet1 = Valet.iCloudSharedGroupValet(with: Self.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet1")!, accessibility: .afterFirstUnlock) + let valet2 = Valet.iCloudSharedGroupValet(with: Self.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet2")!, accessibility: .afterFirstUnlock) + + try valet1.setString(passcode, forKey: key) + XCTAssertEqual(passcode, try valet1.string(forKey: key)) + + XCTAssertThrowsError(try valet2.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + } + func test_stringForKey_withDifferingAccessibility_throwsItemNotFound() throws { try vanillaValet.setString(passcode, forKey: key) @@ -431,6 +506,32 @@ class ValetIntegrationTests: XCTestCase } } } + + func test_objectForKey_withDifferingIdentifierInSameAccessGroup_throwsItemNotFound() throws + { + let valet1 = Valet.sharedGroupValet(with: Self.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet1")!, accessibility: vanillaValet.accessibility) + let valet2 = Valet.sharedGroupValet(with: Self.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet2")!, accessibility: vanillaValet.accessibility) + + try valet1.setObject(passcodeData, forKey: key) + XCTAssertEqual(passcodeData, try valet1.object(forKey: key)) + + XCTAssertThrowsError(try valet2.object(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + } + + func test_objectForKey_withDifferingIdentifierInSameiCloudGroup_throwsItemNotFound() throws + { + let valet1 = Valet.iCloudSharedGroupValet(with: Self.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet1")!, accessibility: .afterFirstUnlock) + let valet2 = Valet.iCloudSharedGroupValet(with: Self.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet2")!, accessibility: .afterFirstUnlock) + + try valet1.setObject(passcodeData, forKey: key) + XCTAssertEqual(passcodeData, try valet1.object(forKey: key)) + + XCTAssertThrowsError(try valet2.object(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + } func test_objectForKey_withDifferingAccessibility_throwsItemNotFound() throws { try vanillaValet.setObject(passcodeData, forKey: key) @@ -650,6 +751,38 @@ class ValetIntegrationTests: XCTestCase XCTAssertEqual(passcode, try vanillaValet.string(forKey: key)) } + func test_removeObjectForKey_isDistinctForDifferingIdentifierInSameAccessGroup() throws + { + let valet1 = Valet.sharedGroupValet(with: Self.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet1")!, accessibility: vanillaValet.accessibility) + let valet2 = Valet.sharedGroupValet(with: Self.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet2")!, accessibility: vanillaValet.accessibility) + + try valet1.setString(passcode, forKey: key) + try valet2.setString(passcode, forKey: key) + + try valet2.removeObject(forKey: key) + + XCTAssertEqual(passcode, try valet1.string(forKey: key)) + XCTAssertThrowsError(try valet2.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + } + + func test_removeObjectForKey_isDistinctForDifferingIdentifierInSameiCloudGroup() throws + { + let valet1 = Valet.iCloudSharedGroupValet(with: Self.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet1")!, accessibility: .afterFirstUnlock) + let valet2 = Valet.iCloudSharedGroupValet(with: Self.sharedAccessGroupIdentifier, identifier: Identifier(nonEmpty: "valet2")!, accessibility: .afterFirstUnlock) + + try valet1.setString(passcode, forKey: key) + try valet2.setString(passcode, forKey: key) + + try valet2.removeObject(forKey: key) + + XCTAssertEqual(passcode, try valet1.string(forKey: key)) + XCTAssertThrowsError(try valet2.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + } + func test_removeObjectForKey_isDistinctForDifferingClasses() throws { guard testEnvironmentIsSigned() else { diff --git a/Tests/ValetTests/SecureEnclaveTests.swift b/Tests/ValetTests/SecureEnclaveTests.swift index 108e9ef6..31a6a125 100644 --- a/Tests/ValetTests/SecureEnclaveTests.swift +++ b/Tests/ValetTests/SecureEnclaveTests.swift @@ -41,7 +41,17 @@ class SecureEnclaveTests: XCTestCase SecureEnclaveAccessControl.allValues().forEach { accessControl in let backingService = SecureEnclaveValet.sharedGroupValet(with: identifier, accessControl: accessControl).service - XCTAssertEqual(backingService, Service.sharedGroup(identifier, .secureEnclave(accessControl))) + XCTAssertEqual(backingService, Service.sharedGroup(identifier, nil, .secureEnclave(accessControl))) + } + } + + func test_init_createsCorrectBackingService_sharedAccess_withIdentifier() { + let groupIdentifier = Valet.sharedAccessGroupIdentifier + let identifier = Identifier(nonEmpty: "id") + + SecureEnclaveAccessControl.allValues().forEach { accessControl in + let backingService = SecureEnclaveValet.sharedGroupValet(with: groupIdentifier, identifier: identifier, accessControl: accessControl).service + XCTAssertEqual(backingService, Service.sharedGroup(groupIdentifier, identifier, .secureEnclave(accessControl))) } } diff --git a/Tests/ValetTests/SinglePromptSecureEnclaveTests.swift b/Tests/ValetTests/SinglePromptSecureEnclaveTests.swift index 47ef6261..10fcf39d 100644 --- a/Tests/ValetTests/SinglePromptSecureEnclaveTests.swift +++ b/Tests/ValetTests/SinglePromptSecureEnclaveTests.swift @@ -43,7 +43,7 @@ class SinglePromptSecureEnclaveTests: XCTestCase SecureEnclaveAccessControl.allValues().forEach { accessControl in let backingService = SinglePromptSecureEnclaveValet.sharedGroupValet(with: identifier, accessControl: accessControl).service - XCTAssertEqual(backingService, Service.sharedGroup(identifier, .singlePromptSecureEnclave(accessControl))) + XCTAssertEqual(backingService, Service.sharedGroup(identifier, nil, .singlePromptSecureEnclave(accessControl))) } } diff --git a/Tests/ValetTests/ValetTests.swift b/Tests/ValetTests/ValetTests.swift index c15ba3ca..817151d3 100644 --- a/Tests/ValetTests/ValetTests.swift +++ b/Tests/ValetTests/ValetTests.swift @@ -41,7 +41,17 @@ class ValetTests: XCTestCase Accessibility.allCases.forEach { accessibility in let backingService = Valet.sharedGroupValet(with: identifier, accessibility: accessibility).service - XCTAssertEqual(backingService, Service.sharedGroup(identifier, .valet(accessibility))) + XCTAssertEqual(backingService, Service.sharedGroup(identifier, nil, .valet(accessibility))) + } + } + + func test_init_createsCorrectBackingService_sharedAccess_withIdentifier() { + let groupIdentifier = Valet.sharedAccessGroupIdentifier + let identifier = Identifier(nonEmpty: "UniquenessIdentifier") + + Accessibility.allCases.forEach { accessibility in + let backingService = Valet.sharedGroupValet(with: groupIdentifier, identifier: identifier, accessibility: accessibility).service + XCTAssertEqual(backingService, Service.sharedGroup(groupIdentifier, identifier, .valet(accessibility))) } } @@ -59,7 +69,7 @@ class ValetTests: XCTestCase CloudAccessibility.allCases.forEach { accessibility in let backingService = Valet.iCloudSharedGroupValet(with: identifier, accessibility: accessibility).service - XCTAssertEqual(backingService, Service.sharedGroup(identifier, .iCloud(accessibility))) + XCTAssertEqual(backingService, Service.sharedGroup(identifier, nil, .iCloud(accessibility))) } } diff --git a/Valet iOS Test Host App/Valet iOS Test Host App.entitlements b/Valet iOS Test Host App/Valet iOS Test Host App.entitlements index f00985b7..1a2ac8b0 100644 --- a/Valet iOS Test Host App/Valet iOS Test Host App.entitlements +++ b/Valet iOS Test Host App/Valet iOS Test Host App.entitlements @@ -9,6 +9,7 @@ keychain-access-groups $(AppIdentifierPrefix)com.squareup.Valet-iOS-Test-Host-App + $(AppIdentifierPrefix)com.squareup.Valet-iOS-Test-Host-App2 diff --git a/Valet macOS Test Host App/Valet_macOS_Test_Host_App.entitlements b/Valet macOS Test Host App/Valet_macOS_Test_Host_App.entitlements index 9e73df49..461e7b53 100644 --- a/Valet macOS Test Host App/Valet_macOS_Test_Host_App.entitlements +++ b/Valet macOS Test Host App/Valet_macOS_Test_Host_App.entitlements @@ -13,6 +13,7 @@ keychain-access-groups $(AppIdentifierPrefix)com.squareup.Valet-macOS-Test-Host-App + $(AppIdentifierPrefix)com.squareup.Valet-macOS-Test-Host-App2 diff --git a/Valet tvOS Test Host App/Valet tvOS Test Host App.entitlements b/Valet tvOS Test Host App/Valet tvOS Test Host App.entitlements index 2225c3e1..375509d3 100644 --- a/Valet tvOS Test Host App/Valet tvOS Test Host App.entitlements +++ b/Valet tvOS Test Host App/Valet tvOS Test Host App.entitlements @@ -9,6 +9,7 @@ keychain-access-groups $(AppIdentifierPrefix)com.squareup.Valet-tvOS-Test-Host-App + $(AppIdentifierPrefix)com.squareup.Valet-tvOS-Test-Host-App2 diff --git a/Valet watchOS Test Host App Extension/Valet watchOS Test Host App Extension.entitlements b/Valet watchOS Test Host App Extension/Valet watchOS Test Host App Extension.entitlements index 8a57f3d8..0a79d0b3 100644 --- a/Valet watchOS Test Host App Extension/Valet watchOS Test Host App Extension.entitlements +++ b/Valet watchOS Test Host App Extension/Valet watchOS Test Host App Extension.entitlements @@ -9,6 +9,7 @@ keychain-access-groups $(AppIdentifierPrefix)com.squareup.ValetTouchIDTestApp.watchkitapp.watchkitextension + $(AppIdentifierPrefix)com.squareup.ValetTouchIDTestApp.watchkitapp.watchkitextension2 From 7bfe6ae3d627c1281e5e9ecdbb59fa2965cdbda6 Mon Sep 17 00:00:00 2001 From: efirestone Date: Thu, 11 May 2023 22:36:51 -0500 Subject: [PATCH 4/4] Bump version to 4.2.0 --- Valet.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Valet.podspec b/Valet.podspec index 39a6aff8..696dc2f0 100644 --- a/Valet.podspec +++ b/Valet.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Valet' - s.version = '4.1.3' + s.version = '4.2.0' s.license = 'Apache License, Version 2.0' s.summary = 'Securely store data on iOS, tvOS, watchOS, or macOS without knowing a thing about how the Keychain works. It\'s easy. We promise.' s.homepage = 'https://github.com/square/Valet'