Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,29 @@ NSString *const myLuggageCombination = [myValet stringForKey:username error:nil]

In addition to allowing the storage of strings, Valet allows the storage of `Data` objects via `setObject(_ object: Data, forKey key: Key)` and `object(forKey key: String)`. Valets created with a different class type, via a different initializer, or with a different accessibility attribute will not be able to read or modify values in `myValet`.

### Sharing Secrets Among Multiple Applications
### Sharing Secrets Among Multiple Applications Using a Keychain Sharing Entitlement

```swift
let mySharedValet = Valet.sharedAccessGroupValet(with: SharedAccessGroupIdentifier(appIDPrefix: "AppID12345", nonEmptyGroup: "Druidia")!, accessibility: .whenUnlocked)
let mySharedValet = Valet.sharedGroupValet(with: SharedGroupIdentifier(appIDPrefix: "AppID12345", nonEmptyGroup: "Druidia")!, accessibility: .whenUnlocked)
```

```objc
VALValet *const mySharedValet = [VALValet sharedAccessGroupValetWithAppIDPrefix:@"AppID12345" sharedAccessGroupIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];
VALValet *const mySharedValet = [VALValet sharedGroupValetWithAppIDPrefix:@"AppID12345" sharedGroupIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];
```

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.
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 `sharedGroupValet` initializer.

### Sharing Secrets Among Multiple Applications Using an App Groups Entitlement

```swift
let mySharedValet = Valet.sharedGroupValet(with: SharedGroupIdentifier(groupPrefix: "group", nonEmptyGroup: "Druidia")!, accessibility: .whenUnlocked)
```

```objc
VALValet *const mySharedValet = [VALValet sharedGroupValetWithGroupPrefix:@"group" sharedGroupIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked];
```

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).

### Sharing Secrets Across Devices with iCloud

Expand Down
22 changes: 11 additions & 11 deletions Sources/Valet/Internal/Service.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ import Foundation

internal enum Service: CustomStringConvertible, Equatable {
case standard(Identifier, Configuration)
case sharedAccessGroup(SharedAccessGroupIdentifier, Configuration)
case sharedGroup(SharedGroupIdentifier, Configuration)

#if os(macOS)
case standardOverride(service: Identifier, Configuration)
case sharedAccessGroupOverride(service: SharedAccessGroupIdentifier, Configuration)
case sharedGroupOverride(service: SharedGroupIdentifier, Configuration)
#endif

// MARK: Equatable
Expand All @@ -48,11 +48,11 @@ internal enum Service: CustomStringConvertible, Equatable {
"VAL_\(configuration.description)_initWithIdentifier:accessibility:_\(identifier)_\(accessibilityDescription)"
}

internal static func sharedAccessGroup(with configuration: Configuration, identifier: SharedAccessGroupIdentifier, accessibilityDescription: String) -> String {
internal static func sharedGroup(with configuration: Configuration, identifier: SharedGroupIdentifier, accessibilityDescription: String) -> String {
"VAL_\(configuration.description)_initWithSharedAccessGroupIdentifier:accessibility:_\(identifier.groupIdentifier)_\(accessibilityDescription)"
}

internal static func sharedAccessGroup(with configuration: Configuration, explicitlySetIdentifier identifier: Identifier, accessibilityDescription: String) -> String {
internal static func sharedGroup(with configuration: Configuration, explicitlySetIdentifier identifier: Identifier, accessibilityDescription: String) -> String {
"VAL_\(configuration.description)_initWithSharedAccessGroupIdentifier:accessibility:_\(identifier)_\(accessibilityDescription)"
}

Expand All @@ -73,15 +73,15 @@ internal enum Service: CustomStringConvertible, Equatable {
case let .standard(_, desiredConfiguration):
configuration = desiredConfiguration

case let .sharedAccessGroup(identifier, desiredConfiguration):
case let .sharedGroup(identifier, desiredConfiguration):
baseQuery[kSecAttrAccessGroup as String] = identifier.description
configuration = desiredConfiguration

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

case let .sharedAccessGroupOverride(identifier, desiredConfiguration):
case let .sharedGroupOverride(identifier, desiredConfiguration):
baseQuery[kSecAttrAccessGroup as String] = identifier.description
configuration = desiredConfiguration
#endif
Expand Down Expand Up @@ -111,19 +111,19 @@ 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 .sharedAccessGroup(identifier, configuration):
service = Service.sharedAccessGroup(with: configuration, identifier: identifier, accessibilityDescription: configuration.accessibility.description)
case let .sharedGroup(identifier, configuration):
service = Service.sharedGroup(with: configuration, identifier: identifier, accessibilityDescription: configuration.accessibility.description)
#if os(macOS)
case let .standardOverride(identifier, _):
service = identifier.description
case let .sharedAccessGroupOverride(identifier, _):
case let .sharedGroupOverride(identifier, _):
service = identifier.groupIdentifier
#endif
}

switch self {
case let .standard(_, configuration),
let .sharedAccessGroup(_, configuration):
let .sharedGroup(_, configuration):
switch configuration {
case .valet, .iCloud:
// Nothing to do here.
Expand All @@ -138,7 +138,7 @@ internal enum Service: CustomStringConvertible, Equatable {

#if os(macOS)
case .standardOverride,
.sharedAccessGroupOverride:
.sharedGroupOverride:
return service
#endif
}
Expand Down
8 changes: 4 additions & 4 deletions Sources/Valet/SecureEnclave.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ public final class SecureEnclave {
case let .standard(identifier, _):
noPromptValet = .valet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly)
#if os(macOS)
case let .sharedAccessGroupOverride(identifier, _):
noPromptValet = .sharedAccessGroupValet(withExplicitlySet: identifier, accessibility: .whenPasscodeSetThisDeviceOnly)
case let .sharedGroupOverride(identifier, _):
noPromptValet = .sharedGroupValet(withExplicitlySet: identifier, accessibility: .whenPasscodeSetThisDeviceOnly)
#endif
case let .sharedAccessGroup(identifier, _):
noPromptValet = .sharedAccessGroupValet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly)
case let .sharedGroup(identifier, _):
noPromptValet = .sharedGroupValet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly)
}

return noPromptValet.canAccessKeychain()
Expand Down
30 changes: 22 additions & 8 deletions Sources/Valet/SecureEnclaveValet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ 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 sharedAccessGroupValet(with identifier: SharedAccessGroupIdentifier, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet {
let key = Service.sharedAccessGroup(identifier, .secureEnclave(accessControl)).description as NSString
public class func sharedGroupValet(with identifier: SharedGroupIdentifier, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet {
let key = Service.sharedGroup(identifier, .secureEnclave(accessControl)).description as NSString
if let existingValet = identifierToValetMap.object(forKey: key) {
return existingValet

Expand Down Expand Up @@ -84,10 +84,10 @@ public final class SecureEnclaveValet: NSObject {
accessControl: accessControl)
}

private convenience init(sharedAccess groupIdentifier: SharedAccessGroupIdentifier, accessControl: SecureEnclaveAccessControl) {
private convenience init(sharedAccess groupIdentifier: SharedGroupIdentifier, accessControl: SecureEnclaveAccessControl) {
self.init(
identifier: groupIdentifier.asIdentifier,
service: .sharedAccessGroup(groupIdentifier, .secureEnclave(accessControl)),
service: .sharedGroup(groupIdentifier, .secureEnclave(accessControl)),
accessControl: accessControl)
}

Expand Down Expand Up @@ -254,12 +254,26 @@ extension SecureEnclaveValet {
/// - 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.
/// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps
@objc(sharedAccessGroupValetWithAppIDPrefix:sharedAccessGroupIdentifier:accessControl:)
public class func 🚫swift_sharedAccessGroupValet(appIDPrefix: String, nonEmptyIdentifier identifier: String, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet? {
guard let identifier = SharedAccessGroupIdentifier(appIDPrefix: appIDPrefix, nonEmptyGroup: identifier) else {
@objc(sharedGroupValetWithAppIDPrefix:sharedGroupIdentifier:accessControl:)
public class func 🚫swift_sharedGroupValet(appIDPrefix: String, nonEmptyIdentifier identifier: String, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet? {
guard let identifier = SharedGroupIdentifier(appIDPrefix: appIDPrefix, nonEmptyGroup: identifier) else {
return nil
}
return sharedAccessGroupValet(with: identifier, accessControl: accessControl)
return sharedGroupValet(with: identifier, accessControl: accessControl)
}

/// - Parameters:
/// - groupPrefix: On iOS, iPadOS, watchOS, and tvOS, this prefix must equal "group". On macOS, this prefix is the application's App ID prefix, which can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty.
/// - identifier: An identifier that corresponds to a value in com.apple.security.application-groups in the application's Entitlements file. This string must not be empty.
/// - 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.
/// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps
@objc(sharedGroupValetWithGroupPrefix:sharedGroupIdentifier:accessControl:)
public class func 🚫swift_sharedGroupValet(groupPrefix: String, nonEmptyIdentifier identifier: String, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet? {
guard let identifier = SharedGroupIdentifier(groupPrefix: groupPrefix, nonEmptyGroup: identifier) else {
return nil
}
return sharedGroupValet(with: identifier, accessControl: accessControl)
}

/// - Parameter key: The key to look up in the keychain.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// SharedAccessGroupIdentifier.swift
// SharedGroupIdentifier.swift
// Valet
//
// Created by Dan Federman on 2/25/20.
Expand All @@ -21,7 +21,7 @@
import Foundation


public struct SharedAccessGroupIdentifier: CustomStringConvertible {
public struct SharedGroupIdentifier: CustomStringConvertible {

// MARK: Initialization

Expand All @@ -35,23 +35,47 @@ public struct SharedAccessGroupIdentifier: CustomStringConvertible {
return nil
}

self.appIDPrefix = appIDPrefix
self.prefix = appIDPrefix
self.groupIdentifier = groupIdentifier
}

/// A representation of a shared app group identifier.
/// - Parameters:
/// - groupPrefix: On iOS, iPadOS, watchOS, and tvOS, this prefix must equal "group". On macOS, this prefix is the application's App ID prefix, which can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty.
/// - groupIdentifier: An identifier that corresponds to a value in com.apple.security.application-groups in the application's Entitlements file. This string must not be empty.
/// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps
public init?(groupPrefix: String, nonEmptyGroup groupIdentifier: String?) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we enforce the group prefix on non-macOS by having separate inits?

#if os(macOS)
public init?(groupPrefix: String, nonEmptyGroup groupIdentifier: String?) {
#else
public init?(nonEmptyGroup groupIdentifier: String?) {
    let groupPrefix = Self.appGroupPrefix
#endif

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need to put the entire initializer in the #if (#if in Swift is way less permissive than Objective-C), but that'd be doable. I can separate that out.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, looking at this more: I think having the groupPrefix makes it clear what the groupIdentifier should be. If I make a single-parameter initializer for non-macOS platforms, then I think I'd need to accept groupIdentifier's that have group. as a prefix, as well as ones that do not. I Kina like this API being explicitly not magical. Thoughts?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think making the groupIdentifier only take the non-prefix string is fine as long as we're clear in the headerdoc what the parameter expects (i.e. "this is the identifier following the 'group.' in the entitlement").

I agree with your point that having the groupPrefix makes it more obvious, but I think that's outweighed by having a parameter that only accepts a single string. Don't feel strongly though.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we switch to separate initializers, we could also clean up the macOS case by removing the concept of a "group prefix" and specifying that it's the app id prefix.

init?(nonEmptyGroup groupIdentifier: String?)
init?(appIDPrefix: String, nonEmptyGroup groupIdentifier: String?)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that! But the tough thing is we already have this initializer for shared access groups:

init?(appIDPrefix: String, nonEmptyGroup groupIdentifier: String?)

There's a good bit of overlap between how these features work. I'm leaning hard towards keeping the APIs explicit for now. We can always add new convenience initializers down the line.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does groupIdentifier need to be optional? Given the non-empty requirement, it feels like we should also make it non-optional.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We made it optional in the other method above. I think it was a convenience, similar to how Identifier works. Open to changing this everywhere. LMK how you're thinking about this.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, happy to match the existing API.

#if os(macOS)
guard !groupPrefix.isEmpty, let groupIdentifier = groupIdentifier, !groupIdentifier.isEmpty else {
return nil
}
#else
guard groupPrefix == Self.appGroupPrefix, let groupIdentifier = groupIdentifier, !groupIdentifier.isEmpty else {
return nil
}
#endif

self.prefix = groupPrefix
self.groupIdentifier = groupIdentifier
}

// MARK: CustomStringConvertible

public var description: String {
return appIDPrefix + "." + groupIdentifier
prefix + "." + groupIdentifier
}

// MARK: Internal Properties

internal let appIDPrefix: String
internal let prefix: String
internal let groupIdentifier: String

internal var asIdentifier: Identifier {
// It is safe to force unwrap because we've already validated that our description is non-empty.
Identifier(nonEmpty: description)!
}

// MARK: Private Static Properties

private static let appGroupPrefix = "group"
}
Loading