Skip to content

Commit 1dfa2ff

Browse files
authored
Require that App ID Prefix be explicitly passed into Shared Access Group Valets (#218)
* Require that App ID Prefix be explicitly passed into Shared Access Group Valets * Add App ID prefix to tests * Create and adopt SharedAccessGroupIdentifier * Update documentation
1 parent 1de8a0c commit 1dfa2ff

23 files changed

+422
-363
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,14 @@ In addition to allowing the storage of strings, Valet allows the storage of `Dat
128128
### Sharing Secrets Among Multiple Applications
129129
130130
```swift
131-
let mySharedValet = Valet.sharedAccessGroupValet(with: Identifier(nonEmpty: "Druidia")!, accessibility: .whenUnlocked)
131+
let mySharedValet = Valet.sharedAccessGroupValet(with: SharedAccessGroupIdentifier(appIDPrefix: "AppID12345", nonEmptyGroup: "Druidia")!, accessibility: .whenUnlocked)
132132
```
133133

134134
```objc
135-
VALValet *const mySharedValet = [VALValet valetWithSharedAccessGroupIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];
135+
VALValet *const mySharedValet = [VALValet sharedAccessGroupValetWithAppIDPrefix:@"AppID12345" sharedAccessGroupIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];
136136
```
137137
138-
This instance can be used to store and retrieve data securely across any app written by the same developer with the value `Druidia` under the `keychain-access-groups` key in the app’s `Entitlements` file, when the device is unlocked. Note that `myValet` and `mySharedValet` can not 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 `sharedAccessGroupValet` initializer.
138+
This instance can be used to store and retrieve data securely across any app written by the same developer that has `AppID12345.Druidia` (or `$(AppIdentifierPrefix)Druidia`) set as a value for the `keychain-access-groups` key in the app’s `Entitlements`, where `AppID12345` is the application’s [App ID prefix](https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps#2974920). This Valet is accessible when the device is unlocked. Note that `myValet` and `mySharedValet` can not 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 `sharedAccessGroupValet` initializer.
139139
140140
### Sharing Secrets Across Devices with iCloud
141141

Sources/Valet/Internal/SecItem.swift

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -31,52 +31,7 @@ internal func execute<ReturnType>(in lock: NSLock, block: () throws -> ReturnTyp
3131

3232

3333
internal final class SecItem {
34-
35-
// MARK: Internal Class Properties
36-
37-
/// Programatically grab the required prefix for the shared access group (i.e. Bundle Seed ID). The value for the kSecAttrAccessGroup key in queries for data that is shared between apps must be of the format bundleSeedID.sharedAccessGroup. For more information on the Bundle Seed ID, see https://developer.apple.com/library/ios/qa/qa1713/_index.html
38-
internal static var sharedAccessGroupPrefix: String? {
39-
var query: [CFString : Any] = [
40-
kSecClass : kSecClassGenericPassword,
41-
kSecAttrAccount : "SharedAccessGroupPrefixPlaceholder",
42-
kSecReturnAttributes : true,
43-
kSecAttrAccessible : Accessibility.afterFirstUnlockThisDeviceOnly.secAccessibilityAttribute
44-
]
45-
46-
if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
47-
// Add kSecUseDataProtectionKeychain to the query to ensure we can retrieve the shared access group prefix.
48-
query[kSecUseDataProtectionKeychain] = true
49-
}
50-
51-
secItemLock.lock()
52-
defer {
53-
secItemLock.unlock()
54-
}
55-
56-
var result: AnyObject? = nil
57-
var status = SecItemCopyMatching(query as CFDictionary, &result)
5834

59-
if status == errSecItemNotFound {
60-
status = SecItemAdd(query as CFDictionary, &result)
61-
}
62-
63-
guard status == errSecSuccess, let queryResult = result as? [CFString : AnyHashable], let accessGroup = queryResult[kSecAttrAccessGroup] as? String else {
64-
// We may not be able to access the shared access group prefix because the accessibility of the above keychain data is set to `afterFirstUnlock`.
65-
// Consumers should always check `canAccessKeychain()` after creating a Valet and before using it. Doing so will catch this error.
66-
return nil
67-
}
68-
69-
let components = accessGroup.components(separatedBy: ".")
70-
if let bundleSeedIdentifier = components.first, !bundleSeedIdentifier.isEmpty {
71-
return bundleSeedIdentifier
72-
73-
} else {
74-
// We may not be able to access the shared access group prefix because the accessibility of the above keychain data is set to `afterFirstUnlock`.
75-
// Consumers should always check `canAccessKeychain()` after creating a Valet and before using it. Doing so will catch this error.
76-
return nil
77-
}
78-
}
79-
8035
// MARK: Internal Class Methods
8136

8237
internal static func copy<DesiredType>(matching query: [String : AnyHashable]) throws -> DesiredType {

Sources/Valet/Internal/Service.swift

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ import Foundation
2323

2424
internal enum Service: CustomStringConvertible, Equatable {
2525
case standard(Identifier, Configuration)
26-
case sharedAccessGroup(Identifier, Configuration)
26+
case sharedAccessGroup(SharedAccessGroupIdentifier, Configuration)
2727

2828
#if os(macOS)
2929
case standardOverride(service: Identifier, Configuration)
30-
case sharedAccessGroupOverride(service: Identifier, Configuration)
30+
case sharedAccessGroupOverride(service: SharedAccessGroupIdentifier, Configuration)
3131
#endif
3232

3333
// MARK: Equatable
@@ -48,13 +48,17 @@ internal enum Service: CustomStringConvertible, Equatable {
4848
"VAL_\(configuration.description)_initWithIdentifier:accessibility:_\(identifier)_\(accessibilityDescription)"
4949
}
5050

51-
internal static func sharedAccessGroup(with configuration: Configuration, identifier: Identifier, accessibilityDescription: String) -> String {
51+
internal static func sharedAccessGroup(with configuration: Configuration, identifier: SharedAccessGroupIdentifier, accessibilityDescription: String) -> String {
52+
"VAL_\(configuration.description)_initWithSharedAccessGroupIdentifier:accessibility:_\(identifier.groupIdentifier)_\(accessibilityDescription)"
53+
}
54+
55+
internal static func sharedAccessGroup(with configuration: Configuration, explicitlySetIdentifier identifier: Identifier, accessibilityDescription: String) -> String {
5256
"VAL_\(configuration.description)_initWithSharedAccessGroupIdentifier:accessibility:_\(identifier)_\(accessibilityDescription)"
5357
}
5458

5559
// MARK: Internal Methods
5660

57-
internal func generateBaseQuery() throws -> [String : AnyHashable] {
61+
internal func generateBaseQuery() -> [String : AnyHashable] {
5862
var baseQuery: [String : AnyHashable] = [
5963
kSecClass as String : kSecClassGenericPassword as String,
6064
kSecAttrService as String : secService,
@@ -70,31 +74,15 @@ internal enum Service: CustomStringConvertible, Equatable {
7074
configuration = desiredConfiguration
7175

7276
case let .sharedAccessGroup(identifier, desiredConfiguration):
73-
guard let sharedAccessGroupPrefix = SecItem.sharedAccessGroupPrefix else {
74-
throw KeychainError.couldNotAccessKeychain
75-
}
76-
if identifier.description.hasPrefix("\(sharedAccessGroupPrefix).") {
77-
// The Bundle Seed ID was passed in as a prefix to the identifier.
78-
baseQuery[kSecAttrAccessGroup as String] = identifier.description
79-
} else {
80-
baseQuery[kSecAttrAccessGroup as String] = "\(sharedAccessGroupPrefix).\(identifier.description)"
81-
}
77+
baseQuery[kSecAttrAccessGroup as String] = identifier.description
8278
configuration = desiredConfiguration
8379

8480
#if os(macOS)
8581
case let .standardOverride(_, desiredConfiguration):
8682
configuration = desiredConfiguration
8783

8884
case let .sharedAccessGroupOverride(identifier, desiredConfiguration):
89-
guard let sharedAccessGroupPrefix = SecItem.sharedAccessGroupPrefix else {
90-
throw KeychainError.couldNotAccessKeychain
91-
}
92-
if identifier.description.hasPrefix("\(sharedAccessGroupPrefix).") {
93-
// The Bundle Seed ID was passed in as a prefix to the identifier.
94-
baseQuery[kSecAttrAccessGroup as String] = identifier.description
95-
} else {
96-
baseQuery[kSecAttrAccessGroup as String] = "\(sharedAccessGroupPrefix).\(identifier.description)"
97-
}
85+
baseQuery[kSecAttrAccessGroup as String] = identifier.description
9886
configuration = desiredConfiguration
9987
#endif
10088
}
@@ -126,9 +114,10 @@ internal enum Service: CustomStringConvertible, Equatable {
126114
case let .sharedAccessGroup(identifier, configuration):
127115
service = Service.sharedAccessGroup(with: configuration, identifier: identifier, accessibilityDescription: configuration.accessibility.description)
128116
#if os(macOS)
129-
case let .standardOverride(identifier, _),
130-
let .sharedAccessGroupOverride(identifier, _):
117+
case let .standardOverride(identifier, _):
131118
service = identifier.description
119+
case let .sharedAccessGroupOverride(identifier, _):
120+
service = identifier.groupIdentifier
132121
#endif
133122
}
134123

Sources/Valet/SecureEnclave.swift

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,24 @@ public final class SecureEnclave {
2525

2626
// MARK: Internal Methods
2727

28-
/// - Parameters:
29-
/// - service: The service of the keychain slice we want to check if we can access.
30-
/// - identifier: A non-empty identifier that scopes the slice of keychain we want to access.
28+
/// - Parameter service: The service of the keychain slice we want to check if we can access.
3129
/// - Returns: `true` if the keychain is accessible for reading and writing, `false` otherwise.
3230
/// - Note: Determined by writing a value to the keychain and then reading it back out.
33-
internal static func canAccessKeychain(with service: Service, identifier: Identifier) -> Bool {
31+
internal static func canAccessKeychain(with service: Service) -> Bool {
3432
// To avoid prompting the user for Touch ID or passcode, create a Valet with our identifier and accessibility and ask it if it can access the keychain.
3533
let noPromptValet: Valet
3634
switch service {
3735
#if os(macOS)
38-
case .standardOverride:
39-
fallthrough
36+
case let .standardOverride(identifier, _):
37+
noPromptValet = .valet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly)
4038
#endif
41-
case .standard:
39+
case let .standard(identifier, _):
4240
noPromptValet = .valet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly)
4341
#if os(macOS)
44-
case .sharedAccessGroupOverride:
45-
fallthrough
42+
case let .sharedAccessGroupOverride(identifier, _):
43+
noPromptValet = .sharedAccessGroupValet(withExplicitlySet: identifier, accessibility: .whenPasscodeSetThisDeviceOnly)
4644
#endif
47-
case .sharedAccessGroup:
45+
case let .sharedAccessGroup(identifier, _):
4846
noPromptValet = .sharedAccessGroupValet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly)
4947
}
5048

Sources/Valet/SecureEnclaveValet.swift

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public final class SecureEnclaveValet: NSObject {
4747
/// - identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file.
4848
/// - accessControl: The desired access control for the SecureEnclaveValet.
4949
/// - Returns: A SecureEnclaveValet that reads/writes keychain elements that can be shared across applications written by the same development team.
50-
public class func sharedAccessGroupValet(with identifier: Identifier, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet {
50+
public class func sharedAccessGroupValet(with identifier: SharedAccessGroupIdentifier, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet {
5151
let key = Service.sharedAccessGroup(identifier, .secureEnclave(accessControl)).description as NSString
5252
if let existingValet = identifierToValetMap.object(forKey: key) {
5353
return existingValet
@@ -84,18 +84,18 @@ public final class SecureEnclaveValet: NSObject {
8484
accessControl: accessControl)
8585
}
8686

87-
private convenience init(sharedAccess identifier: Identifier, accessControl: SecureEnclaveAccessControl) {
87+
private convenience init(sharedAccess groupIdentifier: SharedAccessGroupIdentifier, accessControl: SecureEnclaveAccessControl) {
8888
self.init(
89-
identifier: identifier,
90-
service: .sharedAccessGroup(identifier, .secureEnclave(accessControl)),
89+
identifier: groupIdentifier.asIdentifier,
90+
service: .sharedAccessGroup(groupIdentifier, .secureEnclave(accessControl)),
9191
accessControl: accessControl)
9292
}
9393

9494
private init(identifier: Identifier, service: Service, accessControl: SecureEnclaveAccessControl) {
9595
self.identifier = identifier
9696
self.service = service
9797
self.accessControl = accessControl
98-
_keychainQuery = try? service.generateBaseQuery()
98+
baseKeychainQuery = service.generateBaseQuery()
9999
}
100100

101101
// MARK: Hashable
@@ -116,7 +116,7 @@ public final class SecureEnclaveValet: NSObject {
116116
/// - Note: Determined by writing a value to the keychain and then reading it back out. Will never prompt the user for Face ID, Touch ID, or password.
117117
@objc
118118
public func canAccessKeychain() -> Bool {
119-
SecureEnclave.canAccessKeychain(with: service, identifier: identifier)
119+
SecureEnclave.canAccessKeychain(with: service)
120120
}
121121

122122
/// - Parameters:
@@ -126,7 +126,7 @@ public final class SecureEnclaveValet: NSObject {
126126
@objc
127127
public func setObject(_ object: Data, forKey key: String) throws {
128128
try execute(in: lock) {
129-
try SecureEnclave.setObject(object, forKey: key, options: try keychainQuery())
129+
try SecureEnclave.setObject(object, forKey: key, options: baseKeychainQuery)
130130
}
131131
}
132132

@@ -138,7 +138,7 @@ public final class SecureEnclaveValet: NSObject {
138138
@objc
139139
public func object(forKey key: String, withPrompt userPrompt: String) throws -> Data {
140140
try execute(in: lock) {
141-
try SecureEnclave.object(forKey: key, withPrompt: userPrompt, options: try keychainQuery())
141+
try SecureEnclave.object(forKey: key, withPrompt: userPrompt, options: baseKeychainQuery)
142142
}
143143
}
144144

@@ -148,7 +148,7 @@ public final class SecureEnclaveValet: NSObject {
148148
/// - Note: Will never prompt the user for Face ID, Touch ID, or password.
149149
public func containsObject(forKey key: String) throws -> Bool {
150150
try execute(in: lock) {
151-
try SecureEnclave.containsObject(forKey: key, options: try keychainQuery())
151+
try SecureEnclave.containsObject(forKey: key, options: baseKeychainQuery)
152152
}
153153
}
154154

@@ -159,7 +159,7 @@ public final class SecureEnclaveValet: NSObject {
159159
@objc
160160
public func setString(_ string: String, forKey key: String) throws {
161161
try execute(in: lock) {
162-
try SecureEnclave.setString(string, forKey: key, options: try keychainQuery())
162+
try SecureEnclave.setString(string, forKey: key, options: baseKeychainQuery)
163163
}
164164
}
165165

@@ -171,7 +171,7 @@ public final class SecureEnclaveValet: NSObject {
171171
@objc
172172
public func string(forKey key: String, withPrompt userPrompt: String) throws -> String {
173173
try execute(in: lock) {
174-
try SecureEnclave.string(forKey: key, withPrompt: userPrompt, options: try keychainQuery())
174+
try SecureEnclave.string(forKey: key, withPrompt: userPrompt, options: baseKeychainQuery)
175175
}
176176
}
177177

@@ -181,7 +181,7 @@ public final class SecureEnclaveValet: NSObject {
181181
@objc
182182
public func removeObject(forKey key: String) throws {
183183
try execute(in: lock) {
184-
try Keychain.removeObject(forKey: key, options: try keychainQuery())
184+
try Keychain.removeObject(forKey: key, options: baseKeychainQuery)
185185
}
186186
}
187187

@@ -190,7 +190,7 @@ public final class SecureEnclaveValet: NSObject {
190190
@objc
191191
public func removeAllObjects() throws {
192192
try execute(in: lock) {
193-
try Keychain.removeAllObjects(matching: try keychainQuery())
193+
try Keychain.removeAllObjects(matching: baseKeychainQuery)
194194
}
195195
}
196196

@@ -203,7 +203,7 @@ public final class SecureEnclaveValet: NSObject {
203203
@objc
204204
public func migrateObjects(matching query: [String : AnyHashable], removeOnCompletion: Bool) throws {
205205
try execute(in: lock) {
206-
try Keychain.migrateObjects(matching: query, into: try keychainQuery(), removeOnCompletion: removeOnCompletion)
206+
try Keychain.migrateObjects(matching: query, into: baseKeychainQuery, removeOnCompletion: removeOnCompletion)
207207
}
208208
}
209209

@@ -215,7 +215,7 @@ public final class SecureEnclaveValet: NSObject {
215215
/// - Note: The keychain is not modified if an error is thrown.
216216
@objc
217217
public func migrateObjects(from valet: Valet, removeOnCompletion: Bool) throws {
218-
try migrateObjects(matching: try valet.keychainQuery(), removeOnCompletion: removeOnCompletion)
218+
try migrateObjects(matching: valet.baseKeychainQuery, removeOnCompletion: removeOnCompletion)
219219
}
220220

221221
// MARK: Internal Properties
@@ -225,19 +225,8 @@ public final class SecureEnclaveValet: NSObject {
225225
// MARK: Private Properties
226226

227227
private let lock = NSLock()
228-
private var _keychainQuery: [String : AnyHashable]?
229-
230-
// MARK: Private Methods
228+
private let baseKeychainQuery: [String : AnyHashable]
231229

232-
private func keychainQuery() throws -> [String : AnyHashable] {
233-
if let keychainQuery = _keychainQuery {
234-
return keychainQuery
235-
} else {
236-
let keychainQuery = try service.generateBaseQuery()
237-
_keychainQuery = keychainQuery
238-
return keychainQuery
239-
}
240-
}
241230
}
242231

243232

@@ -260,12 +249,14 @@ extension SecureEnclaveValet {
260249
}
261250

262251
/// - Parameters:
263-
/// - identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file.
252+
/// - 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.
253+
/// - identifier: An identifier that cooresponds to a value in keychain-access-groups in the application's Entitlements file. This string must not be empty.
264254
/// - accessControl: The desired access control for the SecureEnclaveValet.
265255
/// - Returns: A SecureEnclaveValet that reads/writes keychain elements that can be shared across applications written by the same development team.
266-
@objc(sharedAccessGroupValetWithIdentifier:accessControl:)
267-
public class func 🚫swift_sharedAccessGroupValet(with identifier: String, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet? {
268-
guard let identifier = Identifier(nonEmpty: identifier) else {
256+
/// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps
257+
@objc(sharedAccessGroupValetWithAppIDPrefix:sharedAccessGroupIdentifier:accessControl:)
258+
public class func 🚫swift_sharedAccessGroupValet(appIDPrefix: String, nonEmptyIdentifier identifier: String, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet? {
259+
guard let identifier = SharedAccessGroupIdentifier(appIDPrefix: appIDPrefix, nonEmptyGroup: identifier) else {
269260
return nil
270261
}
271262
return sharedAccessGroupValet(with: identifier, accessControl: accessControl)

0 commit comments

Comments
 (0)