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
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 fe968f3d..6f06e734 100644
--- a/Sources/Valet/SinglePromptSecureEnclaveValet.swift
+++ b/Sources/Valet/SinglePromptSecureEnclaveValet.swift
@@ -49,13 +49,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
}
@@ -86,10 +86,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 d5f8e44b..6ab7b921 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 a13653b5..f157fc1f 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 118fe1d0..b9364303 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 302dae11..1aac0be2 100644
--- a/Tests/ValetIntegrationTests/ValetIntegrationTests.swift
+++ b/Tests/ValetIntegrationTests/ValetIntegrationTests.swift
@@ -84,6 +84,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 = {
@@ -106,9 +120,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)
@@ -167,7 +183,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)))
}
}
@@ -185,7 +211,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)))
}
}
@@ -327,6 +363,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)
@@ -441,6 +516,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)
@@ -660,6 +761,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 testEnvironmentIsSignedOrDoesNotRequireEntitlement() 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
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'