From dfcf1fd14216fb7454c9cb067a9288f21252e19e Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Sat, 13 Jun 2020 16:33:10 -0700 Subject: [PATCH] [Valet 4.0] Development Branch (#199) * Do not build branch build on every push on PR branches * Update cocoapods * Start validating podspec on Xcode 11 * Drop Xcode 9 and 10 support * Swift version to 5.0 * Bump minor version of osx_image on .travis.yml in order to access simulators for older OSes * Bump destination for iOS 11 to get CI working * Update tests to reflext iOS 13 simulator's inability to store items that require a passcode to be set * Enable running CI on iOS 10, tvOS 10, and watchOS 3 * Allow kSecAttrService to be a customer-friendly string on Mac * Add a section to the README on choosing the best identifier on a Mac * Update README documentation on choosing a user-friendly identifier * Add ObjC compat layer for new initializers * Remove Always accessibility specifier * Ensure test environment is signed before testing shared access keychain * Add migration helper methods * Use throws rather than return types to indicate error. * Get rid of ErrorHandler * couldNotReadKeychain -> .couldNotAccessKeychain * Make Objective-C bridging methods for accessing values with prompt redundant * Swift 5 updates * Bring API in line with Apple's naming guidelines * Adopt Swift 5 syntax, and enable support for SinglePromptSecureEnclaveValet on tvOS * Fix migrateObjectsFromAlwaysAccessible methods * Fix warning introduced by merge * Bump version to 4.0.0 * Get new migration methods working with Catalina * Update README * Set up code coverage * Run more tests on a single machine * Standardize method naming * Run test coverage on every target * Modernize doc comments * Remove returns * Use SeeAlso * Modernize doc comments * Add Warning * Add objc example * findOrCreate(explicitlySet must use a key that combines service, configuration, accessibility, and sharedAccessGroup to prevent returning the wrong Valet * Utilize testEnvironmentIsSigned before using shared keychains * Use explicitlySetSharedAccessGroupIdentifier when dealing with shared access groups * removeAllObjects() to avoid collisions in tests * Fail test on setup failure * Update whitespace * containsObject(forKey should throw in Swift * Introduce Throws doc comment * Catch closer to the source * Add simple Objective-C compatibility layer tests * Update copyright * Fixup whitespace * try? less in tests * If deleting items throws, then we should surface the failure. * Remove runtime assert, since we will throw the error anyways * Update Mac tests to use try on containsObject * Add description to KeychainError * Fix macOS tests after throwing on removeAllObjects in setUp * Increase test coverage of error files * Use permutation valet rather than vanillaValet multiple times * Indentation and test separation * Add comment re why we're checking for errSecInteractionNotAllowed * Better comment formatting * Use Throws rather than Note * Add final to test classes * Rename internal containsObject methods to performCopy * [Valet 4.0] Add explicit tests for CloudAccessibility (#210) * [Valet 4.0] Get SinglePromptSecureEnclaveIntegrationTests running on tvOS (#209) * Get SinglePromptSecureEnclaveIntegrationTests running on tvOS * Make SinglePromptSecureEnclaveValet available on tvOS 11, not tvOS 10 * [Valet 4.0] Add explicit tests for Configuration (#211) * Use CaseIterable instead of allValues where possible (#212) * Get ValetTouchIDTest building again * 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 * [Valet 4.0] Update migration guide (#221) * Update migration guide for Valet 4.0 * NickEntin feedback Co-Authored-By: Nick Entin * Remove version from Package.swift (#223) * Add headerdoc comment for removing an object from the keychain * Update headerdoc comments for parameters of type SharedAccessGroupIdentifier * Update headerdoc comment for migration method * Update headerdoc comments for objc compatibility methods * Rename MigrationError cases with `InQueryResult` to `ToMigrate` (#227) * Rename InQueryResult -> ToMigrate * Update comments * Fix typo in README (#229) * Create 'Changing an Accessibility Value After Persisting Data' section in README (#232) * Use correct Valet name in README example * Create Changing an Accessibility Value After Persisting Data section * Get watchOS tests running locally (#233) * Support sharing keychain items using App Groups (#230) * Add App Group group.valet.test * Update syntax for Swift 5 * Enable SharedAccessGroup code to semantically handle AppGroups. Rename SharedAccessGroup -> SharedGroup * README updates * Add sharedAppGroupIdentifier tests to Valet * Add sharedAppGroupIdentifier test to SecureEnclave * Add sharedAppGroupIdentifier test to SinglePromptSecureEnclave * Add objective-c compatibility layer --- .travis.yml | 51 +- Gemfile | 2 +- Gemfile.lock | 35 +- Package.swift | 3 +- README.md | 90 ++- Scripts/build.swift | 52 +- Sources/Valet/Accessibility.swift | 41 +- Sources/Valet/CloudAccessibility.swift | 25 +- Sources/Valet/ErrorHandler.swift | 49 -- Sources/Valet/Internal/Keychain.swift | 289 +++----- Sources/Valet/Internal/SecItem.swift | 195 ++--- Sources/Valet/Internal/Service.swift | 94 ++- Sources/Valet/KeychainError.swift | 68 ++ Sources/Valet/KeychainQueryConvertible.swift | 29 - Sources/Valet/MigrationError.swift | 51 ++ Sources/Valet/MigrationResult.swift | 62 -- Sources/Valet/SecureEnclave.swift | 168 ++--- .../Valet/SecureEnclaveAccessControl.swift | 30 +- Sources/Valet/SecureEnclaveValet.swift | 277 ++++---- Sources/Valet/SharedGroupIdentifier.swift | 81 +++ .../SinglePromptSecureEnclaveValet.swift | 296 ++++---- Sources/Valet/SwiftCompatibility.swift | 29 - Sources/Valet/Valet.swift | 667 +++++++++++++----- ...reEnclaveBackwardsCompatibilityTests.swift | 6 +- ...reEnclaveBackwardsCompatibilityTests.swift | 10 +- ...ronizableBackwardsCompatibilityTests.swift | 12 +- .../ValetBackwardsCompatibilityTests.swift | 114 ++- .../CloudIntegrationTests.swift | 56 +- Tests/ValetIntegrationTests/MacTests.swift | 177 ++++- .../ValetIntegrationTests/SecItemTests.swift | 18 - .../SecureEnclaveIntegrationTests.swift | 121 ++-- ...ePromptSecureEnclaveIntegrationTests.swift | 200 ++++-- .../ValetIntegrationTests.swift | 635 ++++++++++------- .../VALSecureEnclaveValetTests.m | 169 +++++ .../VALSinglePromptSecureEnclaveValetTests.m | 199 ++++++ .../VALValetTests.m | 378 ++++++++++ .../ValetTests/CloudAccessibilityTests.swift | 37 + Tests/ValetTests/CloudTests.swift | 9 - Tests/ValetTests/ConfigurationTests.swift | 97 +++ Tests/ValetTests/KeychainErrorTests.swift | 64 ++ Tests/ValetTests/MigrationErrorTests.swift | 43 ++ Tests/ValetTests/SecureEnclaveTests.swift | 15 +- .../SinglePromptSecureEnclaveTests.swift | 18 +- Tests/ValetTests/ValetTests.swift | 69 +- Tests/XCTest-watchOS | 2 +- .../Valet iOS Test Host App.entitlements | 4 + .../Valet_macOS_Test_Host_App.entitlements | 4 + .../Valet tvOS Test Host App.entitlements | 4 + ...tchOS Test Host App Extension.entitlements | 4 + .../Valet watchOS Test Host App.entitlements | 10 + Valet.podspec | 4 +- Valet.xcodeproj/project.pbxproj | 184 +++-- .../ValetTouchIDTestViewController.swift | 37 +- 53 files changed, 3583 insertions(+), 1801 deletions(-) delete mode 100644 Sources/Valet/ErrorHandler.swift create mode 100644 Sources/Valet/KeychainError.swift delete mode 100644 Sources/Valet/KeychainQueryConvertible.swift create mode 100644 Sources/Valet/MigrationError.swift delete mode 100644 Sources/Valet/MigrationResult.swift create mode 100644 Sources/Valet/SharedGroupIdentifier.swift delete mode 100644 Sources/Valet/SwiftCompatibility.swift create mode 100644 Tests/ValetObjectiveCBridgeTests/VALSecureEnclaveValetTests.m create mode 100644 Tests/ValetObjectiveCBridgeTests/VALSinglePromptSecureEnclaveValetTests.m create mode 100644 Tests/ValetObjectiveCBridgeTests/VALValetTests.m create mode 100644 Tests/ValetTests/CloudAccessibilityTests.swift create mode 100644 Tests/ValetTests/ConfigurationTests.swift create mode 100644 Tests/ValetTests/KeychainErrorTests.swift create mode 100644 Tests/ValetTests/MigrationErrorTests.swift create mode 100644 Valet watchOS Test Host App/Valet watchOS Test Host App.entitlements diff --git a/.travis.yml b/.travis.yml index 9db2d5cb..7616668e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,37 +6,32 @@ script: - ./Scripts/ci.sh matrix: include: - - osx_image: xcode11 - env: ACTION="swift-package";PLATFORMS="iOS_13,tvOS_13,macOS_10_15,watchOS_6"; - - osx_image: xcode11 - env: ACTION="xcode";PLATFORMS="iOS_13,tvOS_13,macOS_10_15,watchOS_6"; + - osx_image: xcode11.3 + env: ACTION="xcode";PLATFORMS="iOS_10,tvOS_10,watchOS_3"; after_success: - - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/iOS_13 -t 5165deef-da9c-443d-90ea-bb0620bffe44 - - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/tvOS_13 -t 5165deef-da9c-443d-90ea-bb0620bffe44 - - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/macOS_10_15 -t 5165deef-da9c-443d-90ea-bb0620bffe44 - - osx_image: xcode11 - env: ACTION="carthage" - - - osx_image: xcode10.2 - env: ACTION="xcode";PLATFORMS="iOS_12,tvOS_12,macOS_10_14,watchOS_5"; + - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/iOS_10 -t 5165deef-da9c-443d-90ea-bb0620bffe44 + - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/tvOS_10 -t 5165deef-da9c-443d-90ea-bb0620bffe44 + - osx_image: xcode11.3 + env: ACTION="xcode";PLATFORMS="iOS_11,tvOS_11,watchOS_4"; after_success: - - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/iOS_12 -t 5165deef-da9c-443d-90ea-bb0620bffe44 - - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/tvOS_12 -t 5165deef-da9c-443d-90ea-bb0620bffe44 - - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/macOS_10_14 -t 5165deef-da9c-443d-90ea-bb0620bffe44 - - osx_image: xcode10.2 - env: ACTION="pod-lint";SWIFT_VERSION="5.0" - - osx_image: xcode10.2 - env: ACTION="carthage" - - - osx_image: xcode9 - env: ACTION="xcode";PLATFORMS="iOS_11,tvOS_11,macOS_10_13,watchOS_4"; + - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/iOS_11 -t 5165deef-da9c-443d-90ea-bb0620bffe44 + - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/tvOS_11 -t 5165deef-da9c-443d-90ea-bb0620bffe44 + - osx_image: xcode11.3 + env: ACTION="xcode";PLATFORMS="iOS_12,tvOS_12,watchOS_5"; + after_success: + - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/iOS_12 -t 5165deef-da9c-443d-90ea-bb0620bffe44 + - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/tvOS_12 -t 5165deef-da9c-443d-90ea-bb0620bffe44 + - osx_image: xcode11.3 + env: ACTION="xcode";PLATFORMS="iOS_13,tvOS_13,macOS_10_15,watchOS_6"; after_success: - - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/iOS_11 -t 5165deef-da9c-443d-90ea-bb0620bffe44 - - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/tvOS_11 -t 5165deef-da9c-443d-90ea-bb0620bffe44 - - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/macOS_10_13 -t 5165deef-da9c-443d-90ea-bb0620bffe44 - - osx_image: xcode9 - env: ACTION="pod-lint";SWIFT_VERSION="4.0" - - osx_image: xcode9 + - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/iOS_13 -t 5165deef-da9c-443d-90ea-bb0620bffe44 + - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/tvOS_13 -t 5165deef-da9c-443d-90ea-bb0620bffe44 + - bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/macOS_10_15 -t 5165deef-da9c-443d-90ea-bb0620bffe44 + - osx_image: xcode11.3 + env: ACTION="swift-package";PLATFORMS="iOS_13,tvOS_13,macOS_10_15,watchOS_6"; + - osx_image: xcode11.3 + env: ACTION="pod-lint";SWIFT_VERSION="5.0" + - osx_image: xcode11.3 env: ACTION="carthage" branches: diff --git a/Gemfile b/Gemfile index 53c36c57..09c4d6ef 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source 'https://rubygems.org' do - gem 'cocoapods', '~> 1.7.0' + gem 'cocoapods', '~> 1.8.0' end diff --git a/Gemfile.lock b/Gemfile.lock index 511f6499..2f8c4e07 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,56 +1,63 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.0) + CFPropertyList (3.0.1) activesupport (4.2.11.1) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) + algoliasearch (1.27.1) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) atomos (0.1.3) - claide (1.0.2) - cocoapods (1.7.0) + claide (1.0.3) + cocoapods (1.8.4) activesupport (>= 4.0.2, < 5) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.7.0) + cocoapods-core (= 1.8.4) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 1.2.2, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-stats (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.3.1, < 2.0) + cocoapods-trunk (>= 1.4.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) escape (~> 0.0.4) - fourflusher (>= 2.2.0, < 3.0) + fourflusher (>= 2.3.0, < 3.0) gh_inspector (~> 1.0) molinillo (~> 0.6.6) nap (~> 1.0) ruby-macho (~> 1.4) - xcodeproj (>= 1.8.2, < 2.0) - cocoapods-core (1.7.0) + xcodeproj (>= 1.11.1, < 2.0) + cocoapods-core (1.8.4) activesupport (>= 4.0.2, < 6) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) fuzzy_match (~> 2.0.4) nap (~> 1.0) cocoapods-deintegrate (1.0.4) - cocoapods-downloader (1.2.2) + cocoapods-downloader (1.3.0) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.0) cocoapods-stats (1.1.0) - cocoapods-trunk (1.3.1) + cocoapods-trunk (1.4.1) nap (>= 0.8, < 2.0) netrc (~> 0.11) cocoapods-try (1.1.0) colored2 (3.1.2) concurrent-ruby (1.1.5) escape (0.0.4) - fourflusher (2.2.0) + fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) + httpclient (2.8.3) i18n (0.9.5) concurrent-ruby (~> 1.0) - minitest (5.11.3) + json (2.2.0) + minitest (5.13.0) molinillo (0.6.6) nanaimo (0.2.6) nap (1.1.0) @@ -59,7 +66,7 @@ GEM thread_safe (0.3.6) tzinfo (1.2.5) thread_safe (~> 0.1) - xcodeproj (1.9.0) + xcodeproj (1.13.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -70,7 +77,7 @@ PLATFORMS ruby DEPENDENCIES - cocoapods (~> 1.7.0)! + cocoapods (~> 1.8.0)! BUNDLED WITH 1.17.3 diff --git a/Package.swift b/Package.swift index 824d7206..38de3df9 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,5 @@ let package = Package( name: "Valet", dependencies: []), ], - swiftLanguageVersions: [.v4, .v4_2, .v5] + swiftLanguageVersions: [.v5] ) -let version = Version(3, 2, 8) diff --git a/README.md b/README.md index dfccc15d..221d1e99 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Install with [Swift Package Manager](https://github.com/apple/swift-package-mana ```swift dependencies: [ - .package(url: "https://github.com/Square/Valet", from: "3.0.0"), + .package(url: "https://github.com/Square/Valet", from: "4.0.0"), ], ``` @@ -91,39 +91,79 @@ This `myValet` instance can be used to store and retrieve data securely on this #### Choosing the Best Identifier -The identifier you choose for your Valet is used to create a sandbox for the data your Valet writes to the keychain. Two Valets of the same type created via the same initializer, accessibility value, and identifier will be able to read and write the same key:value pairs; Valets with different identifiers each have their own sandbox. Choose an identifier that describes the kind of data your Valet will protect. You do not need to include your application name or bundleIdentifier in your Valet’s identifier. +The identifier you choose for your Valet is used to create a sandbox for the data your Valet writes to the keychain. Two Valets of the same type created via the same initializer, accessibility value, and identifier will be able to read and write the same key:value pairs; Valets with different identifiers each have their own sandbox. Choose an identifier that describes the kind of data your Valet will protect. You do not need to include your application name or bundle identifier in your Valet’s identifier. + +#### Choosing a User-friendly Identifier on macOS + +```swift +let myValet = Valet.valet(withExplicitlySet: Identifier(nonEmpty: "Druidia")!, accessibility: .whenUnlocked) +``` + +```objc +VALValet *const myValet = [VALValet valetWithExplicitlySetIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked]; +``` + +Mac apps signed with a developer ID may see their Valet’s identifier [shown to their users](https://github.com/square/Valet/issues/140). ⚠️⚠️ While it is possible to explicitly set a user-friendly identifier, note that doing so bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs ⚠️⚠️. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. #### Choosing the Best Accessibility Value The Accessibility enum is used to determine when your secrets can be accessed. It’s a good idea to use the strictest accessibility possible that will allow your app to function. For example, if your app does not run in the background you will want to ensure the secrets can only be read when the phone is unlocked by using `.whenUnlocked` or `.whenUnlockedThisDeviceOnly`. +#### Changing an Accessibility Value After Persisting Data + +```swift +let myOldValet = Valet.valet(withExplicitlySet: Identifier(nonEmpty: "Druidia")!, accessibility: .whenUnlocked) +let myNewValet = Valet.valet(withExplicitlySet: Identifier(nonEmpty: "Druidia")!, accessibility: .afterFirstUnlock) +try? myNewValet.migrateObjects(from: myOldValet, removeOnCompletion: true) +``` + +```objc +VALValet *const myOldValet = [VALValet valetWithExplicitlySetIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked]; +VALValet *const myNewValet = [VALValet valetWithExplicitlySetIdentifier:@"Druidia" accessibility:VALAccessibilityAfterFirstUnlock]; +[myNewValet migrateObjectsFrom:myOldValet removeOnCompletion:true error:nil]; +``` + +The Valet type, identifier, accessibility value, and initializer chosen to create a Valet are combined to create a sandbox within the keychain. This behavior ensures that different Valets can not read or write one another's key:value pairs. If you change a Valet's accessibility after persisting key:value pairs, you must migrate the key:value pairs from the Valet with the no-longer-desired accessibility to the Valet with the desired accessibility to avoid data loss. + ### Reading and Writing ```swift let username = "Skroob" -myValet.set(string: "12345", forKey: username) +try? myValet.setString("12345", forKey: username) let myLuggageCombination = myValet.string(forKey: username) ``` ```objc NSString *const username = @"Skroob"; -[myValet setString:@"12345" forKey:username]; -NSString *const myLuggageCombination = [myValet stringForKey:username]; +[myValet setString:@"12345" forKey:username error:nil]; +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 Using a Keychain Sharing Entitlement + +```swift +let mySharedValet = Valet.sharedGroupValet(with: SharedGroupIdentifier(appIDPrefix: "AppID12345", nonEmptyGroup: "Druidia")!, accessibility: .whenUnlocked) +``` + +```objc +VALValet *const mySharedValet = [VALValet sharedGroupValetWithAppIDPrefix:@"AppID12345" sharedGroupIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked]; ``` -In addition to allowing the storage of strings, Valet allows the storage of `Data` objects via `set(object: Data, forKey key: Key)` and `-objectForKey:`. 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`. +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 +### Sharing Secrets Among Multiple Applications Using an App Groups Entitlement ```swift -let mySharedValet = Valet.sharedAccessGroupValet(with: Identifier(nonEmpty: "Druidia")!, accessibility: .whenUnlocked) +let mySharedValet = Valet.sharedGroupValet(with: SharedGroupIdentifier(groupPrefix: "group", nonEmptyGroup: "Druidia")!, accessibility: .whenUnlocked) ``` ```objc -VALValet *const mySharedValet = [VALValet valetWithSharedAccessGroupIdentifier:@"Druidia" accessibility:VALAccessibilityWhenUnlocked]; +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 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. +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 @@ -190,26 +230,34 @@ Valet guarantees it will never fail to write to or read from the keychain unless ## Requirements -* Xcode 9.0 or later. Earlier versions of Xcode require [Valet version 2.4.2](https://github.com/square/Valet/releases/tag/2.4.2). +* Xcode 11.0 or later. Xcode 10 and Xcode 9 require [Valet version 3.2.8](https://github.com/square/Valet/releases/tag/3.2.8). Earlier versions of Xcode require [Valet version 2.4.2](https://github.com/square/Valet/releases/tag/2.4.2). * iOS 9 or later. * tvOS 9 or later. * watchOS 2 or later. * macOS 10.11 or later. -### Migrating from Valet 2.* +## Migrating from prior Valet versions + +The good news: most Valet configurations do _not_ have to migrate keychain data when upgrading from an older version of Valet. All Valet objects are backwards compatible with their counterparts from prior versions. We have exhaustive unit tests to prove it (search for `test_backwardsCompatibility`). Valets that have had their configurations deprecated by Apple will need to migrate stored data. + +The bad news: there are multiple source-breaking API changes from prior versions. -First the good news: you will _not_ have to migrate your keychain data when upgrading from Valet 2.* to Valet 3.*. All Valet objects are backwards compatible with their Valet 2 counterparts. We have exhaustive unit tests to prove it (search for `test_backwardsCompatibility`). +Both guides below explain the changes required to upgrade to Valet 4. -Now the bad news: the Swift Valet API has slight differences from the Objective-C Valet API. You may have noticed a few of the differences in the sample code above, but here’s a rundown of the changes that may affect you. +### Migrating from Valet 2 1. Initializers have changed in both Swift and Objective-C - both languages use class methods now, which felt more semantically honest (a lot of the time you’re not instantiating a new Valet, you’re re-accessing one you’ve already created). [See example usage above](#basic-initialization). -2. `VALSynchronizableValet` (which allowed keychains to be synced to iCloud) has been replaced by a `Valet.iCloudValet(with:accessibility:)` (or `+[VALValet iCloudValetWithIdentifier:accessibility:]` in Objective-C). [See examples above](#sharing-secrets-across-devices-with-icloud). -3. `setObject(_:forKey:)` has become `set(object:forKey:)` in Swift. The Objective-C API `-setObject:forKey:` remains the same. -4. `setString(_:forKey:)` has become `set(string:forKey:)` in Swift. The Objective-C API `-setString:forKey:` remains the same. -5. `SecureEnclaveValet` and `SinglePromptSecureEnclaveValet` data retrieval methods now return a single enum [SecureEnclave.Result](Sources/SecureEnclave.swift#L28) rather than using an `inout` boolean to signal whether a user cancelled. The Objective-C API remains the same. -6. `migrateObjects(matching:)` and `migrateObjects(from:)` now both return a nonnull [MigrationResult](Sources/MigrationResult.swift#L24). -7. `VALAccessControl` has been renamed to `SecureEnclaveAccessControl` (`VALSecureEnclaveAccessControl` in Objective-C). This enum no longer references `TouchID`; instead it refers to unlocking with `biometric` due to the introduction of Face ID. -8. `Valet`, `SecureEnclaveValet`, and `SinglePromptSecureEnclaveValet` are no longer in the same inheritance tree. All three now inherit directly from `NSObject` and use composition to share code. If you were relying on the subclass hierarchy before, 1) that might be a code smell 2) consider declaring a protocol for the shared behavior you were expecting to make your migration to Valet 3 easier. +1. `VALSynchronizableValet` (which allowed keychains to be synced to iCloud) has been replaced by a `Valet.iCloudValet(with:accessibility:)` (or `+[VALValet iCloudValetWithIdentifier:accessibility:]` in Objective-C). [See examples above](#sharing-secrets-across-devices-with-icloud). +1. `VALAccessControl` has been renamed to `SecureEnclaveAccessControl` (`VALSecureEnclaveAccessControl` in Objective-C). This enum no longer references `TouchID`; instead it refers to unlocking with `biometric` due to the introduction of Face ID. +1. `Valet`, `SecureEnclaveValet`, and `SinglePromptSecureEnclaveValet` are no longer in the same inheritance tree. All three now inherit directly from `NSObject` and use composition to share code. If you were relying on the subclass hierarchy before, 1) that might be a code smell 2) consider declaring a protocol for the shared behavior you were expecting to make your migration to Valet 3 easier. + +You'll also need to continue reading through the [migration from Valet 3](#migration-from-valet-3) section below. + +### Migrating from Valet 3 + +1. The accessibility values `always` and `alwaysThisDeviceOnly` have been removed from Valet, because Apple has deprecated their counterparts (see the documentation for [kSecAttrAccessibleAlways](https://developer.apple.com/documentation/security/ksecattraccessiblealways) and [kSecAttrAccessibleAlwaysThisDeviceOnly](https://developer.apple.com/documentation/security/ksecattraccessiblealwaysthisdeviceonly)). To migrate values stored with `always` accessibility, use the method `migrateObjectsFromAlwaysAccessibleValet(removeOnCompletion:)` on a Valet with your new preferred accessibility. To migrate values stored with `alwaysThisDeviceOnly` accessibility, use the method `migrateObjectsFromAlwaysAccessibleThisDeviceOnlyValet(removeOnCompletion:)` on a Valet with your new preferred accessibility. +1. Most APIs that returned optionals or `Bool` values have been migrated to returning a nonoptional and throwing if an error is encountered. Ignoring the error that can be thrown by each API will keep your code flow behaving the same as it did before. Walking through one example: in Swift, `let secret: String? = myValet.string(forKey: myKey)` becomes `let secret: String? = try? myValet.string(forKey: myKey)`. In Objective-C, `NSString *const secret = [myValet stringForKey:myKey];` becomes `NSString *const secret = [myValet stringForKey:myKey error:nil];`. If you're interested in the reason data wasn't returned, use a [do-catch](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html#ID541) statement in Swift, or pass in an `NSError` to each API call and inspect the output in Objective-C. Each method clearly documents the `Error` type it can `throw`. [See examples above](#reading-and-writing). +1. The class method used to create a Valet that can share secrets between applications using keychain shared access groups has changed. In order to prevent the incorrect detection of the App ID prefix [in rare circumstances](https://github.com/square/Valet/pull/218), the App ID prefix must now be explicitly passed into these methods. [See examples above](#sharing-secrets-across-devices-with-icloud). ## Contributing diff --git a/Scripts/build.swift b/Scripts/build.swift index 98f8a295..3e233420 100755 --- a/Scripts/build.swift +++ b/Scripts/build.swift @@ -21,28 +21,36 @@ enum TaskError: Error { } enum Platform: String, CustomStringConvertible { + case iOS_10 case iOS_11 case iOS_12 case iOS_13 + case tvOS_10 case tvOS_11 case tvOS_12 case tvOS_13 + case macOS_10_12 case macOS_10_13 case macOS_10_14 case macOS_10_15 + case watchOS_3 case watchOS_4 case watchOS_5 case watchOS_6 var destination: String { switch self { + case .iOS_10: + return "platform=iOS Simulator,OS=10.3.1,name=iPad Pro (12.9 inch)" case .iOS_11: - return "platform=iOS Simulator,OS=11.0,name=iPad Pro (12.9-inch) (2nd generation)" + return "platform=iOS Simulator,OS=11.0.1,name=iPad Pro (12.9-inch) (2nd generation)" case .iOS_12: return "platform=iOS Simulator,OS=12.2,name=iPad Pro (12.9-inch) (3rd generation)" case .iOS_13: return "platform=iOS Simulator,OS=13.0,name=iPad Pro (12.9-inch) (3rd generation)" + case .tvOS_10: + return "platform=tvOS Simulator,OS=10.2,name=Apple TV 1080p" case .tvOS_11: return "platform=tvOS Simulator,OS=11.0,name=Apple TV" case .tvOS_12: @@ -50,11 +58,14 @@ enum Platform: String, CustomStringConvertible { case .tvOS_13: return "platform=tvOS Simulator,OS=13.0,name=Apple TV" - case .macOS_10_13, + case .macOS_10_12, + .macOS_10_13, .macOS_10_14, .macOS_10_15: return "platform=OS X" + case .watchOS_3: + return "OS=3.2,name=Apple Watch Series 2 - 42mm" case .watchOS_4: return "OS=4.0,name=Apple Watch Series 2 - 42mm" case .watchOS_5: @@ -66,16 +77,20 @@ enum Platform: String, CustomStringConvertible { var sdk: String { switch self { - case .iOS_11, + case .iOS_10, + .iOS_11, .iOS_12, .iOS_13: return "iphonesimulator" - case .tvOS_11, + case .tvOS_10, + .tvOS_11, .tvOS_12, .tvOS_13: return "appletvsimulator" + case .macOS_10_12: + return "macosx10.12" case .macOS_10_13: return "macosx10.13" case .macOS_10_14: @@ -83,7 +98,8 @@ enum Platform: String, CustomStringConvertible { case .macOS_10_15: return "macosx10.15" - case .watchOS_4, + case .watchOS_3, + .watchOS_4, .watchOS_5, .watchOS_6: return "watchsimulator" @@ -92,18 +108,22 @@ enum Platform: String, CustomStringConvertible { var shouldTest: Bool { switch self { - case .iOS_11, + case .iOS_10, + .iOS_11, .iOS_12, .iOS_13, + .tvOS_10, .tvOS_11, .tvOS_12, .tvOS_13, + .macOS_10_12, .macOS_10_13, .macOS_10_14, .macOS_10_15: return true - case .watchOS_4, + case .watchOS_3, + .watchOS_4, .watchOS_5, .watchOS_6: // watchOS does not support unit testing (yet?). @@ -112,27 +132,31 @@ enum Platform: String, CustomStringConvertible { } var derivedDataPath: String { - return ".build/derivedData/" + description + ".build/derivedData/" + description } var scheme: String { switch self { - case .iOS_11, + case .iOS_10, + .iOS_11, .iOS_12, .iOS_13: return "Valet iOS" - case .tvOS_11, + case .tvOS_10, + .tvOS_11, .tvOS_12, .tvOS_13: return "Valet tvOS" - case .macOS_10_13, + case .macOS_10_12, + .macOS_10_13, .macOS_10_14, .macOS_10_15: return "Valet Mac" - case .watchOS_4, + case .watchOS_3, + .watchOS_4, .watchOS_5, .watchOS_6: return "Valet watchOS" @@ -140,7 +164,7 @@ enum Platform: String, CustomStringConvertible { } var description: String { - return rawValue + rawValue } } @@ -149,7 +173,7 @@ enum Task: String, CustomStringConvertible { case xcode var description: String { - return rawValue + rawValue } var project: String { diff --git a/Sources/Valet/Accessibility.swift b/Sources/Valet/Accessibility.swift index a413acd0..adb0f091 100644 --- a/Sources/Valet/Accessibility.swift +++ b/Sources/Valet/Accessibility.swift @@ -22,23 +22,19 @@ import Foundation @objc(VALAccessibility) -public enum Accessibility: Int, CustomStringConvertible, Equatable { +public enum Accessibility: Int, CaseIterable, CustomStringConvertible, Equatable { /// Valet data can only be accessed while the device is unlocked. This attribute is recommended for data that only needs to be accessible while the application is in the foreground. Valet data with this attribute will migrate to a new device when using encrypted backups. case whenUnlocked = 1 /// Valet data can only be accessed once the device has been unlocked after a restart. This attribute is recommended for data that needs to be accessible by background applications. Valet data with this attribute will migrate to a new device when using encrypted backups. - case afterFirstUnlock - /// Valet data can always be accessed regardless of the lock state of the device. This attribute is not recommended. Valet data with this attribute will migrate to a new device when using encrypted backups. - case always - + case afterFirstUnlock = 2 + /// Valet data can only be accessed while the device is unlocked. This attribute is recommended for items that only need to be accessible while the application is in the foreground. Valet data with this attribute will never migrate to a new device, so these items will be missing after a backup is restored to a new device. No items can be stored in this class on devices without a passcode. Disabling the device passcode will cause all items in this class to be deleted. - case whenPasscodeSetThisDeviceOnly + case whenPasscodeSetThisDeviceOnly = 4 /// Valet data can only be accessed while the device is unlocked. This is recommended for data that only needs to be accessible while the application is in the foreground. Valet data with this attribute will never migrate to a new device, so these items will be missing after a backup is restored to a new device. - case whenUnlockedThisDeviceOnly + case whenUnlockedThisDeviceOnly = 5 /// Valet data can only be accessed once the device has been unlocked after a restart. This is recommended for items that need to be accessible by background applications. Valet data with this attribute will never migrate to a new device, so these items will be missing after a backup is restored to a new device. - case afterFirstUnlockThisDeviceOnly - /// Valet data can always be accessed regardless of the lock state of the device. This option is not recommended. Valet data with this attribute will never migrate to a new device, so these items will be missing after a backup is restored to a new device. - case alwaysThisDeviceOnly - + case afterFirstUnlockThisDeviceOnly = 6 + // MARK: CustomStringConvertible public var description: String { @@ -47,10 +43,6 @@ public enum Accessibility: Int, CustomStringConvertible, Equatable { return "AccessibleAfterFirstUnlock" case .afterFirstUnlockThisDeviceOnly: return "AccessibleAfterFirstUnlockThisDeviceOnly" - case .always: - return "AccessibleAlways" - case .alwaysThisDeviceOnly: - return "AccessibleAlwaysThisDeviceOnly" case .whenPasscodeSetThisDeviceOnly: return "AccessibleWhenPasscodeSetThisDeviceOnly" case .whenUnlocked: @@ -70,10 +62,6 @@ public enum Accessibility: Int, CustomStringConvertible, Equatable { accessibilityAttribute = kSecAttrAccessibleAfterFirstUnlock case .afterFirstUnlockThisDeviceOnly: accessibilityAttribute = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly - case .always: - accessibilityAttribute = kSecAttrAccessibleAlways - case .alwaysThisDeviceOnly: - accessibilityAttribute = kSecAttrAccessibleAlwaysThisDeviceOnly case .whenPasscodeSetThisDeviceOnly: accessibilityAttribute = kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly case .whenUnlocked: @@ -84,18 +72,5 @@ public enum Accessibility: Int, CustomStringConvertible, Equatable { return accessibilityAttribute as String } - - // MARK: Internal - - internal static func allValues() -> [Accessibility] { - return [ - .whenUnlocked, - .afterFirstUnlock, - .always, - .whenPasscodeSetThisDeviceOnly, - .whenUnlockedThisDeviceOnly, - .afterFirstUnlockThisDeviceOnly, - .alwaysThisDeviceOnly - ] - } + } diff --git a/Sources/Valet/CloudAccessibility.swift b/Sources/Valet/CloudAccessibility.swift index acf126bc..5bc13c72 100644 --- a/Sources/Valet/CloudAccessibility.swift +++ b/Sources/Valet/CloudAccessibility.swift @@ -22,18 +22,16 @@ import Foundation @objc(VALCloudAccessibility) -public enum CloudAccessibility: Int, CustomStringConvertible, Equatable { +public enum CloudAccessibility: Int, CaseIterable, CustomStringConvertible, Equatable { /// Valet data can only be accessed while the device is unlocked. This attribute is recommended for data that only needs to be accessible while the application is in the foreground. Valet data with this attribute will migrate to a new device when using encrypted backups. case whenUnlocked = 1 /// Valet data can only be accessed once the device has been unlocked after a restart. This attribute is recommended for data that needs to be accessible by background applications. Valet data with this attribute will migrate to a new device when using encrypted backups. - case afterFirstUnlock - /// Valet data can always be accessed regardless of the lock state of the device. This attribute is not recommended. Valet data with this attribute will migrate to a new device when using encrypted backups. - case always - + case afterFirstUnlock = 2 + // MARK: CustomStringConvertible public var description: String { - return accessibility.description + accessibility.description } // MARK: Public Properties @@ -44,22 +42,11 @@ public enum CloudAccessibility: Int, CustomStringConvertible, Equatable { return .whenUnlocked case .afterFirstUnlock: return .afterFirstUnlock - case .always: - return .always } } public var secAccessibilityAttribute: String { - return accessibility.secAccessibilityAttribute - } - - // MARK: Internal - - internal static func allValues() -> [CloudAccessibility] { - return [ - .whenUnlocked, - .afterFirstUnlock, - .always - ] + accessibility.secAccessibilityAttribute } + } diff --git a/Sources/Valet/ErrorHandler.swift b/Sources/Valet/ErrorHandler.swift deleted file mode 100644 index 5c6c7fc8..00000000 --- a/Sources/Valet/ErrorHandler.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// ErrorHandler.swift -// Valet -// -// Created by Dan Federman and Eric Muller on 9/16/17. -// Copyright © 2017 Square, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -//    http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - - -public final class ErrorHandler { - - // MARK: Public Static Properties - - public static var customAssertBody: ((_ condition: Bool, _ message: String, _ file: StaticString, _ line: UInt) -> Void)? = nil - - // MARK: Internal Static Properties - - internal static let defaultAssertBody: (_ condition: Bool, _ message: String, _ file: StaticString, _ line: UInt) -> Void = { condition, message, file, line in - guard !condition else { - return - } - - Swift.assertionFailure(message, file: file, line: line) - } - - // MARK: Public Static Methods - - public static func assert(_ condition: Bool, _ message: String, file: StaticString = #file, line: UInt = #line) -> Void { - (ErrorHandler.customAssertBody ?? ErrorHandler.defaultAssertBody)(condition, message, file, line) - } - - public static func assertionFailure(_ message: String, file: StaticString = #file, line: UInt = #line) -> Void { - assert(false, message, file: file, line: line) - } -} diff --git a/Sources/Valet/Internal/Keychain.swift b/Sources/Valet/Internal/Keychain.swift index 3e221755..1ba9cfb1 100644 --- a/Sources/Valet/Internal/Keychain.swift +++ b/Sources/Valet/Internal/Keychain.swift @@ -32,11 +32,11 @@ internal final class Keychain { internal static func canAccess(attributes: [String : AnyHashable]) -> Bool { func isCanaryValueInKeychain() -> Bool { - if case let .success(retrievedCanaryValue) = string(forKey: canaryKey, options: attributes), - retrievedCanaryValue == canaryValue { - return true + do { + let retrievedCanaryValue = try string(forKey: canaryKey, options: attributes) + return retrievedCanaryValue == canaryValue - } else { + } catch { return false } } @@ -48,7 +48,7 @@ internal final class Keychain { var secItemQuery = attributes secItemQuery[kSecAttrAccount as String] = canaryKey secItemQuery[kSecValueData as String] = Data(canaryValue.utf8) - _ = SecItem.add(attributes: secItemQuery) + try? SecItem.add(attributes: secItemQuery) return isCanaryValueInKeychain() } @@ -56,23 +56,18 @@ internal final class Keychain { // MARK: Getters - internal static func string(forKey key: String, options: [String : AnyHashable]) -> SecItem.DataResult { - switch object(forKey: key, options: options) { - case let .success(data): - if let string = String(data: data, encoding: .utf8) { - return SecItem.DataResult.success(string) - } else { - return SecItem.DataResult.error(errSecItemNotFound) - } - case let .error(status): - return SecItem.DataResult.error(status) + internal static func string(forKey key: String, options: [String : AnyHashable]) throws -> String { + let data = try object(forKey: key, options: options) + if let string = String(data: data, encoding: .utf8) { + return string + } else { + throw KeychainError.itemNotFound } } - internal static func object(forKey key: String, options: [String : AnyHashable]) -> SecItem.DataResult { + internal static func object(forKey key: String, options: [String : AnyHashable]) throws -> Data { guard !key.isEmpty else { - ErrorHandler.assertionFailure("Can not set a value with an empty key.") - return SecItem.DataResult.error(errSecParam) + throw KeychainError.emptyKey } var secItemQuery = options @@ -80,184 +75,145 @@ internal final class Keychain { secItemQuery[kSecMatchLimit as String] = kSecMatchLimitOne secItemQuery[kSecReturnData as String] = true - return SecItem.copy(matching: secItemQuery) + return try SecItem.copy(matching: secItemQuery) } // MARK: Setters - internal static func set(string: String, forKey key: String, options: [String: AnyHashable]) -> SecItem.Result { + internal static func setString(_ string: String, forKey key: String, options: [String: AnyHashable]) throws { let data = Data(string.utf8) - guard !data.isEmpty else { - ErrorHandler.assertionFailure("Can not set an empty value.") - return .error(errSecParam) - } - - return set(object: data, forKey: key, options: options) + try setObject(data, forKey: key, options: options) } - internal static func set(object: Data, forKey key: String, options: [String: AnyHashable]) -> SecItem.Result { + internal static func setObject(_ object: Data, forKey key: String, options: [String: AnyHashable]) throws { guard !key.isEmpty else { - ErrorHandler.assertionFailure("Can not set a value with an empty key.") - return .error(errSecParam) + throw KeychainError.emptyKey } guard !object.isEmpty else { - ErrorHandler.assertionFailure("Can not set an empty value.") - return .error(errSecParam) + throw KeychainError.emptyValue } var secItemQuery = options secItemQuery[kSecAttrAccount as String] = key #if os(macOS) - // Never update an existing keychain item on OS X, since the existing item could have unauthorized apps in the Access Control List. Fixes zero-day Keychain vuln found here: https://drive.google.com/file/d/0BxxXk1d3yyuZOFlsdkNMSGswSGs/view - _ = SecItem.deleteItems(matching: secItemQuery) - secItemQuery[kSecValueData as String] = object - return SecItem.add(attributes: secItemQuery) + // Never update an existing keychain item on OS X, since the existing item could have unauthorized apps in the Access Control List. Fixes zero-day Keychain vuln found here: https://drive.google.com/file/d/0BxxXk1d3yyuZOFlsdkNMSGswSGs/view + try SecItem.deleteItems(matching: secItemQuery) + secItemQuery[kSecValueData as String] = object + try SecItem.add(attributes: secItemQuery) #else - - if case .success = containsObject(forKey: key, options: options) { - return SecItem.update(attributes: [kSecValueData as String: object], forItemsMatching: secItemQuery) - } else { - secItemQuery[kSecValueData as String] = object - return SecItem.add(attributes: secItemQuery) - } + if performCopy(forKey: key, options: options) == errSecSuccess { + try SecItem.update(attributes: [kSecValueData as String: object], forItemsMatching: secItemQuery) + } else { + secItemQuery[kSecValueData as String] = object + try SecItem.add(attributes: secItemQuery) + } #endif } // MARK: Removal - internal static func removeObject(forKey key: String, options: [String : AnyHashable]) -> SecItem.Result { + internal static func removeObject(forKey key: String, options: [String : AnyHashable]) throws { guard !key.isEmpty else { - ErrorHandler.assertionFailure("Can not set a value with an empty key.") - return .error(errSecParam) + throw KeychainError.emptyKey } var secItemQuery = options secItemQuery[kSecAttrAccount as String] = key - switch SecItem.deleteItems(matching: secItemQuery) { - case .success: - return .success - - case let .error(status): - switch status { - case errSecInteractionNotAllowed, errSecMissingEntitlement: - return .error(status) - - default: - // We succeeded as long as we can confirm that the item is not in the keychain. - return .success - } - } + try SecItem.deleteItems(matching: secItemQuery) } - internal static func removeAllObjects(matching options: [String : AnyHashable]) -> SecItem.Result { - switch SecItem.deleteItems(matching: options) { - case .success: - return .success - - case let .error(status): - switch status { - case errSecInteractionNotAllowed, errSecMissingEntitlement: - return .error(status) - - default: - // We succeeded as long as we can confirm that the item is not in the keychain. - return .success - } - } + internal static func removeAllObjects(matching options: [String : AnyHashable]) throws { + try SecItem.deleteItems(matching: options) } // MARK: Contains - internal static func containsObject(forKey key: String, options: [String : AnyHashable]) -> SecItem.Result { + internal static func performCopy(forKey key: String, options: [String : AnyHashable]) -> OSStatus { guard !key.isEmpty else { - ErrorHandler.assertionFailure("Can not set a value with an empty key.") - return .error(errSecParam) + return errSecParam } var secItemQuery = options secItemQuery[kSecAttrAccount as String] = key - switch SecItem.containsObject(matching: secItemQuery) { - case .success: - return .success - - case let .error(status): - return .error(status) - } + return SecItem.performCopy(matching: secItemQuery) } // MARK: AllObjects - internal static func allKeys(options: [String: AnyHashable]) -> SecItem.DataResult> { + internal static func allKeys(options: [String: AnyHashable]) throws -> Set { var secItemQuery = options secItemQuery[kSecMatchLimit as String] = kSecMatchLimitAll secItemQuery[kSecReturnAttributes as String] = true - - let result: SecItem.DataResult = SecItem.copy(matching: secItemQuery) - switch result { - case let .success(collection): - if let singleMatch = collection as? [String : AnyHashable], let singleKey = singleMatch[kSecAttrAccount as String] as? String, singleKey != canaryKey { - return SecItem.DataResult.success(Set([singleKey])) - + + do { + let collection: Any = try SecItem.copy(matching: secItemQuery) + if let singleMatch = collection as? [String: AnyHashable], let singleKey = singleMatch[kSecAttrAccount as String] as? String, singleKey != canaryKey { + return Set([singleKey]) + } else if let multipleMatches = collection as? [[String: AnyHashable]] { - return SecItem.DataResult.success(Set(multipleMatches.compactMap({ attributes in + return Set(multipleMatches.compactMap({ attributes in let key = attributes[kSecAttrAccount as String] as? String return key != canaryKey ? key : nil - }))) + })) } else { - return SecItem.DataResult.success(Set()) + return Set() } - - case let .error(status): - return SecItem.DataResult.error(status) + + } catch KeychainError.itemNotFound { + // Nothing was found. That's fine. + return Set() + } catch { + // This isn't a recoverable error. Throw. + throw error } } // MARK: Migration - internal static func migrateObjects(matching query: [String : AnyHashable], into destinationAttributes: [String : AnyHashable], removeOnCompletion: Bool) -> MigrationResult { - guard query.count > 0 else { - ErrorHandler.assertionFailure("Migration requires secItemQuery to contain values.") - return .invalidQuery + internal static func migrateObjects(matching query: [String : AnyHashable], into destinationAttributes: [String : AnyHashable], removeOnCompletion: Bool) throws { + guard !query.isEmpty else { + // Migration requires secItemQuery to contain values. + throw MigrationError.invalidQuery } guard query[kSecMatchLimit as String] as? String as CFString? != kSecMatchLimitOne else { - ErrorHandler.assertionFailure("Migration requires kSecMatchLimit to be set to kSecMatchLimitAll.") - return .invalidQuery + // Migration requires kSecMatchLimit to be set to kSecMatchLimitAll. + throw MigrationError.invalidQuery } guard query[kSecReturnData as String] as? Bool != true else { - ErrorHandler.assertionFailure("kSecReturnData is not supported in a migration query.") - return .invalidQuery + // kSecReturnData is not supported in a migration query. + throw MigrationError.invalidQuery } guard query[kSecReturnAttributes as String] as? Bool != false else { - ErrorHandler.assertionFailure("Migration requires kSecReturnAttributes to be set to kCFBooleanTrue.") - return .invalidQuery + // Migration requires kSecReturnAttributes to be set to kCFBooleanTrue. + throw MigrationError.invalidQuery } guard query[kSecReturnRef as String] as? Bool != true else { - ErrorHandler.assertionFailure("kSecReturnRef is not supported in a migration query.") - return .invalidQuery + // kSecReturnRef is not supported in a migration query. + throw MigrationError.invalidQuery } guard query[kSecReturnPersistentRef as String] as? Bool != false else { - ErrorHandler.assertionFailure("Migration requires kSecReturnPersistentRef to be set to kCFBooleanTrue.") - return .invalidQuery + // Migration requires kSecReturnPersistentRef to be set to kCFBooleanTrue. + throw MigrationError.invalidQuery } guard query[kSecClass as String] as? String as CFString? == kSecClassGenericPassword else { - ErrorHandler.assertionFailure("Migration requires kSecClass to be set to kSecClassGenericPassword to avoid data loss.") - return .invalidQuery + // Migration requires kSecClass to be set to kSecClassGenericPassword to avoid data loss. + throw MigrationError.invalidQuery } guard query[kSecAttrAccessControl as String] == nil else { - ErrorHandler.assertionFailure("kSecAttrAccessControl is not supported in a migration query. Keychain items can not be migrated en masse from the Secure Enclave.") - return .invalidQuery + // kSecAttrAccessControl is not supported in a migration query. Keychain items can not be migrated en masse from the Secure Enclave. + throw MigrationError.invalidQuery } var secItemQuery = query @@ -267,64 +223,46 @@ internal final class Keychain { secItemQuery[kSecReturnRef as String] = false secItemQuery[kSecReturnPersistentRef as String] = true - let result: SecItem.DataResult = SecItem.copy(matching: secItemQuery) + let collection: Any = try SecItem.copy(matching: secItemQuery) let retrievedItemsToMigrate: [[String: AnyHashable]] - switch result { - case let .success(collection): - if let singleMatch = collection as? [String : AnyHashable] { - retrievedItemsToMigrate = [singleMatch] - - } else if let multipleMatches = collection as? [[String: AnyHashable]] { - retrievedItemsToMigrate = multipleMatches - - } else { - return .dataInQueryResultInvalid - } - - case let .error(status): - switch status { - case errSecItemNotFound: - return .noItemsToMigrateFound - - case errSecParam: - return .invalidQuery - - default: - return .couldNotReadKeychain - } + if let singleMatch = collection as? [String : AnyHashable] { + retrievedItemsToMigrate = [singleMatch] + + } else if let multipleMatches = collection as? [[String: AnyHashable]] { + retrievedItemsToMigrate = multipleMatches + + } else { + throw MigrationError.dataToMigrateInvalid } // Now that we have the persistent refs with attributes, get the data associated with each keychain entry. var retrievedItemsToMigrateWithData = [[String : AnyHashable]]() for retrievedItem in retrievedItemsToMigrate { guard let retrievedPersistentRef = retrievedItem[kSecValuePersistentRef as String] else { - return .couldNotReadKeychain + throw KeychainError.couldNotAccessKeychain + } let retrieveDataQuery: [String : AnyHashable] = [ kSecValuePersistentRef as String : retrievedPersistentRef, kSecReturnData as String : true ] - - let retrievedData: SecItem.DataResult = SecItem.copy(matching: retrieveDataQuery) - switch retrievedData { - case let .success(data): + + do { + let data: Data = try SecItem.copy(matching: retrieveDataQuery) guard !data.isEmpty else { - return .dataInQueryResultInvalid + throw MigrationError.dataToMigrateInvalid } var retrievedItemToMigrateWithData = retrievedItem retrievedItemToMigrateWithData[kSecValueData as String] = data retrievedItemsToMigrateWithData.append(retrievedItemToMigrateWithData) - - case let .error(status): - if status == errSecItemNotFound { - // It is possible for metadata-only items to exist in the keychain that do not have data associated with them. Ignore this entry. - continue - - } else { - return .couldNotReadKeychain - } + } catch KeychainError.itemNotFound { + // It is possible for metadata-only items to exist in the keychain that do not have data associated with them. Ignore this entry. + continue + + } catch { + throw error } } @@ -337,22 +275,22 @@ internal final class Keychain { } guard !key.isEmpty else { - return .keyInQueryResultInvalid + throw MigrationError.keyToMigrateInvalid } guard !keysToMigrate.contains(key) else { - return .duplicateKeyInQueryResult + throw MigrationError.duplicateKeyToMigrate } guard let data = keychainEntry[kSecValueData as String] as? Data, !data.isEmpty else { - return .dataInQueryResultInvalid + throw MigrationError.dataToMigrateInvalid } - - guard case let .error(status) = Keychain.containsObject(forKey: key, options: destinationAttributes), status == errSecItemNotFound else { - return .keyInQueryResultAlreadyExistsInValet + + if Keychain.performCopy(forKey: key, options: destinationAttributes) == errSecItemNotFound { + keysToMigrate.insert(key) + } else { + throw MigrationError.keyToMigrateAlreadyExistsInValet } - - keysToMigrate.insert(key) } // All looks good. Time to actually migrate. @@ -360,41 +298,40 @@ internal final class Keychain { func revertMigration() { // Something has gone wrong. Remove all migrated items. for alreadyMigratedKey in alreadyMigratedKeys { - _ = Keychain.removeObject(forKey: alreadyMigratedKey, options: destinationAttributes) + try? Keychain.removeObject(forKey: alreadyMigratedKey, options: destinationAttributes) } } for keychainEntry in retrievedItemsToMigrateWithData { guard let key = keychainEntry[kSecAttrAccount as String] as? String else { revertMigration() - return .keyInQueryResultInvalid + throw MigrationError.keyToMigrateInvalid } guard let value = keychainEntry[kSecValueData as String] as? Data else { revertMigration() - return .dataInQueryResultInvalid + throw MigrationError.dataToMigrateInvalid } - - switch Keychain.set(object: value, forKey: key, options: destinationAttributes) { - case .success: + + do { + try Keychain.setObject(value, forKey: key, options: destinationAttributes) alreadyMigratedKeys.append(key) - - case .error: + } catch { revertMigration() - return .couldNotWriteToKeychain + throw error } } // Remove data if requested. if removeOnCompletion { - guard Keychain.removeAllObjects(matching: query).didSucceed else { + do { + try Keychain.removeAllObjects(matching: query) + } catch { revertMigration() - return .removalFailed + throw MigrationError.removalFailed } // We're done! } - - return .success } } diff --git a/Sources/Valet/Internal/SecItem.swift b/Sources/Valet/Internal/SecItem.swift index 4cf4db2d..2c0f7aff 100644 --- a/Sources/Valet/Internal/SecItem.swift +++ b/Sources/Valet/Internal/SecItem.swift @@ -21,109 +21,22 @@ import Foundation -internal func execute(in lock: NSLock, block: () -> ReturnType) -> ReturnType { +internal func execute(in lock: NSLock, block: () throws -> ReturnType) rethrows -> ReturnType { lock.lock() defer { lock.unlock() } - return block() + return try block() } internal final class SecItem { - - // MARK: Internal Enum - - internal enum DataResult { - case success(SuccessType) - case error(OSStatus) - - var value: SuccessType? { - switch self { - case let .success(value): - return value - - case .error: - return nil - } - - } - } - - internal enum Result { - case success - case error(OSStatus) - - var didSucceed: Bool { - switch self { - case .success: - return true - - case .error: - return false - } - - } - } - - // MARK: Internal Class Properties - - /// 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 - internal static var sharedAccessGroupPrefix: String { - var query: [CFString : Any] = [ - kSecClass : kSecClassGenericPassword, - kSecAttrAccount : "SharedAccessGroupAlwaysAccessiblePrefixPlaceholder", - kSecReturnAttributes : true, - kSecAttrAccessible : Accessibility.alwaysThisDeviceOnly.secAccessibilityAttribute, - ] - - if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) { - // Add kSecUseDataProtectionKeychain to the query to ensure we can retrieve the shared access group prefix. - #if swift(>=5.1) - query[kSecUseDataProtectionKeychain] = true - #else - query["nleg" as CFString] = true // kSecUseDataProtectionKeychain for Xcode 9 and Xcode 10 compatibility. - #endif - } - - secItemLock.lock() - defer { - secItemLock.unlock() - } - - var result: AnyObject? = nil - var status = SecItemCopyMatching(query as CFDictionary, &result) - - if status == errSecItemNotFound { - status = SecItemAdd(query as CFDictionary, &result) - } - - guard status == errSecSuccess, let queryResult = result as? [CFString : AnyHashable], let accessGroup = queryResult[kSecAttrAccessGroup] as? String else { - ErrorHandler.assertionFailure("Could not find shared access group prefix.") - // We should always be able to access the shared access group prefix because the accessibility of the above keychain data is set to `always`. - // In other words, we should never hit this code. This code is here as a failsafe to prevent a crash in a scenario where the keychain is entirely hosed. - // Consumers should always check `canAccessKeychain()` after creating a Valet and before using it. Doing so will catch this error. - return "INVALID_SHARED_ACCESS_GROUP_PREFIX" - } - let components = accessGroup.components(separatedBy: ".") - if let bundleSeedIdentifier = components.first, !bundleSeedIdentifier.isEmpty { - return bundleSeedIdentifier - - } else { - // We should always be able to access the shared access group prefix because the accessibility of the above keychain data is set to `always`. - // In other words, we should never hit this code. This code is here as a failsafe to prevent a crash in a scenario where the keychain is entirely hosed. - // Consumers should always check `canAccessKeychain()` after creating a Valet and before using it. Doing so will catch this error. - return "INVALID_SHARED_ACCESS_GROUP_PREFIX" - } - } - // MARK: Internal Class Methods - internal static func copy(matching query: [String : AnyHashable]) -> DataResult { - guard query.count > 0 else { - ErrorHandler.assertionFailure("Must provide a query with at least one item") - return .error(errSecParam) + internal static func copy(matching query: [String : AnyHashable]) throws -> DesiredType { + if query.isEmpty { + assertionFailure("Must provide a query with at least one item") } var status = errSecNotAvailable @@ -134,46 +47,36 @@ internal final class SecItem { if status == errSecSuccess { if let result = result as? DesiredType { - return .success(result) + return result } else { // The query failed to pull out a value object of the desired type, but did find metadata matching this query. // This can happen because either the query didn't ask for return data via [kSecReturnData : true], or because a metadata-only item existed in the keychain. - return .error(errSecItemNotFound) + throw KeychainError.itemNotFound } } else { - ErrorHandler.assert(status != errSecMissingEntitlement, "A 'Missing Entitlements' error occurred. This is likely due to an Apple Keychain bug. As a workaround try running on a device that is not attached to a debugger.\n\nMore information: https://forums.developer.apple.com/thread/4743") - - return .error(status) + throw KeychainError(status: status) } } - internal static func containsObject(matching query: [String : AnyHashable]) -> Result { - guard query.count > 0 else { - ErrorHandler.assertionFailure("Must provide a query with at least one item") - return .error(errSecParam) + internal static func performCopy(matching query: [String : AnyHashable]) -> OSStatus { + guard !query.isEmpty else { + // Must provide a query with at least one item + return errSecParam } var status = errSecNotAvailable execute(in: secItemLock) { status = SecItemCopyMatching(query as CFDictionary, nil) } - - if status == errSecSuccess { - return .success - - } else { - ErrorHandler.assert(status != errSecMissingEntitlement, "A 'Missing Entitlements' error occurred. This is likely due to an Apple Keychain bug. As a workaround try running on a device that is not attached to a debugger.\n\nMore information: https://forums.developer.apple.com/thread/4743") - - return .error(status) - } + + return status } - internal static func add(attributes: [String : AnyHashable]) -> Result { - guard attributes.count > 0 else { - ErrorHandler.assertionFailure("Must provide attributes with at least one item") - return .error(errSecParam) + internal static func add(attributes: [String : AnyHashable]) throws { + if attributes.isEmpty { + assertionFailure("Must provide attributes with at least one item") } var status = errSecNotAvailable @@ -182,25 +85,22 @@ internal final class SecItem { status = SecItemAdd(attributes as CFDictionary, &result) } - if status == errSecSuccess { - return .success - - } else { - ErrorHandler.assert(status != errSecMissingEntitlement, "A 'Missing Entitlements' error occurred. This is likely due to an Apple Keychain bug. As a workaround try running on a device that is not attached to a debugger.\n\nMore information: https://forums.developer.apple.com/thread/4743") - - return .error(status) + switch status { + case errSecSuccess: + // We're done! + break + default: + throw KeychainError(status: status) } } - internal static func update(attributes: [String : AnyHashable], forItemsMatching query: [String : AnyHashable]) -> Result { - guard attributes.count > 0 else { - ErrorHandler.assertionFailure("Must provide attributes with at least one item") - return .error(errSecParam) + internal static func update(attributes: [String : AnyHashable], forItemsMatching query: [String : AnyHashable]) throws { + if attributes.isEmpty { + assertionFailure("Must provide attributes with at least one item") } - guard query.count > 0 else { - ErrorHandler.assertionFailure("Must provide a query with at least one item") - return .error(errSecParam) + if query.isEmpty { + assertionFailure("Must provide a query with at least one item") } var status = errSecNotAvailable @@ -208,20 +108,18 @@ internal final class SecItem { status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) } - if status == errSecSuccess { - return .success - - } else { - ErrorHandler.assert(status != errSecMissingEntitlement, "A 'Missing Entitlements' error occurred. This is likely due to an Apple Keychain bug. As a workaround try running on a device that is not attached to a debugger.\n\nMore information: https://forums.developer.apple.com/thread/4743") - - return .error(status) + switch status { + case errSecSuccess: + // We're done! + break + default: + throw KeychainError(status: status) } } - internal static func deleteItems(matching query: [String : AnyHashable]) -> Result { - guard query.count > 0 else { - ErrorHandler.assertionFailure("Must provide a query with at least one item") - return .error(errSecParam) + internal static func deleteItems(matching query: [String : AnyHashable]) throws { + if query.isEmpty { + assertionFailure("Must provide a query with at least one item") } var secItemQuery = query @@ -235,12 +133,23 @@ internal final class SecItem { } if status == errSecSuccess { - return .success + // We're done! } else { - ErrorHandler.assert(status != errSecMissingEntitlement, "A 'Missing Entitlements' error occurred. This is likely due to an Apple Keychain bug. As a workaround try running on a device that is not attached to a debugger.\n\nMore information: https://forums.developer.apple.com/thread/4743") - - return .error(status) + switch KeychainError(status: status) { + case .couldNotAccessKeychain: + throw KeychainError.couldNotAccessKeychain + + case .missingEntitlement: + throw KeychainError.missingEntitlement + + case .emptyKey, + .emptyValue, + .itemNotFound, + .userCancelled: + // We succeeded as long as we can confirm that the item is not in the keychain. + break + } } } diff --git a/Sources/Valet/Internal/Service.swift b/Sources/Valet/Internal/Service.swift index f8d46947..016048d7 100644 --- a/Sources/Valet/Internal/Service.swift +++ b/Sources/Valet/Internal/Service.swift @@ -23,20 +23,39 @@ import Foundation internal enum Service: CustomStringConvertible, Equatable { case standard(Identifier, Configuration) - case sharedAccessGroup(Identifier, Configuration) - + case sharedGroup(SharedGroupIdentifier, Configuration) + + #if os(macOS) + case standardOverride(service: Identifier, Configuration) + case sharedGroupOverride(service: SharedGroupIdentifier, Configuration) + #endif + // MARK: Equatable internal static func ==(lhs: Service, rhs: Service) -> Bool { - return lhs.description == rhs.description + lhs.description == rhs.description } // MARK: CustomStringConvertible internal var description: String { - return secService + secService } - + + // MARK: Internal Static Methods + + internal static func standard(with configuration: Configuration, identifier: Identifier, accessibilityDescription: String) -> String { + "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, explicitlySetIdentifier identifier: Identifier, accessibilityDescription: String) -> String { + "VAL_\(configuration.description)_initWithSharedAccessGroupIdentifier:accessibility:_\(identifier)_\(accessibilityDescription)" + } + // MARK: Internal Methods internal func generateBaseQuery() -> [String : AnyHashable] { @@ -46,11 +65,7 @@ internal enum Service: CustomStringConvertible, Equatable { ] if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) { - #if swift(>=5.1) baseQuery[kSecUseDataProtectionKeychain as String] = true - #else - baseQuery["nleg"] = true // kSecUseDataProtectionKeychain for Xcode 9 and Xcode 10 compatibility. - #endif } let configuration: Configuration @@ -58,10 +73,18 @@ internal enum Service: CustomStringConvertible, Equatable { case let .standard(_, desiredConfiguration): configuration = desiredConfiguration - case let .sharedAccessGroup(identifier, desiredConfiguration): - ErrorHandler.assert(!identifier.description.hasPrefix("\(SecItem.sharedAccessGroupPrefix)."), "Do not add the Bundle Seed ID as a prefix to your identifier. Valet prepends this value for you. Your Valet will not be able to access the keychain with the provided configuration") - baseQuery[kSecAttrAccessGroup as String] = "\(SecItem.sharedAccessGroupPrefix).\(identifier.description)" + case let .sharedGroup(identifier, desiredConfiguration): + baseQuery[kSecAttrAccessGroup as String] = identifier.description + configuration = desiredConfiguration + + #if os(macOS) + case let .standardOverride(_, desiredConfiguration): + configuration = desiredConfiguration + + case let .sharedGroupOverride(identifier, desiredConfiguration): + baseQuery[kSecAttrAccessGroup as String] = identifier.description configuration = desiredConfiguration + #endif } switch configuration { @@ -87,28 +110,37 @@ internal enum Service: CustomStringConvertible, Equatable { var service: String switch self { case let .standard(identifier, configuration): - service = "VAL_\(configuration.description)_initWithIdentifier:accessibility:_\(identifier)_\(configuration.accessibility.description)" - case let .sharedAccessGroup(identifier, configuration): - service = "VAL_\(configuration.description)_initWithSharedAccessGroupIdentifier:accessibility:_\(identifier)_\(configuration.accessibility.description)" + 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) + #if os(macOS) + case let .standardOverride(identifier, _): + service = identifier.description + case let .sharedGroupOverride(identifier, _): + service = identifier.groupIdentifier + #endif } - - let configuration: Configuration + switch self { - case let .standard(_, desiredConfiguration), - let .sharedAccessGroup(_, desiredConfiguration): - configuration = desiredConfiguration - } - - switch configuration { - case .valet, .iCloud: - // Nothing to do here. - break + case let .standard(_, configuration), + let .sharedGroup(_, configuration): + switch configuration { + case .valet, .iCloud: + // Nothing to do here. + break - case let .secureEnclave(accessControl), - let .singlePromptSecureEnclave(accessControl): - service += accessControl.description + case let .secureEnclave(accessControl), + let .singlePromptSecureEnclave(accessControl): + service += accessControl.description + } + + return service + + #if os(macOS) + case .standardOverride, + .sharedGroupOverride: + return service + #endif } - - return service } } diff --git a/Sources/Valet/KeychainError.swift b/Sources/Valet/KeychainError.swift new file mode 100644 index 00000000..2a5c1c15 --- /dev/null +++ b/Sources/Valet/KeychainError.swift @@ -0,0 +1,68 @@ +// +// KeychainError.swift +// Valet +// +// Created by Dan Federman and Eric Muller on 9/16/17. +// Copyright © 2017 Square Inc. +// +// Licensed under the Apache License Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing software +// distributed under the License is distributed on an "AS IS" BASIS +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + + +@objc(VALKeychainError) +public enum KeychainError: Int, CaseIterable, CustomStringConvertible, Error, Equatable { + /// The keychain could not be accessed. + case couldNotAccessKeychain + /// User dismissed the user-presence prompt. + case userCancelled + /// No data was found for the requested key. + case itemNotFound + /// The application does not have the proper entitlements to perform the requested action. + /// This may be due to an Apple Keychain bug. As a workaround try running on a device that is not attached to a debugger. + /// - SeeAlso: https://forums.developer.apple.com/thread/4743 + case missingEntitlement + /// The key provided is empty. + case emptyKey + /// The value provided is empty. + case emptyValue + + init(status: OSStatus) { + switch status { + case errSecItemNotFound: + self = .itemNotFound + case errSecUserCanceled, + errSecAuthFailed: + self = .userCancelled + case errSecMissingEntitlement: + self = .missingEntitlement + default: + self = .couldNotAccessKeychain + } + } + + // MARK: CustomStringConvertible + + public var description: String { + switch self { + case .couldNotAccessKeychain: return "KeychainError.couldNotAccessKeychain" + case .emptyKey: return "KeychainError.emptyKey" + case .emptyValue: return "KeychainError.emptyValue" + case .itemNotFound: return "KeychainError.itemNotFound" + case .missingEntitlement: return "KeychainError.missingEntitlement" + case .userCancelled: return "KeychainError.userCancelled" + } + } + +} diff --git a/Sources/Valet/KeychainQueryConvertible.swift b/Sources/Valet/KeychainQueryConvertible.swift deleted file mode 100644 index c79d5096..00000000 --- a/Sources/Valet/KeychainQueryConvertible.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// KeychainQueryConvertible.swift -// Valet -// -// Created by Dan Federman and Eric Muller on 9/17/17. -// Copyright © 2017 Square, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -//    http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - - -@objc(VALKeychainQueryConvertible) -public protocol KeychainQueryConvertible { - - var keychainQuery: [String : AnyHashable] { get } - -} diff --git a/Sources/Valet/MigrationError.swift b/Sources/Valet/MigrationError.swift new file mode 100644 index 00000000..b912a132 --- /dev/null +++ b/Sources/Valet/MigrationError.swift @@ -0,0 +1,51 @@ +// +// MigrationError.swift +// Valet +// +// Created by Dan Federman and Eric Muller on 9/16/17. +// Copyright © 2017 Square Inc. +// +// Licensed under the Apache License Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing software +// distributed under the License is distributed on an "AS IS" BASIS +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + + +@objc(VALMigrationResult) +public enum MigrationError: Int, CaseIterable, CustomStringConvertible, Error, Equatable { + /// Migration failed because the keychain query was not valid. + case invalidQuery + /// Migration failed because a key staged for migration was invalid. + case keyToMigrateInvalid + /// Migration failed because some data staged for migration was invalid. + case dataToMigrateInvalid + /// Migration failed because two equivalent keys were staged for migration. + case duplicateKeyToMigrate + /// Migration failed because a key staged for migration duplicates a key already managed by Valet. + case keyToMigrateAlreadyExistsInValet + /// Migration failed because removing the migrated data from the keychain failed. + case removalFailed + + // MARK: CustomStringConvertible + + public var description: String { + switch self { + case .invalidQuery: return "MigrationError.invalidQuery" + case .keyToMigrateInvalid: return "MigrationError.keyToMigrateInvalid" + case .dataToMigrateInvalid: return "MigrationError.dataToMigrateInvalid" + case .duplicateKeyToMigrate: return "MigrationError.duplicateKeyToMigrate" + case .keyToMigrateAlreadyExistsInValet: return "MigrationError.keyToMigrateAlreadyExistsInValet" + case .removalFailed: return "MigrationError.removalFailed" + } + } +} diff --git a/Sources/Valet/MigrationResult.swift b/Sources/Valet/MigrationResult.swift deleted file mode 100644 index d52cd9bd..00000000 --- a/Sources/Valet/MigrationResult.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// MigrationResult.swift -// Valet -// -// Created by Dan Federman and Eric Muller on 9/16/17. -// Copyright © 2017 Square Inc. -// -// Licensed under the Apache License Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -//    http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing software -// distributed under the License is distributed on an "AS IS" BASIS -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -@objc(VALMigrationResult) -public enum MigrationResult: Int, Equatable, CustomStringConvertible { - /// Migration succeeded. - case success = 1 - /// Migration failed because the keychain query was not valid. - case invalidQuery - /// Migration failed because no items to migrate were found. - case noItemsToMigrateFound - /// Migration failed because the keychain could not be read. - case couldNotReadKeychain - /// Migration failed because a key in the query result could not be read. - case keyInQueryResultInvalid - /// Migration failed because some data in the query result could not be read. - case dataInQueryResultInvalid - /// Migration failed because two keys with the same value were found in the keychain. - case duplicateKeyInQueryResult - /// Migration failed because a key in the keychain duplicates a key already managed by Valet. - case keyInQueryResultAlreadyExistsInValet - /// Migration failed because writing to the keychain failed. - case couldNotWriteToKeychain - /// Migration failed because removing the migrated data from the keychain failed. - case removalFailed - - // MARK: CustomStringConvertible - - public var description: String { - switch self { - case .success: return "success" - case .invalidQuery: return "invalidQuery" - case .noItemsToMigrateFound: return "noItemsToMigrateFound" - case .couldNotReadKeychain: return "couldNotReadKeychain" - case .keyInQueryResultInvalid: return "keyInQueryResultInvalid" - case .dataInQueryResultInvalid: return "dataInQueryResultInvalid" - case .duplicateKeyInQueryResult: return "duplicateKeyInQueryResult" - case .keyInQueryResultAlreadyExistsInValet: return "keyInQueryResultAlreadyExistsInValet" - case .couldNotWriteToKeychain: return "couldNotWriteToKeychain" - case .removalFailed: return "removalFailed" - } - } -} diff --git a/Sources/Valet/SecureEnclave.swift b/Sources/Valet/SecureEnclave.swift index a75f20ef..d8be2908 100644 --- a/Sources/Valet/SecureEnclave.swift +++ b/Sources/Valet/SecureEnclave.swift @@ -21,137 +21,107 @@ import Foundation -@available(macOS 10.11, *) public final class SecureEnclave { - - // MARK: Result - - public enum Result: Equatable { - /// Data was retrieved from the keychain. - case success(Type) - /// User dismissed the user-presence prompt. - case userCancelled - /// No data was found for the requested key. - case itemNotFound - - // MARK: Initialization - - init(_ dataResult: SecItem.DataResult) { - switch dataResult { - case let .success(value): - self = .success(value) - - case let .error(status): - let userCancelled = (status == errSecUserCanceled || status == errSecAuthFailed) - if userCancelled { - self = .userCancelled - } else { - self = .itemNotFound - } - } - } - - // MARK: Equatable - public static func ==(lhs: Result, rhs: Result) -> Bool { - switch (lhs, rhs) { - case let (.success(lhsResult), .success(rhsResult)): - return lhsResult == rhsResult - case (.userCancelled, .userCancelled): - return true - case (.itemNotFound, .itemNotFound): - return true - case (.success, _), - (.userCancelled, _), - (.itemNotFound, _): - return false - } - } - } - // MARK: Internal Methods - - /// - parameter service: The service of the keychain slice we want to check if we can access. - /// - parameter identifier: A non-empty identifier that scopes the slice of keychain we want to access. - /// - returns: `true` if the keychain is accessible for reading and writing, `false` otherwise. - /// - note: Determined by writing a value to the keychain and then reading it back out. - internal static func canAccessKeychain(with service: Service, identifier: Identifier) -> Bool { + + /// - Parameter service: The service of the keychain slice we want to check if we can access. + /// - Returns: `true` if the keychain is accessible for reading and writing, `false` otherwise. + /// - Note: Determined by writing a value to the keychain and then reading it back out. + internal static func canAccessKeychain(with service: Service) -> Bool { // 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. let noPromptValet: Valet switch service { - case .standard: + #if os(macOS) + case let .standardOverride(identifier, _): noPromptValet = .valet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) - case .sharedAccessGroup: - noPromptValet = .sharedAccessGroupValet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) + #endif + case let .standard(identifier, _): + noPromptValet = .valet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) + #if os(macOS) + case let .sharedGroupOverride(identifier, _): + noPromptValet = .sharedGroupValet(withExplicitlySet: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) + #endif + case let .sharedGroup(identifier, _): + noPromptValet = .sharedGroupValet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) } return noPromptValet.canAccessKeychain() } - - /// - parameter object: A Data value to be inserted into the keychain. - /// - parameter key: A Key that can be used to retrieve the `object` from the keychain. - /// - parameter options: A base query used to scope the calls in the keychain. - /// - returns: `false` if the keychain is not accessible. - @discardableResult - internal static func set(object: Data, forKey key: String, options: [String : AnyHashable]) -> Bool { + + /// - Parameters: + /// - object: A Data value to be inserted into the keychain. + /// - key: A key that can be used to retrieve the `object` from the keychain. + /// - options: A base query used to scope the calls in the keychain. + /// - Throws: An error of type `KeychainError`. + internal static func setObject(_ object: Data, forKey key: String, options: [String : AnyHashable]) throws { // Remove the key before trying to set it. This will prevent us from calling SecItemUpdate on an item stored on the Secure Enclave, which would cause iOS to prompt the user for authentication. - _ = Keychain.removeObject(forKey: key, options: options) + try Keychain.removeObject(forKey: key, options: options) - return Keychain.set(object: object, forKey: key, options: options).didSucceed + try Keychain.setObject(object, forKey: key, options: options) } - - /// - parameter key: A Key used to retrieve the desired object from the keychain. - /// - parameter userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. - /// - parameter options: A base query used to scope the calls in the keychain. - /// - returns: The data currently stored in the keychain for the provided key. Returns `.itemNotFound` if no object exists in the keychain for the specified key, or if the keychain is inaccessible. Returns `.userCancelled` if the user cancels the user-presence prompt. - internal static func object(forKey key: String, withPrompt userPrompt: String, options: [String : AnyHashable]) -> Result { + + /// - Parameters: + /// - key: A key used to retrieve the desired object from the keychain. + /// - userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. + /// - options: A base query used to scope the calls in the keychain. + /// - Returns: The data currently stored in the keychain for the provided key. + /// - Throws: An error of type `KeychainError`. + internal static func object(forKey key: String, withPrompt userPrompt: String, options: [String : AnyHashable]) throws -> Data { var secItemQuery = options if !userPrompt.isEmpty { secItemQuery[kSecUseOperationPrompt as String] = userPrompt } - return Result(Keychain.object(forKey: key, options: secItemQuery)) + return try Keychain.object(forKey: key, options: secItemQuery) } - - /// - parameter key: The key to look up in the keychain. - /// - parameter options: A base query used to scope the calls in the keychain. - /// - returns: `true` if a value has been set for the given key, `false` otherwise. - internal static func containsObject(forKey key: String, options: [String : AnyHashable]) -> Bool { + + /// - Parameters: + /// - key: The key to look up in the keychain. + /// - options: A base query used to scope the calls in the keychain. + /// - Returns: `true` if a value has been set for the given key, `false` otherwise. + /// - Throws: An error of type `KeychainError`. + internal static func containsObject(forKey key: String, options: [String : AnyHashable]) throws -> Bool { var secItemQuery = options secItemQuery[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail - - switch Keychain.containsObject(forKey: key, options: secItemQuery) { - case .success: + + let status = Keychain.performCopy(forKey: key, options: secItemQuery) + switch status { + case errSecSuccess, + errSecInteractionNotAllowed: + // An item exists in the keychain if we could successfully copy the item, or if we got an error telling us we weren't allowed to copy the item since we couldn't prompt the user. return true - - case let .error(status): - let keyAlreadyInKeychain = (status == errSecInteractionNotAllowed || status == errSecSuccess) - return keyAlreadyInKeychain + case errSecItemNotFound: + return false + default: + throw KeychainError(status: status) } } - - /// - parameter string: A String value to be inserted into the keychain. - /// - parameter key: A Key that can be used to retrieve the `string` from the keychain. - /// - parameter options: A base query used to scope the calls in the keychain. - /// - returns: `true` if the operation succeeded, or `false` if the keychain is not accessible. - @discardableResult - internal static func set(string: String, forKey key: String, options: [String : AnyHashable]) -> Bool { + + /// - Parameters: + /// - string: A String value to be inserted into the keychain. + /// - key: A key that can be used to retrieve the `string` from the keychain. + /// - options: A base query used to scope the calls in the keychain. + /// - Throws: An error of type `KeychainError`. + internal static func setString(_ string: String, forKey key: String, options: [String : AnyHashable]) throws { // Remove the key before trying to set it. This will prevent us from calling SecItemUpdate on an item stored on the Secure Enclave, which would cause iOS to prompt the user for authentication. - _ = Keychain.removeObject(forKey: key, options: options) + try Keychain.removeObject(forKey: key, options: options) - return Keychain.set(string: string, forKey: key, options: options).didSucceed + try Keychain.setString(string, forKey: key, options: options) } - - /// - parameter key: A Key used to retrieve the desired object from the keychain. - /// - parameter userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. - /// - parameter options: A base query used to scope the calls in the keychain. - /// - returns: The string currently stored in the keychain for the provided key. Returns `nil` if no string exists in the keychain for the specified key, or if the keychain is inaccessible. - internal static func string(forKey key: String, withPrompt userPrompt: String, options: [String : AnyHashable]) -> Result { + + /// - Parameters: + /// - key: A key used to retrieve the desired object from the keychain. + /// - userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. + /// - options: A base query used to scope the calls in the keychain. + /// - Returns: The string currently stored in the keychain for the provided key. + /// - Throws: An error of type `KeychainError`. + internal static func string(forKey key: String, withPrompt userPrompt: String, options: [String : AnyHashable]) throws -> String { var secItemQuery = options if !userPrompt.isEmpty { secItemQuery[kSecUseOperationPrompt as String] = userPrompt } - return Result(Keychain.string(forKey: key, options: secItemQuery)) + return try Keychain.string(forKey: key, options: secItemQuery) } } diff --git a/Sources/Valet/SecureEnclaveAccessControl.swift b/Sources/Valet/SecureEnclaveAccessControl.swift index 46e65da3..522677ed 100644 --- a/Sources/Valet/SecureEnclaveAccessControl.swift +++ b/Sources/Valet/SecureEnclaveAccessControl.swift @@ -24,19 +24,17 @@ import Foundation @objc(VALSecureEnclaveAccessControl) public enum SecureEnclaveAccessControl: Int, CustomStringConvertible, Equatable { /// Access to keychain elements requires user presence verification via Touch ID, Face ID, or device Passcode. Keychain elements are still accessible by Touch ID even if fingers are added or removed. Touch ID does not have to be available or enrolled. - /// - version: Available on iOS 8 or later, tvOS 8 or later, watchOS 2.0 or later, and macOS 10.11 or later. case userPresence = 1 /// Access to keychain elements requires user presence verification via Face ID, or any finger enrolled in Touch ID. Keychain elements remain accessible via Face ID or Touch ID after faces or fingers are added or removed. Face ID must be enabled with at least one face enrolled, or Touch ID must be available and at least one finger must be enrolled. - /// - version: Available on iOS 9 or later, tvOS 9 or later, watchOS 2.0 or later, and macOS 10.12.1 or later. + @available(macOS 10.12.1, *) case biometricAny /// Access to keychain elements requires user presence verification via the face currently enrolled in Face ID, or fingers currently enrolled in Touch ID. Previously written keychain elements become inaccessible when faces or fingers are added or removed. Face ID must be enabled with at least one face enrolled, or Touch ID must be available and at least one finger must be enrolled. - /// - version: Available on iOS 9 or later, tvOS 9 or later, watchOS 2.0 or later, and macOS 10.12.1 or later. + @available(macOS 10.12.1, *) case biometricCurrentSet /// Access to keychain elements requires user presence verification via device Passcode. - /// - version: Available on iOS 9 or later, tvOS 9 or later, watchOS 2.0 or later, and macOS 10.11 or later. case devicePasscode // MARK: CustomStringConvertible @@ -53,14 +51,14 @@ public enum SecureEnclaveAccessControl: Int, CustomStringConvertible, Equatable if #available(macOS 10.12.1, *) { return "_AccessControlTouchIDAnyFingerprint" } else { - ErrorHandler.assertionFailure(".biometricAny requires macOS 10.12.1.") + assertionFailure(".biometricAny requires macOS 10.12.1.") return "" } case .biometricCurrentSet: if #available(macOS 10.12.1, *) { return "_AccessControlTouchIDCurrentFingerprintSet" } else { - ErrorHandler.assertionFailure(".biometricCurrentSet requires macOS 10.12.1.") + assertionFailure(".biometricCurrentSet requires macOS 10.12.1.") return "" } case .devicePasscode: @@ -75,29 +73,33 @@ public enum SecureEnclaveAccessControl: Int, CustomStringConvertible, Equatable case .userPresence: return .userPresence case .biometricAny: - if #available(macOS 10.12.1, *) { - return .init(rawValue: 2) // .biometryAny with Xcode 9 compatibility. + if #available(iOS 11.3, tvOS 11.3, watchOS 4.3, macOS 10.13.4, *) { + return .biometryAny + } else if #available(macOS 10.12.1, *) { + return .touchIDAny } else { - ErrorHandler.assertionFailure(".biometricAny requires macOS 10.12.1.") + assertionFailure(".biometricAny requires macOS 10.12.1.") return .userPresence } case .biometricCurrentSet: - if #available(macOS 10.12.1, *) { - return .init(rawValue: 8) // .biometryCurrentSet with Xcode 9 compatibility. + if #available(iOS 11.3, tvOS 11.3, watchOS 4.3, macOS 10.13.4, *) { + return .biometryCurrentSet + } else if #available(macOS 10.12.1, *) { + return .touchIDCurrentSet } else { - ErrorHandler.assertionFailure(".biometricCurrentSet requires macOS 10.12.1.") + assertionFailure(".biometricCurrentSet requires macOS 10.12.1.") return .userPresence } case .devicePasscode: if #available(macOS 10.11, *) { return .devicePasscode } else { - ErrorHandler.assertionFailure(".devicePasscode requires macOS 10.11.") + assertionFailure(".devicePasscode requires macOS 10.11.") return .userPresence } } } - + internal static func allValues() -> [SecureEnclaveAccessControl] { if #available(macOS 10.12.1, *) { return [ diff --git a/Sources/Valet/SecureEnclaveValet.swift b/Sources/Valet/SecureEnclaveValet.swift index bc98d4e2..7ea6a602 100644 --- a/Sources/Valet/SecureEnclaveValet.swift +++ b/Sources/Valet/SecureEnclaveValet.swift @@ -22,14 +22,15 @@ import Foundation /// Reads and writes keychain elements that are stored on the Secure Enclave using Accessibility attribute `.whenPasscodeSetThisDeviceOnly`. Accessing these keychain elements will require the user to confirm their presence via Touch ID, Face ID, or passcode entry. If no passcode is set on the device, accessing the keychain via a `SecureEnclaveValet` will fail. Data is removed from the Secure Enclave when the user removes a passcode from the device. -@available(macOS 10.11, *) @objc(VALSecureEnclaveValet) public final class SecureEnclaveValet: NSObject { // MARK: Public Class Methods - - /// - parameter identifier: A non-empty string that uniquely identifies a SecureEnclaveValet. - /// - returns: A SecureEnclaveValet that reads/writes keychain elements with the desired flavor. + + /// - Parameters: + /// - identifier: A non-empty string that uniquely identifies a SecureEnclaveValet. + /// - accessControl: The desired access control for the SecureEnclaveValet. + /// - Returns: A SecureEnclaveValet that reads/writes keychain elements with the desired flavor. public class func valet(with identifier: Identifier, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet { let key = Service.standard(identifier, .secureEnclave(accessControl)).description as NSString if let existingValet = identifierToValetMap.object(forKey: key) { @@ -41,11 +42,13 @@ public final class SecureEnclaveValet: NSObject { return valet } } - - /// - parameter identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. - /// - 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: Identifier, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet { - let key = Service.sharedAccessGroup(identifier, .secureEnclave(accessControl)).description as NSString + + /// - Parameters: + /// - 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 if let existingValet = identifierToValetMap.object(forKey: key) { return existingValet @@ -58,9 +61,9 @@ public final class SecureEnclaveValet: NSObject { // MARK: Equatable - /// - returns: `true` if lhs and rhs both read from and write to the same sandbox within the keychain. + /// - Returns: `true` if lhs and rhs both read from and write to the same sandbox within the keychain. public static func ==(lhs: SecureEnclaveValet, rhs: SecureEnclaveValet) -> Bool { - return lhs.service == rhs.service + lhs.service == rhs.service } // MARK: Private Class Properties @@ -74,24 +77,31 @@ public final class SecureEnclaveValet: NSObject { fatalError("Use the class methods above to create usable SecureEnclaveValet objects") } - private init(identifier: Identifier, accessControl: SecureEnclaveAccessControl) { - service = .standard(identifier, .secureEnclave(accessControl)) - keychainQuery = service.generateBaseQuery() - self.identifier = identifier - self.accessControl = accessControl + private convenience init(identifier: Identifier, accessControl: SecureEnclaveAccessControl) { + self.init( + identifier: identifier, + service: .standard(identifier, .secureEnclave(accessControl)), + accessControl: accessControl) } - private init(sharedAccess identifier: Identifier, accessControl: SecureEnclaveAccessControl) { - service = .sharedAccessGroup(identifier, .secureEnclave(accessControl)) - keychainQuery = service.generateBaseQuery() + private convenience init(sharedAccess groupIdentifier: SharedGroupIdentifier, accessControl: SecureEnclaveAccessControl) { + self.init( + identifier: groupIdentifier.asIdentifier, + service: .sharedGroup(groupIdentifier, .secureEnclave(accessControl)), + accessControl: accessControl) + } + + private init(identifier: Identifier, service: Service, accessControl: SecureEnclaveAccessControl) { self.identifier = identifier + self.service = service self.accessControl = accessControl + baseKeychainQuery = service.generateBaseQuery() } // MARK: Hashable public override var hash: Int { - return service.description.hashValue + service.description.hashValue } // MARK: Public Properties @@ -102,103 +112,110 @@ public final class SecureEnclaveValet: NSObject { // MARK: Public Methods - /// - returns: `true` if the keychain is accessible for reading and writing, `false` otherwise. - /// - 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. + /// - Returns: `true` if the keychain is accessible for reading and writing, `false` otherwise. + /// - 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. @objc public func canAccessKeychain() -> Bool { - return SecureEnclave.canAccessKeychain(with: service, identifier: identifier) + SecureEnclave.canAccessKeychain(with: service) } - - /// - parameter object: A Data value to be inserted into the keychain. - /// - parameter key: A Key that can be used to retrieve the `object` from the keychain. - /// - returns: `false` if the keychain is not accessible. - @objc(setObject:forKey:) - @discardableResult - public func set(object: Data, forKey key: String) -> Bool { - return execute(in: lock) { - return SecureEnclave.set(object: object, forKey: key, options: keychainQuery) + + /// - Parameters: + /// - object: A Data value to be inserted into the keychain. + /// - key: A key that can be used to retrieve the `object` from the keychain. + /// - Throws: An error of type `KeychainError`. + @objc + public func setObject(_ object: Data, forKey key: String) throws { + try execute(in: lock) { + try SecureEnclave.setObject(object, forKey: key, options: baseKeychainQuery) } } - - /// - parameter key: A Key used to retrieve the desired object from the keychain. - /// - parameter userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. - /// - returns: The data currently stored in the keychain for the provided key. Returns `.itemNotFound` if no object exists in the keychain for the specified key, or if the keychain is inaccessible. Returns `.userCancelled` if the user cancels the user-presence prompt. - public func object(forKey key: String, withPrompt userPrompt: String) -> SecureEnclave.Result { - return execute(in: lock) { - return SecureEnclave.object(forKey: key, withPrompt: userPrompt, options: keychainQuery) + + /// - Parameters: + /// - key: A key used to retrieve the desired object from the keychain. + /// - userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. + /// - Returns: The data currently stored in the keychain for the provided key. + /// - Throws: An error of type `KeychainError`. + @objc + public func object(forKey key: String, withPrompt userPrompt: String) throws -> Data { + try execute(in: lock) { + try SecureEnclave.object(forKey: key, withPrompt: userPrompt, options: baseKeychainQuery) } } - - /// - parameter key: The key to look up in the keychain. - /// - returns: `true` if a value has been set for the given key, `false` otherwise. - /// - note: Will never prompt the user for Face ID, Touch ID, or password. - @objc(containsObjectForKey:) - public func containsObject(forKey key: String) -> Bool { - return execute(in: lock) { - return SecureEnclave.containsObject(forKey: key, options: keychainQuery) + + /// - Parameter key: The key to look up in the keychain. + /// - Returns: `true` if a value has been set for the given key, `false` otherwise. + /// - Throws: An error of type `KeychainError`. + /// - Note: Will never prompt the user for Face ID, Touch ID, or password. + public func containsObject(forKey key: String) throws -> Bool { + try execute(in: lock) { + try SecureEnclave.containsObject(forKey: key, options: baseKeychainQuery) } } - - /// - parameter string: A String value to be inserted into the keychain. - /// - parameter key: A Key that can be used to retrieve the `string` from the keychain. - /// - returns: `true` if the operation succeeded, or `false` if the keychain is not accessible. - @objc(setString:forKey:) - @discardableResult - public func set(string: String, forKey key: String) -> Bool { - return execute(in: lock) { - return SecureEnclave.set(string: string, forKey: key, options: keychainQuery) + + /// - Parameters: + /// - string: A String value to be inserted into the keychain. + /// - key: A key that can be used to retrieve the `string` from the keychain. + /// - Throws: An error of type `KeychainError`. + @objc + public func setString(_ string: String, forKey key: String) throws { + try execute(in: lock) { + try SecureEnclave.setString(string, forKey: key, options: baseKeychainQuery) } } - - /// - parameter key: A Key used to retrieve the desired object from the keychain. - /// - parameter userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. - /// - returns: The string currently stored in the keychain for the provided key. Returns `nil` if no string exists in the keychain for the specified key, or if the keychain is inaccessible. - public func string(forKey key: String, withPrompt userPrompt: String) -> SecureEnclave.Result { - return execute(in: lock) { - return SecureEnclave.string(forKey: key, withPrompt: userPrompt, options: keychainQuery) + + /// - Parameters: + /// - key: A key used to retrieve the desired object from the keychain. + /// - userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. + /// - Returns: The string currently stored in the keychain for the provided key. + /// - Throws: An error of type `KeychainError`. + @objc + public func string(forKey key: String, withPrompt userPrompt: String) throws -> String { + try execute(in: lock) { + try SecureEnclave.string(forKey: key, withPrompt: userPrompt, options: baseKeychainQuery) } } /// Removes a key/object pair from the keychain. - /// - returns: `false` if the keychain is not accessible. - @objc(removeObjectForKey:) - @discardableResult - public func removeObject(forKey key: String) -> Bool { - return execute(in: lock) { - return Keychain.removeObject(forKey: key, options: keychainQuery).didSucceed + /// - Parameter key: A key used to remove the desired object from the keychain. + /// - Throws: An error of type `KeychainError`. + @objc + public func removeObject(forKey key: String) throws { + try execute(in: lock) { + try Keychain.removeObject(forKey: key, options: baseKeychainQuery) } } /// Removes all key/object pairs accessible by this Valet instance from the keychain. - /// - returns: `false` if the keychain is not accessible. + /// - Throws: An error of type `KeychainError`. @objc - @discardableResult - public func removeAllObjects() -> Bool { - return execute(in: lock) { - return Keychain.removeAllObjects(matching: keychainQuery).didSucceed + public func removeAllObjects() throws { + try execute(in: lock) { + try Keychain.removeAllObjects(matching: baseKeychainQuery) } } /// Migrates objects matching the input query into the receiving SecureEnclaveValet instance. - /// - parameter query: The query with which to retrieve existing keychain data via a call to SecItemCopyMatching. - /// - parameter removeOnCompletion: If `true`, the migrated data will be removed from the keychain if the migration succeeds. - /// - returns: Whether the migration succeeded or failed. - /// - note: The keychain is not modified if a failure occurs. - @objc(migrateObjectsMatchingQuery:removeOnCompletion:) - public func migrateObjects(matching query: [String : AnyHashable], removeOnCompletion: Bool) -> MigrationResult { - return execute(in: lock) { - return Keychain.migrateObjects(matching: query, into: keychainQuery, removeOnCompletion: removeOnCompletion) + /// - Parameters: + /// - query: The query with which to retrieve existing keychain data via a call to SecItemCopyMatching. + /// - removeOnCompletion: If `true`, the migrated data will be removed from the keychain if the migration succeeds. + /// - Throws: An error of type `KeychainError` or `MigrationError`. + /// - Note: The keychain is not modified if an error is thrown. + @objc + public func migrateObjects(matching query: [String : AnyHashable], removeOnCompletion: Bool) throws { + try execute(in: lock) { + try Keychain.migrateObjects(matching: query, into: baseKeychainQuery, removeOnCompletion: removeOnCompletion) } } /// Migrates objects matching the vended keychain query into the receiving SecureEnclaveValet instance. - /// - parameter keychain: An objects whose vended keychain query is used to retrieve existing keychain data via a call to SecItemCopyMatching. - /// - parameter removeOnCompletion: If `true`, the migrated data will be removed from the keychfain if the migration succeeds. - /// - returns: Whether the migration succeeded or failed. - /// - note: The keychain is not modified if a failure occurs. - @objc(migrateObjectsFromKeychain:removeOnCompletion:) - public func migrateObjects(from keychain: KeychainQueryConvertible, removeOnCompletion: Bool) -> MigrationResult { - return migrateObjects(matching: keychain.keychainQuery, removeOnCompletion: removeOnCompletion) + /// - Parameters: + /// - valet: A Valet whose vended keychain query is used to retrieve existing keychain data via a call to SecItemCopyMatching. + /// - removeOnCompletion: If `true`, the migrated data will be removed from the keychfain if the migration succeeds. + /// - Throws: An error of type `KeychainError` or `MigrationError`. + /// - Note: The keychain is not modified if an error is thrown. + @objc + public func migrateObjects(from valet: Valet, removeOnCompletion: Bool) throws { + try migrateObjects(matching: valet.baseKeychainQuery, removeOnCompletion: removeOnCompletion) } // MARK: Internal Properties @@ -206,22 +223,23 @@ public final class SecureEnclaveValet: NSObject { internal let service: Service // MARK: Private Properties - + private let lock = NSLock() - private let keychainQuery: [String : AnyHashable] + private let baseKeychainQuery: [String : AnyHashable] + } // MARK: - Objective-C Compatibility -@available(macOS 10.11, *) extension SecureEnclaveValet { // MARK: Public Class Methods - - /// - parameter identifier: A non-empty string that uniquely identifies a SecureEnclaveValet. - /// - returns: A SecureEnclaveValet that reads/writes keychain elements with the desired flavor. + /// - Parameters: + /// - identifier: A non-empty string that uniquely identifies a SecureEnclaveValet. + /// - accessControl: The desired access control for the SecureEnclaveValet. + /// - Returns: A SecureEnclaveValet that reads/writes keychain elements with the desired flavor. @objc(valetWithIdentifier:accessControl:) public class func 🚫swift_valet(with identifier: String, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet? { guard let identifier = Identifier(nonEmpty: identifier) else { @@ -229,50 +247,45 @@ extension SecureEnclaveValet { } return valet(with: identifier, accessControl: accessControl) } - - /// - parameter identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. - /// - returns: A SecureEnclaveValet that reads/writes keychain elements that can be shared across applications written by the same development team. - @objc(sharedAccessGroupValetWithIdentifier:accessControl:) - public class func 🚫swift_sharedAccessGroupValet(with identifier: String, accessControl: SecureEnclaveAccessControl) -> SecureEnclaveValet? { - guard let identifier = Identifier(nonEmpty: identifier) else { + + /// - Parameters: + /// - appIDPrefix: The application's App ID prefix. This string can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty. + /// - identifier: An identifier that cooresponds to a value in keychain-access-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(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) } - - // MARK: Public Methods - - /// - parameter key: A Key used to retrieve the desired object from the keychain. - /// - parameter userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. - /// - returns: The data currently stored in the keychain for the provided key. Returns `nil` if no object exists in the keychain for the specified key, or if the keychain is inaccessible. - @available(swift, obsoleted: 1.0) - @objc(objectForKey:userPrompt:userCancelled:) - public func 🚫swift_object(forKey key: String, withPrompt userPrompt: String, userCancelled: UnsafeMutablePointer?) -> Data? { - switch object(forKey: key, withPrompt: userPrompt) { - case let .success(data): - return data - case .userCancelled: - userCancelled?.pointee = true - return nil - case .itemNotFound: + + /// - 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: A Key used to retrieve the desired object from the keychain. - /// - parameter userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. - /// - returns: The string currently stored in the keychain for the provided key. Returns `nil` if no string exists in the keychain for the specified key, or if the keychain is inaccessible. + + /// - Parameter key: The key to look up in the keychain. + /// - Returns: `true` if a value has been set for the given key, `false` otherwise. Will return `false` if the keychain is not accessible. + /// - Note: Will never prompt the user for Face ID, Touch ID, or password. @available(swift, obsoleted: 1.0) - @objc(stringForKey:userPrompt:userCancelled:) - public func 🚫swift_string(forKey key: String, withPrompt userPrompt: String, userCancelled: UnsafeMutablePointer?) -> String? { - switch string(forKey: key, withPrompt: userPrompt) { - case let .success(string): - return string - case .userCancelled: - userCancelled?.pointee = true - return nil - case .itemNotFound: - return nil + @objc(containsObjectForKey:) + public func 🚫swift_containsObject(forKey key: String) -> Bool { + guard let containsObject = try? containsObject(forKey: key) else { + return false } + return containsObject } + } diff --git a/Sources/Valet/SharedGroupIdentifier.swift b/Sources/Valet/SharedGroupIdentifier.swift new file mode 100644 index 00000000..a8973df8 --- /dev/null +++ b/Sources/Valet/SharedGroupIdentifier.swift @@ -0,0 +1,81 @@ +// +// SharedGroupIdentifier.swift +// Valet +// +// Created by Dan Federman on 2/25/20. +// Copyright © 2020 Square, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//    http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + + +public struct SharedGroupIdentifier: CustomStringConvertible { + + // MARK: Initialization + + /// A representation of a shared access group identifier. + /// - Parameters: + /// - appIDPrefix: The application's App ID prefix. This string can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty. + /// - groupIdentifier: An identifier that cooresponds to a value in keychain-access-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?(appIDPrefix: String, nonEmptyGroup groupIdentifier: String?) { + guard !appIDPrefix.isEmpty, let groupIdentifier = groupIdentifier, !groupIdentifier.isEmpty else { + return nil + } + + 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?) { + #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 { + prefix + "." + groupIdentifier + } + + // MARK: Internal Properties + + 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" +} diff --git a/Sources/Valet/SinglePromptSecureEnclaveValet.swift b/Sources/Valet/SinglePromptSecureEnclaveValet.swift index d0bf24d7..53bd00f5 100644 --- a/Sources/Valet/SinglePromptSecureEnclaveValet.swift +++ b/Sources/Valet/SinglePromptSecureEnclaveValet.swift @@ -18,21 +18,23 @@ // limitations under the License. // -#if os(iOS) || os(macOS) +#if canImport(LocalAuthentication) import LocalAuthentication import Foundation /// Reads and writes keychain elements that are stored on the Secure Enclave using Accessibility attribute `.whenPasscodeSetThisDeviceOnly`. The first access of these keychain elements will require the user to confirm their presence via Touch ID, Face ID, or passcode entry. If no passcode is set on the device, accessing the keychain via a `SinglePromptSecureEnclaveValet` will fail. Data is removed from the Secure Enclave when the user removes a passcode from the device. -@available(macOS 10.11, *) +@available(tvOS 11.0, *) @objc(VALSinglePromptSecureEnclaveValet) public final class SinglePromptSecureEnclaveValet: NSObject { // MARK: Public Class Methods - - /// - parameter identifier: A non-empty string that uniquely identifies a SinglePromptSecureEnclaveValet. - /// - returns: A SinglePromptSecureEnclaveValet that reads/writes keychain elements with the desired flavor. + + /// - Parameters: + /// - identifier: A non-empty string that uniquely identifies a SinglePromptSecureEnclaveValet. + /// - accessControl: The desired access control for the SinglePromptSecureEnclaveValet. + /// - Returns: A SinglePromptSecureEnclaveValet that reads/writes keychain elements with the desired flavor. public class func valet(with identifier: Identifier, accessControl: SecureEnclaveAccessControl) -> SinglePromptSecureEnclaveValet { let key = Service.standard(identifier, .singlePromptSecureEnclave(accessControl)).description as NSString if let existingValet = identifierToValetMap.object(forKey: key) { @@ -44,11 +46,13 @@ public final class SinglePromptSecureEnclaveValet: NSObject { return valet } } - - /// - parameter identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. - /// - returns: A SinglePromptSecureEnclaveValet that reads/writes keychain elements that can be shared across applications written by the same development team. - public class func sharedAccessGroupValet(with identifier: Identifier, accessControl: SecureEnclaveAccessControl) -> SinglePromptSecureEnclaveValet { - let key = Service.sharedAccessGroup(identifier, .singlePromptSecureEnclave(accessControl)).description as NSString + + /// - Parameters: + /// - 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 if let existingValet = identifierToValetMap.object(forKey: key) { return existingValet @@ -61,9 +65,9 @@ public final class SinglePromptSecureEnclaveValet: NSObject { // MARK: Equatable - /// - returns: `true` if lhs and rhs both read from and write to the same sandbox within the keychain. + /// - Returns: `true` if lhs and rhs both read from and write to the same sandbox within the keychain. public static func ==(lhs: SinglePromptSecureEnclaveValet, rhs: SinglePromptSecureEnclaveValet) -> Bool { - return lhs.service == rhs.service + lhs.service == rhs.service } // MARK: Private Class Properties @@ -77,24 +81,31 @@ public final class SinglePromptSecureEnclaveValet: NSObject { fatalError("Use the class methods above to create usable SinglePromptSecureEnclaveValet objects") } - private init(identifier: Identifier, accessControl: SecureEnclaveAccessControl) { - service = .standard(identifier, .singlePromptSecureEnclave(accessControl)) - baseKeychainQuery = service.generateBaseQuery() - self.identifier = identifier - self.accessControl = accessControl + private convenience init(identifier: Identifier, accessControl: SecureEnclaveAccessControl) { + self.init( + identifier: identifier, + service: .standard(identifier, .singlePromptSecureEnclave(accessControl)), + accessControl: accessControl) } - private init(sharedAccess identifier: Identifier, accessControl: SecureEnclaveAccessControl) { - service = .sharedAccessGroup(identifier, .singlePromptSecureEnclave(accessControl)) - baseKeychainQuery = service.generateBaseQuery() + private convenience init(sharedAccess groupIdentifier: SharedGroupIdentifier, accessControl: SecureEnclaveAccessControl) { + self.init( + identifier: groupIdentifier.asIdentifier, + service: .sharedGroup(groupIdentifier, .singlePromptSecureEnclave(accessControl)), + accessControl: accessControl) + } + + private init(identifier: Identifier, service: Service, accessControl: SecureEnclaveAccessControl) { self.identifier = identifier + self.service = service self.accessControl = accessControl + baseKeychainQuery = service.generateBaseQuery() } // MARK: Hashable public override var hash: Int { - return service.description.hashValue + service.description.hashValue } // MARK: Public Properties @@ -105,60 +116,66 @@ public final class SinglePromptSecureEnclaveValet: NSObject { // MARK: Public Methods - /// - returns: `true` if the keychain is accessible for reading and writing, `false` otherwise. - /// - 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. + /// - Returns: `true` if the keychain is accessible for reading and writing, `false` otherwise. + /// - 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. @objc public func canAccessKeychain() -> Bool { - return SecureEnclave.canAccessKeychain(with: service, identifier: identifier) + SecureEnclave.canAccessKeychain(with: service) } - - /// - parameter object: A Data value to be inserted into the keychain. - /// - parameter key: A Key that can be used to retrieve the `object` from the keychain. - /// - returns: `false` if the keychain is not accessible. - @objc(setObject:forKey:) - @discardableResult - public func set(object: Data, forKey key: String) -> Bool { - return execute(in: lock) { - return SecureEnclave.set(object: object, forKey: key, options: baseKeychainQuery) + + /// - Parameters: + /// - object: A Data value to be inserted into the keychain. + /// - key: A key that can be used to retrieve the `object` from the keychain. + /// - Throws: An error of type `KeychainError`. + @objc + public func setObject(_ object: Data, forKey key: String) throws { + try execute(in: lock) { + try SecureEnclave.setObject(object, forKey: key, options: baseKeychainQuery) } } - - /// - parameter key: A Key used to retrieve the desired object from the keychain. - /// - parameter userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. If the `SinglePromptSecureEnclaveValet` has already been unlocked, no prompt will be shown. - /// - returns: The data currently stored in the keychain for the provided key. Returns `.itemNotFound` if no object exists in the keychain for the specified key, or if the keychain is inaccessible. Returns `.userCancelled` if the user cancels the user-presence prompt. - public func object(forKey key: String, withPrompt userPrompt: String) -> SecureEnclave.Result { - return execute(in: lock) { - return SecureEnclave.object(forKey: key, withPrompt: userPrompt, options: continuedAuthenticationKeychainQuery) + + /// - Parameters: + /// - key: A key used to retrieve the desired object from the keychain. + /// - userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. If the `SinglePromptSecureEnclaveValet` has already been unlocked, no prompt will be shown. + /// - Returns: The data currently stored in the keychain for the provided key. + /// - Throws: An error of type `KeychainError`. + @objc + public func object(forKey key: String, withPrompt userPrompt: String) throws -> Data { + try execute(in: lock) { + try SecureEnclave.object(forKey: key, withPrompt: userPrompt, options: try continuedAuthenticationKeychainQuery()) } } - - /// - parameter key: The key to look up in the keychain. - /// - returns: `true` if a value has been set for the given key, `false` otherwise. - /// - note: Will never prompt the user for Face ID, Touch ID, or password. - @objc(containsObjectForKey:) - public func containsObject(forKey key: String) -> Bool { - return execute(in: lock) { - return SecureEnclave.containsObject(forKey: key, options: baseKeychainQuery) + + /// - Parameter key: The key to look up in the keychain. + /// - Returns: `true` if a value has been set for the given key, `false` otherwise. + /// - Throws: An error of type `KeychainError`. + /// - Note: Will never prompt the user for Face ID, Touch ID, or password. + public func containsObject(forKey key: String) throws -> Bool { + try execute(in: lock) { + try SecureEnclave.containsObject(forKey: key, options: baseKeychainQuery) } } - - /// - parameter string: A String value to be inserted into the keychain. - /// - parameter key: A Key that can be used to retrieve the `string` from the keychain. - /// - returns: `true` if the operation succeeded, or `false` if the keychain is not accessible. - @objc(setString:forKey:) - @discardableResult - public func set(string: String, forKey key: String) -> Bool { - return execute(in: lock) { - return SecureEnclave.set(string: string, forKey: key, options: baseKeychainQuery) + + /// - Parameters: + /// - string: A String value to be inserted into the keychain. + /// - key: A key that can be used to retrieve the `string` from the keychain. + /// - Throws: An error of type `KeychainError`. + @objc + public func setString(_ string: String, forKey key: String) throws { + try execute(in: lock) { + try SecureEnclave.setString(string, forKey: key, options: baseKeychainQuery) } } - - /// - parameter key: A Key used to retrieve the desired object from the keychain. - /// - parameter userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. If the `SinglePromptSecureEnclaveValet` has already been unlocked, no prompt will be shown. - /// - returns: The string currently stored in the keychain for the provided key. Returns `nil` if no string exists in the keychain for the specified key, or if the keychain is inaccessible. - public func string(forKey key: String, withPrompt userPrompt: String) -> SecureEnclave.Result { - return execute(in: lock) { - return SecureEnclave.string(forKey: key, withPrompt: userPrompt, options: continuedAuthenticationKeychainQuery) + + /// - Parameters: + /// - key: A key used to retrieve the desired object from the keychain. + /// - userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. If the `SinglePromptSecureEnclaveValet` has already been unlocked, no prompt will be shown. + /// - Returns: The string currently stored in the keychain for the provided key. + /// - Throws: An error of type `KeychainError`. + @objc + public func string(forKey key: String, withPrompt userPrompt: String) throws -> String { + try execute(in: lock) { + try SecureEnclave.string(forKey: key, withPrompt: userPrompt, options: try continuedAuthenticationKeychainQuery()) } } @@ -171,60 +188,62 @@ public final class SinglePromptSecureEnclaveValet: NSObject { } } - /// - parameter userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. If the `SinglePromptSecureEnclaveValet` has already been unlocked, no prompt will be shown. - /// - returns: The set of all (String) keys currently stored in this Valet instance. - @objc(allKeysWithUserPrompt:) - public func allKeys(userPrompt: String) -> Set { - return execute(in: lock) { - var secItemQuery = continuedAuthenticationKeychainQuery + /// - Parameter userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. If the `SinglePromptSecureEnclaveValet` has already been unlocked, no prompt will be shown. If no items are found, will return an empty set. + /// - Returns: The set of all (String) keys currently stored in this Valet instance. + /// - Throws: An error of type `KeychainError`. + @objc + public func allKeys(userPrompt: String) throws -> Set { + try execute(in: lock) { + var secItemQuery = try continuedAuthenticationKeychainQuery() if !userPrompt.isEmpty { secItemQuery[kSecUseOperationPrompt as String] = userPrompt } - return Keychain.allKeys(options: secItemQuery).value ?? Set() + return try Keychain.allKeys(options: secItemQuery) } } /// Removes a key/object pair from the keychain. - /// - returns: `false` if the keychain is not accessible. - @objc(removeObjectForKey:) - @discardableResult - public func removeObject(forKey key: String) -> Bool { - return execute(in: lock) { - return Keychain.removeObject(forKey: key, options: baseKeychainQuery).didSucceed + /// - Parameter key: A key used to remove the desired object from the keychain. + /// - Throws: An error of type `KeychainError`. + @objc + public func removeObject(forKey key: String) throws { + try execute(in: lock) { + try Keychain.removeObject(forKey: key, options: baseKeychainQuery) } } /// Removes all key/object pairs accessible by this Valet instance from the keychain. - /// - returns: `false` if the keychain is not accessible. + /// - Throws: An error of type `KeychainError`. @objc - @discardableResult - public func removeAllObjects() -> Bool { - return execute(in: lock) { - return Keychain.removeAllObjects(matching: baseKeychainQuery).didSucceed + public func removeAllObjects() throws { + try execute(in: lock) { + try Keychain.removeAllObjects(matching: baseKeychainQuery) } } /// Migrates objects matching the input query into the receiving SinglePromptSecureEnclaveValet instance. - /// - parameter query: The query with which to retrieve existing keychain data via a call to SecItemCopyMatching. - /// - parameter removeOnCompletion: If `true`, the migrated data will be removed from the keychain if the migration succeeds. - /// - returns: Whether the migration succeeded or failed. - /// - note: The keychain is not modified if a failure occurs. - @objc(migrateObjectsMatchingQuery:removeOnCompletion:) - public func migrateObjects(matching query: [String : AnyHashable], removeOnCompletion: Bool) -> MigrationResult { - return execute(in: lock) { - return Keychain.migrateObjects(matching: query, into: baseKeychainQuery, removeOnCompletion: removeOnCompletion) + /// - Parameters: + /// - query: The query with which to retrieve existing keychain data via a call to SecItemCopyMatching. + /// - removeOnCompletion: If `true`, the migrated data will be removed from the keychain if the migration succeeds. + /// - Throws: An error of type `KeychainError` or `MigrationError`. + /// - Note: The keychain is not modified if an error is thrown. + @objc + public func migrateObjects(matching query: [String : AnyHashable], removeOnCompletion: Bool) throws { + try execute(in: lock) { + try Keychain.migrateObjects(matching: query, into: baseKeychainQuery, removeOnCompletion: removeOnCompletion) } } /// Migrates objects matching the vended keychain query into the receiving SinglePromptSecureEnclaveValet instance. - /// - parameter keychain: An objects whose vended keychain query is used to retrieve existing keychain data via a call to SecItemCopyMatching. - /// - parameter removeOnCompletion: If `true`, the migrated data will be removed from the keychfain if the migration succeeds. - /// - returns: Whether the migration succeeded or failed. - /// - note: The keychain is not modified if a failure occurs. - @objc(migrateObjectsFromKeychain:removeOnCompletion:) - public func migrateObjects(from keychain: KeychainQueryConvertible, removeOnCompletion: Bool) -> MigrationResult { - return migrateObjects(matching: keychain.keychainQuery, removeOnCompletion: removeOnCompletion) + /// - Parameters: + /// - valet: A Valet whose vended keychain query is used to retrieve existing keychain data via a call to SecItemCopyMatching. + /// - removeOnCompletion: If `true`, the migrated data will be removed from the keychfain if the migration succeeds. + /// - Throws: An error of type `KeychainError` or `MigrationError`. + /// - Note: The keychain is not modified if an error is thrown. + @objc + public func migrateObjects(from valet: Valet, removeOnCompletion: Bool) throws { + try migrateObjects(matching: valet.baseKeychainQuery, removeOnCompletion: removeOnCompletion) } // MARK: Internal Properties @@ -234,14 +253,14 @@ public final class SinglePromptSecureEnclaveValet: NSObject { // MARK: Private Properties private let lock = NSLock() - private let baseKeychainQuery: [String : AnyHashable] private var localAuthenticationContext = LAContext() + private let baseKeychainQuery: [String : AnyHashable] /// A keychain query dictionary that allows for continued read access to the Secure Enclave after the a single unlock event. /// This query should be used when retrieving keychain data, but should not be used for keychain writes or `containsObject` checks. /// Using this query in a `containsObject` check can cause a false positive in the case where an element has been removed from /// the keychain by the operating system due to a face, fingerprint, or password change. - private var continuedAuthenticationKeychainQuery: [String : AnyHashable] { + private func continuedAuthenticationKeychainQuery() throws -> [String : AnyHashable] { var keychainQuery = baseKeychainQuery keychainQuery[kSecUseAuthenticationContext as String] = localAuthenticationContext return keychainQuery @@ -252,13 +271,15 @@ public final class SinglePromptSecureEnclaveValet: NSObject { // MARK: - Objective-C Compatibility -@available(macOS 10.11, *) +@available(tvOS 11.0, *) extension SinglePromptSecureEnclaveValet { // MARK: Public Class Methods - - /// - parameter identifier: A non-empty string that uniquely identifies a SinglePromptSecureEnclaveValet. - /// - returns: A SinglePromptSecureEnclaveValet that reads/writes keychain elements with the desired flavor. + + /// - Parameters: + /// - identifier: A non-empty string that uniquely identifies a SinglePromptSecureEnclaveValet. + /// - accessControl: The desired access control for the SinglePromptSecureEnclaveValet. + /// - Returns: A SinglePromptSecureEnclaveValet that reads/writes keychain elements with the desired flavor. @objc(valetWithIdentifier:accessControl:) public class func 🚫swift_valet(with identifier: String, accessControl: SecureEnclaveAccessControl) -> SinglePromptSecureEnclaveValet? { guard let identifier = Identifier(nonEmpty: identifier) else { @@ -267,52 +288,47 @@ extension SinglePromptSecureEnclaveValet { return valet(with: identifier, accessControl: accessControl) } - - /// - parameter identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. - /// - returns: A SinglePromptSecureEnclaveValet that reads/writes keychain elements that can be shared across applications written by the same development team. - @objc(sharedAccessGroupValetWithIdentifier:accessControl:) - public class func 🚫swift_sharedAccessGroupValet(with identifier: String, accessControl: SecureEnclaveAccessControl) -> SinglePromptSecureEnclaveValet? { - guard let identifier = Identifier(nonEmpty: identifier) else { + + /// - Parameters: + /// - appIDPrefix: The application's App ID prefix. This string can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty. + /// - identifier: An identifier that cooresponds to a value in keychain-access-groups in the application's Entitlements file. This string must not be empty. + /// - 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. + /// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps + @objc(sharedGroupValetWithAppIDPrefix:sharedGroupIdentifier:accessControl:) + public class func 🚫swift_sharedGroupValet(appIDPrefix: String, nonEmptyIdentifier identifier: String, accessControl: SecureEnclaveAccessControl) -> SinglePromptSecureEnclaveValet? { + guard let identifier = SharedGroupIdentifier(appIDPrefix: appIDPrefix, nonEmptyGroup: identifier) else { return nil } - return sharedAccessGroupValet(with: identifier, accessControl: accessControl) + return sharedGroupValet(with: identifier, accessControl: accessControl) } - - // MARK: Public Methods - - /// - parameter key: A Key used to retrieve the desired object from the keychain. - /// - parameter userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. - /// - returns: The data currently stored in the keychain for the provided key. Returns `nil` if no object exists in the keychain for the specified key, or if the keychain is inaccessible. - @available(swift, obsoleted: 1.0) - @objc(objectForKey:userPrompt:userCancelled:) - public func 🚫swift_object(forKey key: String, withPrompt userPrompt: String, userCancelled: UnsafeMutablePointer?) -> Data? { - switch object(forKey: key, withPrompt: userPrompt) { - case let .success(data): - return data - case .userCancelled: - userCancelled?.pointee = true - return nil - case .itemNotFound: + + /// - 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 SinglePromptSecureEnclaveValet. + /// - Returns: A SinglePromptSecureEnclaveValet 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) -> SinglePromptSecureEnclaveValet? { + guard let identifier = SharedGroupIdentifier(groupPrefix: groupPrefix, nonEmptyGroup: identifier) else { return nil } + return sharedGroupValet(with: identifier, accessControl: accessControl) } - - /// - parameter key: A Key used to retrieve the desired object from the keychain. - /// - parameter userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. - /// - returns: The string currently stored in the keychain for the provided key. Returns `nil` if no string exists in the keychain for the specified key, or if the keychain is inaccessible. + + /// - Parameter key: The key to look up in the keychain. + /// - Returns: `true` if a value has been set for the given key, `false` otherwise. Will return `false` if the keychain is not accessible. + /// - Note: Will never prompt the user for Face ID, Touch ID, or password. @available(swift, obsoleted: 1.0) - @objc(stringForKey:userPrompt:userCancelled:) - public func 🚫swift_string(forKey key: String, withPrompt userPrompt: String, userCancelled: UnsafeMutablePointer?) -> String? { - switch string(forKey: key, withPrompt: userPrompt) { - case let .success(string): - return string - case .userCancelled: - userCancelled?.pointee = true - return nil - case .itemNotFound: - return nil + @objc(containsObjectForKey:) + public func 🚫swift_containsObject(forKey key: String) -> Bool { + guard let containsObject = try? containsObject(forKey: key) else { + return false } + return containsObject } + } #endif diff --git a/Sources/Valet/SwiftCompatibility.swift b/Sources/Valet/SwiftCompatibility.swift deleted file mode 100644 index 7800178a..00000000 --- a/Sources/Valet/SwiftCompatibility.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// SwiftCompatibility.swift -// Valet -// -// Created by Gordon Fontenot on 7/26/18. -// Copyright © 2017 Square, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#if !swift(>=4.1) - -extension Sequence { - func compactMap(_ transform: (Element) throws -> T?) rethrows -> [T] { - return try flatMap(transform) - } -} - -#endif diff --git a/Sources/Valet/Valet.swift b/Sources/Valet/Valet.swift index dd77168d..addc6978 100644 --- a/Sources/Valet/Valet.swift +++ b/Sources/Valet/Valet.swift @@ -23,43 +23,93 @@ import Foundation /// Reads and writes keychain elements. @objc(VALValet) -public final class Valet: NSObject, KeychainQueryConvertible { +public final class Valet: NSObject { // MARK: Public Class Methods - - /// - parameter identifier: A non-empty string that uniquely identifies a Valet. - /// - parameter accessibility: The desired accessibility for the Valet. - /// - returns: A Valet that reads/writes keychain elements with the desired accessibility and identifier. + + /// - Parameters: + /// - identifier: A non-empty string that uniquely identifies a Valet. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet that reads/writes keychain elements with the desired accessibility and identifier. public class func valet(with identifier: Identifier, accessibility: Accessibility) -> Valet { - return findOrCreate(identifier, configuration: .valet(accessibility)) + findOrCreate(identifier, configuration: .valet(accessibility)) } - /// - parameter identifier: A non-empty string that uniquely identifies a Valet. - /// - parameter accessibility: The desired accessibility for the Valet. - /// - returns: A Valet (synchronized with iCloud) that reads/writes keychain elements with the desired accessibility and identifier. + /// - Parameters: + /// - identifier: A non-empty string that uniquely identifies a Valet. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet (synchronized with iCloud) that reads/writes keychain elements with the desired accessibility and identifier. public class func iCloudValet(with identifier: Identifier, accessibility: CloudAccessibility) -> Valet { - return findOrCreate(identifier, configuration: .iCloud(accessibility)) + findOrCreate(identifier, configuration: .iCloud(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 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)) + } + + /// - 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)) + } + + #if os(macOS) + /// Creates a Valet with an explicitly set kSecAttrService. + /// - Parameters: + /// - identifier: A non-empty string that uniquely identifies a Valet. Must be unique relative to other Valet identifiers. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet that reads/writes keychain elements with the desired accessibility and identifier. + /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. + /// - SeeAlso: https://github.com/square/Valet/issues/140 + public class func valet(withExplicitlySet identifier: Identifier, accessibility: Accessibility) -> Valet { + findOrCreate(explicitlySet: identifier, configuration: .valet(accessibility)) + } + + /// Creates an iCloud Valet with an explicitly set kSecAttrService. + /// - Parameters: + /// - identifier: A non-empty string that uniquely identifies a Valet. Must be unique relative to other Valet identifiers. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet (synchronized with iCloud) that reads/writes keychain elements with the desired accessibility and identifier. + /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. + /// - SeeAlso: https://github.com/square/Valet/issues/140 + public class func iCloudValet(withExplicitlySet identifier: Identifier, accessibility: CloudAccessibility) -> Valet { + findOrCreate(explicitlySet: identifier, configuration: .iCloud(accessibility)) } - /// - parameter identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. - /// - parameter 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 sharedAccessGroupValet(with identifier: Identifier, accessibility: Accessibility) -> Valet { - return findOrCreate(identifier, configuration: .valet(accessibility), sharedAccessGroup: true) + /// Creates a shared-access-group Valet with an explicitly set kSecAttrService. + /// - Parameters: + /// - identifier: The identifier for the Valet's shared access group. Must correspond with the value for keychain-access-groups in your Entitlements file. Must be unique relative to other Valet identifiers. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet that reads/writes keychain elements that can be shared across applications written by the same development team. + /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. + /// - SeeAlso: https://github.com/square/Valet/issues/140 + public class func sharedGroupValet(withExplicitlySet identifier: SharedGroupIdentifier, accessibility: Accessibility) -> Valet { + findOrCreate(explicitlySet: identifier, configuration: .valet(accessibility)) } - /// - parameter identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. - /// - parameter 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 iCloudSharedAccessGroupValet(with identifier: Identifier, accessibility: CloudAccessibility) -> Valet { - return findOrCreate(identifier, configuration: .iCloud(accessibility), sharedAccessGroup: true) + /// Creates an iCloud-shared-access-group Valet with an explicitly set kSecAttrService. + /// - Parameters: + /// - identifier: The identifier for the Valet's shared access group. Must correspond with the value for keychain-access-groups in your Entitlements file. Must be unique relative to other Valet identifiers. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet (synchronized with iCloud) that reads/writes keychain elements that can be shared across applications written by the same development team. + /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. + /// - SeeAlso: https://github.com/square/Valet/issues/140 + public class func iCloudSharedGroupValet(withExplicitlySet identifier: SharedGroupIdentifier, accessibility: CloudAccessibility) -> Valet { + findOrCreate(explicitlySet: identifier, configuration: .iCloud(accessibility)) } + #endif // MARK: Equatable - /// - returns: `true` if lhs and rhs both read from and write to the same sandbox within the keychain. + /// - Returns: `true` if lhs and rhs both read from and write to the same sandbox within the keychain. public static func ==(lhs: Valet, rhs: Valet) -> Bool { - return lhs.service == rhs.service + lhs.service == rhs.service } // MARK: Private Class Properties @@ -68,24 +118,61 @@ public final class Valet: NSObject, KeychainQueryConvertible { // MARK: Private Class Functions - /// - returns: a Valet with the given Identifier, Flavor (and a shared access group service if requested) - private class func findOrCreate(_ identifier: Identifier, configuration: Configuration, sharedAccessGroup: Bool = false) -> Valet { - let service: Service = sharedAccessGroup ? .sharedAccessGroup(identifier, configuration) : .standard(identifier, configuration) + private class func findOrCreate(_ identifier: Identifier, configuration: Configuration) -> Valet { + let service: Service = .standard(identifier, configuration) let key = service.description as NSString if let existingValet = identifierToValetMap.object(forKey: key) { return existingValet } else { - let valet: Valet - if sharedAccessGroup { - valet = Valet(sharedAccess: identifier, configuration: configuration) - } else { - valet = Valet(identifier: identifier, configuration: configuration) - } + let valet = Valet(identifier: identifier, configuration: configuration) + identifierToValetMap.setObject(valet, forKey: key) + return valet + } + } + + private class func findOrCreate(_ identifier: SharedGroupIdentifier, configuration: Configuration) -> Valet { + let service: Service = .sharedGroup(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) + identifierToValetMap.setObject(valet, forKey: key) + return valet + } + } + + + #if os(macOS) + private class func findOrCreate(explicitlySet identifier: Identifier, configuration: Configuration) -> Valet { + let service: Service = .standardOverride(service: identifier, configuration) + let key = service.description + configuration.description + configuration.accessibility.description + identifier.description as NSString + if let existingValet = identifierToValetMap.object(forKey: key) { + return existingValet + + } else { + let valet = Valet(overrideIdentifier: identifier, configuration: configuration) + identifierToValetMap.setObject(valet, forKey: key) + return valet + } + } + + private class func findOrCreate(explicitlySet identifier: SharedGroupIdentifier, configuration: Configuration) -> Valet { + let service: Service = .sharedGroupOverride(service: identifier, configuration) + let key = service.description + configuration.description + configuration.accessibility.description + identifier.description as NSString + if let existingValet = identifierToValetMap.object(forKey: key) { + return existingValet + + } else { + let valet = Valet(overrideSharedAccess: identifier, configuration: configuration) identifierToValetMap.setObject(valet, forKey: key) return valet } } + + #endif // MARK: Initialization @@ -94,36 +181,56 @@ public final class Valet: NSObject, KeychainQueryConvertible { fatalError("Use the class methods above to create usable Valet objects") } - private init(identifier: Identifier, configuration: Configuration) { + private convenience init(identifier: Identifier, configuration: Configuration) { + self.init( + identifier: identifier, + service: .standard(identifier, configuration), + configuration: configuration) + } + + private convenience init(sharedAccess groupIdentifier: SharedGroupIdentifier, configuration: Configuration) { + self.init( + identifier: groupIdentifier.asIdentifier, + service: .sharedGroup(groupIdentifier, configuration), + configuration: configuration) + } + + private init(identifier: Identifier, service: Service, configuration: Configuration) { self.identifier = identifier self.configuration = configuration - service = .standard(identifier, configuration) + self.service = service accessibility = configuration.accessibility - keychainQuery = service.generateBaseQuery() + baseKeychainQuery = service.generateBaseQuery() } - - private init(sharedAccess identifier: Identifier, configuration: Configuration) { - self.identifier = identifier + + #if os(macOS) + private init(overrideIdentifier: Identifier, configuration: Configuration) { + self.identifier = overrideIdentifier + self.configuration = configuration + service = .standardOverride(service: identifier, configuration) + accessibility = configuration.accessibility + baseKeychainQuery = service.generateBaseQuery() + } + + private init(overrideSharedAccess identifier: SharedGroupIdentifier, configuration: Configuration) { + self.identifier = identifier.asIdentifier self.configuration = configuration - service = .sharedAccessGroup(identifier, configuration) + service = .sharedGroupOverride(service: identifier, configuration) accessibility = configuration.accessibility - keychainQuery = service.generateBaseQuery() + baseKeychainQuery = service.generateBaseQuery() } + #endif // MARK: CustomStringConvertible public override var description: String { - return "\(super.description) \(identifier.description) \(configuration.prettyDescription)" + "\(super.description) \(identifier.description) \(configuration.prettyDescription)" } - - // MARK: KeychainQueryConvertible - - public let keychainQuery: [String : AnyHashable] - + // MARK: Hashable public override var hash: Int { - return service.description.hashValue + service.description.hashValue } // MARK: Public Properties @@ -134,130 +241,209 @@ public final class Valet: NSObject, KeychainQueryConvertible { // MARK: Public Methods - /// - returns: `true` if the keychain is accessible for reading and writing, `false` otherwise. - /// - note: Determined by writing a value to the keychain and then reading it back out. + /// - Returns: `true` if the keychain is accessible for reading and writing, `false` otherwise. + /// - Note: Determined by writing a value to the keychain and then reading it back out. @objc public func canAccessKeychain() -> Bool { - return execute(in: lock) { - return Keychain.canAccess(attributes: keychainQuery) + execute(in: lock) { + return Keychain.canAccess(attributes: baseKeychainQuery) } } - - /// - parameter object: A Data value to be inserted into the keychain. - /// - parameter key: A Key that can be used to retrieve the `object` from the keychain. - /// - returns: `false` if the keychain is not accessible. - @objc(setObject:forKey:) - @discardableResult - public func set(object: Data, forKey key: String) -> Bool { - return execute(in: lock) { - return Keychain.set(object: object, forKey: key, options: keychainQuery).didSucceed + + /// - Parameters: + /// - object: A Data value to be inserted into the keychain. + /// - key: A key that can be used to retrieve the `object` from the keychain. + /// - Throws: An error of type `KeychainError`. + @objc + public func setObject(_ object: Data, forKey key: String) throws { + try execute(in: lock) { + try Keychain.setObject(object, forKey: key, options: baseKeychainQuery) } } - - /// - parameter key: A Key used to retrieve the desired object from the keychain. - /// - returns: The data currently stored in the keychain for the provided key. Returns `nil` if no object exists in the keychain for the specified key, or if the keychain is inaccessible. - @objc(objectForKey:) - public func object(forKey key: String) -> Data? { - return execute(in: lock) { - return Keychain.object(forKey: key, options: keychainQuery).value + + /// - Parameter key: A key used to retrieve the desired object from the keychain. + /// - Returns: The data currently stored in the keychain for the provided key. + /// - Throws: An error of type `KeychainError`. + @objc + public func object(forKey key: String) throws -> Data { + try execute(in: lock) { + try Keychain.object(forKey: key, options: baseKeychainQuery) } } - - /// - parameter key: The key to look up in the keychain. - /// - returns: `true` if a value has been set for the given key, `false` otherwise. - @objc(containsObjectForKey:) - public func containsObject(forKey key: String) -> Bool { - return execute(in: lock) { - return Keychain.containsObject(forKey: key, options: keychainQuery).didSucceed + + /// - Parameter key: The key to look up in the keychain. + /// - Returns: `true` if a value has been set for the given key, `false` otherwise. + /// - Throws: An error of type `KeychainError`. + public func containsObject(forKey key: String) throws -> Bool { + try execute(in: lock) { + let status = Keychain.performCopy(forKey: key, options: baseKeychainQuery) + switch status { + case errSecSuccess: + return true + case errSecItemNotFound: + return false + default: + throw KeychainError(status: status) + } } } - - /// - parameter string: A String value to be inserted into the keychain. - /// - parameter key: A Key that can be used to retrieve the `string` from the keychain. - /// - returns: `true` if the operation succeeded, or `false` if the keychain is not accessible. - @objc(setString:forKey:) - @discardableResult - public func set(string: String, forKey key: String) -> Bool { - return execute(in: lock) { - return Keychain.set(string: string, forKey: key, options: keychainQuery).didSucceed + + /// - Parameters: + /// - string: A String value to be inserted into the keychain. + /// - key: A key that can be used to retrieve the `string` from the keychain. + /// - Throws: An error of type `KeychainError`. + @objc + public func setString(_ string: String, forKey key: String) throws { + try execute(in: lock) { + try Keychain.setString(string, forKey: key, options: baseKeychainQuery) } } - - /// - parameter key: A Key used to retrieve the desired object from the keychain. - /// - returns: The string currently stored in the keychain for the provided key. Returns `nil` if no string exists in the keychain for the specified key, or if the keychain is inaccessible. - @objc(stringForKey:) - public func string(forKey key: String) -> String? { - return execute(in: lock) { - return Keychain.string(forKey: key, options: keychainQuery).value + + /// - Parameter key: A key used to retrieve the desired object from the keychain. + /// - Returns: The string currently stored in the keychain for the provided key. + /// - Throws: An error of type `KeychainError`. + @objc + public func string(forKey key: String) throws -> String { + try execute(in: lock) { + try Keychain.string(forKey: key, options: baseKeychainQuery) } } - /// - returns: The set of all (String) keys currently stored in this Valet instance. + /// - Returns: The set of all (String) keys currently stored in this Valet instance. If no items are found, will return an empty set. + /// - Throws: An error of type `KeychainError`. @objc - public func allKeys() -> Set { - return execute(in: lock) { - return Keychain.allKeys(options: keychainQuery).value ?? Set() + public func allKeys() throws -> Set { + try execute(in: lock) { + try Keychain.allKeys(options: baseKeychainQuery) } } /// Removes a key/object pair from the keychain. - /// - returns: `false` if the keychain is not accessible. - @objc(removeObjectForKey:) - @discardableResult - public func removeObject(forKey key: String) -> Bool { - return execute(in: lock) { - return Keychain.removeObject(forKey: key, options: keychainQuery).didSucceed + /// - Parameter key: A key used to remove the desired object from the keychain. + /// - Throws: An error of type `KeychainError`. + /// - Note: No error is thrown if the `key` is not found in the keychain. + @objc + public func removeObject(forKey key: String) throws { + try execute(in: lock) { + try Keychain.removeObject(forKey: key, options: baseKeychainQuery) } } /// Removes all key/object pairs accessible by this Valet instance from the keychain. - /// - returns: `false` if the keychain is not accessible. + /// - Throws: An error of type `KeychainError`. @objc - @discardableResult - public func removeAllObjects() -> Bool { - return execute(in: lock) { - return Keychain.removeAllObjects(matching: keychainQuery).didSucceed + public func removeAllObjects() throws { + try execute(in: lock) { + try Keychain.removeAllObjects(matching: baseKeychainQuery) } } - + /// Migrates objects matching the input query into the receiving Valet instance. - /// - parameter query: The query with which to retrieve existing keychain data via a call to SecItemCopyMatching. - /// - parameter removeOnCompletion: If `true`, the migrated data will be removed from the keychain if the migration succeeds. - /// - returns: Whether the migration succeeded or failed. - /// - note: The keychain is not modified if a failure occurs. - @objc(migrateObjectsMatchingQuery:removeOnCompletion:) - public func migrateObjects(matching query: [String : AnyHashable], removeOnCompletion: Bool) -> MigrationResult { - return execute(in: lock) { - return Keychain.migrateObjects(matching: query, into: keychainQuery, removeOnCompletion: removeOnCompletion) + /// - Parameters: + /// - query: The query with which to retrieve existing keychain data via a call to SecItemCopyMatching. + /// - removeOnCompletion: If `true`, the migrated data will be removed from the keychain if the migration succeeds. + /// - Throws: An error of type `KeychainError` or `MigrationError`. + /// - Note: The keychain is not modified if an error is thrown. + @objc + public func migrateObjects(matching query: [String : AnyHashable], removeOnCompletion: Bool) throws { + try execute(in: lock) { + try Keychain.migrateObjects(matching: query, into: baseKeychainQuery, removeOnCompletion: removeOnCompletion) } } - /// Migrates objects matching the vended keychain query into the receiving Valet instance. - /// - parameter keychain: An objects whose vended keychain query is used to retrieve existing keychain data via a call to SecItemCopyMatching. - /// - parameter removeOnCompletion: If `true`, the migrated data will be removed from the keychfain if the migration succeeds. - /// - returns: Whether the migration succeeded or failed. - /// - note: The keychain is not modified if a failure occurs. - @objc(migrateObjectsFromValet:removeOnCompletion:) - public func migrateObjects(from keychain: KeychainQueryConvertible, removeOnCompletion: Bool) -> MigrationResult { - return migrateObjects(matching: keychain.keychainQuery, removeOnCompletion: removeOnCompletion) + /// Migrates objects in the input Valet into the receiving Valet instance. + /// - Parameters: + /// - valet: The Valet used to retrieve the existing keychain data that should be migrated. + /// - removeOnCompletion: If `true`, the migrated data will be removed from the keychain if the migration succeeds. + /// - Throws: An error of type `KeychainError` or `MigrationError`. + /// - Note: The keychain is not modified if an error is thrown. + @objc + public func migrateObjects(from valet: Valet, removeOnCompletion: Bool) throws { + try migrateObjects(matching: valet.baseKeychainQuery, removeOnCompletion: removeOnCompletion) + } + + /// Call this method if your Valet used to have its accessibility set to `always`. + /// This method migrates objects set on a Valet with the same type and identifier, but with its accessibility set to `always` (which was possible prior to Valet 4.0) to the current Valet. + /// - Parameter removeOnCompletion: If `true`, the migrated data will be removed from the keychain if the migration succeeds. + /// - Throws: An error of type `KeychainError` or `MigrationError`. + /// - Note: The keychain is not modified if an error is thrown. + @objc + public func migrateObjectsFromAlwaysAccessibleValet(removeOnCompletion: Bool) throws { + var keychainQuery = baseKeychainQuery + + #if os(macOS) + if #available(OSX 10.15, *) { + // Don't over-specify our query. We don't know if the values were written post-Catalina. + keychainQuery[kSecUseDataProtectionKeychain as String] = nil + } + #endif + + keychainQuery[kSecAttrAccessible as String] = "dk" // kSecAttrAccessibleAlways, but with the value hardcoded to avoid a build warning. + let accessibilityDescription = "AccessibleAlways" + let serviceAttribute: String + switch service { + case let .sharedGroup(sharedGroupIdentifier, _): + serviceAttribute = Service.sharedGroup(with: configuration, identifier: sharedGroupIdentifier, accessibilityDescription: accessibilityDescription) + case .standard: + serviceAttribute = Service.standard(with: configuration, identifier: identifier, accessibilityDescription: accessibilityDescription) + #if os(macOS) + case let .sharedGroupOverride(sharedGroupIdentifier, _): + serviceAttribute = sharedGroupIdentifier.description + case .standardOverride: + serviceAttribute = identifier.description + #endif + } + keychainQuery[kSecAttrService as String] = serviceAttribute + try migrateObjects(matching: keychainQuery, removeOnCompletion: removeOnCompletion) + } + + /// Call this method if your Valet used to have its accessibility set to `alwaysThisDeviceOnly`. + /// This method migrates objects set on a Valet with the same type and identifier, but with its accessibility set to `alwaysThisDeviceOnly` (which was possible prior to Valet 4.0) to the current Valet. + /// - Parameter removeOnCompletion: If `true`, the migrated data will be removed from the keychain if the migration succeeds. + /// - Throws: An error of type `KeychainError` or `MigrationError`. + /// - Note: The keychain is not modified if an error is thrown. + @objc + public func migrateObjectsFromAlwaysAccessibleThisDeviceOnlyValet(removeOnCompletion: Bool) throws { + var keychainQuery = baseKeychainQuery + + #if os(macOS) + if #available(OSX 10.15, *) { + // Don't over-specify our query. We don't know if the values were written post-Catalina. + keychainQuery[kSecUseDataProtectionKeychain as String] = nil + } + #endif + + keychainQuery[kSecAttrAccessible as String] = "dku" // kSecAttrAccessibleAlwaysThisDeviceOnly, but with the value hardcoded to avoid a build warning. + let accessibilityDescription = "AccessibleAlwaysThisDeviceOnly" + let serviceAttribute: String + switch service { + case let .sharedGroup(identifier, _): + serviceAttribute = Service.sharedGroup(with: configuration, identifier: identifier, accessibilityDescription: accessibilityDescription) + case .standard: + serviceAttribute = Service.standard(with: configuration, identifier: identifier, accessibilityDescription: accessibilityDescription) + #if os(macOS) + case .sharedGroupOverride: + serviceAttribute = Service.sharedGroup(with: configuration, explicitlySetIdentifier: identifier, accessibilityDescription: accessibilityDescription) + case .standardOverride: + serviceAttribute = Service.standard(with: configuration, identifier: identifier, accessibilityDescription: accessibilityDescription) + #endif + } + keychainQuery[kSecAttrService as String] = serviceAttribute + try migrateObjects(matching: keychainQuery, removeOnCompletion: removeOnCompletion) } #if os(macOS) /// Migrates objects that were written to this Valet prior to macOS 10.15 to a format that can be read on macOS 10.15 and later. The new format is backwards compatible, allowing these values to be read on older operating systems. - /// - returns: Whether the migration succeeded or failed. - /// - note: The keychain is not modified if a failure occurs. This method can only be called from macOS 10.15 or later. + /// - Throws: An error of type `KeychainError` or `MigrationError`. + /// - Note: The keychain is not modified if an error is thrown. @available(macOS 10.15, *) - @objc(migrateObjectsFromPreCatalina) - public func migrateObjectsFromPreCatalina() -> MigrationResult { - var baseQuery = keychainQuery - #if swift(>=5.1) - baseQuery[kSecUseDataProtectionKeychain as String] = false - #else - baseQuery["nleg"] = false // kSecUseDataProtectionKeychain for Xcode 9 and Xcode 10 compatibility. - #endif + @objc + public func migrateObjectsFromPreCatalina() throws { + var keychainQuery = baseKeychainQuery + keychainQuery[kSecUseDataProtectionKeychain as String] = false // We do not need to remove these items on completion, since we are updating the kSecUseDataProtectionKeychain attribute in-place. - return migrateObjects(matching: baseQuery, removeOnCompletion: false) + try migrateObjects(matching: keychainQuery, removeOnCompletion: false) } #endif @@ -265,6 +451,7 @@ public final class Valet: NSObject, KeychainQueryConvertible { internal let configuration: Configuration internal let service: Service + internal let baseKeychainQuery: [String : AnyHashable] // MARK: Private Properties @@ -278,10 +465,11 @@ public final class Valet: NSObject, KeychainQueryConvertible { extension Valet { // MARK: Public Class Methods - - /// - parameter identifier: A non-empty string that uniquely identifies a Valet. - /// - parameter accessibility: The desired accessibility for the Valet. - /// - returns: A Valet that reads/writes keychain elements with the desired accessibility. + + /// - Parameters: + /// - identifier: A non-empty string that uniquely identifies a Valet. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet that reads/writes keychain elements with the desired accessibility. @available(swift, obsoleted: 1.0) @objc(valetWithIdentifier:accessibility:) public class func 🚫swift_vanillaValet(with identifier: String, accessibility: Accessibility) -> Valet? { @@ -290,10 +478,11 @@ extension Valet { } return valet(with: identifier, accessibility: accessibility) } - - /// - parameter identifier: A non-empty string that uniquely identifies a Valet. - /// - parameter accessibility: The desired accessibility for the Valet. - /// - returns: A Valet that reads/writes iCloud-shared keychain elements with the desired accessibility. + + /// - Parameters: + /// - identifier: A non-empty string that uniquely identifies a Valet. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet that reads/writes iCloud-shared keychain elements with the desired accessibility. @available(swift, obsoleted: 1.0) @objc(iCloudValetWithIdentifier:accessibility:) public class func 🚫swift_iCloudValet(with identifier: String, accessibility: CloudAccessibility) -> Valet? { @@ -302,31 +491,147 @@ extension Valet { } return iCloudValet(with: identifier, accessibility: accessibility) } - - /// - parameter identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. - /// - parameter 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. + + /// - Parameters: + /// - appIDPrefix: The application's App ID prefix. This string can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty. + /// - identifier: An identifier that corresponds to a value in keychain-access-groups in the application's Entitlements file. This string must not be empty. + /// - 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. + /// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps + @available(swift, obsoleted: 1.0) + @objc(sharedGroupValetWithAppIDPrefix:sharedGroupIdentifier:accessibility:) + public class func 🚫swift_vanillaSharedGroupValet(appIDPrefix: String, nonEmptyIdentifier identifier: String, accessibility: Accessibility) -> Valet? { + guard let identifier = SharedGroupIdentifier(appIDPrefix: appIDPrefix, nonEmptyGroup: identifier) else { + return nil + } + return sharedGroupValet(with: identifier, accessibility: accessibility) + } + + /// - 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. + /// - 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. + /// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps + @available(swift, obsoleted: 1.0) + @objc(sharedGroupValetWithGroupPrefix:sharedGroupIdentifier:accessibility:) + public class func 🚫swift_vanillaSharedGroupValet(groupPrefix: String, nonEmptyIdentifier identifier: String, accessibility: Accessibility) -> Valet? { + guard let identifier = SharedGroupIdentifier(groupPrefix: groupPrefix, nonEmptyGroup: identifier) else { + return nil + } + return sharedGroupValet(with: identifier, accessibility: accessibility) + } + + /// - Parameters: + /// - appIDPrefix: The application's App ID prefix. This string can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty. + /// - identifier: An identifier that corresponds to a value in keychain-access-groups in the application's Entitlements file. This string must not be empty. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet that reads/writes iCloud-shared keychain elements that can be shared across applications written by the same development team. + @available(swift, obsoleted: 1.0) + @objc(iCloudValetWithAppIDPrefix:sharedGroupIdentifier:accessibility:) + public class func 🚫swift_iCloudSharedGroupValet(appIDPrefix: String, nonEmptyIdentifier identifier: String, accessibility: CloudAccessibility) -> Valet? { + guard let identifier = SharedGroupIdentifier(appIDPrefix: appIDPrefix, nonEmptyGroup: identifier) else { + return nil + } + return iCloudSharedGroupValet(with: identifier, accessibility: accessibility) + } + + /// - 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. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet that reads/writes iCloud-shared keychain elements that can be shared across applications written by the same development team. + @available(swift, obsoleted: 1.0) + @objc(iCloudValetWithGroupPrefix:sharedGroupIdentifier:accessibility:) + public class func 🚫swift_iCloudSharedGroupValet(groupPrefix: String, nonEmptyIdentifier identifier: String, accessibility: CloudAccessibility) -> Valet? { + guard let identifier = SharedGroupIdentifier(groupPrefix: groupPrefix, nonEmptyGroup: identifier) else { + return nil + } + return iCloudSharedGroupValet(with: identifier, accessibility: accessibility) + } + + #if os(macOS) + /// Creates a Valet with an explicitly set kSecAttrService. + /// - Parameters: + /// - identifier: A non-empty string that uniquely identifies a Valet. Must be unique relative to other Valet identifiers. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet that reads/writes keychain elements with the desired accessibility and identifier. + /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. + /// - SeeAlso: https://github.com/square/Valet/issues/140 @available(swift, obsoleted: 1.0) - @objc(valetWithSharedAccessGroupIdentifier:accessibility:) - public class func 🚫swift_vanillaSharedAccessGroupValet(with identifier: String, accessibility: Accessibility) -> Valet? { + @objc(valetWithExplicitlySetIdentifier:accessibility:) + public class func 🚫swift_valet(withExplicitlySet identifier: String, accessibility: Accessibility) -> Valet? { guard let identifier = Identifier(nonEmpty: identifier) else { return nil } - return sharedAccessGroupValet(with: identifier, accessibility: accessibility) + return findOrCreate(explicitlySet: identifier, configuration: .valet(accessibility)) } - - /// - parameter identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. - /// - parameter accessibility: The desired accessibility for the Valet. - /// - returns: A Valet that reads/writes iCloud-shared keychain elements that can be shared across applications written by the same development team. + + /// Creates an iCloud Valet with an explicitly set kSecAttrService. + /// - Parameters: + /// - identifier: A non-empty string that uniquely identifies a Valet. Must be unique relative to other Valet identifiers. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet (synchronized with iCloud) that reads/writes keychain elements with the desired accessibility and identifier. + /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. + /// - SeeAlso: https://github.com/square/Valet/issues/140 @available(swift, obsoleted: 1.0) - @objc(iCloudValetWithSharedAccessGroupIdentifier:accessibility:) - public class func 🚫swift_iCloudSharedAccessGroupValet(with identifier: String, accessibility: CloudAccessibility) -> Valet? { + @objc(iCloudValetWithExplicitlySetIdentifier:accessibility:) + public class func 🚫swift_iCloudValet(withExplicitlySet identifier: String, accessibility: CloudAccessibility) -> Valet? { guard let identifier = Identifier(nonEmpty: identifier) else { return nil } - return iCloudSharedAccessGroupValet(with: identifier, accessibility: accessibility) + return findOrCreate(explicitlySet: identifier, configuration: .iCloud(accessibility)) } - + + /// Creates a shared-access-group Valet with an explicitly set kSecAttrService. + /// - Parameters: + /// - appIDPrefix: The application's App ID prefix. This string can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty. + /// - identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. Must be unique relative to other Valet identifiers. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet that reads/writes keychain elements that can be shared across applications written by the same development team. + /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. + /// - SeeAlso: https://github.com/square/Valet/issues/140 + /// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps + @available(swift, obsoleted: 1.0) + @objc(valetWithAppIDPrefix:explicitlySetSharedGroupIdentifier:accessibility:) + public class func 🚫swift_sharedGroupValet(appIDPrefix: String, withExplicitlySet identifier: String, accessibility: Accessibility) -> Valet? { + guard let identifier = SharedGroupIdentifier(appIDPrefix: appIDPrefix, nonEmptyGroup: identifier) else { + return nil + } + return findOrCreate(explicitlySet: identifier, configuration: .valet(accessibility)) + } + + /// Creates an iCloud-shared-access-group Valet with an explicitly set kSecAttrService. + /// - Parameters: + /// - appIDPrefix: The application's App ID prefix. This string can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty. + /// - identifier: A non-empty string that must correspond with the value for keychain-access-groups in your Entitlements file. Must be unique relative to other Valet identifiers. + /// - accessibility: The desired accessibility for the Valet. + /// - Returns: A Valet (synchronized with iCloud) that reads/writes keychain elements that can be shared across applications written by the same development team. + /// - Warning: Using an explicitly set kSecAttrService bypasses this project’s guarantee that one Valet type will not have access to one another type’s key:value pairs. To maintain this guarantee, ensure that each Valet’s identifier is globally unique. + /// - SeeAlso: https://github.com/square/Valet/issues/140 + /// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps + @available(swift, obsoleted: 1.0) + @objc(iCloudValetWithAppIDPrefix:explicitlySetSharedGroupIdentifier:accessibility:) + public class func 🚫swift_iCloudSharedGroupValet(appIDPrefix: String, withExplicitlySet identifier: String, accessibility: CloudAccessibility) -> Valet? { + guard let identifier = SharedGroupIdentifier(appIDPrefix: appIDPrefix, nonEmptyGroup: identifier) else { + return nil + } + return findOrCreate(explicitlySet: identifier, configuration: .iCloud(accessibility)) + } + #endif + + /// - Parameter key: The key to look up in the keychain. + /// - Returns: `true` if a value has been set for the given key, `false` otherwise. Will return `false` if the keychain is not accessible. + /// - Note: Will never prompt the user for Face ID, Touch ID, or password. + @available(swift, obsoleted: 1.0) + @objc(containsObjectForKey:) + public func 🚫swift_containsObject(forKey key: String) -> Bool { + guard let containsObject = try? containsObject(forKey: key) else { + return false + } + return containsObject + } + } // MARK: - Testing @@ -335,18 +640,54 @@ internal extension Valet { // MARK: Permutations - class func permutations(with identifier: Identifier, shared: Bool = false) -> [Valet] { - return Accessibility.allValues().map { accessibility in - let valet: Valet = shared ? .sharedAccessGroupValet(with: identifier, accessibility: accessibility) : .valet(with: identifier, accessibility: accessibility) - return valet + class func permutations(with identifier: Identifier) -> [Valet] { + Accessibility.allCases.map { accessibility in + .valet(with: identifier, accessibility: accessibility) } } - class func iCloudPermutations(with identifier: Identifier, shared: Bool = false) -> [Valet] { - return CloudAccessibility.allValues().map { cloudAccessibility in - let valet: Valet = shared ? .iCloudSharedAccessGroupValet(with: identifier, accessibility: cloudAccessibility) : .iCloudValet(with: identifier, accessibility: cloudAccessibility) - return valet + class func permutations(with identifier: SharedGroupIdentifier) -> [Valet] { + Accessibility.allCases.map { accessibility in + .sharedGroupValet(with: identifier, accessibility: accessibility) + } + } + + class func iCloudPermutations(with identifier: Identifier) -> [Valet] { + CloudAccessibility.allCases.map { cloudAccessibility in + .iCloudValet(with: identifier, accessibility: cloudAccessibility) + } + } + + class func iCloudPermutations(with identifier: SharedGroupIdentifier) -> [Valet] { + CloudAccessibility.allCases.map { cloudAccessibility in + .iCloudSharedGroupValet(with: identifier, accessibility: cloudAccessibility) + } + } + + #if os(macOS) + class func permutations(withExplictlySet identifier: Identifier, shared: Bool = false) -> [Valet] { + Accessibility.allCases.map { accessibility in + .valet(withExplicitlySet: identifier, accessibility: accessibility) } } + class func permutations(withExplictlySet identifier: SharedGroupIdentifier) -> [Valet] { + Accessibility.allCases.map { accessibility in + .sharedGroupValet(withExplicitlySet: identifier, accessibility: accessibility) + } + } + + class func iCloudPermutations(withExplictlySet identifier: Identifier, shared: Bool = false) -> [Valet] { + CloudAccessibility.allCases.map { cloudAccessibility in + .iCloudValet(withExplicitlySet: identifier, accessibility: cloudAccessibility) + } + } + + class func iCloudPermutations(withExplictlySet identifier: SharedGroupIdentifier) -> [Valet] { + CloudAccessibility.allCases.map { cloudAccessibility in + .iCloudSharedGroupValet(withExplicitlySet: identifier, accessibility: cloudAccessibility) + } + } + #endif + } diff --git a/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SecureEnclaveBackwardsCompatibilityTests.swift b/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SecureEnclaveBackwardsCompatibilityTests.swift index 557b03fd..60f487c0 100644 --- a/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SecureEnclaveBackwardsCompatibilityTests.swift +++ b/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SecureEnclaveBackwardsCompatibilityTests.swift @@ -27,15 +27,15 @@ import XCTest extension SecureEnclaveIntegrationTests { @available (*, deprecated) - func test_backwardsCompatibility_withLegacyValet() + func test_backwardsCompatibility_withLegacyValet() throws { - guard testEnvironmentIsSigned() else { + guard testEnvironmentIsSigned() && testEnvironmentSupportsWhenPasscodeSet() else { return } let deprecatedValet = VALLegacySecureEnclaveValet(identifier: valet.identifier.description)! XCTAssertTrue(deprecatedValet.setString(passcode, forKey: key)) - XCTAssertEqual(.success(passcode), valet.string(forKey: key, withPrompt: "")) + XCTAssertEqual(passcode, try valet.string(forKey: key, withPrompt: "")) } } diff --git a/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SinglePromptSecureEnclaveBackwardsCompatibilityTests.swift b/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SinglePromptSecureEnclaveBackwardsCompatibilityTests.swift index bed0b38a..8e52ecc0 100644 --- a/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SinglePromptSecureEnclaveBackwardsCompatibilityTests.swift +++ b/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SinglePromptSecureEnclaveBackwardsCompatibilityTests.swift @@ -24,19 +24,19 @@ import LegacyValet import XCTest -@available(tvOS 10.0, *) +@available(tvOS 11.0, *) extension SinglePromptSecureEnclaveIntegrationTests { @available (*, deprecated) - func test_backwardsCompatibility_withLegacyValet() + func test_backwardsCompatibility_withLegacyValet() throws { - guard testEnvironmentIsSigned() else { + guard testEnvironmentIsSigned() && testEnvironmentSupportsWhenPasscodeSet() else { return } - let deprecatedValet = VALLegacySinglePromptSecureEnclaveValet(identifier: valet.identifier.description)! + let deprecatedValet = VALLegacySinglePromptSecureEnclaveValet(identifier: valet().identifier.description)! XCTAssertTrue(deprecatedValet.setString(passcode, forKey: key)) - XCTAssertEqual(.success(passcode), valet.string(forKey: key, withPrompt: "")) + XCTAssertEqual(passcode, try valet().string(forKey: key, withPrompt: "")) } } diff --git a/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SynchronizableBackwardsCompatibilityTests.swift b/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SynchronizableBackwardsCompatibilityTests.swift index 2c3665db..970c828b 100644 --- a/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SynchronizableBackwardsCompatibilityTests.swift +++ b/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SynchronizableBackwardsCompatibilityTests.swift @@ -28,30 +28,30 @@ extension CloudIntegrationTests { // MARK: Backwards Compatibility - func test_backwardsCompatibility_withLegacyValet() { + func test_backwardsCompatibility_withLegacyValet() throws { guard testEnvironmentIsSigned() else { return } let identifier = Identifier(nonEmpty: "BackwardsCompatibilityTest")! - Valet.iCloudCurrentAndLegacyPermutations(with: identifier).forEach { permutation, legacyValet in + try Valet.iCloudCurrentAndLegacyPermutations(with: identifier).forEach { permutation, legacyValet in legacyValet.setString(passcode, forKey: key) XCTAssertNotNil(legacyValet.string(forKey: key)) - XCTAssertEqual(legacyValet.string(forKey: key), permutation.string(forKey: key), "\(permutation) was not able to read from legacy counterpart: \(legacyValet)") + XCTAssertEqual(legacyValet.string(forKey: key), try permutation.string(forKey: key), "\(permutation) was not able to read from legacy counterpart: \(legacyValet)") } } - func test_backwardsCompatibility_withSharedAccessGroupLegacyValet() { + func test_backwardsCompatibility_withSharedAccessGroupLegacyValet() throws { guard testEnvironmentIsSigned() else { return } - Valet.iCloudCurrentAndLegacyPermutations(with: Valet.sharedAccessGroupIdentifier, shared: true).forEach { permutation, legacyValet in + try Valet.iCloudCurrentAndLegacyPermutations(with: Valet.sharedAccessGroupIdentifier).forEach { permutation, legacyValet in legacyValet.setString(passcode, forKey: key) XCTAssertNotNil(legacyValet.string(forKey: key)) - XCTAssertEqual(legacyValet.string(forKey: key), permutation.string(forKey: key), "\(permutation) was not able to read from legacy counterpart: \(legacyValet)") + XCTAssertEqual(legacyValet.string(forKey: key), try permutation.string(forKey: key), "\(permutation) was not able to read from legacy counterpart: \(legacyValet)") } } diff --git a/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/ValetBackwardsCompatibilityTests.swift b/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/ValetBackwardsCompatibilityTests.swift index 16f2ba8a..1d5f9d2b 100644 --- a/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/ValetBackwardsCompatibilityTests.swift +++ b/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/ValetBackwardsCompatibilityTests.swift @@ -30,15 +30,24 @@ import XCTest internal extension Valet { var legacyIdentifier: String { - return identifier.description + switch service { + case let .sharedGroup(sharedAccessGroupIdentifier, _): + return sharedAccessGroupIdentifier.groupIdentifier + case let .standard(identifier, _): + return identifier.description + #if os(macOS) + case let .sharedGroupOverride(identifier, _): + return identifier.groupIdentifier + case let .standardOverride(identifier, _): + return identifier.description + #endif + } } var legacyAccessibility: VALLegacyAccessibility { switch accessibility { case .afterFirstUnlock: return .afterFirstUnlock case .afterFirstUnlockThisDeviceOnly: return .afterFirstUnlockThisDeviceOnly - case .always: return .always - case .alwaysThisDeviceOnly: return .alwaysThisDeviceOnly case .whenPasscodeSetThisDeviceOnly: return .whenPasscodeSetThisDeviceOnly case .whenUnlocked: return .whenUnlocked case .whenUnlockedThisDeviceOnly: return .whenUnlockedThisDeviceOnly @@ -51,15 +60,25 @@ internal extension Valet { switch service { case .standard: return VALLegacyValet(identifier: legacyIdentifier, accessibility: legacyAccessibility)! - case .sharedAccessGroup: + case .sharedGroup: return VALLegacyValet(sharedAccessGroupIdentifier: legacyIdentifier, accessibility: legacyAccessibility)! + #if os(macOS) + case .standardOverride, + .sharedGroupOverride: + fatalError("There is no legacy Valet for a service override valet") + #endif } case .iCloud: switch service { case .standard: return VALSynchronizableValet(identifier: legacyIdentifier, accessibility: legacyAccessibility)! - case .sharedAccessGroup: + case .sharedGroup: return VALSynchronizableValet(sharedAccessGroupIdentifier: legacyIdentifier, accessibility: legacyAccessibility)! + #if os(macOS) + case .standardOverride, + .sharedGroupOverride: + fatalError("There is no legacy Valet for a service override valet") + #endif } default: @@ -69,15 +88,27 @@ internal extension Valet { // MARK: Permutations - class func currentAndLegacyPermutations(with identifier: Identifier, shared: Bool = false) -> [(Valet, VALLegacyValet)] { - return permutations(with: identifier, shared: shared).map { - return ($0, $0.legacyValet) + class func currentAndLegacyPermutations(with identifier: Identifier) -> [(Valet, VALLegacyValet)] { + permutations(with: identifier).map { + ($0, $0.legacyValet) + } + } + + class func currentAndLegacyPermutations(with identifier: SharedGroupIdentifier) -> [(Valet, VALLegacyValet)] { + permutations(with: identifier).map { + ($0, $0.legacyValet) } } - class func iCloudCurrentAndLegacyPermutations(with identifier: Identifier, shared: Bool = false) -> [(Valet, VALSynchronizableValet)] { - return iCloudPermutations(with: identifier, shared: shared).map { - return ($0, $0.legacyValet as! VALSynchronizableValet) + class func iCloudCurrentAndLegacyPermutations(with identifier: Identifier) -> [(Valet, VALSynchronizableValet)] { + iCloudPermutations(with: identifier).map { + ($0, $0.legacyValet as! VALSynchronizableValet) + } + } + + class func iCloudCurrentAndLegacyPermutations(with identifier: SharedGroupIdentifier) -> [(Valet, VALSynchronizableValet)] { + iCloudPermutations(with: identifier).map { + ($0, $0.legacyValet as! VALSynchronizableValet) } } } @@ -88,32 +119,77 @@ class ValetBackwardsCompatibilityIntegrationTests: ValetIntegrationTests { // MARK: Tests - func test_backwardsCompatibility_withLegacyValet() { - Valet.currentAndLegacyPermutations(with: vanillaValet.identifier).forEach { permutation, legacyValet in + func test_backwardsCompatibility_withLegacyValet() throws { + try Valet.currentAndLegacyPermutations(with: vanillaValet.identifier).forEach { permutation, legacyValet in legacyValet.setString(passcode, forKey: key) XCTAssertNotNil(legacyValet.string(forKey: key)) if #available(OSX 10.15, *) { #if os(macOS) - _ = permutation.migrateObjectsFromPreCatalina() + try permutation.migrateObjectsFromPreCatalina() #endif } - XCTAssertEqual(legacyValet.string(forKey: key), permutation.string(forKey: key), "\(permutation) was not able to read from legacy counterpart: \(legacyValet)") + XCTAssertEqual(legacyValet.string(forKey: key), try permutation.string(forKey: key), "\(permutation) was not able to read from legacy counterpart: \(legacyValet)") } } - func test_backwardsCompatibility_withLegacySharedAccessGroupValet() { - Valet.currentAndLegacyPermutations(with: Valet.sharedAccessGroupIdentifier, shared: true).forEach { permutation, legacyValet in + func test_backwardsCompatibility_withLegacySharedAccessGroupValet() throws { + guard testEnvironmentIsSigned() else { + return + } + try Valet.currentAndLegacyPermutations(with: Valet.sharedAccessGroupIdentifier).forEach { permutation, legacyValet in legacyValet.setString(passcode, forKey: key) XCTAssertNotNil(legacyValet.string(forKey: key)) if #available(OSX 10.15, *) { #if os(macOS) - _ = permutation.migrateObjectsFromPreCatalina() + try permutation.migrateObjectsFromPreCatalina() #endif } - XCTAssertEqual(legacyValet.string(forKey: key), permutation.string(forKey: key), "\(permutation) was not able to read from legacy counterpart: \(legacyValet)") + XCTAssertEqual(legacyValet.string(forKey: key), try permutation.string(forKey: key), "\(permutation) was not able to read from legacy counterpart: \(legacyValet)") + } + } + + func test_migrateObjectsFromAlwaysAccessibleValet_forwardsCompatibility_fromLegacyValet() throws { + let alwaysAccessibleLegacyValet = VALLegacyValet(identifier: vanillaValet.identifier.description, accessibility: .always)! + alwaysAccessibleLegacyValet.setString(passcode, forKey: key) + + let valet = Valet.valet(with: vanillaValet.identifier, accessibility: .afterFirstUnlock) + XCTAssertNoThrow(try valet.migrateObjectsFromAlwaysAccessibleValet(removeOnCompletion: true)) + XCTAssertEqual(try valet.string(forKey: key), passcode) + } + + func test_migrateObjectsFromAlwaysAccessibleThisDeviceOnlyValet_forwardsCompatibility_fromLegacyValet() throws { + let alwaysAccessibleLegacyValet = VALLegacyValet(identifier: vanillaValet.identifier.description, accessibility: .alwaysThisDeviceOnly)! + alwaysAccessibleLegacyValet.setString(passcode, forKey: key) + + let valet = Valet.valet(with: vanillaValet.identifier, accessibility: .afterFirstUnlockThisDeviceOnly) + XCTAssertNoThrow(try valet.migrateObjectsFromAlwaysAccessibleThisDeviceOnlyValet(removeOnCompletion: true)) + XCTAssertEqual(try valet.string(forKey: key), passcode) + } + + func test_migrateObjectsFromAlwaysAccessibleValet_forwardsCompatibility_withLegacySharedAccessGroupValet() throws { + guard testEnvironmentIsSigned() else { + return } + let alwaysAccessibleLegacyValet = VALLegacyValet(sharedAccessGroupIdentifier: Valet.sharedAccessGroupIdentifier.groupIdentifier, accessibility: .always)! + alwaysAccessibleLegacyValet.setString(passcode, forKey: key) + + let valet = Valet.sharedGroupValet(with: Valet.sharedAccessGroupIdentifier, accessibility: .afterFirstUnlock) + XCTAssertNoThrow(try valet.migrateObjectsFromAlwaysAccessibleValet(removeOnCompletion: true)) + XCTAssertEqual(try valet.string(forKey: key), passcode) + } + + func test_migrateObjectsFromAlwaysAccessibleThisDeviceOnlyValet_forwardsCompatibility_withLegacySharedAccessGroupValet() throws { + guard testEnvironmentIsSigned() else { + return + } + let alwaysAccessibleLegacyValet = VALLegacyValet(sharedAccessGroupIdentifier: Valet.sharedAccessGroupIdentifier.groupIdentifier, accessibility: .alwaysThisDeviceOnly)! + alwaysAccessibleLegacyValet.setString(passcode, forKey: key) + + let valet = Valet.sharedGroupValet(with: Valet.sharedAccessGroupIdentifier, accessibility: .afterFirstUnlockThisDeviceOnly) + XCTAssertNoThrow(try valet.migrateObjectsFromAlwaysAccessibleThisDeviceOnlyValet(removeOnCompletion: true)) + XCTAssertEqual(try valet.string(forKey: key), passcode) } } diff --git a/Tests/ValetIntegrationTests/CloudIntegrationTests.swift b/Tests/ValetIntegrationTests/CloudIntegrationTests.swift index 462c2623..c9db877b 100644 --- a/Tests/ValetIntegrationTests/CloudIntegrationTests.swift +++ b/Tests/ValetIntegrationTests/CloudIntegrationTests.swift @@ -30,7 +30,7 @@ class CloudIntegrationTests: XCTestCase static let accessibility = CloudAccessibility.whenUnlocked var allPermutations: [Valet] { return (testEnvironmentIsSigned() - ? Valet.iCloudPermutations(with: CloudIntegrationTests.identifier) + Valet.iCloudPermutations(with: ValetIntegrationTests.identifier, shared: true) + ? Valet.iCloudPermutations(with: CloudIntegrationTests.identifier.asIdentifier) + Valet.iCloudPermutations(with: CloudIntegrationTests.identifier) : []) } let key = "key" @@ -39,15 +39,17 @@ class CloudIntegrationTests: XCTestCase override func setUp() { super.setUp() - - ErrorHandler.customAssertBody = { _, _, _, _ in - // Nothing to do here. - } - allPermutations.forEach { testValet in testValet.removeAllObjects() } + allPermutations.forEach { testValet in + do { + try testValet.removeAllObjects() + } catch { + XCTFail("Error removing objects from Valet \(testValet): \(error)") + } + } } - func test_synchronizableValet_isDistinctFromVanillaValetWithEqualConfiguration() + func test_synchronizableValet_isDistinctFromVanillaValetWithEqualConfiguration() throws { guard testEnvironmentIsSigned() else { return @@ -58,33 +60,39 @@ class CloudIntegrationTests: XCTestCase let iCloudValet = Valet.iCloudValet(with: identifier, accessibility: .afterFirstUnlock) // Setting - XCTAssertTrue(iCloudValet.set(string: "butts", forKey: "cloud")) - XCTAssertEqual("butts", iCloudValet.string(forKey: "cloud")) - XCTAssertNil(vanillaValet.string(forKey: "cloud")) + try iCloudValet.setString("butts", forKey: "cloud") + XCTAssertEqual("butts", try iCloudValet.string(forKey: "cloud")) + XCTAssertThrowsError(try vanillaValet.string(forKey: "cloud")) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } // Removal - XCTAssertTrue(vanillaValet.set(string: "snake people", forKey: "millennials")) - XCTAssertTrue(iCloudValet.removeObject(forKey: "millennials")) - XCTAssertEqual("snake people", vanillaValet.string(forKey: "millennials")) + try vanillaValet.setString("snake people", forKey: "millennials") + try iCloudValet.removeObject(forKey: "millennials") + XCTAssertEqual("snake people", try vanillaValet.string(forKey: "millennials")) } - func test_setStringForKey() + func test_setStringForKey() throws { - allPermutations.forEach { valet in - XCTAssertNil(valet.string(forKey: key), "\(valet) read item from keychain that should not exist") - XCTAssertTrue(valet.set(string: passcode, forKey: key), "\(valet) could not set item in keychain") - XCTAssertEqual(passcode, valet.string(forKey: key)) + try allPermutations.forEach { valet in + XCTAssertThrowsError(try valet.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + try valet.setString(passcode, forKey: key) + XCTAssertEqual(passcode, try valet.string(forKey: key)) } } - func test_removeObjectForKey() + func test_removeObjectForKey() throws { - allPermutations.forEach { valet in - XCTAssertTrue(valet.set(string: passcode, forKey: key), "\(valet) could not set item in keychain") - XCTAssertEqual(passcode, valet.string(forKey: key), "\(valet) read incorrect value from keychain.") + try allPermutations.forEach { valet in + try valet.setString(passcode, forKey: key) + XCTAssertEqual(passcode, try valet.string(forKey: key)) - XCTAssertTrue(valet.removeObject(forKey: key), "\(valet) did not remove item from keychain.") - XCTAssertNil(valet.string(forKey: key), "\(valet) found removed item in keychain.") + try valet.removeObject(forKey: key) + XCTAssertThrowsError(try valet.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } } diff --git a/Tests/ValetIntegrationTests/MacTests.swift b/Tests/ValetIntegrationTests/MacTests.swift index 596ad098..7407879a 100644 --- a/Tests/ValetIntegrationTests/MacTests.swift +++ b/Tests/ValetIntegrationTests/MacTests.swift @@ -20,7 +20,8 @@ import Foundation import XCTest -import Valet + +@testable import Valet #if os(macOS) class ValetMacTests: XCTestCase @@ -28,14 +29,14 @@ class ValetMacTests: XCTestCase // This test verifies that we are neutralizing the zero-day Mac OS X Access Control List vulnerability. // Whitepaper: https://drive.google.com/file/d/0BxxXk1d3yyuZOFlsdkNMSGswSGs/view // Square Corner blog post: https://corner.squareup.com/2015/06/valet-beats-the-ox-x-keychain-access-control-list-zero-day-vulnerability.html - func test_setStringForKey_neutralizesMacOSAccessControlListVuln() + func test_setStringForKey_neutralizesMacOSAccessControlListVuln() throws { let valet = Valet.valet(with: Identifier(nonEmpty: "MacOSVulnTest")!, accessibility: .whenUnlocked) let vulnKey = "KeepIt" let vulnValue = "Secret" - valet.removeObject(forKey: vulnKey) + try valet.removeObject(forKey: vulnKey) - var query = valet.keychainQuery + var query = valet.baseKeychainQuery query[kSecAttrAccount as String] = vulnKey var accessList: SecAccess? @@ -65,7 +66,7 @@ class ValetMacTests: XCTestCase XCTAssertEqual(SecItemAdd(accessListQuery as CFDictionary, nil), errSecSuccess) // The potentially vulnerable keychain item should exist in our Valet now. - XCTAssertTrue(valet.containsObject(forKey: vulnKey)) + XCTAssertTrue(try valet.containsObject(forKey: vulnKey)) // Obtain a reference to the vulnerable keychain entry. query[kSecReturnWorkingReference as String] = true @@ -90,7 +91,7 @@ class ValetMacTests: XCTestCase // Update the vulnerable value with Valet - we should have deleted the existing item, making the entry no longer vulnerable. let updatedValue = "Safe" - XCTAssertTrue(valet.set(string: updatedValue, forKey: vulnKey)) + try valet.setString(updatedValue, forKey: vulnKey) // We should no longer be able to access the keychain item via the ref. let queryWithVulnerableReferenceAndAttributes = [ @@ -103,20 +104,162 @@ class ValetMacTests: XCTestCase // This is not be the case upon setting a breakpoint and inspecting before the valet.setString(, forKey:) call above. } + func test_withExplicitlySet_assignsExplicitIdentifier() throws { + let explicitlySetIdentifier = Identifier(nonEmpty: #function)! + Valet.permutations(withExplictlySet: explicitlySetIdentifier, shared: false).forEach { + XCTAssertEqual($0.baseKeychainQuery[kSecAttrService as String], explicitlySetIdentifier.description) + } + + Valet.iCloudPermutations(withExplictlySet: explicitlySetIdentifier, shared: false).forEach { + XCTAssertEqual($0.baseKeychainQuery[kSecAttrService as String], explicitlySetIdentifier.description) + } + + guard testEnvironmentIsSigned() else { + return + } + + Valet.permutations(withExplictlySet: explicitlySetIdentifier, shared: true).forEach { + XCTAssertEqual($0.baseKeychainQuery[kSecAttrService as String], explicitlySetIdentifier.description) + } + + Valet.iCloudPermutations(withExplictlySet: explicitlySetIdentifier, shared: true).forEach { + XCTAssertEqual($0.baseKeychainQuery[kSecAttrService as String], explicitlySetIdentifier.description) + } + } + + func test_withExplicitlySet_canAccessKeychain() throws { + guard testEnvironmentIsSigned() else { + return + } + + let explicitlySetIdentifier = Identifier(nonEmpty: #function)! + try Valet.permutations(withExplictlySet: explicitlySetIdentifier, shared: false).forEach { + XCTAssertTrue($0.canAccessKeychain()) + + try $0.removeAllObjects() + } + + try Valet.iCloudPermutations(withExplictlySet: explicitlySetIdentifier, shared: false).forEach { + XCTAssertTrue($0.canAccessKeychain()) + + try $0.removeAllObjects() + } + + let explicitlySetSharedGroupIdentifier = Identifier(nonEmpty: "9XUJ7M53NG.com.squareup.Valet-macOS-Test-Host-App")! + try Valet.permutations(withExplictlySet: explicitlySetSharedGroupIdentifier, shared: true).forEach { + XCTAssertTrue($0.canAccessKeychain()) + + try $0.removeAllObjects() + } + + try Valet.iCloudPermutations(withExplictlySet: explicitlySetSharedGroupIdentifier, shared: true).forEach { + XCTAssertTrue($0.canAccessKeychain()) + + try $0.removeAllObjects() + } + } + + func test_withExplicitlySet_canReadWrittenString() throws { + guard testEnvironmentIsSigned() else { + return + } + + let explicitlySetIdentifier = Identifier(nonEmpty: #function)! + let key = "key" + let passcode = "12345" + + try Valet.permutations(withExplictlySet: explicitlySetIdentifier, shared: false).forEach { + try $0.setString(passcode, forKey: key) + XCTAssertEqual(try $0.string(forKey: key), passcode) + + try $0.removeAllObjects() + } + + try Valet.iCloudPermutations(withExplictlySet: explicitlySetIdentifier, shared: false).forEach { + try $0.setString(passcode, forKey: key) + XCTAssertEqual(try $0.string(forKey: key), passcode) + + try $0.removeAllObjects() + } + + let explicitlySetSharedGroupIdentifier = Identifier(nonEmpty: "9XUJ7M53NG.com.squareup.Valet-macOS-Test-Host-App")! + try Valet.permutations(withExplictlySet: explicitlySetSharedGroupIdentifier, shared: true).forEach { + try $0.setString(passcode, forKey: key) + XCTAssertEqual(try $0.string(forKey: key), passcode) + + try $0.removeAllObjects() + } + + try Valet.iCloudPermutations(withExplictlySet: explicitlySetSharedGroupIdentifier, shared: true).forEach { + try $0.setString(passcode, forKey: key) + XCTAssertEqual(try $0.string(forKey: key), passcode) + + try $0.removeAllObjects() + } + } + + func test_withExplicitlySet_vendsSameObjectWhenSameConfigurationRequested() { + let explicitlySetIdentifier = Identifier(nonEmpty: #function)! + var permutations1 = Valet.permutations(withExplictlySet: explicitlySetIdentifier, shared: false) + var permutations2 = Valet.permutations(withExplictlySet: explicitlySetIdentifier, shared: false) + for (index, permutation) in permutations1.enumerated() { + XCTAssertTrue(permutation === permutations2[index], "Two Valets with \(accessibilityValues[index]) were not identical") + } + + permutations1 = Valet.iCloudPermutations(withExplictlySet: explicitlySetIdentifier, shared: false) + permutations2 = Valet.iCloudPermutations(withExplictlySet: explicitlySetIdentifier, shared: false) + for (index, permutation) in permutations1.enumerated() { + XCTAssertTrue(permutation === permutations2[index], "Two iCloud Valets with \(accessibilityValues[index]) were not identical") + } + + let explicitlySetSharedGroupIdentifier = Identifier(nonEmpty: "com.squareup.Valet-macOS-Test-Host-App")! + permutations1 = Valet.permutations(withExplictlySet: explicitlySetSharedGroupIdentifier, shared: true) + permutations2 = Valet.permutations(withExplictlySet: explicitlySetSharedGroupIdentifier, shared: true) + for (index, permutation) in permutations1.enumerated() { + XCTAssertTrue(permutation === permutations2[index], "Two shared Valets with \(accessibilityValues[index]) were not identical") + } + + permutations1 = Valet.iCloudPermutations(withExplictlySet: explicitlySetSharedGroupIdentifier, shared: true) + permutations2 = Valet.iCloudPermutations(withExplictlySet: explicitlySetSharedGroupIdentifier, shared: true) + for (index, permutation) in permutations1.enumerated() { + XCTAssertTrue(permutation === permutations2[index], "Two shared iCloud Valets with \(accessibilityValues[index]) were not identical") + } + } + + func test_withExplicitlySet_createsObjectWithCorrectAccessibility() { + let explicitlySetIdentifier = Identifier(nonEmpty: #function)! + var permutations = Valet.permutations(withExplictlySet: explicitlySetIdentifier, shared: false) + for (index, permutation) in permutations.enumerated() { + XCTAssertEqual(accessibilityValues[index], permutation.accessibility) + } + + permutations = Valet.iCloudPermutations(withExplictlySet: explicitlySetIdentifier, shared: false) + for (index, permutation) in permutations.enumerated() { + XCTAssertEqual(accessibilityValues[index], permutation.accessibility) + } + + let explicitlySetSharedGroupIdentifier = Identifier(nonEmpty: "com.squareup.Valet-macOS-Test-Host-App")! + permutations = Valet.permutations(withExplictlySet: explicitlySetSharedGroupIdentifier, shared: true) + for (index, permutation) in permutations.enumerated() { + XCTAssertEqual(accessibilityValues[index], permutation.accessibility) + } + + permutations = Valet.iCloudPermutations(withExplictlySet: explicitlySetSharedGroupIdentifier, shared: true) + for (index, permutation) in permutations.enumerated() { + XCTAssertEqual(accessibilityValues[index], permutation.accessibility) + } + } + // MARK: Migration - PreCatalina - func test_migrateObjectsFromPreCatalina_migratesDataWrittenPreCatalina() { + func test_migrateObjectsFromPreCatalina_migratesDataWrittenPreCatalina() throws { guard #available(macOS 10.15, *) else { return } let valet = Valet.valet(with: Identifier(nonEmpty: "PreCatalinaTest")!, accessibility: .afterFirstUnlock) - var preCatalinaWriteQuery = valet.keychainQuery - #if swift(>=5.1) + var preCatalinaWriteQuery = valet.baseKeychainQuery preCatalinaWriteQuery[kSecUseDataProtectionKeychain as String] = nil - #else - preCatalinaWriteQuery["nleg"] = nil // kSecUseDataProtectionKeychain for Xcode 9 and Xcode 10 compatibility. - #endif let key = "PreCatalinaKey" let object = Data("PreCatalinaValue".utf8) @@ -127,10 +270,14 @@ class ValetMacTests: XCTestCase SecItemDelete(preCatalinaWriteQuery as CFDictionary) XCTAssertEqual(SecItemAdd(preCatalinaWriteQuery as CFDictionary, nil), errSecSuccess) - XCTAssertNil(valet.object(forKey: key)) - XCTAssertEqual(valet.migrateObjectsFromPreCatalina(), .success) - XCTAssertEqual(valet.object(forKey: key), object) + XCTAssertThrowsError(try valet.object(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + XCTAssertNoThrow(try valet.migrateObjectsFromPreCatalina()) + XCTAssertEqual(try valet.object(forKey: key), object) } + private let accessibilityValues = Accessibility.allCases + } #endif diff --git a/Tests/ValetIntegrationTests/SecItemTests.swift b/Tests/ValetIntegrationTests/SecItemTests.swift index d6579d54..2fcda4b5 100644 --- a/Tests/ValetIntegrationTests/SecItemTests.swift +++ b/Tests/ValetIntegrationTests/SecItemTests.swift @@ -25,23 +25,5 @@ import XCTest class SecItemTests: XCTestCase { - - func test_sharedAccessGroupPrefix_findsPrefix() { - #if os(watchOS) || os(tvOS) || os(iOS) - // CocoaPods app host DSL does not provide ability to edit the app host settings such - // the `DEVELOPER TEAM` so for now skip this assertion. - #if !COCOAPODS - XCTAssertEqual(SecItem.sharedAccessGroupPrefix, "9XUJ7M53NG") - #endif - #elseif os(macOS) - guard testEnvironmentIsSigned() else { - return - } - XCTAssertEqual(SecItem.sharedAccessGroupPrefix, "9XUJ7M53NG") - #else - // Currently unsupported build configuration. This next line will compile-time error. - doNotCommentOutThisLine() - #endif - } } diff --git a/Tests/ValetIntegrationTests/SecureEnclaveIntegrationTests.swift b/Tests/ValetIntegrationTests/SecureEnclaveIntegrationTests.swift index d49e06ed..509f47c7 100644 --- a/Tests/ValetIntegrationTests/SecureEnclaveIntegrationTests.swift +++ b/Tests/ValetIntegrationTests/SecureEnclaveIntegrationTests.swift @@ -34,39 +34,44 @@ class SecureEnclaveIntegrationTests: XCTestCase override func setUp() { super.setUp() - - ErrorHandler.customAssertBody = { _, _, _, _ in - // Nothing to do here. + + guard testEnvironmentIsSigned() else { + return + } + do { + try valet.removeAllObjects() + } catch { + XCTFail("Error removing objects from Valet \(valet): \(error)") } - - valet.removeObject(forKey: key) } // MARK: Equality - func test_secureEnclaveValetsWithEqualConfiguration_canAccessSameData() + func test_secureEnclaveValetsWithEqualConfiguration_canAccessSameData() throws { - guard testEnvironmentIsSigned() else { + guard testEnvironmentIsSigned() && testEnvironmentSupportsWhenPasscodeSet() else { return } - XCTAssertTrue(valet.set(string: passcode, forKey: key)) + try valet.setString(passcode, forKey: key) let equivalentValet = SecureEnclaveValet.valet(with: valet.identifier, accessControl: valet.accessControl) XCTAssertEqual(valet, equivalentValet) - XCTAssertEqual(.success(passcode), equivalentValet.string(forKey: key, withPrompt: "")) + XCTAssertEqual(passcode, try equivalentValet.string(forKey: key, withPrompt: "")) } - func test_secureEnclaveValetsWithDifferingAccessControl_canNotAccessSameData() + func test_secureEnclaveValetsWithDifferingAccessControl_canNotAccessSameData() throws { - guard testEnvironmentIsSigned() else { + guard testEnvironmentIsSigned() && testEnvironmentSupportsWhenPasscodeSet() else { return } - XCTAssertTrue(valet.set(string: passcode, forKey: key)) + try valet.setString(passcode, forKey: key) let equivalentValet = SecureEnclaveValet.valet(with: valet.identifier, accessControl: .devicePasscode) XCTAssertNotEqual(valet, equivalentValet) - XCTAssertEqual(.success(passcode), valet.string(forKey: key, withPrompt: "")) - XCTAssertEqual(.itemNotFound, equivalentValet.string(forKey: key, withPrompt: "")) + XCTAssertEqual(passcode, try valet.string(forKey: key, withPrompt: "")) + XCTAssertThrowsError(try equivalentValet.string(forKey: key, withPrompt: "")) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } // MARK: canAccessKeychain @@ -77,15 +82,9 @@ class SecureEnclaveIntegrationTests: XCTestCase return } - #if swift(>=4.1) let permutations: [SecureEnclaveValet] = SecureEnclaveAccessControl.allValues().compactMap { accessControl in return .valet(with: valet.identifier, accessControl: accessControl) } - #else - let permutations: [SecureEnclaveValet] = SecureEnclaveAccessControl.allValues().flatMap { accessControl in - return .valet(with: valet.identifier, accessControl: accessControl) - } - #endif for permutation in permutations { XCTAssertTrue(permutation.canAccessKeychain()) @@ -96,35 +95,33 @@ class SecureEnclaveIntegrationTests: XCTestCase guard testEnvironmentIsSigned() else { return } - - let sharedAccessGroupIdentifier: Identifier - #if os(iOS) - sharedAccessGroupIdentifier = Identifier(nonEmpty: "com.squareup.Valet-iOS-Test-Host-App")! - #elseif os(macOS) - sharedAccessGroupIdentifier = Identifier(nonEmpty: "com.squareup.Valet-macOS-Test-Host-App")! - #elseif os(tvOS) - sharedAccessGroupIdentifier = Identifier(nonEmpty: "com.squareup.Valet-tvOS-Test-Host-App")! - #elseif os(watchOS) - sharedAccessGroupIdentifier = Identifier(nonEmpty: "com.squareup.ValetTouchIDTestApp.watchkitapp.watchkitextension")! - #else - XCTFail() - #endif - #if swift(>=4.1) let permutations: [SecureEnclaveValet] = SecureEnclaveAccessControl.allValues().compactMap { accessControl in - return .sharedAccessGroupValet(with: sharedAccessGroupIdentifier, accessControl: accessControl) + return .sharedGroupValet(with: Valet.sharedAccessGroupIdentifier, accessControl: accessControl) } - #else - let permutations: [SecureEnclaveValet] = SecureEnclaveAccessControl.allValues().flatMap { accessControl in - return .sharedAccessGroupValet(with: sharedAccessGroupIdentifier, accessControl: accessControl) - } - #endif for permutation in permutations { XCTAssertTrue(permutation.canAccessKeychain()) } } - + + #if !os(macOS) + // We can't test app groups on macOS without a paid developer account, which we don't have. + func test_canAccessKeychain_sharedAppGroup() { + guard testEnvironmentIsSigned() else { + return + } + + let permutations: [SecureEnclaveValet] = SecureEnclaveAccessControl.allValues().compactMap { accessControl in + return .sharedGroupValet(with: Valet.sharedAppGroupIdentifier, accessControl: accessControl) + } + + for permutation in permutations { + XCTAssertTrue(permutation.canAccessKeychain()) + } + } + #endif + // MARK: Migration func test_migrateObjectsMatchingQuery_failsForBadQuery() @@ -137,20 +134,22 @@ class SecureEnclaveIntegrationTests: XCTestCase kSecClass as String: kSecClassGenericPassword as String, kSecAttrAccessControl as String: "Fake access control" ] - XCTAssertEqual(.invalidQuery, valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) + XCTAssertThrowsError(try valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) { error in + XCTAssertEqual(error as? MigrationError, .invalidQuery) + } } - func test_migrateObjectsFromValet_migratesSuccessfullyToSecureEnclave() + func test_migrateObjectsFromValet_migratesSuccessfullyToSecureEnclave() throws { - guard testEnvironmentIsSigned() else { + guard testEnvironmentIsSigned() && testEnvironmentSupportsWhenPasscodeSet() else { return } let plainOldValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) // Clean up any dangling keychain items before we start this test. - valet.removeAllObjects() - plainOldValet.removeAllObjects() + try valet.removeAllObjects() + try plainOldValet.removeAllObjects() let keyValuePairs = [ "yo": "dawg", @@ -161,45 +160,47 @@ class SecureEnclaveIntegrationTests: XCTestCase ] for (key, value) in keyValuePairs { - plainOldValet.set(string: value, forKey: key) + try plainOldValet.setString(value, forKey: key) } - XCTAssertEqual(.success, valet.migrateObjects(from: plainOldValet, removeOnCompletion: true)) + try valet.migrateObjects(from: plainOldValet, removeOnCompletion: true) for (key, value) in keyValuePairs { - XCTAssertEqual(.success(value), valet.string(forKey: key, withPrompt: "")) - XCTAssertNil(plainOldValet.string(forKey: key)) + XCTAssertEqual(value, try valet.string(forKey: key, withPrompt: "")) + XCTAssertThrowsError(try plainOldValet.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } - // Clean up items for the next test run (allKeys and removeAllObjects are unsupported in VALSecureEnclaveValet. + // Clean up items for the next test run (allKeys and removeAllObjects are unsupported in VALSecureEnclaveValet). for key in keyValuePairs.keys { - XCTAssertTrue(valet.removeObject(forKey: key)) + try valet.removeObject(forKey: key) } } - func test_migrateObjectsFromValet_migratesSuccessfullyAfterCanAccessKeychainCalls() { - guard testEnvironmentIsSigned() else { + func test_migrateObjectsFromValet_migratesSuccessfullyAfterCanAccessKeychainCalls() throws { + guard testEnvironmentIsSigned() && testEnvironmentSupportsWhenPasscodeSet() else { return } let otherValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me_To_Valet")!, accessibility: .afterFirstUnlock) // Clean up any dangling keychain items before we start this test. - valet.removeAllObjects() - otherValet.removeAllObjects() + try valet.removeAllObjects() + try otherValet.removeAllObjects() let keyStringPairToMigrateMap = ["foo" : "bar", "testing" : "migration", "is" : "quite", "entertaining" : "if", "you" : "don't", "screw" : "up"] for (key, value) in keyStringPairToMigrateMap { - XCTAssertTrue(otherValet.set(string: value, forKey: key)) + try otherValet.setString(value, forKey: key) } XCTAssertTrue(valet.canAccessKeychain()) XCTAssertTrue(otherValet.canAccessKeychain()) - XCTAssertEqual(.success, valet.migrateObjects(from: otherValet, removeOnCompletion: false)) + try valet.migrateObjects(from: otherValet, removeOnCompletion: false) for (key, value) in keyStringPairToMigrateMap { - XCTAssertEqual(valet.string(forKey: key, withPrompt: ""), .success(value)) - XCTAssertEqual(otherValet.string(forKey: key), value) + XCTAssertEqual(try valet.string(forKey: key, withPrompt: ""), value) + XCTAssertEqual(try otherValet.string(forKey: key), value) } } } diff --git a/Tests/ValetIntegrationTests/SinglePromptSecureEnclaveIntegrationTests.swift b/Tests/ValetIntegrationTests/SinglePromptSecureEnclaveIntegrationTests.swift index 12303f11..029b5ec8 100644 --- a/Tests/ValetIntegrationTests/SinglePromptSecureEnclaveIntegrationTests.swift +++ b/Tests/ValetIntegrationTests/SinglePromptSecureEnclaveIntegrationTests.swift @@ -20,98 +20,123 @@ import Foundation @testable import Valet -import LegacyValet import XCTest +#if canImport(LocalAuthentication) class SinglePromptSecureEnclaveIntegrationTests: XCTestCase { static let identifier = Identifier(nonEmpty: "valet_testing")! - let valet = SinglePromptSecureEnclaveValet.valet(with: SinglePromptSecureEnclaveTests.identifier, accessControl: .userPresence) + + @available(tvOS 11.0, *) + func valet() -> SinglePromptSecureEnclaveValet { + .valet(with: SinglePromptSecureEnclaveTests.identifier, accessControl: .userPresence) + } let key = "key" let passcode = "topsecret" override func setUp() { super.setUp() - - ErrorHandler.customAssertBody = { _, _, _, _ in - // Nothing to do here. - } - valet.removeObject(forKey: key) + guard #available(tvOS 11.0, *) else { + return + } + guard testEnvironmentIsSigned() else { + return + } + do { + try valet().removeAllObjects() + } catch { + XCTFail("Error removing objects from Valet \(valet()): \(error)") + } } // MARK: Equality - func test_SinglePromptSecureEnclaveValetsWithEqualConfiguration_canAccessSameData() + func test_SinglePromptSecureEnclaveValetsWithEqualConfiguration_canAccessSameData() throws { - guard testEnvironmentIsSigned() else { + guard #available(tvOS 11.0, *) else { + return + } + guard testEnvironmentIsSigned() && testEnvironmentSupportsWhenPasscodeSet() else { return } - XCTAssertTrue(valet.set(string: passcode, forKey: key)) - let equivalentValet = SinglePromptSecureEnclaveValet.valet(with: valet.identifier, accessControl: valet.accessControl) - XCTAssertEqual(valet, equivalentValet) - XCTAssertEqual(.success(passcode), equivalentValet.string(forKey: key, withPrompt: "")) + try valet().setString(passcode, forKey: key) + let equivalentValet = SinglePromptSecureEnclaveValet.valet(with: valet().identifier, accessControl: valet().accessControl) + XCTAssertEqual(valet(), equivalentValet) + XCTAssertEqual(passcode, try equivalentValet.string(forKey: key, withPrompt: "")) } - func test_SinglePromptSecureEnclaveValetsWithDifferingAccessControl_canNotAccessSameData() + func test_SinglePromptSecureEnclaveValetsWithDifferingAccessControl_canNotAccessSameData() throws { - guard testEnvironmentIsSigned() else { + guard #available(tvOS 11.0, *) else { + return + } + guard testEnvironmentIsSigned() && testEnvironmentSupportsWhenPasscodeSet() else { return } - XCTAssertTrue(valet.set(string: passcode, forKey: key)) - let equivalentValet = SecureEnclaveValet.valet(with: valet.identifier, accessControl: .devicePasscode) - XCTAssertNotEqual(valet, equivalentValet) - XCTAssertEqual(.success(passcode), valet.string(forKey: key, withPrompt: "")) - XCTAssertEqual(.itemNotFound, equivalentValet.string(forKey: key, withPrompt: "")) + try valet().setString(passcode, forKey: key) + let equivalentValet = SecureEnclaveValet.valet(with: valet().identifier, accessControl: .devicePasscode) + XCTAssertNotEqual(valet(), equivalentValet) + XCTAssertEqual(passcode, try valet().string(forKey: key, withPrompt: "")) + XCTAssertThrowsError(try equivalentValet.string(forKey: key, withPrompt: "")) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } // MARK: allKeys - func test_allKeys() + func test_allKeys() throws { - guard testEnvironmentIsSigned() else { + guard #available(tvOS 11.0, *) else { + return + } + guard testEnvironmentIsSigned() && testEnvironmentSupportsWhenPasscodeSet() else { return } - XCTAssertEqual(valet.allKeys(userPrompt: ""), Set()) + XCTAssertEqual(try valet().allKeys(userPrompt: ""), Set()) - XCTAssertTrue(valet.set(string: passcode, forKey: key)) - XCTAssertEqual(valet.allKeys(userPrompt: ""), Set(arrayLiteral: key)) + try valet().setString(passcode, forKey: key) + XCTAssertEqual(try valet().allKeys(userPrompt: ""), Set(arrayLiteral: key)) - XCTAssertTrue(valet.set(string: "monster", forKey: "cookie")) - XCTAssertEqual(valet.allKeys(userPrompt: ""), Set(arrayLiteral: key, "cookie")) + try valet().setString("monster", forKey: "cookie") + XCTAssertEqual(try valet().allKeys(userPrompt: ""), Set(arrayLiteral: key, "cookie")) - valet.removeAllObjects() - XCTAssertEqual(valet.allKeys(userPrompt: ""), Set()) + try valet().removeAllObjects() + XCTAssertEqual(try valet().allKeys(userPrompt: ""), Set()) } - func test_allKeys_doesNotReflectValetImplementationDetails() { + func test_allKeys_doesNotReflectValetImplementationDetails() throws { + guard #available(tvOS 11.0, *) else { + return + } + guard testEnvironmentIsSigned() && testEnvironmentSupportsWhenPasscodeSet() else { + return + } + // Under the hood, Valet inserts a canary when calling `canAccessKeychain()` - this should not appear in `allKeys()`. - _ = valet.canAccessKeychain() - XCTAssertEqual(valet.allKeys(userPrompt: "it me"), Set()) + _ = valet().canAccessKeychain() + XCTAssertEqual(try valet().allKeys(userPrompt: "it me"), Set()) } // MARK: canAccessKeychain func test_canAccessKeychain() { + guard #available(tvOS 11.0, *) else { + return + } guard testEnvironmentIsSigned() else { return } - #if swift(>=4.1) let permutations: [SecureEnclaveValet] = SecureEnclaveAccessControl.allValues().compactMap { accessControl in - return .valet(with: valet.identifier, accessControl: accessControl) + return .valet(with: valet().identifier, accessControl: accessControl) } - #else - let permutations: [SecureEnclaveValet] = SecureEnclaveAccessControl.allValues().flatMap { accessControl in - return .valet(with: valet.identifier, accessControl: accessControl) - } - #endif for permutation in permutations { XCTAssertTrue(permutation.canAccessKeychain()) @@ -119,38 +144,49 @@ class SinglePromptSecureEnclaveIntegrationTests: XCTestCase } func test_canAccessKeychain_sharedAccessGroup() { + guard #available(tvOS 11.0, *) else { + return + } guard testEnvironmentIsSigned() else { return } - let sharedAccessGroupIdentifier: Identifier - #if os(iOS) - sharedAccessGroupIdentifier = Identifier(nonEmpty: "com.squareup.Valet-iOS-Test-Host-App")! - #elseif os(macOS) - sharedAccessGroupIdentifier = Identifier(nonEmpty: "com.squareup.Valet-macOS-Test-Host-App")! - #else - XCTFail() - #endif - - #if swift(>=4.1) let permutations: [SecureEnclaveValet] = SecureEnclaveAccessControl.allValues().compactMap { accessControl in - return .sharedAccessGroupValet(with: sharedAccessGroupIdentifier, accessControl: accessControl) + return .sharedGroupValet(with: Valet.sharedAccessGroupIdentifier, accessControl: accessControl) } - #else - let permutations: [SecureEnclaveValet] = SecureEnclaveAccessControl.allValues().flatMap { accessControl in - return .sharedAccessGroupValet(with: sharedAccessGroupIdentifier, accessControl: accessControl) + + for permutation in permutations { + XCTAssertTrue(permutation.canAccessKeychain()) + } + } + + #if !os(macOS) + // We can't test app groups on macOS without a paid developer account, which we don't have. + func test_canAccessKeychain_sharedAppGroup() { + guard #available(tvOS 11.0, *) else { + return + } + guard testEnvironmentIsSigned() else { + return + } + + let permutations: [SecureEnclaveValet] = SecureEnclaveAccessControl.allValues().compactMap { accessControl in + return .sharedGroupValet(with: Valet.sharedAppGroupIdentifier, accessControl: accessControl) } - #endif for permutation in permutations { XCTAssertTrue(permutation.canAccessKeychain()) } } - + #endif + // MARK: Migration func test_migrateObjectsMatchingQuery_failsForBadQuery() { + guard #available(tvOS 11.0, *) else { + return + } guard testEnvironmentIsSigned() else { return } @@ -159,21 +195,26 @@ class SinglePromptSecureEnclaveIntegrationTests: XCTestCase kSecClass as String: kSecClassGenericPassword as String, kSecAttrAccessControl as String: "Fake access control" ] - XCTAssertEqual(.invalidQuery, valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) + XCTAssertThrowsError(try valet().migrateObjects(matching: invalidQuery, removeOnCompletion: false)) { error in + XCTAssertEqual(error as? MigrationError, MigrationError.invalidQuery) + } } - func test_migrateObjectsFromValet_migratesSuccessfullyToSecureEnclave() + func test_migrateObjectsFromValet_migratesSuccessfullyToSecureEnclave() throws { - guard testEnvironmentIsSigned() else { + guard #available(tvOS 11.0, *) else { + return + } + guard testEnvironmentIsSigned() && testEnvironmentSupportsWhenPasscodeSet() else { return } let plainOldValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) // Clean up any dangling keychain items before we start this test. - valet.removeAllObjects() - plainOldValet.removeAllObjects() - + try valet().removeAllObjects() + try plainOldValet.removeAllObjects() + let keyValuePairs = [ "yo": "dawg", "we": "heard", @@ -183,45 +224,52 @@ class SinglePromptSecureEnclaveIntegrationTests: XCTestCase ] for (key, value) in keyValuePairs { - plainOldValet.set(string: value, forKey: key) + try plainOldValet.setString(value, forKey: key) } - XCTAssertEqual(.success, valet.migrateObjects(from: plainOldValet, removeOnCompletion: true)) + try valet().migrateObjects(from: plainOldValet, removeOnCompletion: true) for (key, value) in keyValuePairs { - XCTAssertEqual(.success(value), valet.string(forKey: key, withPrompt: "")) - XCTAssertNil(plainOldValet.string(forKey: key)) + XCTAssertEqual(value, try valet().string(forKey: key, withPrompt: "")) + XCTAssertThrowsError(try plainOldValet.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } // Clean up items for the next test run (allKeys and removeAllObjects are unsupported in VALSecureEnclaveValet. for key in keyValuePairs.keys { - XCTAssertTrue(valet.removeObject(forKey: key)) + try valet().removeObject(forKey: key) } } - func test_migrateObjectsFromValet_migratesSuccessfullyAfterCanAccessKeychainCalls() { - guard testEnvironmentIsSigned() else { + func test_migrateObjectsFromValet_migratesSuccessfullyAfterCanAccessKeychainCalls() throws { + guard #available(tvOS 11.0, *) else { + return + } + guard testEnvironmentIsSigned() && testEnvironmentSupportsWhenPasscodeSet() else { return } let otherValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me_To_Valet")!, accessibility: .afterFirstUnlock) // Clean up any dangling keychain items before we start this test. - valet.removeAllObjects() - otherValet.removeAllObjects() - + try valet().removeAllObjects() + try otherValet.removeAllObjects() + let keyStringPairToMigrateMap = ["foo" : "bar", "testing" : "migration", "is" : "quite", "entertaining" : "if", "you" : "don't", "screw" : "up"] for (key, value) in keyStringPairToMigrateMap { - XCTAssertTrue(otherValet.set(string: value, forKey: key)) + try otherValet.setString(value, forKey: key) } - XCTAssertTrue(valet.canAccessKeychain()) + XCTAssertTrue(valet().canAccessKeychain()) XCTAssertTrue(otherValet.canAccessKeychain()) - XCTAssertEqual(.success, valet.migrateObjects(from: otherValet, removeOnCompletion: false)) + try valet().migrateObjects(from: otherValet, removeOnCompletion: false) for (key, value) in keyStringPairToMigrateMap { - XCTAssertEqual(valet.string(forKey: key, withPrompt: ""), .success(value)) - XCTAssertEqual(otherValet.string(forKey: key), value) + XCTAssertEqual(try valet().string(forKey: key, withPrompt: ""), value) + XCTAssertEqual(try otherValet.string(forKey: key), value) } } } + +#endif diff --git a/Tests/ValetIntegrationTests/ValetIntegrationTests.swift b/Tests/ValetIntegrationTests/ValetIntegrationTests.swift index 2ec21e6e..7f2e8bb3 100644 --- a/Tests/ValetIntegrationTests/ValetIntegrationTests.swift +++ b/Tests/ValetIntegrationTests/ValetIntegrationTests.swift @@ -24,24 +24,32 @@ import XCTest @testable import Valet -/// - returns: `true` when the test environment is signed. +/// - Returns: `true` when the test environment is signed. /// - The Valet Mac Tests target is left without a host app on master. Mac test host app signing requires CI to have the Developer team credentials down in keychain, which we can't easily accomplish. -/// - note: In order to test changes locally, set the Valet Mac Tests host to Valet macOS Test Host App, delete all VAL_* keychain items in your keychain via Keychain Access.app, and run Mac tests. +/// - Note: In order to test changes locally, set the Valet Mac Tests host to Valet macOS Test Host App, delete all VAL_* keychain items in your keychain via Keychain Access.app, and run Mac tests. func testEnvironmentIsSigned() -> Bool { // Our test host apps for iOS and Mac are both signed, so testing for a custom bundle identifier is analogous to testing signing. guard Bundle.main.bundleIdentifier != nil && Bundle.main.bundleIdentifier != "com.apple.dt.xctest.tool" else { #if os(iOS) || os(tvOS) - XCTFail("test bundle should be signed") + XCTFail("test bundle should be signed") #endif return false } + return true +} + +func testEnvironmentSupportsWhenPasscodeSet() -> Bool { if let simulatorVersionInfo = ProcessInfo.processInfo.environment["SIMULATOR_VERSION_INFO"], simulatorVersionInfo.contains("iOS 13") || simulatorVersionInfo.contains("tvOS 13") { - // Xcode 11's simulator does not support code-signing. + // iOS and tvOS 13 simulators fail to store items in a Valet that has a + // kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly flag. The documentation for this flag says: + // "No items can be stored in this class on devices without a passcode". I currently do not + // understand why prior simulators work with this flag, given that no simulators have a passcode. return false + } else { return true } @@ -52,20 +60,34 @@ internal extension Valet { // MARK: Shared Access Group - static var sharedAccessGroupIdentifier: Identifier = { - let sharedAccessGroupIdentifier: Identifier + static var sharedAccessGroupIdentifier: SharedGroupIdentifier = { + #if os(iOS) + return SharedGroupIdentifier(appIDPrefix: "9XUJ7M53NG", nonEmptyGroup: "com.squareup.Valet-iOS-Test-Host-App")! + #elseif os(macOS) + return SharedGroupIdentifier(appIDPrefix: "9XUJ7M53NG", nonEmptyGroup: "com.squareup.Valet-macOS-Test-Host-App")! + #elseif os(tvOS) + return SharedGroupIdentifier(appIDPrefix: "9XUJ7M53NG", nonEmptyGroup: "com.squareup.Valet-tvOS-Test-Host-App")! + #elseif os(watchOS) + return SharedGroupIdentifier(appIDPrefix: "9XUJ7M53NG", nonEmptyGroup: "com.squareup.ValetTouchIDTestApp.watchkitapp.watchkitextension")! + #else + XCTFail() + #endif + }() + + // MARK: Shared App Group + + static var sharedAppGroupIdentifier: SharedGroupIdentifier = { #if os(iOS) - sharedAccessGroupIdentifier = Identifier(nonEmpty: "com.squareup.Valet-iOS-Test-Host-App")! + return SharedGroupIdentifier(groupPrefix: "group", nonEmptyGroup: "valet.test")! #elseif os(macOS) - sharedAccessGroupIdentifier = Identifier(nonEmpty: "com.squareup.Valet-macOS-Test-Host-App")! + return SharedGroupIdentifier(groupPrefix: "9XUJ7M53NG", nonEmptyGroup: "valet.test")! #elseif os(tvOS) - sharedAccessGroupIdentifier = Identifier(nonEmpty: "com.squareup.Valet-tvOS-Test-Host-App")! + return SharedGroupIdentifier(groupPrefix: "group", nonEmptyGroup: "valet.test")! #elseif os(watchOS) - sharedAccessGroupIdentifier = Identifier(nonEmpty: "com.squareup.ValetTouchIDTestApp.watchkitapp.watchkitextension")! + return SharedGroupIdentifier(groupPrefix: "group", nonEmptyGroup: "valet.test")! #else - XCTFail() + XCTFail() #endif - return sharedAccessGroupIdentifier }() } @@ -73,15 +95,23 @@ internal extension Valet { class ValetIntegrationTests: XCTestCase { - static let identifier = Valet.sharedAccessGroupIdentifier + static let sharedAccessGroupIdentifier = Valet.sharedAccessGroupIdentifier + static let sharedAppGroupIdentifier = Valet.sharedAppGroupIdentifier var allPermutations: [Valet] { - return Valet.permutations(with: ValetIntegrationTests.identifier) - + (testEnvironmentIsSigned() ? Valet.permutations(with: ValetIntegrationTests.identifier, shared: true) : []) + var signedPermutations = Valet.permutations(with: ValetIntegrationTests.sharedAccessGroupIdentifier) + #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) + #endif + return Valet.permutations(with: ValetIntegrationTests.sharedAccessGroupIdentifier.asIdentifier) + + (testEnvironmentIsSigned() + ? signedPermutations + : []) } - let vanillaValet = Valet.valet(with: identifier, accessibility: .whenUnlocked) + let vanillaValet = Valet.valet(with: sharedAccessGroupIdentifier.asIdentifier, accessibility: .whenUnlocked) // FIXME: Need a different flavor (Synchronizable must be tested in a signed environment) - let anotherFlavor = Valet.iCloudValet(with: identifier, accessibility: .whenUnlocked) + let anotherFlavor = Valet.iCloudValet(with: sharedAccessGroupIdentifier.asIdentifier, accessibility: .whenUnlocked) let key = "key" let passcode = "topsecret" @@ -92,18 +122,23 @@ class ValetIntegrationTests: XCTestCase override func setUp() { super.setUp() - - ErrorHandler.customAssertBody = { _, _, _, _ in - // Nothing to do here. - } - - vanillaValet.removeAllObjects() - anotherFlavor.removeAllObjects() - allPermutations.forEach { testingValet in testingValet.removeAllObjects() } + let permutations: [Valet] + if testEnvironmentIsSigned() { + permutations = [vanillaValet, anotherFlavor] + allPermutations + } else { + permutations = [vanillaValet] + allPermutations + } + permutations.forEach { testingValet in + do { + try testingValet.removeAllObjects() + } catch { + XCTFail("Error removing objects from Valet \(testingValet): \(error)") + } + } - XCTAssertTrue(vanillaValet.allKeys().isEmpty) - XCTAssertTrue(anotherFlavor.allKeys().isEmpty) + XCTAssertEqual(try? vanillaValet.allKeys(), Set()) + XCTAssertEqual(try? anotherFlavor.allKeys(), Set()) } // MARK: Initialization @@ -111,36 +146,36 @@ class ValetIntegrationTests: XCTestCase func test_init_createsCorrectBackingService() { let identifier = ValetTests.identifier - Accessibility.allValues().forEach { accessibility in + Accessibility.allCases.forEach { accessibility in let backingService = Valet.valet(with: identifier, accessibility: accessibility).service XCTAssertEqual(backingService, Service.standard(identifier, .valet(accessibility))) } } func test_init_createsCorrectBackingService_sharedAccess() { - let identifier = ValetTests.identifier + let identifier = Valet.sharedAccessGroupIdentifier - Accessibility.allValues().forEach { accessibility in - let backingService = Valet.sharedAccessGroupValet(with: identifier, accessibility: accessibility).service - XCTAssertEqual(backingService, Service.sharedAccessGroup(identifier, .valet(accessibility))) + Accessibility.allCases.forEach { accessibility in + let backingService = Valet.sharedGroupValet(with: identifier, accessibility: accessibility).service + XCTAssertEqual(backingService, Service.sharedGroup(identifier, .valet(accessibility))) } } func test_init_createsCorrectBackingService_cloud() { let identifier = ValetTests.identifier - CloudAccessibility.allValues().forEach { accessibility in + CloudAccessibility.allCases.forEach { accessibility in let backingService = Valet.iCloudValet(with: identifier, accessibility: accessibility).service XCTAssertEqual(backingService, Service.standard(identifier, .iCloud(accessibility))) } } func test_init_createsCorrectBackingService_cloudSharedAccess() { - let identifier = ValetTests.identifier + let identifier = Valet.sharedAccessGroupIdentifier - CloudAccessibility.allValues().forEach { accessibility in - let backingService = Valet.iCloudSharedAccessGroupValet(with: identifier, accessibility: accessibility).service - XCTAssertEqual(backingService, Service.sharedAccessGroup(identifier, .iCloud(accessibility))) + CloudAccessibility.allCases.forEach { accessibility in + let backingService = Valet.iCloudSharedGroupValet(with: identifier, accessibility: accessibility).service + XCTAssertEqual(backingService, Service.sharedGroup(identifier, .iCloud(accessibility))) } } @@ -159,10 +194,24 @@ class ValetIntegrationTests: XCTestCase return } - Valet.permutations(with: Valet.sharedAccessGroupIdentifier, shared: true).forEach { permutation in + Valet.permutations(with: Valet.sharedAccessGroupIdentifier).forEach { permutation in + XCTAssertTrue(permutation.canAccessKeychain(), "\(permutation) could not access keychain.") + } + } + + #if !os(macOS) + // We can't test app groups on macOS without a paid developer account, which we don't have. + func test_canAccessKeychain_sharedAppGroup() + { + guard testEnvironmentIsSigned() else { + return + } + + Valet.permutations(with: Valet.sharedAppGroupIdentifier).forEach { permutation in XCTAssertTrue(permutation.canAccessKeychain(), "\(permutation) could not access keychain.") } } + #endif func test_canAccessKeychain_Performance() { @@ -173,127 +222,133 @@ class ValetIntegrationTests: XCTestCase // MARK: containsObjectForKey - func test_containsObjectForKey() + func test_containsObjectForKey() throws { - allPermutations.forEach { valet in - XCTAssertFalse(valet.containsObject(forKey: key), "\(valet) found object for key that should not exist") + try allPermutations.forEach { valet in + XCTAssertFalse(try valet.containsObject(forKey: key), "\(valet) found object for key that should not exist") - XCTAssertTrue(valet.set(string: passcode, forKey: key), "\(valet) could not set item in keychain") - XCTAssertTrue(valet.containsObject(forKey: key), "\(valet) could not find item it has set in keychain") + try valet.setString(passcode, forKey: key) + XCTAssertTrue(try valet.containsObject(forKey: key), "\(valet) could not find item it has set in keychain") - XCTAssertTrue(valet.removeObject(forKey: key), "\(valet) could not remove item in keychain") - XCTAssertFalse(valet.containsObject(forKey: key), "\(valet) found removed item in keychain") + try valet.removeObject(forKey: key) + XCTAssertFalse(try valet.containsObject(forKey: key), "\(valet) found removed item in keychain") } } // MARK: allKeys - func test_allKeys() + func test_allKeys() throws { - allPermutations.forEach { valet in - XCTAssertEqual(valet.allKeys(), Set(), "\(valet) found keys that should not exist") + try allPermutations.forEach { valet in + XCTAssertEqual(try valet.allKeys(), Set(), "\(valet) found keys that should not exist") - XCTAssertTrue(valet.set(string: passcode, forKey: key), "\(valet) could not set item in keychain") - XCTAssertEqual(valet.allKeys(), Set(arrayLiteral: key)) + try valet.setString(passcode, forKey: key) + XCTAssertEqual(try valet.allKeys(), Set(arrayLiteral: key)) - XCTAssertTrue(valet.set(string: "monster", forKey: "cookie"), "\(valet) could not set item in keychain") - XCTAssertEqual(valet.allKeys(), Set(arrayLiteral: key, "cookie")) + try valet.setString("monster", forKey: "cookie") + XCTAssertEqual(try valet.allKeys(), Set(arrayLiteral: key, "cookie")) - valet.removeAllObjects() - XCTAssertEqual(valet.allKeys(), Set(), "\(valet) found keys that should not exist") + try valet.removeAllObjects() + XCTAssertEqual(try valet.allKeys(), Set(), "\(valet) found keys that should not exist") } } - func test_allKeys_doesNotReflectValetImplementationDetails() { - allPermutations.forEach { valet in + func test_allKeys_doesNotReflectValetImplementationDetails() throws { + try allPermutations.forEach { valet in // Under the hood, Valet inserts a canary when calling `canAccessKeychain()` - this should not appear in `allKeys()`. _ = valet.canAccessKeychain() - XCTAssertEqual(valet.allKeys(), Set()) + XCTAssertEqual(try valet.allKeys(), Set(), "\(valet) found keys that should not exist") } } - func test_allKeys_remainsUntouchedForUnequalValets() + func test_allKeys_remainsUntouchedForUnequalValets() throws { - vanillaValet.set(string: passcode, forKey: key) - XCTAssertEqual(vanillaValet.allKeys(), Set(arrayLiteral: key)) + try vanillaValet.setString(passcode, forKey: key) + XCTAssertEqual(try vanillaValet.allKeys(), Set(arrayLiteral: key)) // Different Identifier let differingIdentifier = Valet.valet(with: Identifier(nonEmpty: "nope")!, accessibility: vanillaValet.accessibility) - XCTAssertEqual(differingIdentifier.allKeys(), Set()) + XCTAssertEqual(try differingIdentifier.allKeys(), Set()) // Different Accessibility - let differingAccessibility = Valet.valet(with: vanillaValet.identifier, accessibility: .always) - XCTAssertEqual(differingAccessibility.allKeys(), Set()) + let differingAccessibility = Valet.valet(with: vanillaValet.identifier, accessibility: .whenUnlockedThisDeviceOnly) + XCTAssertEqual(try differingAccessibility.allKeys(), Set()) // Different Kind - XCTAssertEqual(anotherFlavor.allKeys(), Set()) + XCTAssertEqual(try anotherFlavor.allKeys(), Set()) } // MARK: string(forKey:) - func test_stringForKey_isNilForInvalidKey() + func test_stringForKey_throwsItemNotFoundForKeyWithNoValue() throws { - allPermutations.forEach { valet in - XCTAssertNil(valet.string(forKey: key), "\(valet) found item that should not exit") + try allPermutations.forEach { valet in + XCTAssertThrowsError(try valet.string(forKey: key), "\(valet) found item that should not exit") { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } } - func test_stringForKey_retrievesStringForValidKey() + func test_stringForKey_retrievesStringForValidKey() throws { - allPermutations.forEach { valet in - XCTAssertTrue(valet.set(string: passcode, forKey: key), "\(valet) could not set item in keychain") - XCTAssertEqual(passcode, valet.string(forKey: key)) + try allPermutations.forEach { valet in + try valet.setString(passcode, forKey: key) + XCTAssertEqual(passcode, try valet.string(forKey: key)) } } - func test_stringForKey_equivalentValetsCanAccessSameData() + func test_stringForKey_equivalentValetsCanAccessSameData() throws { let equalValet = Valet.valet(with: vanillaValet.identifier, accessibility: vanillaValet.accessibility) - XCTAssertEqual(0, equalValet.allKeys().count) + XCTAssertEqual(0, try equalValet.allKeys().count) XCTAssertEqual(vanillaValet, equalValet) - XCTAssertTrue(vanillaValet.set(string: "monster", forKey: "cookie")) - XCTAssertEqual("monster", equalValet.string(forKey: "cookie")) + try vanillaValet.setString("monster", forKey: "cookie") + XCTAssertEqual("monster", try equalValet.string(forKey: "cookie")) } - func test_stringForKey_withDifferingIdentifier_isNil() + func test_stringForKey_withDifferingIdentifier_throwsItemNotFound() throws { - XCTAssertTrue(vanillaValet.set(string: passcode, forKey: key)) - XCTAssertEqual(passcode, vanillaValet.string(forKey: key)) + try vanillaValet.setString(passcode, forKey: key) + XCTAssertEqual(passcode, try vanillaValet.string(forKey: key)) let differingIdentifier = Valet.valet(with: Identifier(nonEmpty: "wat")!, accessibility: vanillaValet.accessibility) - XCTAssertNil(differingIdentifier.string(forKey: key)) + XCTAssertThrowsError(try differingIdentifier.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } - func test_stringForKey_withDifferingAccessibility_isNil() + func test_stringForKey_withDifferingAccessibility_throwsItemNotFound() throws { - XCTAssertTrue(vanillaValet.set(string: passcode, forKey: key)) - XCTAssertEqual(passcode, vanillaValet.string(forKey: key)) + try vanillaValet.setString(passcode, forKey: key) + XCTAssertEqual(passcode, try vanillaValet.string(forKey: key)) let differingAccessibility = Valet.valet(with: vanillaValet.identifier, accessibility: .afterFirstUnlockThisDeviceOnly) - XCTAssertNil(differingAccessibility.string(forKey: key)) + XCTAssertThrowsError(try differingAccessibility.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } - func test_stringForKey_withEquivalentConfigurationButDifferingFlavor_isNil() + func test_stringForKey_withEquivalentConfigurationButDifferingFlavor_throwsItemNotFound() throws { guard testEnvironmentIsSigned() else { return } - XCTAssertTrue(vanillaValet.set(string: "monster", forKey: "cookie")) - XCTAssertEqual("monster", vanillaValet.string(forKey: "cookie")) + try vanillaValet.setString("monster", forKey: "cookie") + XCTAssertEqual("monster", try vanillaValet.string(forKey: "cookie")) - XCTAssertNil(anotherFlavor.string(forKey: "cookie")) + XCTAssertThrowsError(try anotherFlavor.string(forKey: "cookie")) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } #if !os(macOS) - func test_objectForKey_canReadItemsWithout_kSecUseDataProtectionKeychain_when_kSecUseDataProtectionKeychain_isSetToTrueInKeychainQuery() { + func test_objectForKey_canReadItemsWithout_kSecUseDataProtectionKeychain_when_kSecUseDataProtectionKeychain_isSetToTrueInKeychainQuery() throws { let valet = Valet.valet(with: Identifier(nonEmpty: "DataProtectionTest")!, accessibility: .afterFirstUnlock) - var dataProtectionWriteQuery = valet.keychainQuery - #if swift(>=5.1) - dataProtectionWriteQuery[kSecUseDataProtectionKeychain as String] = nil - #else - dataProtectionWriteQuery["nleg"] = nil // kSecUseDataProtectionKeychain for Xcode 9 and Xcode 10 compatibility. - #endif + var dataProtectionWriteQuery = valet.baseKeychainQuery + if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) { + dataProtectionWriteQuery[kSecUseDataProtectionKeychain as String] = nil + } let key = "DataProtectionKey" let object = Data("DataProtectionValue".utf8) @@ -304,134 +359,157 @@ class ValetIntegrationTests: XCTestCase SecItemDelete(dataProtectionWriteQuery as CFDictionary) XCTAssertEqual(SecItemAdd(dataProtectionWriteQuery as CFDictionary, nil), errSecSuccess) - XCTAssertEqual(valet.object(forKey: key), object) // If this breaks, it means Apple has changed behavior of SecItemCopy. It means that we need to remove `kSecUseDataProtectionKeychain` from our query on non-Mac platforms. + XCTAssertEqual(try valet.object(forKey: key), object) // If this breaks, it means Apple has changed behavior of SecItemCopy. It means that we need to remove `kSecUseDataProtectionKeychain` from our query on non-Mac platforms. } #endif // MARK: set(string:forKey:) - func test_setStringForKey_successfullyUpdatesExistingKey() + func test_setStringForKey_successfullyUpdatesExistingKey() throws { - allPermutations.forEach { valet in - XCTAssertNil(valet.string(forKey: key)) - valet.set(string: "1", forKey: key) - XCTAssertEqual("1", valet.string(forKey: key)) - valet.set(string: "2", forKey: key) - XCTAssertEqual("2", valet.string(forKey: key)) + try allPermutations.forEach { valet in + XCTAssertThrowsError(try valet.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + + try valet.setString("1", forKey: key) + XCTAssertEqual("1", try valet.string(forKey: key)) + try valet.setString("2", forKey: key) + XCTAssertEqual("2", try valet.string(forKey: key)) } } - func test_setStringForKey_failsForInvalidValue() { - allPermutations.forEach { valet in - XCTAssertFalse(valet.set(string: "", forKey: key)) + func test_setStringForKey_throwsEmptyValueOnInvalidValue() throws { + try allPermutations.forEach { valet in + XCTAssertThrowsError(try valet.setString("", forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, KeychainError.emptyValue) + } } } - - func test_setStringForKey_failsForInvalidKey() { - allPermutations.forEach { valet in - XCTAssertFalse(valet.set(string: passcode, forKey: "")) + + func test_setStringForKey_throwsEmptyKeyOnInvalidKey() throws { + try allPermutations.forEach { valet in + XCTAssertThrowsError(try valet.setString(passcode, forKey: "")) { error in + XCTAssertEqual(error as? KeychainError, KeychainError.emptyKey) + } } } - + // MARK: object(forKey:) - - func test_objectForKey_isNilForInvalidKey() { - allPermutations.forEach { valet in - XCTAssertNil(valet.object(forKey: key)) + + func test_objectForKey_throwsItemNotFoundWhenNoObjectExists() throws { + try allPermutations.forEach { valet in + XCTAssertThrowsError(try valet.object(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } } - func test_objectForKey_succeedsForValidKey() { - allPermutations.forEach { valet in - valet.set(object: passcodeData, forKey: key) - XCTAssertEqual(passcodeData, valet.object(forKey: key)) + func test_objectForKey_succeedsForValidKey() throws { + try allPermutations.forEach { valet in + try valet.setObject(passcodeData, forKey: key) + XCTAssertEqual(passcodeData, try valet.object(forKey: key)) } } - func test_objectForKey_equivalentValetsCanAccessSameData() { + func test_objectForKey_equivalentValetsCanAccessSameData() throws { let equalValet = Valet.valet(with: vanillaValet.identifier, accessibility: vanillaValet.accessibility) - XCTAssertEqual(0, equalValet.allKeys().count) + XCTAssertEqual(0, try equalValet.allKeys().count) XCTAssertEqual(vanillaValet, equalValet) - XCTAssertTrue(vanillaValet.set(object: passcodeData, forKey: key)) - XCTAssertEqual(passcodeData, equalValet.object(forKey: key)) + try vanillaValet.setObject(passcodeData, forKey: key) + XCTAssertEqual(passcodeData, try equalValet.object(forKey: key)) } - func test_objectForKey_withDifferingIdentifier_isNil() { - XCTAssertTrue(vanillaValet.set(object: passcodeData, forKey: key)) - XCTAssertEqual(passcodeData, vanillaValet.object(forKey: key)) - - let differingIdentifier = Valet.valet(with: Identifier(nonEmpty: "wat")!, accessibility: vanillaValet.accessibility) - XCTAssertNil(differingIdentifier.object(forKey: key)) + func test_objectForKey_withDifferingIdentifier_throwsItemNotFound() throws { + try allPermutations.forEach { valet in + try valet.setObject(passcodeData, forKey: key) + XCTAssertEqual(passcodeData, try valet.object(forKey: key)) + + let differingIdentifier = Valet.valet(with: Identifier(nonEmpty: "wat")!, accessibility: valet.accessibility) + XCTAssertThrowsError(try differingIdentifier.object(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + } } - func test_objectForKey_withDifferingAccessibility_isNil() { - XCTAssertTrue(vanillaValet.set(object: passcodeData, forKey: key)) - XCTAssertEqual(passcodeData, vanillaValet.object(forKey: key)) + func test_objectForKey_withDifferingAccessibility_throwsItemNotFound() throws { + try vanillaValet.setObject(passcodeData, forKey: key) + XCTAssertEqual(passcodeData, try vanillaValet.object(forKey: key)) let differingAccessibility = Valet.valet(with: vanillaValet.identifier, accessibility: .afterFirstUnlockThisDeviceOnly) - XCTAssertNil(differingAccessibility.object(forKey: key)) + XCTAssertThrowsError(try differingAccessibility.object(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } - func test_objectForKey_withEquivalentConfigurationButDifferingFlavor_isNil() { + func test_objectForKey_withEquivalentConfigurationButDifferingFlavor_throwsItemNotFound() throws { guard testEnvironmentIsSigned() else { return } - XCTAssertTrue(vanillaValet.set(object: passcodeData, forKey: key)) - XCTAssertEqual(passcodeData, vanillaValet.object(forKey: key)) - - XCTAssertNil(anotherFlavor.object(forKey: key)) + try vanillaValet.setObject(passcodeData, forKey: key) + XCTAssertEqual(passcodeData, try vanillaValet.object(forKey: key)) + + XCTAssertThrowsError(try anotherFlavor.object(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } // MARK: set(object:forKey:) - - func test_setObjectForKey_successfullyUpdatesExistingKey() { - allPermutations.forEach { valet in + + func test_setObjectForKey_successfullyUpdatesExistingKey() throws { + try allPermutations.forEach { valet in let firstValue = Data("first".utf8) let secondValue = Data("second".utf8) - valet.set(object: firstValue, forKey: key) - XCTAssertEqual(firstValue, valet.object(forKey: key)) - valet.set(object: secondValue, forKey: key) - XCTAssertEqual(secondValue, valet.object(forKey: key)) + try valet.setObject(firstValue, forKey: key) + XCTAssertEqual(firstValue, try valet.object(forKey: key)) + try valet.setObject(secondValue, forKey: key) + XCTAssertEqual(secondValue, try valet.object(forKey: key)) } } - func test_setObjectForKey_failsForInvalidKey() { - allPermutations.forEach { valet in - XCTAssertFalse(valet.set(object: passcodeData, forKey: "")) + func test_setObjectForKey_throwsEmptyKeyOnInvalidKey() throws { + try allPermutations.forEach { valet in + XCTAssertThrowsError(try valet.setObject(passcodeData, forKey: "")) { error in + XCTAssertEqual(error as? KeychainError, .emptyKey) + } } } - func test_setObjectForKey_failsForEmptyData() { - allPermutations.forEach { valet in + func test_setObjectForKey_throwsEmptyValueOnEmptyData() throws { + try allPermutations.forEach { valet in let emptyData = Data() XCTAssertTrue(emptyData.isEmpty) - XCTAssertFalse(valet.set(object: emptyData, forKey: key)) + XCTAssertThrowsError(try valet.setObject(emptyData, forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .emptyValue) + } } } // Mark: String/Object Equivalence - func test_stringForKey_succeedsForDataBackedByString() { - allPermutations.forEach { valet in - XCTAssertTrue(valet.set(object: passcodeData, forKey: key)) - XCTAssertEqual(passcode, valet.string(forKey: key)) + func test_stringForKey_succeedsForDataBackedByString() throws { + try allPermutations.forEach { valet in + try valet.setObject(passcodeData, forKey: key) + XCTAssertEqual(passcode, try valet.string(forKey: key)) } } - func test_stringForKey_failsForDataNotBackedByString() { - allPermutations.forEach { valet in + func test_stringForKey_failsForDataNotBackedByString() throws { + try allPermutations.forEach { valet in let dictionary = [ "that's no" : "moon" ] let nonStringData = NSKeyedArchiver.archivedData(withRootObject: dictionary) - XCTAssertTrue(valet.set(object: nonStringData, forKey: key)) - XCTAssertNil(valet.string(forKey: key)) + try valet.setObject(nonStringData, forKey: key) + XCTAssertThrowsError(try valet.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } } - func test_objectForKey_succeedsForStrings() { - allPermutations.forEach { valet in - XCTAssertTrue(valet.set(string: passcode, forKey: key)) - XCTAssertEqual(passcodeData, valet.object(forKey: key)) + func test_objectForKey_succeedsForStrings() throws { + try allPermutations.forEach { valet in + try valet.setString(passcode, forKey: key) + XCTAssertEqual(passcodeData, try valet.object(forKey: key)) } } @@ -443,8 +521,20 @@ class ValetIntegrationTests: XCTestCase let removeQueue = DispatchQueue(label: "Remove Object Queue", attributes: .concurrent) for _ in 1...50 { - setQueue.async { XCTAssertTrue(self.vanillaValet.set(string: self.passcode, forKey: self.key)) } - removeQueue.async { XCTAssertTrue(self.vanillaValet.removeObject(forKey: self.key)) } + setQueue.async { + do { + try self.vanillaValet.setString(self.passcode, forKey: self.key) + } catch { + XCTFail("Threw \(error) trying to write value") + } + } + removeQueue.async { + do { + try self.vanillaValet.removeObject(forKey: self.key) + } catch { + XCTFail("Threw \(error) trying to remove value") + } + } } let setQueueExpectation = expectation(description: "\(#function): Set String Queue") @@ -468,9 +558,20 @@ class ValetIntegrationTests: XCTestCase let expectation = self.expectation(description: #function) setStringQueue.async { - XCTAssertTrue(self.vanillaValet.set(string: self.passcode, forKey: self.key)) + do { + try self.vanillaValet.setString(self.passcode, forKey: self.key) + } catch { + XCTFail("Threw \(error) trying to set value") + } + stringForKeyQueue.async { - XCTAssertEqual(self.vanillaValet.string(forKey: self.key), self.passcode) + do { + let stringForKey = try self.vanillaValet.string(forKey: self.key) + XCTAssertEqual(stringForKey, self.passcode) + } catch { + XCTFail("Threw \(error) trying to read value") + } + expectation.fulfill() } } @@ -488,11 +589,22 @@ class ValetIntegrationTests: XCTestCase setStringQueue.async { let backgroundValet = Valet.valet(with: backgroundIdentifier, accessibility: .whenUnlocked) - XCTAssertTrue(backgroundValet.set(string: self.passcode, forKey: self.key)) - stringForKeyQueue.async { - XCTAssertEqual(backgroundValet.string(forKey: self.key), self.passcode) + do { + try backgroundValet.setString(self.passcode, forKey: self.key) + } catch { + XCTFail("Threw \(error) trying to write value") expectation.fulfill() } + stringForKeyQueue.async { + do { + let stringForKey = try backgroundValet.string(forKey: self.key) + XCTAssertEqual(stringForKey, self.passcode) + expectation.fulfill() + } catch { + XCTFail("Threw \(error) trying to read value") + expectation.fulfill() + } + } } waitForExpectations(timeout: 5.0, handler: nil) @@ -500,122 +612,141 @@ class ValetIntegrationTests: XCTestCase // MARK: Removal - func test_removeObjectForKey_succeedsWhenKeyIsNotPresent() + func test_removeObjectForKey_succeedsWhenKeyIsNotPresent() throws { - allPermutations.forEach { valet in - XCTAssertTrue(valet.removeObject(forKey: "derp")) + try allPermutations.forEach { valet in + try valet.removeObject(forKey: "derp") } } - func test_removeObjectForKey_succeedsWhenKeyIsPresent() + func test_removeObjectForKey_succeedsWhenKeyIsPresent() throws { - allPermutations.forEach { valet in - XCTAssertTrue(valet.set(string: passcode, forKey: key)) - XCTAssertTrue(valet.removeObject(forKey: key)) - XCTAssertNil(valet.string(forKey: key)) + try allPermutations.forEach { valet in + try valet.setString(passcode, forKey: key) + try valet.removeObject(forKey: key) + XCTAssertThrowsError(try valet.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } } - func test_removeObjectForKey_isDistinctForDifferingAccessibility() + func test_removeObjectForKey_isDistinctForDifferingAccessibility() throws { - let differingAccessibility = Valet.valet(with: vanillaValet.identifier, accessibility: .always) - XCTAssertTrue(vanillaValet.set(string: passcode, forKey: key)) + let differingAccessibility = Valet.valet(with: vanillaValet.identifier, accessibility: .whenUnlockedThisDeviceOnly) + try vanillaValet.setString(passcode, forKey: key) - XCTAssertTrue(differingAccessibility.removeObject(forKey: key)) + try differingAccessibility.removeObject(forKey: key) - XCTAssertEqual(passcode, vanillaValet.string(forKey: key)) + XCTAssertEqual(passcode, try vanillaValet.string(forKey: key)) } - func test_removeObjectForKey_isDistinctForDifferingIdentifier() + func test_removeObjectForKey_isDistinctForDifferingIdentifier() throws { let differingIdentifier = Valet.valet(with: Identifier(nonEmpty: "no")!, accessibility: vanillaValet.accessibility) - XCTAssertTrue(vanillaValet.set(string: passcode, forKey: key)) + try vanillaValet.setString(passcode, forKey: key) - XCTAssertTrue(differingIdentifier.removeObject(forKey: key)) + try differingIdentifier.removeObject(forKey: key) - XCTAssertEqual(passcode, vanillaValet.string(forKey: key)) + XCTAssertEqual(passcode, try vanillaValet.string(forKey: key)) } - func test_removeObjectForKey_isDistinctForDifferingClasses() + func test_removeObjectForKey_isDistinctForDifferingClasses() throws { guard testEnvironmentIsSigned() else { return } - XCTAssertTrue(vanillaValet.set(string: passcode, forKey: key)) - XCTAssertTrue(anotherFlavor.set(string: passcode, forKey: key)) + try vanillaValet.setString(passcode, forKey: key) + try anotherFlavor.setString(passcode, forKey: key) - XCTAssertTrue(vanillaValet.removeObject(forKey: key)) + try vanillaValet.removeObject(forKey: key) - XCTAssertNil(vanillaValet.string(forKey: key)) - XCTAssertEqual(passcode, anotherFlavor.string(forKey: key)) + XCTAssertThrowsError(try vanillaValet.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + XCTAssertEqual(passcode, try anotherFlavor.string(forKey: key)) } // MARK: Migration - Query - func test_migrateObjectsMatching_failsIfQueryHasNoInputClass() + func test_migrateObjectsMatching_failsIfQueryHasNoInputClass() throws { guard testEnvironmentIsSigned() else { return } - vanillaValet.set(string: passcode, forKey: key) + try vanillaValet.setString(passcode, forKey: key) + + let valetKeychainQuery = vanillaValet.baseKeychainQuery // Test for base query success. - XCTAssertEqual(anotherFlavor.migrateObjects(matching: vanillaValet.keychainQuery, removeOnCompletion: false), .success) - XCTAssertEqual(passcode, anotherFlavor.string(forKey: key)) + try anotherFlavor.migrateObjects(matching: valetKeychainQuery, removeOnCompletion: false) + XCTAssertEqual(passcode, try anotherFlavor.string(forKey: key)) - var mutableQuery = vanillaValet.keychainQuery + var mutableQuery = valetKeychainQuery mutableQuery.removeValue(forKey: kSecClass as String) // Without a kSecClass, the migration should fail. - XCTAssertEqual(.invalidQuery, anotherFlavor.migrateObjects(matching: mutableQuery, removeOnCompletion: false)) + XCTAssertThrowsError(try anotherFlavor.migrateObjects(matching: mutableQuery, removeOnCompletion: false)) { error in + XCTAssertEqual(error as? MigrationError, .invalidQuery) + } mutableQuery[kSecClass as String] = kSecClassInternetPassword // Without a kSecClass set to something other than kSecClassGenericPassword, the migration should fail. - XCTAssertEqual(.invalidQuery, anotherFlavor.migrateObjects(matching: mutableQuery, removeOnCompletion: false)) + XCTAssertThrowsError(try anotherFlavor.migrateObjects(matching: mutableQuery, removeOnCompletion: false)) { error in + XCTAssertEqual(error as? MigrationError, .invalidQuery) + } } - func test_migrateObjectsMatching_failsIfNoItemsMatchQuery() + func test_migrateObjectsMatching_failsIfNoItemsMatchQuery() throws { guard testEnvironmentIsSigned() else { return } - - let noItemsFoundError = MigrationResult.noItemsToMigrateFound let queryWithNoMatches = [ kSecClass as String: kSecClassGenericPassword as String, kSecAttrService as String: "Valet_Does_Not_Exist" ] - XCTAssertEqual(noItemsFoundError, vanillaValet.migrateObjects(matching: queryWithNoMatches, removeOnCompletion: false)) - XCTAssertEqual(noItemsFoundError, vanillaValet.migrateObjects(matching: queryWithNoMatches, removeOnCompletion: true)) + XCTAssertThrowsError(try vanillaValet.migrateObjects(matching: queryWithNoMatches, removeOnCompletion: false)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + XCTAssertThrowsError(try vanillaValet.migrateObjects(matching: queryWithNoMatches, removeOnCompletion: true)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + + let valetKeychainQuery = vanillaValet.baseKeychainQuery // Our test Valet has not yet been written to, migration should fail: - XCTAssertEqual(noItemsFoundError, anotherFlavor.migrateObjects(matching: vanillaValet.keychainQuery, removeOnCompletion: false)) - XCTAssertEqual(noItemsFoundError, anotherFlavor.migrateObjects(matching: vanillaValet.keychainQuery, removeOnCompletion: true)) + XCTAssertThrowsError(try anotherFlavor.migrateObjects(matching: valetKeychainQuery, removeOnCompletion: false)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } + XCTAssertThrowsError(try anotherFlavor.migrateObjects(matching: valetKeychainQuery, removeOnCompletion: true)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } - func test_migrateObjectsMatching_bailsOutIfConflictExistsInQueryResult() + func test_migrateObjectsMatching_bailsOutIfConflictExistsToMigrate() throws { let migrationValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) - migrationValet.removeAllObjects() - - XCTAssertTrue(vanillaValet.set(string: passcode, forKey: key)) + try migrationValet.removeAllObjects() + let anotherValet = Valet.valet(with: Identifier(nonEmpty: #function)!, accessibility: .whenUnlocked) - XCTAssertTrue(anotherValet.set(string: passcode, forKey: key)) + try vanillaValet.setString(passcode, forKey: key) + try anotherValet.setString(passcode, forKey:key) let conflictingQuery = [ kSecClass as String: kSecClassGenericPassword as String, kSecAttrAccount as String: key ] - XCTAssertEqual(.duplicateKeyInQueryResult, migrationValet.migrateObjects(matching: conflictingQuery, removeOnCompletion: false)) - anotherValet.removeAllObjects() + XCTAssertThrowsError(try migrationValet.migrateObjects(matching: conflictingQuery, removeOnCompletion: false)) { error in + XCTAssertEqual(error as? MigrationError, .duplicateKeyToMigrate) + } } - func test_migrateObjectsMatching_withAccountNameAsData_doesNotRaiseException() + func test_migrateObjectsMatching_withAccountNameAsData_doesNotRaiseException() throws { let identifier = "Keychain_With_Account_Name_As_NSData" @@ -635,26 +766,26 @@ class ValetIntegrationTests: XCTestCase kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: identifier ] - let migrationResult = vanillaValet.migrateObjects(matching: query, removeOnCompletion: false) - - XCTAssertEqual(migrationResult, .keyInQueryResultInvalid) + XCTAssertThrowsError(try vanillaValet.migrateObjects(matching: query, removeOnCompletion: false)) { error in + XCTAssertEqual(error as? MigrationError, .keyToMigrateInvalid) + } } // MARK: Migration - Valet - func test_migrateObjectsFromValet_migratesSingleKeyValuePairSuccessfully() + func test_migrateObjectsFromValet_migratesSingleKeyValuePairSuccessfully() throws { guard testEnvironmentIsSigned() else { return } - anotherFlavor.set(string: "foo", forKey: "bar") - _ = vanillaValet.migrateObjects(from: anotherFlavor, removeOnCompletion: false) - _ = vanillaValet.allKeys() - XCTAssertEqual("foo", vanillaValet.string(forKey: "bar")) + try anotherFlavor.setString("foo", forKey: "bar") + try vanillaValet.migrateObjects(from: anotherFlavor, removeOnCompletion: false) + _ = try vanillaValet.allKeys() + XCTAssertEqual("foo", try vanillaValet.string(forKey: "bar")) } - func test_migrateObjectsFromValet_migratesMultipleKeyValuePairsSuccessfully() + func test_migrateObjectsFromValet_migratesMultipleKeyValuePairsSuccessfully() throws { guard testEnvironmentIsSigned() else { return @@ -669,20 +800,20 @@ class ValetIntegrationTests: XCTestCase ] for (key, value) in keyValuePairs { - anotherFlavor.set(string: value, forKey: key) + try anotherFlavor.setString(value, forKey: key) } - XCTAssertEqual(vanillaValet.migrateObjects(from: anotherFlavor, removeOnCompletion: false), .success) + try vanillaValet.migrateObjects(from: anotherFlavor, removeOnCompletion: false) // Both the migration target and the previous Valet should hold all key/value pairs. - XCTAssertEqual(vanillaValet.allKeys(), anotherFlavor.allKeys()) + XCTAssertEqual(try vanillaValet.allKeys(), try anotherFlavor.allKeys()) for (key, value) in keyValuePairs { - XCTAssertEqual(vanillaValet.string(forKey: key), value) - XCTAssertEqual(anotherFlavor.string(forKey: key), value) + XCTAssertEqual(try vanillaValet.string(forKey: key), value) + XCTAssertEqual(try anotherFlavor.string(forKey: key), value) } } - func test_migrateObjectsFromValet_removesOnCompletionWhenRequested() + func test_migrateObjectsFromValet_removesOnCompletionWhenRequested() throws { guard testEnvironmentIsSigned() else { return @@ -697,20 +828,22 @@ class ValetIntegrationTests: XCTestCase ] for (key, value) in keyValuePairs { - anotherFlavor.set(string: value, forKey: key) + try anotherFlavor.setString(value, forKey: key) } - XCTAssertEqual(vanillaValet.migrateObjects(from: anotherFlavor, removeOnCompletion: true), .success) + try vanillaValet.migrateObjects(from: anotherFlavor, removeOnCompletion: true) // The migration target should hold all key/value pairs, the previous Valet should be empty. - XCTAssertEqual(0, anotherFlavor.allKeys().count) + XCTAssertEqual(0, try anotherFlavor.allKeys().count) for (key, value) in keyValuePairs { - XCTAssertEqual(vanillaValet.string(forKey: key), value) - XCTAssertNil(anotherFlavor.string(forKey: key)) + XCTAssertEqual(try vanillaValet.string(forKey: key), value) + XCTAssertThrowsError(try anotherFlavor.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } } } - func test_migrateObjectsFromValet_leavesKeychainUntouchedWhenConflictsExist() + func test_migrateObjectsFromValet_leavesKeychainUntouchedWhenConflictsExist() throws { guard testEnvironmentIsSigned() else { return @@ -725,44 +858,46 @@ class ValetIntegrationTests: XCTestCase ] for (key, value) in keyValuePairs { - anotherFlavor.set(string: value, forKey: key) + try anotherFlavor.setString(value, forKey: key) } - vanillaValet.set(string: "adrian", forKey: "yo") + try vanillaValet.setString("adrian", forKey: "yo") - XCTAssertEqual(1, vanillaValet.allKeys().count) - XCTAssertEqual(keyValuePairs.count, anotherFlavor.allKeys().count) + XCTAssertEqual(1, try vanillaValet.allKeys().count) + XCTAssertEqual(keyValuePairs.count, try anotherFlavor.allKeys().count) - XCTAssertEqual(.keyInQueryResultAlreadyExistsInValet, vanillaValet.migrateObjects(from: anotherFlavor, removeOnCompletion: true)) + XCTAssertThrowsError(try vanillaValet.migrateObjects(from: anotherFlavor, removeOnCompletion: true)) { error in + XCTAssertEqual(error as? MigrationError, .keyToMigrateAlreadyExistsInValet) + } // Neither Valet should have seen any changes. - XCTAssertEqual(1, vanillaValet.allKeys().count) - XCTAssertEqual(keyValuePairs.count, anotherFlavor.allKeys().count) + XCTAssertEqual(1, try vanillaValet.allKeys().count) + XCTAssertEqual(keyValuePairs.count, try anotherFlavor.allKeys().count) - XCTAssertEqual("adrian", vanillaValet.string(forKey: "yo")) + XCTAssertEqual("adrian", try vanillaValet.string(forKey: "yo")) for (key, value) in keyValuePairs { - XCTAssertEqual(anotherFlavor.string(forKey: key), value) + XCTAssertEqual(try anotherFlavor.string(forKey: key), value) } } - func test_migrateObjectsFromValetRemoveOnCompletion_migratesDataSuccessfullyWhenBothValetsHavePreviouslyCalled_canAccessKeychain() { + func test_migrateObjectsFromValetRemoveOnCompletion_migratesDataSuccessfullyWhenBothValetsHavePreviouslyCalled_canAccessKeychain() throws { let otherValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me_To_Valet")!, accessibility: .afterFirstUnlock) // Clean up any dangling keychain items before we start this test. - otherValet.removeAllObjects() + try otherValet.removeAllObjects() let keyStringPairToMigrateMap = ["foo" : "bar", "testing" : "migration", "is" : "quite", "entertaining" : "if", "you" : "don't", "screw" : "up"] for (key, value) in keyStringPairToMigrateMap { - XCTAssertTrue(otherValet.set(string: value, forKey: key)) + try otherValet.setString(value, forKey: key) } XCTAssertTrue(vanillaValet.canAccessKeychain()) XCTAssertTrue(otherValet.canAccessKeychain()) - XCTAssertEqual(vanillaValet.migrateObjects(from: otherValet, removeOnCompletion: false), .success) + try vanillaValet.migrateObjects(from: otherValet, removeOnCompletion: false) for (key, value) in keyStringPairToMigrateMap { - XCTAssertEqual(vanillaValet.string(forKey: key), value ) - XCTAssertEqual(otherValet.string(forKey: key), value) + XCTAssertEqual(try vanillaValet.string(forKey: key), value) + XCTAssertEqual(try otherValet.string(forKey: key), value) } } diff --git a/Tests/ValetObjectiveCBridgeTests/VALSecureEnclaveValetTests.m b/Tests/ValetObjectiveCBridgeTests/VALSecureEnclaveValetTests.m new file mode 100644 index 00000000..03bd9b8d --- /dev/null +++ b/Tests/ValetObjectiveCBridgeTests/VALSecureEnclaveValetTests.m @@ -0,0 +1,169 @@ +// Created by Dan Federman on 1/16/20. +// Copyright © 2020 Square, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//    http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import + +@interface VALSecureEnclaveValetTests : XCTestCase +@end + +@implementation VALSecureEnclaveValetTests + +- (NSString *)identifier; +{ + return @"identifier"; +} + +- (NSString *)appIDPrefix; +{ + return @"9XUJ7M53NG"; +} + +- (NSString *)sharedAccessGroupIdentifier; +{ +#if TARGET_OS_IPHONE + return @"com.squareup.Valet-iOS-Test-Host-App"; +#elif TARGET_OS_WATCH + return @"com.squareup.ValetTouchIDTestApp.watchkitapp.watchkitextension"; +#elif TARGET_OS_MAC + return @"com.squareup.Valet-macOS-Test-Host-App"; +#else + // This will fail + return @""; +#endif +} + +- (NSString *)groupPrefix; +{ +#if TARGET_OS_IPHONE + return @"group"; +#elif TARGET_OS_WATCH + return @"group"; +#elif TARGET_OS_MAC + return self.appIDPrefix; +#else + // This will fail + return @""; +#endif +} + +- (NSString *)sharedAppGroupIdentifier; +{ + return @"valet.test"; +} + +- (void)test_valetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlDevicePasscode; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet valetWithIdentifier:self.identifier accessControl:VALSecureEnclaveAccessControlDevicePasscode]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlDevicePasscode); + XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); +} + +- (void)test_valetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlUserPresence; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet valetWithIdentifier:self.identifier accessControl:VALSecureEnclaveAccessControlUserPresence]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlUserPresence); + XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); +} + +- (void)test_valetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricAny; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet valetWithIdentifier:self.identifier accessControl:VALSecureEnclaveAccessControlBiometricAny]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricAny); + XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); +} + +- (void)test_valetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricCurrentSet; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet valetWithIdentifier:self.identifier accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricCurrentSet); + XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); +} + +- (void)test_valetWithIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet valetWithIdentifier:@"" accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; + XCTAssertNil(valet); +} + +- (void)test_sharedAccessGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlDevicePasscode; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessControl:VALSecureEnclaveAccessControlDevicePasscode]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlDevicePasscode); + XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); +} + +- (void)test_sharedAccessGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlUserPresence; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessControl:VALSecureEnclaveAccessControlUserPresence]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlUserPresence); + XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); +} + +- (void)test_sharedAccessGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricAny; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessControl:VALSecureEnclaveAccessControlBiometricAny]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricAny); + XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); +} + +- (void)test_sharedAccessGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricCurrentSet; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricCurrentSet); + XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); +} + +- (void)test_sharedAccessGroupValetWithIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:@"" accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; + XCTAssertNil(valet); +} + +- (void)test_sharedAppGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlDevicePasscode; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessControl:VALSecureEnclaveAccessControlDevicePasscode]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlDevicePasscode); + XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); +} + +- (void)test_sharedAppGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlUserPresence; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessControl:VALSecureEnclaveAccessControlUserPresence]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlUserPresence); + XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); +} + +- (void)test_sharedAppGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricAny; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessControl:VALSecureEnclaveAccessControlBiometricAny]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricAny); + XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); +} + +- (void)test_sharedAppGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricCurrentSet; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricCurrentSet); + XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); +} + +- (void)test_sharedAppGroupValetWithIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:@"" accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; + XCTAssertNil(valet); +} + +@end diff --git a/Tests/ValetObjectiveCBridgeTests/VALSinglePromptSecureEnclaveValetTests.m b/Tests/ValetObjectiveCBridgeTests/VALSinglePromptSecureEnclaveValetTests.m new file mode 100644 index 00000000..6782236c --- /dev/null +++ b/Tests/ValetObjectiveCBridgeTests/VALSinglePromptSecureEnclaveValetTests.m @@ -0,0 +1,199 @@ +// Created by Dan Federman on 1/16/20. +// Copyright © 2020 Square, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//    http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import + +@interface VALSinglePromptSecureEnclaveValetTests : XCTestCase +@end + +@implementation VALSinglePromptSecureEnclaveValetTests + +- (NSString *)identifier; +{ + return @"identifier"; +} + +- (NSString *)appIDPrefix; +{ + return @"9XUJ7M53NG"; +} + +- (NSString *)sharedAccessGroupIdentifier; +{ +#if TARGET_OS_IPHONE + return @"com.squareup.Valet-iOS-Test-Host-App"; +#elif TARGET_OS_WATCH + return @"com.squareup.ValetTouchIDTestApp.watchkitapp.watchkitextension"; +#elif TARGET_OS_MAC + return @"com.squareup.Valet-macOS-Test-Host-App"; +#else + // This will fail + return @""; +#endif +} + +- (NSString *)groupPrefix; +{ +#if TARGET_OS_IPHONE + return @"group"; +#elif TARGET_OS_WATCH + return @"group"; +#elif TARGET_OS_MAC + return self.appIDPrefix; +#else + // This will fail + return @""; +#endif +} + +- (NSString *)sharedAppGroupIdentifier; +{ + return @"valet.test"; +} + +- (void)test_valetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlDevicePasscode; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet valetWithIdentifier:self.identifier accessControl:VALSecureEnclaveAccessControlDevicePasscode]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlDevicePasscode); + XCTAssertEqual([valet class], [VALSinglePromptSecureEnclaveValet class]); + } +} + +- (void)test_valetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlUserPresence; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet valetWithIdentifier:self.identifier accessControl:VALSecureEnclaveAccessControlUserPresence]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlUserPresence); + XCTAssertEqual([valet class], [VALSinglePromptSecureEnclaveValet class]); + } +} + +- (void)test_valetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricAny; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet valetWithIdentifier:self.identifier accessControl:VALSecureEnclaveAccessControlBiometricAny]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricAny); + XCTAssertEqual([valet class], [VALSinglePromptSecureEnclaveValet class]); + } +} + +- (void)test_valetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricCurrentSet; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet valetWithIdentifier:self.identifier accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricCurrentSet); + XCTAssertEqual([valet class], [VALSinglePromptSecureEnclaveValet class]); + } +} + +- (void)test_valetWithIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet valetWithIdentifier:@"" accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; + XCTAssertNil(valet); + } +} + +- (void)test_sharedAccessGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlDevicePasscode; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessControl:VALSecureEnclaveAccessControlDevicePasscode]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlDevicePasscode); + XCTAssertEqual([valet class], [VALSinglePromptSecureEnclaveValet class]); + } +} + +- (void)test_sharedAccessGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlUserPresence; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessControl:VALSecureEnclaveAccessControlUserPresence]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlUserPresence); + XCTAssertEqual([valet class], [VALSinglePromptSecureEnclaveValet class]); + } +} + +- (void)test_sharedAccessGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricAny; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessControl:VALSecureEnclaveAccessControlBiometricAny]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricAny); + XCTAssertEqual([valet class], [VALSinglePromptSecureEnclaveValet class]); + } +} + +- (void)test_sharedAccessGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricCurrentSet; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricCurrentSet); + XCTAssertEqual([valet class], [VALSinglePromptSecureEnclaveValet class]); + } +} + +- (void)test_sharedAccessGroupValetWithIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:@"" accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; + XCTAssertNil(valet); + } +} + +- (void)test_sharedAppGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlDevicePasscode; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessControl:VALSecureEnclaveAccessControlDevicePasscode]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlDevicePasscode); + XCTAssertEqual([valet class], [VALSinglePromptSecureEnclaveValet class]); + } +} + +- (void)test_sharedAppGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlUserPresence; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessControl:VALSecureEnclaveAccessControlUserPresence]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlUserPresence); + XCTAssertEqual([valet class], [VALSinglePromptSecureEnclaveValet class]); + } +} + +- (void)test_sharedAppGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricAny; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessControl:VALSecureEnclaveAccessControlBiometricAny]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricAny); + XCTAssertEqual([valet class], [VALSinglePromptSecureEnclaveValet class]); + } +} + +- (void)test_sharedAppGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricCurrentSet; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; + XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricCurrentSet); + XCTAssertEqual([valet class], [VALSinglePromptSecureEnclaveValet class]); + } +} + +- (void)test_sharedAppGroupValetWithIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + if (@available(tvOS 11.0, *)) { + VALSinglePromptSecureEnclaveValet *const valet = [VALSinglePromptSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:@"" accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; + XCTAssertNil(valet); + } +} + +@end diff --git a/Tests/ValetObjectiveCBridgeTests/VALValetTests.m b/Tests/ValetObjectiveCBridgeTests/VALValetTests.m new file mode 100644 index 00000000..4014d04c --- /dev/null +++ b/Tests/ValetObjectiveCBridgeTests/VALValetTests.m @@ -0,0 +1,378 @@ +// Created by Dan Federman on 1/16/20. +// Copyright © 2020 Square, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//    http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import + +@interface VALValetTests : XCTestCase +@end + +@implementation VALValetTests + +- (NSString *)identifier; +{ + return @"identifier"; +} + +- (NSString *)appIDPrefix; +{ + return @"9XUJ7M53NG"; +} + +- (NSString *)sharedAccessGroupIdentifier; +{ +#if TARGET_OS_IPHONE + return @"com.squareup.Valet-iOS-Test-Host-App"; +#elif TARGET_OS_WATCH + return @"com.squareup.ValetTouchIDTestApp.watchkitapp.watchkitextension"; +#elif TARGET_OS_MAC + return @"com.squareup.Valet-macOS-Test-Host-App"; +#else + // This will fail + return @""; +#endif +} + +- (NSString *)groupPrefix; +{ +#if TARGET_OS_IPHONE + return @"group"; +#elif TARGET_OS_WATCH + return @"group"; +#elif TARGET_OS_MAC + return self.appIDPrefix; +#else + // This will fail + return @""; +#endif +} + +- (NSString *)sharedAppGroupIdentifier; +{ + return @"valet.test"; +} + +- (void)test_valetWithIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenUnlocked; +{ + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlocked); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithIdentifier_accessibility_returnsCorrectValet_VALAccessibilityAfterFirstUnlock; +{ + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityAfterFirstUnlock]; + XCTAssertEqual(valet.accessibility, VALAccessibilityAfterFirstUnlock); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenPasscodeSetThisDeviceOnly; +{ + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenPasscodeSetThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenPasscodeSetThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenUnlockedThisDeviceOnly; +{ + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlockedThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlockedThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithIdentifier_accessibility_returnsCorrectValet_VALAccessibilityAfterFirstUnlockThisDeviceOnly; +{ + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityAfterFirstUnlockThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityAfterFirstUnlockThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + VALValet *const valet = [VALValet valetWithIdentifier:@"" accessibility:VALAccessibilityAfterFirstUnlockThisDeviceOnly]; + XCTAssertNil(valet); +} + +- (void)test_iCloudValetWithIdentifier_accessibility_returnsCorrectValet_VALCloudAccessibilityWhenUnlocked; +{ + VALValet *const valet = [VALValet iCloudValetWithIdentifier:self.identifier accessibility:VALCloudAccessibilityWhenUnlocked]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlocked); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_iCloudValetWithIdentifier_accessibility_returnsCorrectValet_VALCloudAccessibilityAfterFirstUnlock; +{ + VALValet *const valet = [VALValet iCloudValetWithIdentifier:self.identifier accessibility:VALCloudAccessibilityAfterFirstUnlock]; + XCTAssertEqual(valet.accessibility, VALAccessibilityAfterFirstUnlock); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_iCloudValetWithIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + VALValet *const valet = [VALValet iCloudValetWithIdentifier:@"" accessibility:VALCloudAccessibilityAfterFirstUnlock]; + XCTAssertNil(valet); +} + +- (void)test_valetWithSharedAccessGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenUnlocked; +{ + VALValet *const valet = [VALValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessibility:VALAccessibilityWhenUnlocked]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlocked); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithSharedAccessGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityAfterFirstUnlock; +{ + VALValet *const valet = [VALValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessibility:VALAccessibilityAfterFirstUnlock]; + XCTAssertEqual(valet.accessibility, VALAccessibilityAfterFirstUnlock); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithSharedAccessGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenPasscodeSetThisDeviceOnly; +{ + VALValet *const valet = [VALValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessibility:VALAccessibilityWhenPasscodeSetThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenPasscodeSetThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithSharedAccessGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenUnlockedThisDeviceOnly; +{ + VALValet *const valet = [VALValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessibility:VALAccessibilityWhenUnlockedThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlockedThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithSharedAccessGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityAfterFirstUnlockThisDeviceOnly; +{ + VALValet *const valet = [VALValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessibility:VALAccessibilityAfterFirstUnlockThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityAfterFirstUnlockThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithSharedAccessGroupIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + VALValet *const valet = [VALValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:@"" accessibility:VALAccessibilityAfterFirstUnlockThisDeviceOnly]; + XCTAssertNil(valet); +} + +- (void)test_valetWithSharedAppGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenUnlocked; +{ + VALValet *const valet = [VALValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessibility:VALAccessibilityWhenUnlocked]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlocked); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithSharedAppGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityAfterFirstUnlock; +{ + VALValet *const valet = [VALValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessibility:VALAccessibilityAfterFirstUnlock]; + XCTAssertEqual(valet.accessibility, VALAccessibilityAfterFirstUnlock); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithSharedAppGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenPasscodeSetThisDeviceOnly; +{ + VALValet *const valet = [VALValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessibility:VALAccessibilityWhenPasscodeSetThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenPasscodeSetThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithSharedAppGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenUnlockedThisDeviceOnly; +{ + VALValet *const valet = [VALValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessibility:VALAccessibilityWhenUnlockedThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlockedThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithSharedAppGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityAfterFirstUnlockThisDeviceOnly; +{ + VALValet *const valet = [VALValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessibility:VALAccessibilityAfterFirstUnlockThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityAfterFirstUnlockThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithSharedAppGroupIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + VALValet *const valet = [VALValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:@"" accessibility:VALAccessibilityAfterFirstUnlockThisDeviceOnly]; + XCTAssertNil(valet); +} + +- (void)test_iCloudValetWithSharedAccessGroupIdentifier_accessibility_returnsCorrectValet_VALCloudAccessibilityWhenUnlocked; +{ + VALValet *const valet = [VALValet iCloudValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessibility:VALCloudAccessibilityWhenUnlocked]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlocked); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_iCloudValetWithSharedAccessGroupIdentifier_accessibility_returnsCorrectValet_VALCloudAccessibilityAfterFirstUnlock; +{ + VALValet *const valet = [VALValet iCloudValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessibility:VALCloudAccessibilityAfterFirstUnlock]; + XCTAssertEqual(valet.accessibility, VALAccessibilityAfterFirstUnlock); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_iCloudValetWithSharedAccessGroupIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + VALValet *const valet = [VALValet iCloudValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:@"" accessibility:VALCloudAccessibilityAfterFirstUnlock]; + XCTAssertNil(valet); +} + +- (void)test_iCloudValetWithSharedAppGroupIdentifier_accessibility_returnsCorrectValet_VALCloudAccessibilityWhenUnlocked; +{ + VALValet *const valet = [VALValet iCloudValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessibility:VALCloudAccessibilityWhenUnlocked]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlocked); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_iCloudValetWithSharedAppGroupIdentifier_accessibility_returnsCorrectValet_VALCloudAccessibilityAfterFirstUnlock; +{ + VALValet *const valet = [VALValet iCloudValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessibility:VALCloudAccessibilityAfterFirstUnlock]; + XCTAssertEqual(valet.accessibility, VALAccessibilityAfterFirstUnlock); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_iCloudValetWithSharedAppGroupIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + VALValet *const valet = [VALValet iCloudValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:@"" accessibility:VALCloudAccessibilityAfterFirstUnlock]; + XCTAssertNil(valet); +} + +// MARK: Mac Tests + +#if TARGET_OS_OSX + +- (void)test_valetWithExplicitlySetIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenUnlocked; +{ + VALValet *const valet = [VALValet valetWithExplicitlySetIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlocked); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithExplicitlySetIdentifier_accessibility_returnsCorrectValet_VALAccessibilityAfterFirstUnlock; +{ + VALValet *const valet = [VALValet valetWithExplicitlySetIdentifier:self.identifier accessibility:VALAccessibilityAfterFirstUnlock]; + XCTAssertEqual(valet.accessibility, VALAccessibilityAfterFirstUnlock); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithExplicitlySetIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenPasscodeSetThisDeviceOnly; +{ + VALValet *const valet = [VALValet valetWithExplicitlySetIdentifier:self.identifier accessibility:VALAccessibilityWhenPasscodeSetThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenPasscodeSetThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithExplicitlySetIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenUnlockedThisDeviceOnly; +{ + VALValet *const valet = [VALValet valetWithExplicitlySetIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlockedThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlockedThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithExplicitlySetIdentifier_accessibility_returnsCorrectValet_VALAccessibilityAfterFirstUnlockThisDeviceOnly; +{ + VALValet *const valet = [VALValet valetWithExplicitlySetIdentifier:self.identifier accessibility:VALAccessibilityAfterFirstUnlockThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityAfterFirstUnlockThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithExplicitlySetIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + VALValet *const valet = [VALValet valetWithExplicitlySetIdentifier:@"" accessibility:VALAccessibilityAfterFirstUnlockThisDeviceOnly]; + XCTAssertNil(valet); +} + +- (void)test_iCloudValetWithExplicitlySetIdentifier_accessibility_returnsCorrectValet_VALCloudAccessibilityWhenUnlocked; +{ + VALValet *const valet = [VALValet iCloudValetWithExplicitlySetIdentifier:self.identifier accessibility:VALCloudAccessibilityWhenUnlocked]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlocked); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_iCloudValetWithExplicitlySetIdentifier_accessibility_returnsCorrectValet_VALCloudAccessibilityAfterFirstUnlock; +{ + VALValet *const valet = [VALValet iCloudValetWithExplicitlySetIdentifier:self.identifier accessibility:VALCloudAccessibilityAfterFirstUnlock]; + XCTAssertEqual(valet.accessibility, VALAccessibilityAfterFirstUnlock); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_iCloudValetWithExplicitlySetIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + VALValet *const valet = [VALValet iCloudValetWithExplicitlySetIdentifier:@"" accessibility:VALCloudAccessibilityAfterFirstUnlock]; + XCTAssertNil(valet); +} + +- (void)test_valetWithExplicitlySetSharedGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenUnlocked; +{ + VALValet *const valet = [VALValet valetWithAppIDPrefix:self.appIDPrefix explicitlySetSharedGroupIdentifier:self.sharedAccessGroupIdentifier accessibility:VALAccessibilityWhenUnlocked]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlocked); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithExplicitlySetSharedGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityAfterFirstUnlock; +{ + VALValet *const valet = [VALValet valetWithAppIDPrefix:self.appIDPrefix explicitlySetSharedGroupIdentifier:self.sharedAccessGroupIdentifier accessibility:VALAccessibilityAfterFirstUnlock]; + XCTAssertEqual(valet.accessibility, VALAccessibilityAfterFirstUnlock); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithExplicitlySetSharedGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenPasscodeSetThisDeviceOnly; +{ + VALValet *const valet = [VALValet valetWithAppIDPrefix:self.appIDPrefix explicitlySetSharedGroupIdentifier:self.sharedAccessGroupIdentifier accessibility:VALAccessibilityWhenPasscodeSetThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenPasscodeSetThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithExplicitlySetSharedGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenUnlockedThisDeviceOnly; +{ + VALValet *const valet = [VALValet valetWithAppIDPrefix:self.appIDPrefix explicitlySetSharedGroupIdentifier:self.sharedAccessGroupIdentifier accessibility:VALAccessibilityWhenUnlockedThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlockedThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithExplicitlySetSharedGroupIdentifier_accessibility_returnsCorrectValet_VALAccessibilityAfterFirstUnlockThisDeviceOnly; +{ + VALValet *const valet = [VALValet valetWithAppIDPrefix:self.appIDPrefix explicitlySetSharedGroupIdentifier:self.sharedAccessGroupIdentifier accessibility:VALAccessibilityAfterFirstUnlockThisDeviceOnly]; + XCTAssertEqual(valet.accessibility, VALAccessibilityAfterFirstUnlockThisDeviceOnly); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_valetWithExplicitlySetSharedGroupIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + VALValet *const valet = [VALValet valetWithAppIDPrefix:self.appIDPrefix explicitlySetSharedGroupIdentifier:@"" accessibility:VALAccessibilityAfterFirstUnlockThisDeviceOnly]; + XCTAssertNil(valet); +} + +- (void)test_iCloudValetWithExplicitlySetSharedGroupIdentifier_accessibility_returnsCorrectValet_VALCloudAccessibilityWhenUnlocked; +{ + VALValet *const valet = [VALValet iCloudValetWithAppIDPrefix:self.appIDPrefix explicitlySetSharedGroupIdentifier:self.sharedAccessGroupIdentifier accessibility:VALCloudAccessibilityWhenUnlocked]; + XCTAssertEqual(valet.accessibility, VALAccessibilityWhenUnlocked); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_iCloudValetWithExplicitlySetSharedGroupIdentifier_accessibility_returnsCorrectValet_VALCloudAccessibilityAfterFirstUnlock; +{ + VALValet *const valet = [VALValet iCloudValetWithAppIDPrefix:self.appIDPrefix explicitlySetSharedGroupIdentifier:self.sharedAccessGroupIdentifier accessibility:VALCloudAccessibilityAfterFirstUnlock]; + XCTAssertEqual(valet.accessibility, VALCloudAccessibilityAfterFirstUnlock); + XCTAssertEqual([valet class], [VALValet class]); +} + +- (void)test_iCloudValetWithExplicitlySetSharedGroupIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; +{ + VALValet *const valet = [VALValet iCloudValetWithAppIDPrefix:self.appIDPrefix explicitlySetSharedGroupIdentifier:@"" accessibility:VALCloudAccessibilityAfterFirstUnlock]; + XCTAssertNil(valet); +} + +#endif + +@end diff --git a/Tests/ValetTests/CloudAccessibilityTests.swift b/Tests/ValetTests/CloudAccessibilityTests.swift new file mode 100644 index 00000000..e41358e1 --- /dev/null +++ b/Tests/ValetTests/CloudAccessibilityTests.swift @@ -0,0 +1,37 @@ +// Created by Dan Federman on 1/20/20. +// Copyright © 2020 Square, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//    http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest + +@testable import Valet + + +final class CloudAccessibilityTests: XCTestCase { + + func test_description_mirrorsAccessibilityCounterpartDescription() { + CloudAccessibility.allCases.forEach { + XCTAssertEqual($0.description, $0.accessibility.description) + } + } + + func test_secAccessibilityAttribute_mirrorsAccessibilityCounterpartSecAccessibilityAttribute() { + CloudAccessibility.allCases.forEach { + XCTAssertEqual($0.secAccessibilityAttribute, $0.accessibility.secAccessibilityAttribute) + } + } + +} diff --git a/Tests/ValetTests/CloudTests.swift b/Tests/ValetTests/CloudTests.swift index 785ab23e..01948f75 100644 --- a/Tests/ValetTests/CloudTests.swift +++ b/Tests/ValetTests/CloudTests.swift @@ -30,15 +30,6 @@ class CloudTests: XCTestCase static let accessibility = CloudAccessibility.whenUnlocked let valet = Valet.iCloudValet(with: identifier, accessibility: accessibility) - override func setUp() - { - super.setUp() - - ErrorHandler.customAssertBody = { _, _, _, _ in - // Nothing to do here. - } - } - // MARK: Equality func test_synchronizableValet_isDistinctFromVanillaValetWithEqualConfiguration() diff --git a/Tests/ValetTests/ConfigurationTests.swift b/Tests/ValetTests/ConfigurationTests.swift new file mode 100644 index 00000000..47b0ffb0 --- /dev/null +++ b/Tests/ValetTests/ConfigurationTests.swift @@ -0,0 +1,97 @@ +// Created by Dan Federman on 1/20/20. +// Copyright © 2020 Square, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//    http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest + +@testable import Valet + + +final class ConfigurationTests: XCTestCase { + + func test_description_valet_mirrorsLegacyName() { + Accessibility.allCases.forEach { + XCTAssertEqual(Configuration.valet($0).description, "VALValet") + } + } + + func test_description_iCloud_mirrorsLegacyName() { + CloudAccessibility.allCases.forEach { + XCTAssertEqual(Configuration.iCloud($0).description, "VALSynchronizableValet") + } + } + + func test_description_secureEnclave_mirrorsLegacyName() { + SecureEnclaveAccessControl.allValues().forEach { + XCTAssertEqual(Configuration.secureEnclave($0).description, "VALSecureEnclaveValet") + } + } + + func test_description_singlePromptSecureEnclave_mirrorsLegacyName() { + SecureEnclaveAccessControl.allValues().forEach { + XCTAssertEqual(Configuration.singlePromptSecureEnclave($0).description, "VALSinglePromptSecureEnclaveValet") + } + } + + func test_accessibility_valet_returnsPassedInAccessibility() { + Accessibility.allCases.forEach { + XCTAssertEqual(Configuration.valet($0).accessibility, $0) + } + } + + func test_accessibility_iCloud_returnsPassedInAccessibility() { + CloudAccessibility.allCases.forEach { + XCTAssertEqual(Configuration.iCloud($0).accessibility, $0.accessibility) + } + } + + func test_accessibility_secureEnclave_returnsWhenPassCodeSetThisDeviceOnly() { + SecureEnclaveAccessControl.allValues().forEach { + XCTAssertEqual(Configuration.secureEnclave($0).accessibility, Accessibility.whenPasscodeSetThisDeviceOnly) + } + } + + func test_accessibility_singlePromptSecureEnclave_returnsWhenPassCodeSetThisDeviceOnly() { + SecureEnclaveAccessControl.allValues().forEach { + XCTAssertEqual(Configuration.singlePromptSecureEnclave($0).accessibility, Accessibility.whenPasscodeSetThisDeviceOnly) + } + } + + func test_prettyDescription_valet_isHumanReadable() { + Accessibility.allCases.forEach { + XCTAssertEqual(Configuration.valet($0).prettyDescription, "\($0) (Valet)") + } + } + + func test_prettyDescription_iCloud_isHumanReadable() { + CloudAccessibility.allCases.forEach { + XCTAssertEqual(Configuration.iCloud($0).prettyDescription, "\($0) (iCloud)") + } + } + + func test_prettyDescription_secureEnclave_isHumanReadable() { + SecureEnclaveAccessControl.allValues().forEach { + XCTAssertEqual(Configuration.secureEnclave($0).prettyDescription, "\(Accessibility.whenPasscodeSetThisDeviceOnly) (Secure Enclave)") + } + } + + func test_prettyDescription_singlePromptSecureEnclave_isHumanReadable() { + SecureEnclaveAccessControl.allValues().forEach { + XCTAssertEqual(Configuration.singlePromptSecureEnclave($0).prettyDescription, "\(Accessibility.whenPasscodeSetThisDeviceOnly) (Single Prompt)") + } + } + +} diff --git a/Tests/ValetTests/KeychainErrorTests.swift b/Tests/ValetTests/KeychainErrorTests.swift new file mode 100644 index 00000000..fbcbb710 --- /dev/null +++ b/Tests/ValetTests/KeychainErrorTests.swift @@ -0,0 +1,64 @@ +// Created by Dan Federman on 1/20/20. +// Copyright © 2020 Square, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//    http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest + +@testable import Valet + + +final class KeychainErrorTests: XCTestCase { + + func test_initStatus_createsNotFoundErrorFrom_errSecItemNotFound() { + XCTAssertEqual(KeychainError(status: errSecItemNotFound), KeychainError.itemNotFound) + } + + func test_initStatus_createsUserCancelledFrom_errSecUserCanceled() { + XCTAssertEqual(KeychainError(status: errSecUserCanceled), KeychainError.userCancelled) + } + + func test_initStatus_createsUserCancelledFrom_errSecAuthFailed() { + XCTAssertEqual(KeychainError(status: errSecAuthFailed), KeychainError.userCancelled) + } + + func test_initStatus_createsMissingEntitlementFrom_errSecMissingEntitlement() { + XCTAssertEqual(KeychainError(status: errSecMissingEntitlement), KeychainError.missingEntitlement) + } + + func test_initStatus_createsCouldNotAccessKeychainFrom_errSecNotAvailable() { + XCTAssertEqual(KeychainError(status: errSecNotAvailable), KeychainError.couldNotAccessKeychain) + } + + func test_description_createsHumanReadableDescription() { + KeychainError.allCases.forEach { + switch $0 { + case .couldNotAccessKeychain: + XCTAssertEqual($0.description, "KeychainError.couldNotAccessKeychain") + case .emptyKey: + XCTAssertEqual($0.description, "KeychainError.emptyKey") + case .emptyValue: + XCTAssertEqual($0.description, "KeychainError.emptyValue") + case .itemNotFound: + XCTAssertEqual($0.description, "KeychainError.itemNotFound") + case .missingEntitlement: + XCTAssertEqual($0.description, "KeychainError.missingEntitlement") + case .userCancelled: + XCTAssertEqual($0.description, "KeychainError.userCancelled") + } + + } + } +} diff --git a/Tests/ValetTests/MigrationErrorTests.swift b/Tests/ValetTests/MigrationErrorTests.swift new file mode 100644 index 00000000..287ebc8f --- /dev/null +++ b/Tests/ValetTests/MigrationErrorTests.swift @@ -0,0 +1,43 @@ +// Created by Dan Federman on 1/20/20. +// Copyright © 2020 Square, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//    http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest + +@testable import Valet + + +final class MigrationErrorTests: XCTestCase { + + func test_description_createsHumanReadableDescription() { + MigrationError.allCases.forEach { + switch $0 { + case .invalidQuery: + XCTAssertEqual($0.description, "MigrationError.invalidQuery") + case .keyToMigrateInvalid: + XCTAssertEqual($0.description, "MigrationError.keyToMigrateInvalid") + case .dataToMigrateInvalid: + XCTAssertEqual($0.description, "MigrationError.dataToMigrateInvalid") + case .duplicateKeyToMigrate: + XCTAssertEqual($0.description, "MigrationError.duplicateKeyToMigrate") + case .keyToMigrateAlreadyExistsInValet: + XCTAssertEqual($0.description, "MigrationError.keyToMigrateAlreadyExistsInValet") + case .removalFailed: + XCTAssertEqual($0.description, "MigrationError.removalFailed") + } + } + } +} diff --git a/Tests/ValetTests/SecureEnclaveTests.swift b/Tests/ValetTests/SecureEnclaveTests.swift index 971cb548..bc1a7893 100644 --- a/Tests/ValetTests/SecureEnclaveTests.swift +++ b/Tests/ValetTests/SecureEnclaveTests.swift @@ -29,15 +29,6 @@ class SecureEnclaveTests: XCTestCase static let identifier = Identifier(nonEmpty: "valet_testing")! let valet = SecureEnclaveValet.valet(with: identifier, accessControl: .userPresence) - override func setUp() - { - super.setUp() - - ErrorHandler.customAssertBody = { _, _, _, _ in - // Nothing to do here. - } - } - // MARK: Initialization func test_init_createsCorrectBackingService() { @@ -50,11 +41,11 @@ class SecureEnclaveTests: XCTestCase } func test_init_createsCorrectBackingService_sharedAccess() { - let identifier = ValetTests.identifier + let identifier = Valet.sharedAccessGroupIdentifier SecureEnclaveAccessControl.allValues().forEach { accessControl in - let backingService = SecureEnclaveValet.sharedAccessGroupValet(with: identifier, accessControl: accessControl).service - XCTAssertEqual(backingService, Service.sharedAccessGroup(identifier, .secureEnclave(accessControl))) + let backingService = SecureEnclaveValet.sharedGroupValet(with: identifier, accessControl: accessControl).service + XCTAssertEqual(backingService, Service.sharedGroup(identifier, .secureEnclave(accessControl))) } } diff --git a/Tests/ValetTests/SinglePromptSecureEnclaveTests.swift b/Tests/ValetTests/SinglePromptSecureEnclaveTests.swift index bd8151d7..89d32585 100644 --- a/Tests/ValetTests/SinglePromptSecureEnclaveTests.swift +++ b/Tests/ValetTests/SinglePromptSecureEnclaveTests.swift @@ -18,27 +18,19 @@ // limitations under the License. // -#if os(iOS) || os(macOS) +#if canImport(LocalAuthentication) import Foundation @testable import Valet import XCTest +@available(tvOS 11.0, *) class SinglePromptSecureEnclaveTests: XCTestCase { static let identifier = Identifier(nonEmpty: "valet_testing")! let valet = SinglePromptSecureEnclaveValet.valet(with: SinglePromptSecureEnclaveTests.identifier, accessControl: .userPresence) - override func setUp() - { - super.setUp() - - ErrorHandler.customAssertBody = { _, _, _, _ in - // Nothing to do here. - } - } - // MARK: Initialization func test_init_createsCorrectBackingService() { @@ -51,11 +43,11 @@ class SinglePromptSecureEnclaveTests: XCTestCase } func test_init_createsCorrectBackingService_sharedAccess() { - let identifier = ValetTests.identifier + let identifier = Valet.sharedAccessGroupIdentifier SecureEnclaveAccessControl.allValues().forEach { accessControl in - let backingService = SinglePromptSecureEnclaveValet.sharedAccessGroupValet(with: identifier, accessControl: accessControl).service - XCTAssertEqual(backingService, Service.sharedAccessGroup(identifier, .singlePromptSecureEnclave(accessControl))) + let backingService = SinglePromptSecureEnclaveValet.sharedGroupValet(with: identifier, accessControl: accessControl).service + XCTAssertEqual(backingService, Service.sharedGroup(identifier, .singlePromptSecureEnclave(accessControl))) } } diff --git a/Tests/ValetTests/ValetTests.swift b/Tests/ValetTests/ValetTests.swift index 863187e6..d9a9e7b4 100644 --- a/Tests/ValetTests/ValetTests.swift +++ b/Tests/ValetTests/ValetTests.swift @@ -29,54 +29,41 @@ class ValetTests: XCTestCase static let identifier = Identifier(nonEmpty: "valet_testing")! let valet = Valet.valet(with: identifier, accessibility: .whenUnlocked) - // MARK: XCTestCase - - override func setUp() - { - super.setUp() - - ErrorHandler.customAssertBody = { _, _, _, _ in - // Nothing to do here. - } - - valet.removeAllObjects() - } - // MARK: Initialization func test_init_createsCorrectBackingService() { let identifier = ValetTests.identifier - Accessibility.allValues().forEach { accessibility in + Accessibility.allCases.forEach { accessibility in let backingService = Valet.valet(with: identifier, accessibility: accessibility).service XCTAssertEqual(backingService, Service.standard(identifier, .valet(accessibility))) } } func test_init_createsCorrectBackingService_sharedAccess() { - let identifier = ValetTests.identifier + let identifier = Valet.sharedAccessGroupIdentifier - Accessibility.allValues().forEach { accessibility in - let backingService = Valet.sharedAccessGroupValet(with: identifier, accessibility: accessibility).service - XCTAssertEqual(backingService, Service.sharedAccessGroup(identifier, .valet(accessibility))) + Accessibility.allCases.forEach { accessibility in + let backingService = Valet.sharedGroupValet(with: identifier, accessibility: accessibility).service + XCTAssertEqual(backingService, Service.sharedGroup(identifier, .valet(accessibility))) } } func test_init_createsCorrectBackingService_cloud() { let identifier = ValetTests.identifier - CloudAccessibility.allValues().forEach { accessibility in + CloudAccessibility.allCases.forEach { accessibility in let backingService = Valet.iCloudValet(with: identifier, accessibility: accessibility).service XCTAssertEqual(backingService, Service.standard(identifier, .iCloud(accessibility))) } } func test_init_createsCorrectBackingService_cloudSharedAccess() { - let identifier = ValetTests.identifier + let identifier = Valet.sharedAccessGroupIdentifier - CloudAccessibility.allValues().forEach { accessibility in - let backingService = Valet.iCloudSharedAccessGroupValet(with: identifier, accessibility: accessibility).service - XCTAssertEqual(backingService, Service.sharedAccessGroup(identifier, .iCloud(accessibility))) + CloudAccessibility.allCases.forEach { accessibility in + let backingService = Valet.iCloudSharedGroupValet(with: identifier, accessibility: accessibility).service + XCTAssertEqual(backingService, Service.sharedGroup(identifier, .iCloud(accessibility))) } } @@ -104,7 +91,7 @@ class ValetTests: XCTestCase func test_valetsWithDifferingAccessibility_areNotEqual() { - let differingAccessibility = Valet.valet(with: valet.identifier, accessibility: .always) + let differingAccessibility = Valet.valet(with: valet.identifier, accessibility: .whenUnlockedThisDeviceOnly) XCTAssertNotEqual(valet, differingAccessibility) } @@ -112,38 +99,48 @@ class ValetTests: XCTestCase func test_migrateObjectsMatching_failsForBadQueries() { - let invalidQueryError = MigrationResult.invalidQuery - - XCTAssertEqual(invalidQueryError, valet.migrateObjects(matching: [:], removeOnCompletion: false)) - XCTAssertEqual(invalidQueryError, valet.migrateObjects(matching: [:], removeOnCompletion: true)) + XCTAssertThrowsError(try valet.migrateObjects(matching: [:], removeOnCompletion: false)) { error in + XCTAssertEqual(error as? MigrationError, .invalidQuery) + } + XCTAssertThrowsError(try valet.migrateObjects(matching: [:], removeOnCompletion: true)) { error in + XCTAssertEqual(error as? MigrationError, .invalidQuery) + } var invalidQuery: [String: AnyHashable] = [ kSecClass as String: kSecClassGenericPassword, kSecMatchLimit as String: kSecMatchLimitOne ] // Migration queries should have kSecMatchLimit set to .All - XCTAssertEqual(invalidQueryError, valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) + XCTAssertThrowsError(try valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) { error in + XCTAssertEqual(error as? MigrationError, .invalidQuery) + } invalidQuery = [ kSecClass as String: kSecClassGenericPassword, kSecReturnData as String: kCFBooleanTrue ] // Migration queries do not support kSecReturnData - XCTAssertEqual(invalidQueryError, valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) + XCTAssertThrowsError(try valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) { error in + XCTAssertEqual(error as? MigrationError, .invalidQuery) + } invalidQuery = [ kSecClass as String: kSecClassGenericPassword, kSecReturnRef as String: kCFBooleanTrue ] // Migration queries do not support kSecReturnRef - XCTAssertEqual(invalidQueryError, valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) + XCTAssertThrowsError(try valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) { error in + XCTAssertEqual(error as? MigrationError, .invalidQuery) + } invalidQuery = [ kSecClass as String: kSecClassGenericPassword, kSecReturnPersistentRef as String: kCFBooleanFalse ] // Migration queries must have kSecReturnPersistentRef set to true - XCTAssertEqual(invalidQueryError, valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) + XCTAssertThrowsError(try valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) { error in + XCTAssertEqual(error as? MigrationError, .invalidQuery) + } invalidQuery = [ @@ -151,14 +148,18 @@ class ValetTests: XCTestCase kSecReturnAttributes as String: kCFBooleanFalse ] // Migration queries must have kSecReturnAttributes set to true - XCTAssertEqual(invalidQueryError, valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) + XCTAssertThrowsError(try valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) { error in + XCTAssertEqual(error as? MigrationError, .invalidQuery) + } invalidQuery = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccessControl as String: NSNull() ] // Migration queries must not have kSecAttrAccessControl set - XCTAssertEqual(invalidQueryError, valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) + XCTAssertThrowsError(try valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) { error in + XCTAssertEqual(error as? MigrationError, .invalidQuery) + } } } diff --git a/Tests/XCTest-watchOS b/Tests/XCTest-watchOS index 54596f48..913a187a 160000 --- a/Tests/XCTest-watchOS +++ b/Tests/XCTest-watchOS @@ -1 +1 @@ -Subproject commit 54596f4818cb2edc123087b2a72124b9fc38da8d +Subproject commit 913a187a1f946976ebe83181131caafd2a0782b6 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 6ac9c834..f00985b7 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 @@ -2,6 +2,10 @@ + com.apple.security.application-groups + + group.valet.test + keychain-access-groups $(AppIdentifierPrefix)com.squareup.Valet-iOS-Test-Host-App 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 a7e49f67..9e73df49 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 @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + $(TeamIdentifierPrefix)valet.test + com.apple.security.files.user-selected.read-only keychain-access-groups 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 064929f8..2225c3e1 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 @@ -2,6 +2,10 @@ + com.apple.security.application-groups + + group.valet.test + keychain-access-groups $(AppIdentifierPrefix)com.squareup.Valet-tvOS-Test-Host-App 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 033442ae..8a57f3d8 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 @@ -2,6 +2,10 @@ + com.apple.security.application-groups + + group.valet.test + keychain-access-groups $(AppIdentifierPrefix)com.squareup.ValetTouchIDTestApp.watchkitapp.watchkitextension diff --git a/Valet watchOS Test Host App/Valet watchOS Test Host App.entitlements b/Valet watchOS Test Host App/Valet watchOS Test Host App.entitlements new file mode 100644 index 00000000..62171960 --- /dev/null +++ b/Valet watchOS Test Host App/Valet watchOS Test Host App.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.valet.test + + + diff --git a/Valet.podspec b/Valet.podspec index be7b65ed..7a83f696 100644 --- a/Valet.podspec +++ b/Valet.podspec @@ -1,12 +1,12 @@ Pod::Spec.new do |s| s.name = 'Valet' - s.version = '3.2.8' + s.version = '4.0.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' s.authors = 'Square' s.source = { :git => 'https://github.com/square/Valet.git', :tag => s.version } - s.swift_version = '4.0', '4.1', '4.2', '5.0' + s.swift_version = '5.0' s.source_files = 'Sources/Valet/**/*.{swift,h}' s.public_header_files = 'Sources/Valet/*.h' s.frameworks = 'Security' diff --git a/Valet.xcodeproj/project.pbxproj b/Valet.xcodeproj/project.pbxproj index 63058f06..fcfa7094 100644 --- a/Valet.xcodeproj/project.pbxproj +++ b/Valet.xcodeproj/project.pbxproj @@ -41,14 +41,10 @@ 1612FD3622A9C95500FC1142 /* ValetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD0D22A9C95500FC1142 /* ValetTests.swift */; }; 1612FD3722A9C95500FC1142 /* ValetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD0D22A9C95500FC1142 /* ValetTests.swift */; }; 1612FD3822A9C96900FC1142 /* MacTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FCFE22A9C95400FC1142 /* MacTests.swift */; }; - 1612FD7D22A9CAAB00FC1142 /* MigrationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6B22A9CAAB00FC1142 /* MigrationResult.swift */; }; - 1612FD7E22A9CAAB00FC1142 /* MigrationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6B22A9CAAB00FC1142 /* MigrationResult.swift */; }; - 1612FD7F22A9CAAB00FC1142 /* MigrationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6B22A9CAAB00FC1142 /* MigrationResult.swift */; }; - 1612FD8022A9CAAB00FC1142 /* MigrationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6B22A9CAAB00FC1142 /* MigrationResult.swift */; }; - 1612FD8122A9CAAB00FC1142 /* KeychainQueryConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6C22A9CAAB00FC1142 /* KeychainQueryConvertible.swift */; }; - 1612FD8222A9CAAB00FC1142 /* KeychainQueryConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6C22A9CAAB00FC1142 /* KeychainQueryConvertible.swift */; }; - 1612FD8322A9CAAB00FC1142 /* KeychainQueryConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6C22A9CAAB00FC1142 /* KeychainQueryConvertible.swift */; }; - 1612FD8422A9CAAB00FC1142 /* KeychainQueryConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6C22A9CAAB00FC1142 /* KeychainQueryConvertible.swift */; }; + 1612FD7D22A9CAAB00FC1142 /* MigrationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6B22A9CAAB00FC1142 /* MigrationError.swift */; }; + 1612FD7E22A9CAAB00FC1142 /* MigrationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6B22A9CAAB00FC1142 /* MigrationError.swift */; }; + 1612FD7F22A9CAAB00FC1142 /* MigrationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6B22A9CAAB00FC1142 /* MigrationError.swift */; }; + 1612FD8022A9CAAB00FC1142 /* MigrationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6B22A9CAAB00FC1142 /* MigrationError.swift */; }; 1612FD8522A9CAAB00FC1142 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6D22A9CAAB00FC1142 /* SecureEnclave.swift */; }; 1612FD8622A9CAAB00FC1142 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6D22A9CAAB00FC1142 /* SecureEnclave.swift */; }; 1612FD8722A9CAAB00FC1142 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD6D22A9CAAB00FC1142 /* SecureEnclave.swift */; }; @@ -81,10 +77,6 @@ 1612FDA222A9CAAB00FC1142 /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7522A9CAAB00FC1142 /* Accessibility.swift */; }; 1612FDA322A9CAAB00FC1142 /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7522A9CAAB00FC1142 /* Accessibility.swift */; }; 1612FDA422A9CAAB00FC1142 /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7522A9CAAB00FC1142 /* Accessibility.swift */; }; - 1612FDA522A9CAAB00FC1142 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7622A9CAAB00FC1142 /* ErrorHandler.swift */; }; - 1612FDA622A9CAAB00FC1142 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7622A9CAAB00FC1142 /* ErrorHandler.swift */; }; - 1612FDA722A9CAAB00FC1142 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7622A9CAAB00FC1142 /* ErrorHandler.swift */; }; - 1612FDA822A9CAAB00FC1142 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7622A9CAAB00FC1142 /* ErrorHandler.swift */; }; 1612FDA922A9CAAB00FC1142 /* Valet.h in Headers */ = {isa = PBXBuildFile; fileRef = 1612FD7722A9CAAB00FC1142 /* Valet.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1612FDAA22A9CAAB00FC1142 /* Valet.h in Headers */ = {isa = PBXBuildFile; fileRef = 1612FD7722A9CAAB00FC1142 /* Valet.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1612FDAB22A9CAAB00FC1142 /* Valet.h in Headers */ = {isa = PBXBuildFile; fileRef = 1612FD7722A9CAAB00FC1142 /* Valet.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -95,10 +87,6 @@ 1612FDB022A9CAAB00FC1142 /* SecureEnclaveAccessControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7822A9CAAB00FC1142 /* SecureEnclaveAccessControl.swift */; }; 1612FDB122A9CAAB00FC1142 /* SinglePromptSecureEnclaveValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7922A9CAAB00FC1142 /* SinglePromptSecureEnclaveValet.swift */; }; 1612FDB222A9CAAB00FC1142 /* SinglePromptSecureEnclaveValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7922A9CAAB00FC1142 /* SinglePromptSecureEnclaveValet.swift */; }; - 1612FDB522A9CAAB00FC1142 /* SwiftCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7A22A9CAAB00FC1142 /* SwiftCompatibility.swift */; }; - 1612FDB622A9CAAB00FC1142 /* SwiftCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7A22A9CAAB00FC1142 /* SwiftCompatibility.swift */; }; - 1612FDB722A9CAAB00FC1142 /* SwiftCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7A22A9CAAB00FC1142 /* SwiftCompatibility.swift */; }; - 1612FDB822A9CAAB00FC1142 /* SwiftCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7A22A9CAAB00FC1142 /* SwiftCompatibility.swift */; }; 1612FDB922A9CAAB00FC1142 /* SecureEnclaveValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7B22A9CAAB00FC1142 /* SecureEnclaveValet.swift */; }; 1612FDBA22A9CAAB00FC1142 /* SecureEnclaveValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7B22A9CAAB00FC1142 /* SecureEnclaveValet.swift */; }; 1612FDBB22A9CAAB00FC1142 /* SecureEnclaveValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7B22A9CAAB00FC1142 /* SecureEnclaveValet.swift */; }; @@ -140,8 +128,38 @@ 165CDDCB204B26D400C96C2E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165CDDCA204B26D400C96C2E /* ViewController.swift */; }; 165CDDCE204B26D400C96C2E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 165CDDCC204B26D400C96C2E /* Main.storyboard */; }; 165CDDD0204B26D500C96C2E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 165CDDCF204B26D500C96C2E /* Assets.xcassets */; }; + 167E24F123D4B89800889121 /* SinglePromptSecureEnclaveIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD0622A9C95400FC1142 /* SinglePromptSecureEnclaveIntegrationTests.swift */; }; + 167E24FE23D6235000889121 /* KeychainErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E24FD23D6235000889121 /* KeychainErrorTests.swift */; }; + 167E24FF23D6235000889121 /* KeychainErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E24FD23D6235000889121 /* KeychainErrorTests.swift */; }; + 167E250023D6235000889121 /* KeychainErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E24FD23D6235000889121 /* KeychainErrorTests.swift */; }; + 167E250223D624EF00889121 /* MigrationErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E250123D624EF00889121 /* MigrationErrorTests.swift */; }; + 167E250323D624EF00889121 /* MigrationErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E250123D624EF00889121 /* MigrationErrorTests.swift */; }; + 167E250423D624EF00889121 /* MigrationErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E250123D624EF00889121 /* MigrationErrorTests.swift */; }; + 167E250723D62CAA00889121 /* CloudAccessibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E250623D62CAA00889121 /* CloudAccessibilityTests.swift */; }; + 167E250823D62CAA00889121 /* CloudAccessibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E250623D62CAA00889121 /* CloudAccessibilityTests.swift */; }; + 167E250923D62CAA00889121 /* CloudAccessibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E250623D62CAA00889121 /* CloudAccessibilityTests.swift */; }; + 167E251023D6328E00889121 /* ConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E250F23D6328E00889121 /* ConfigurationTests.swift */; }; + 167E251123D6328E00889121 /* ConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E250F23D6328E00889121 /* ConfigurationTests.swift */; }; + 167E251223D6328E00889121 /* ConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E250F23D6328E00889121 /* ConfigurationTests.swift */; }; 168909381F7199D60057F636 /* Valet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26E6827C1BA8B3F900EFF4EA /* Valet.framework */; }; 168909391F7199D60057F636 /* Valet.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 26E6827C1BA8B3F900EFF4EA /* Valet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 1693E29523B2D24600F8D97A /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1693E29423B2D24600F8D97A /* KeychainError.swift */; }; + 1693E29623B2D24600F8D97A /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1693E29423B2D24600F8D97A /* KeychainError.swift */; }; + 1693E29723B2D24600F8D97A /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1693E29423B2D24600F8D97A /* KeychainError.swift */; }; + 1693E29823B2D24600F8D97A /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1693E29423B2D24600F8D97A /* KeychainError.swift */; }; + 16967AE82405CAF800DC2B2D /* SharedGroupIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16967AE72405CAF800DC2B2D /* SharedGroupIdentifier.swift */; }; + 16967AE92405CC7400DC2B2D /* SharedGroupIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16967AE72405CAF800DC2B2D /* SharedGroupIdentifier.swift */; }; + 16967AEA2405CC7500DC2B2D /* SharedGroupIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16967AE72405CAF800DC2B2D /* SharedGroupIdentifier.swift */; }; + 16967AEB2405CC7600DC2B2D /* SharedGroupIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16967AE72405CAF800DC2B2D /* SharedGroupIdentifier.swift */; }; + 169E9A6723D181DC001B69F5 /* VALSinglePromptSecureEnclaveValetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 169E9A6423D181DC001B69F5 /* VALSinglePromptSecureEnclaveValetTests.m */; }; + 169E9A6823D181DC001B69F5 /* VALSinglePromptSecureEnclaveValetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 169E9A6423D181DC001B69F5 /* VALSinglePromptSecureEnclaveValetTests.m */; }; + 169E9A6923D181DC001B69F5 /* VALSinglePromptSecureEnclaveValetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 169E9A6423D181DC001B69F5 /* VALSinglePromptSecureEnclaveValetTests.m */; }; + 169E9A6A23D181DC001B69F5 /* VALValetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 169E9A6523D181DC001B69F5 /* VALValetTests.m */; }; + 169E9A6B23D181DC001B69F5 /* VALValetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 169E9A6523D181DC001B69F5 /* VALValetTests.m */; }; + 169E9A6C23D181DC001B69F5 /* VALValetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 169E9A6523D181DC001B69F5 /* VALValetTests.m */; }; + 169E9A6D23D181DC001B69F5 /* VALSecureEnclaveValetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 169E9A6623D181DC001B69F5 /* VALSecureEnclaveValetTests.m */; }; + 169E9A6E23D181DC001B69F5 /* VALSecureEnclaveValetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 169E9A6623D181DC001B69F5 /* VALSecureEnclaveValetTests.m */; }; + 169E9A6F23D181DC001B69F5 /* VALSecureEnclaveValetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 169E9A6623D181DC001B69F5 /* VALSecureEnclaveValetTests.m */; }; 169FC990215ECFCE00C2D6BD /* Valet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16DF6ADA204B45EB00F8E0A4 /* Valet.framework */; }; 169FC991215ECFCE00C2D6BD /* Valet.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 16DF6ADA204B45EB00F8E0A4 /* Valet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 169FC992215ECFCE00C2D6BD /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16D77541215BFC1D004F060C /* XCTest.framework */; }; @@ -159,6 +177,19 @@ 16DF6B0D204B496800F8E0A4 /* Valet watchOS Test Host App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 16DF6AF3204B496800F8E0A4 /* Valet watchOS Test Host App.app */; }; 16DF6B1F204B4DA600F8E0A4 /* Valet.framework in Resources */ = {isa = PBXBuildFile; fileRef = 16DF6ADA204B45EB00F8E0A4 /* Valet.framework */; }; 3251BD06224C865E0007453B /* XCTest.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 16D77541215BFC1D004F060C /* XCTest.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 32644C24248313210037F517 /* SecItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FCFF22A9C95400FC1142 /* SecItemTests.swift */; }; + 32644C25248313210037F517 /* CloudIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD0022A9C95400FC1142 /* CloudIntegrationTests.swift */; }; + 32644C2A248313210037F517 /* SinglePromptSecureEnclaveIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD0622A9C95400FC1142 /* SinglePromptSecureEnclaveIntegrationTests.swift */; }; + 32644C2B248313210037F517 /* ValetIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD0722A9C95400FC1142 /* ValetIntegrationTests.swift */; }; + 32644C2C248313210037F517 /* SecureEnclaveIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD0822A9C95400FC1142 /* SecureEnclaveIntegrationTests.swift */; }; + 32644C2D248313210037F517 /* CloudAccessibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E250623D62CAA00889121 /* CloudAccessibilityTests.swift */; }; + 32644C2E248313210037F517 /* SinglePromptSecureEnclaveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD0A22A9C95500FC1142 /* SinglePromptSecureEnclaveTests.swift */; }; + 32644C2F248313210037F517 /* SecureEnclaveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD0B22A9C95500FC1142 /* SecureEnclaveTests.swift */; }; + 32644C30248313210037F517 /* CloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD0C22A9C95500FC1142 /* CloudTests.swift */; }; + 32644C31248313210037F517 /* ConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E250F23D6328E00889121 /* ConfigurationTests.swift */; }; + 32644C32248313210037F517 /* ValetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD0D22A9C95500FC1142 /* ValetTests.swift */; }; + 32644C33248313210037F517 /* MigrationErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E250123D624EF00889121 /* MigrationErrorTests.swift */; }; + 32644C34248313210037F517 /* KeychainErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167E24FD23D6235000889121 /* KeychainErrorTests.swift */; }; 32E7115B2336B03800018E15 /* SinglePromptSecureEnclaveValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7922A9CAAB00FC1142 /* SinglePromptSecureEnclaveValet.swift */; }; 32E7115C2336B03800018E15 /* SinglePromptSecureEnclaveValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD7922A9CAAB00FC1142 /* SinglePromptSecureEnclaveValet.swift */; }; 32E7115E2336B90800018E15 /* SinglePromptSecureEnclaveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1612FD0A22A9C95500FC1142 /* SinglePromptSecureEnclaveTests.swift */; }; @@ -368,8 +399,7 @@ 1612FD0B22A9C95500FC1142 /* SecureEnclaveTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureEnclaveTests.swift; sourceTree = ""; }; 1612FD0C22A9C95500FC1142 /* CloudTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudTests.swift; sourceTree = ""; }; 1612FD0D22A9C95500FC1142 /* ValetTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValetTests.swift; sourceTree = ""; }; - 1612FD6B22A9CAAB00FC1142 /* MigrationResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationResult.swift; sourceTree = ""; }; - 1612FD6C22A9CAAB00FC1142 /* KeychainQueryConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainQueryConvertible.swift; sourceTree = ""; }; + 1612FD6B22A9CAAB00FC1142 /* MigrationError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationError.swift; sourceTree = ""; }; 1612FD6D22A9CAAB00FC1142 /* SecureEnclave.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureEnclave.swift; sourceTree = ""; }; 1612FD6E22A9CAAB00FC1142 /* CloudAccessibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudAccessibility.swift; sourceTree = ""; }; 1612FD6F22A9CAAB00FC1142 /* Valet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Valet.swift; sourceTree = ""; }; @@ -378,11 +408,9 @@ 1612FD7322A9CAAB00FC1142 /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; 1612FD7422A9CAAB00FC1142 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; 1612FD7522A9CAAB00FC1142 /* Accessibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; - 1612FD7622A9CAAB00FC1142 /* ErrorHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; }; 1612FD7722A9CAAB00FC1142 /* Valet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Valet.h; sourceTree = ""; }; 1612FD7822A9CAAB00FC1142 /* SecureEnclaveAccessControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureEnclaveAccessControl.swift; sourceTree = ""; }; 1612FD7922A9CAAB00FC1142 /* SinglePromptSecureEnclaveValet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SinglePromptSecureEnclaveValet.swift; sourceTree = ""; }; - 1612FD7A22A9CAAB00FC1142 /* SwiftCompatibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftCompatibility.swift; sourceTree = ""; }; 1612FD7B22A9CAAB00FC1142 /* SecureEnclaveValet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureEnclaveValet.swift; sourceTree = ""; }; 1612FD7C22A9CAAB00FC1142 /* Identifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Identifier.swift; sourceTree = ""; }; 1612FDD322A9CB2200FC1142 /* LegacyValet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LegacyValet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -400,12 +428,22 @@ 16212CD923072C6F00C84B17 /* ValetDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ValetDefines.h; path = LegacyValet/ValetDefines.h; sourceTree = ""; }; 16212CDA23072C6F00C84B17 /* VALLegacySecureEnclaveValet_Protected.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = VALLegacySecureEnclaveValet_Protected.h; path = LegacyValet/VALLegacySecureEnclaveValet_Protected.h; sourceTree = ""; }; 16212CFB23072CB700C84B17 /* LegacyValet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LegacyValet.h; path = LegacyValet/Public/LegacyValet.h; sourceTree = ""; }; + 163F018D23D77E9800BBD3E3 /* Valet watchOS Test Host App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Valet watchOS Test Host App.entitlements"; sourceTree = ""; }; 165CDDC6204B26D400C96C2E /* Valet tvOS Test Host App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Valet tvOS Test Host App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 165CDDC8204B26D400C96C2E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 165CDDCA204B26D400C96C2E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 165CDDCF204B26D500C96C2E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 165CDDD1204B26D500C96C2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 165CDDD5204B26F700C96C2E /* Valet tvOS Test Host App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Valet tvOS Test Host App.entitlements"; sourceTree = ""; }; + 167E24FD23D6235000889121 /* KeychainErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainErrorTests.swift; sourceTree = ""; }; + 167E250123D624EF00889121 /* MigrationErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationErrorTests.swift; sourceTree = ""; }; + 167E250623D62CAA00889121 /* CloudAccessibilityTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudAccessibilityTests.swift; sourceTree = ""; }; + 167E250F23D6328E00889121 /* ConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationTests.swift; sourceTree = ""; }; + 1693E29423B2D24600F8D97A /* KeychainError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainError.swift; sourceTree = ""; }; + 16967AE72405CAF800DC2B2D /* SharedGroupIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedGroupIdentifier.swift; sourceTree = ""; }; + 169E9A6423D181DC001B69F5 /* VALSinglePromptSecureEnclaveValetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VALSinglePromptSecureEnclaveValetTests.m; sourceTree = ""; }; + 169E9A6523D181DC001B69F5 /* VALValetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VALValetTests.m; sourceTree = ""; }; + 169E9A6623D181DC001B69F5 /* VALSecureEnclaveValetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VALSecureEnclaveValetTests.m; sourceTree = ""; }; 16B5856C1F71DBE00038EE30 /* Valet macOS Test Host App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Valet macOS Test Host App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 16B5856E1F71DBE00038EE30 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 16B585701F71DBE00038EE30 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -591,10 +629,14 @@ 1612FD0922A9C95500FC1142 /* ValetTests */ = { isa = PBXGroup; children = ( + 167E250623D62CAA00889121 /* CloudAccessibilityTests.swift */, 1612FD0A22A9C95500FC1142 /* SinglePromptSecureEnclaveTests.swift */, 1612FD0B22A9C95500FC1142 /* SecureEnclaveTests.swift */, 1612FD0C22A9C95500FC1142 /* CloudTests.swift */, + 167E250F23D6328E00889121 /* ConfigurationTests.swift */, 1612FD0D22A9C95500FC1142 /* ValetTests.swift */, + 167E250123D624EF00889121 /* MigrationErrorTests.swift */, + 167E24FD23D6235000889121 /* KeychainErrorTests.swift */, ); path = ValetTests; sourceTree = ""; @@ -602,18 +644,17 @@ 1612FD6A22A9CAAB00FC1142 /* Valet */ = { isa = PBXGroup; children = ( - 1612FD6B22A9CAAB00FC1142 /* MigrationResult.swift */, - 1612FD6C22A9CAAB00FC1142 /* KeychainQueryConvertible.swift */, + 1612FD6B22A9CAAB00FC1142 /* MigrationError.swift */, 1612FD6D22A9CAAB00FC1142 /* SecureEnclave.swift */, 1612FD6E22A9CAAB00FC1142 /* CloudAccessibility.swift */, 1612FD6F22A9CAAB00FC1142 /* Valet.swift */, 1612FD7522A9CAAB00FC1142 /* Accessibility.swift */, - 1612FD7622A9CAAB00FC1142 /* ErrorHandler.swift */, + 1693E29423B2D24600F8D97A /* KeychainError.swift */, 1612FD7722A9CAAB00FC1142 /* Valet.h */, 1612FD7822A9CAAB00FC1142 /* SecureEnclaveAccessControl.swift */, 1612FD7922A9CAAB00FC1142 /* SinglePromptSecureEnclaveValet.swift */, - 1612FD7A22A9CAAB00FC1142 /* SwiftCompatibility.swift */, 1612FD7B22A9CAAB00FC1142 /* SecureEnclaveValet.swift */, + 16967AE72405CAF800DC2B2D /* SharedGroupIdentifier.swift */, 1612FD7C22A9CAAB00FC1142 /* Identifier.swift */, 1612FD7022A9CAAB00FC1142 /* Internal */, ); @@ -681,6 +722,16 @@ name = Frameworks; sourceTree = ""; }; + 169E9A5B23D17A43001B69F5 /* ValetObjectiveCBridgeTests */ = { + isa = PBXGroup; + children = ( + 169E9A6623D181DC001B69F5 /* VALSecureEnclaveValetTests.m */, + 169E9A6423D181DC001B69F5 /* VALSinglePromptSecureEnclaveValetTests.m */, + 169E9A6523D181DC001B69F5 /* VALValetTests.m */, + ); + path = ValetObjectiveCBridgeTests; + sourceTree = ""; + }; 16B5856D1F71DBE00038EE30 /* Valet macOS Test Host App */ = { isa = PBXGroup; children = ( @@ -705,6 +756,7 @@ 16DF6AF4204B496800F8E0A4 /* Valet watchOS Test Host App */ = { isa = PBXGroup; children = ( + 163F018D23D77E9800BBD3E3 /* Valet watchOS Test Host App.entitlements */, 16DF6AF5204B496800F8E0A4 /* Interface.storyboard */, 16DF6AF8204B496800F8E0A4 /* Assets.xcassets */, 16DF6AFA204B496800F8E0A4 /* Info.plist */, @@ -737,6 +789,7 @@ 16E04BEB1F71B70500E8552D /* Tests */ = { isa = PBXGroup; children = ( + 169E9A5B23D17A43001B69F5 /* ValetObjectiveCBridgeTests */, 16E04BF51F71B73700E8552D /* Info.plist */, 1612FCFD22A9C95400FC1142 /* ValetIntegrationTests */, 1612FD0922A9C95500FC1142 /* ValetTests */, @@ -1225,7 +1278,7 @@ 16C3B096204B1E4C00B4D0B4 = { CreatedOnToolsVersion = 9.2; DevelopmentTeam = 9XUJ7M53NG; - LastSwiftMigration = 1020; + LastSwiftMigration = 1130; ProvisioningStyle = Automatic; TestTargetID = 165CDDC5204B26D400C96C2E; }; @@ -1275,13 +1328,13 @@ EA1E1F8D1A8C46090067C991 = { CreatedOnToolsVersion = 6.1.1; DevelopmentTeam = 9XUJ7M53NG; - LastSwiftMigration = 1020; + LastSwiftMigration = 1130; ProvisioningStyle = Automatic; TestTargetID = 371150A41E2962D8004A45D4; }; EAEAA88B1B167A8700F7AA98 = { CreatedOnToolsVersion = 6.3.2; - LastSwiftMigration = 1020; + LastSwiftMigration = 1130; ProvisioningStyle = Automatic; }; EAF894731B053E0400EDAD6C = { @@ -1507,10 +1560,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1693E29723B2D24600F8D97A /* KeychainError.swift in Sources */, 1612FDBB22A9CAAB00FC1142 /* SecureEnclaveValet.swift in Sources */, 1612FD8F22A9CAAB00FC1142 /* Valet.swift in Sources */, 1612FD9322A9CAAB00FC1142 /* SecItem.swift in Sources */, - 1612FDB722A9CAAB00FC1142 /* SwiftCompatibility.swift in Sources */, 32E7115B2336B03800018E15 /* SinglePromptSecureEnclaveValet.swift in Sources */, 1612FDBF22A9CAAB00FC1142 /* Identifier.swift in Sources */, 1612FD8B22A9CAAB00FC1142 /* CloudAccessibility.swift in Sources */, @@ -1519,10 +1572,9 @@ 1612FD8722A9CAAB00FC1142 /* SecureEnclave.swift in Sources */, 1612FD9F22A9CAAB00FC1142 /* Configuration.swift in Sources */, 1612FDA322A9CAAB00FC1142 /* Accessibility.swift in Sources */, - 1612FD7F22A9CAAB00FC1142 /* MigrationResult.swift in Sources */, + 1612FD7F22A9CAAB00FC1142 /* MigrationError.swift in Sources */, + 16967AEA2405CC7500DC2B2D /* SharedGroupIdentifier.swift in Sources */, 1612FD9722A9CAAB00FC1142 /* Service.swift in Sources */, - 1612FDA722A9CAAB00FC1142 /* ErrorHandler.swift in Sources */, - 1612FD8322A9CAAB00FC1142 /* KeychainQueryConvertible.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1532,12 +1584,20 @@ files = ( 1612FD1322A9C95500FC1142 /* SecItemTests.swift in Sources */, 1612FD3122A9C95500FC1142 /* SecureEnclaveTests.swift in Sources */, + 167E251223D6328E00889121 /* ConfigurationTests.swift in Sources */, + 167E24F123D4B89800889121 /* SinglePromptSecureEnclaveIntegrationTests.swift in Sources */, + 169E9A6C23D181DC001B69F5 /* VALValetTests.m in Sources */, 1612FD2B22A9C95500FC1142 /* SecureEnclaveIntegrationTests.swift in Sources */, + 167E250023D6235000889121 /* KeychainErrorTests.swift in Sources */, 1612FD1622A9C95500FC1142 /* CloudIntegrationTests.swift in Sources */, 1612FD2822A9C95500FC1142 /* ValetIntegrationTests.swift in Sources */, 1612FD3722A9C95500FC1142 /* ValetTests.swift in Sources */, 1612FD3422A9C95500FC1142 /* CloudTests.swift in Sources */, + 167E250423D624EF00889121 /* MigrationErrorTests.swift in Sources */, 32E7115E2336B90800018E15 /* SinglePromptSecureEnclaveTests.swift in Sources */, + 169E9A6923D181DC001B69F5 /* VALSinglePromptSecureEnclaveValetTests.m in Sources */, + 167E250923D62CAA00889121 /* CloudAccessibilityTests.swift in Sources */, + 169E9A6F23D181DC001B69F5 /* VALSecureEnclaveValetTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1545,10 +1605,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1693E29823B2D24600F8D97A /* KeychainError.swift in Sources */, 1612FDBC22A9CAAB00FC1142 /* SecureEnclaveValet.swift in Sources */, 1612FD9022A9CAAB00FC1142 /* Valet.swift in Sources */, 1612FD9422A9CAAB00FC1142 /* SecItem.swift in Sources */, - 1612FDB822A9CAAB00FC1142 /* SwiftCompatibility.swift in Sources */, 32E7115C2336B03800018E15 /* SinglePromptSecureEnclaveValet.swift in Sources */, 1612FDC022A9CAAB00FC1142 /* Identifier.swift in Sources */, 1612FD8C22A9CAAB00FC1142 /* CloudAccessibility.swift in Sources */, @@ -1557,10 +1617,9 @@ 1612FD8822A9CAAB00FC1142 /* SecureEnclave.swift in Sources */, 1612FDA022A9CAAB00FC1142 /* Configuration.swift in Sources */, 1612FDA422A9CAAB00FC1142 /* Accessibility.swift in Sources */, - 1612FD8022A9CAAB00FC1142 /* MigrationResult.swift in Sources */, + 1612FD8022A9CAAB00FC1142 /* MigrationError.swift in Sources */, + 16967AEB2405CC7600DC2B2D /* SharedGroupIdentifier.swift in Sources */, 1612FD9822A9CAAB00FC1142 /* Service.swift in Sources */, - 1612FDA822A9CAAB00FC1142 /* ErrorHandler.swift in Sources */, - 1612FD8422A9CAAB00FC1142 /* KeychainQueryConvertible.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1568,6 +1627,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 32644C24248313210037F517 /* SecItemTests.swift in Sources */, + 32644C25248313210037F517 /* CloudIntegrationTests.swift in Sources */, + 32644C2A248313210037F517 /* SinglePromptSecureEnclaveIntegrationTests.swift in Sources */, + 32644C2B248313210037F517 /* ValetIntegrationTests.swift in Sources */, + 32644C2C248313210037F517 /* SecureEnclaveIntegrationTests.swift in Sources */, + 32644C2D248313210037F517 /* CloudAccessibilityTests.swift in Sources */, + 32644C2E248313210037F517 /* SinglePromptSecureEnclaveTests.swift in Sources */, + 32644C2F248313210037F517 /* SecureEnclaveTests.swift in Sources */, + 32644C30248313210037F517 /* CloudTests.swift in Sources */, + 32644C31248313210037F517 /* ConfigurationTests.swift in Sources */, + 32644C32248313210037F517 /* ValetTests.swift in Sources */, + 32644C33248313210037F517 /* MigrationErrorTests.swift in Sources */, + 32644C34248313210037F517 /* KeychainErrorTests.swift in Sources */, 16DF6B07204B496800F8E0A4 /* ExtensionDelegate.swift in Sources */, 16DF6B05204B496800F8E0A4 /* InterfaceController.swift in Sources */, ); @@ -1577,10 +1649,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 16967AE82405CAF800DC2B2D /* SharedGroupIdentifier.swift in Sources */, + 1693E29523B2D24600F8D97A /* KeychainError.swift in Sources */, 1612FDB922A9CAAB00FC1142 /* SecureEnclaveValet.swift in Sources */, 1612FD8D22A9CAAB00FC1142 /* Valet.swift in Sources */, 1612FD9122A9CAAB00FC1142 /* SecItem.swift in Sources */, - 1612FDB522A9CAAB00FC1142 /* SwiftCompatibility.swift in Sources */, 1612FDBD22A9CAAB00FC1142 /* Identifier.swift in Sources */, 1612FDB122A9CAAB00FC1142 /* SinglePromptSecureEnclaveValet.swift in Sources */, 1612FD8922A9CAAB00FC1142 /* CloudAccessibility.swift in Sources */, @@ -1589,10 +1662,8 @@ 1612FD8522A9CAAB00FC1142 /* SecureEnclave.swift in Sources */, 1612FD9D22A9CAAB00FC1142 /* Configuration.swift in Sources */, 1612FDA122A9CAAB00FC1142 /* Accessibility.swift in Sources */, - 1612FD7D22A9CAAB00FC1142 /* MigrationResult.swift in Sources */, + 1612FD7D22A9CAAB00FC1142 /* MigrationError.swift in Sources */, 1612FD9522A9CAAB00FC1142 /* Service.swift in Sources */, - 1612FDA522A9CAAB00FC1142 /* ErrorHandler.swift in Sources */, - 1612FD8122A9CAAB00FC1142 /* KeychainQueryConvertible.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1600,10 +1671,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1693E29623B2D24600F8D97A /* KeychainError.swift in Sources */, 1612FDBA22A9CAAB00FC1142 /* SecureEnclaveValet.swift in Sources */, 1612FD8E22A9CAAB00FC1142 /* Valet.swift in Sources */, 1612FD9222A9CAAB00FC1142 /* SecItem.swift in Sources */, - 1612FDB622A9CAAB00FC1142 /* SwiftCompatibility.swift in Sources */, 1612FDBE22A9CAAB00FC1142 /* Identifier.swift in Sources */, 1612FDB222A9CAAB00FC1142 /* SinglePromptSecureEnclaveValet.swift in Sources */, 1612FD8A22A9CAAB00FC1142 /* CloudAccessibility.swift in Sources */, @@ -1612,10 +1683,9 @@ 1612FD8622A9CAAB00FC1142 /* SecureEnclave.swift in Sources */, 1612FD9E22A9CAAB00FC1142 /* Configuration.swift in Sources */, 1612FDA222A9CAAB00FC1142 /* Accessibility.swift in Sources */, - 1612FD7E22A9CAAB00FC1142 /* MigrationResult.swift in Sources */, + 1612FD7E22A9CAAB00FC1142 /* MigrationError.swift in Sources */, + 16967AE92405CC7400DC2B2D /* SharedGroupIdentifier.swift in Sources */, 1612FD9622A9CAAB00FC1142 /* Service.swift in Sources */, - 1612FDA622A9CAAB00FC1142 /* ErrorHandler.swift in Sources */, - 1612FD8222A9CAAB00FC1142 /* KeychainQueryConvertible.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1634,16 +1704,23 @@ files = ( 1612FD1122A9C95500FC1142 /* SecItemTests.swift in Sources */, 1612FD1422A9C95500FC1142 /* CloudIntegrationTests.swift in Sources */, + 169E9A6A23D181DC001B69F5 /* VALValetTests.m in Sources */, 1612FD3522A9C95500FC1142 /* ValetTests.swift in Sources */, 1612FD2922A9C95500FC1142 /* SecureEnclaveIntegrationTests.swift in Sources */, 1612FD2322A9C95500FC1142 /* SinglePromptSecureEnclaveIntegrationTests.swift in Sources */, 1612FD1722A9C95500FC1142 /* SinglePromptSecureEnclaveBackwardsCompatibilityTests.swift in Sources */, + 167E250223D624EF00889121 /* MigrationErrorTests.swift in Sources */, 1612FD1D22A9C95500FC1142 /* SecureEnclaveBackwardsCompatibilityTests.swift in Sources */, 1612FD2F22A9C95500FC1142 /* SecureEnclaveTests.swift in Sources */, 1612FD3222A9C95500FC1142 /* CloudTests.swift in Sources */, 1612FD2C22A9C95500FC1142 /* SinglePromptSecureEnclaveTests.swift in Sources */, + 169E9A6723D181DC001B69F5 /* VALSinglePromptSecureEnclaveValetTests.m in Sources */, 1612FD2622A9C95500FC1142 /* ValetIntegrationTests.swift in Sources */, 1612FD2022A9C95500FC1142 /* ValetBackwardsCompatibilityTests.swift in Sources */, + 169E9A6D23D181DC001B69F5 /* VALSecureEnclaveValetTests.m in Sources */, + 167E251023D6328E00889121 /* ConfigurationTests.swift in Sources */, + 167E250723D62CAA00889121 /* CloudAccessibilityTests.swift in Sources */, + 167E24FE23D6235000889121 /* KeychainErrorTests.swift in Sources */, 1612FD1A22A9C95500FC1142 /* SynchronizableBackwardsCompatibilityTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1654,6 +1731,7 @@ files = ( 1612FD1222A9C95500FC1142 /* SecItemTests.swift in Sources */, 1612FD1522A9C95500FC1142 /* CloudIntegrationTests.swift in Sources */, + 167E251123D6328E00889121 /* ConfigurationTests.swift in Sources */, 1612FD3622A9C95500FC1142 /* ValetTests.swift in Sources */, 1612FD2A22A9C95500FC1142 /* SecureEnclaveIntegrationTests.swift in Sources */, 1612FD2422A9C95500FC1142 /* SinglePromptSecureEnclaveIntegrationTests.swift in Sources */, @@ -1662,7 +1740,13 @@ 1612FD3022A9C95500FC1142 /* SecureEnclaveTests.swift in Sources */, 1612FD3322A9C95500FC1142 /* CloudTests.swift in Sources */, 1612FD3822A9C96900FC1142 /* MacTests.swift in Sources */, + 167E250823D62CAA00889121 /* CloudAccessibilityTests.swift in Sources */, + 167E24FF23D6235000889121 /* KeychainErrorTests.swift in Sources */, + 167E250323D624EF00889121 /* MigrationErrorTests.swift in Sources */, 1612FD2D22A9C95500FC1142 /* SinglePromptSecureEnclaveTests.swift in Sources */, + 169E9A6823D181DC001B69F5 /* VALSinglePromptSecureEnclaveValetTests.m in Sources */, + 169E9A6E23D181DC001B69F5 /* VALSecureEnclaveValetTests.m in Sources */, + 169E9A6B23D181DC001B69F5 /* VALValetTests.m in Sources */, 1612FD2722A9C95500FC1142 /* ValetIntegrationTests.swift in Sources */, 1612FD2122A9C95500FC1142 /* ValetBackwardsCompatibilityTests.swift in Sources */, 1612FD1B22A9C95500FC1142 /* SynchronizableBackwardsCompatibilityTests.swift in Sources */, @@ -2125,6 +2209,7 @@ CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; @@ -2139,6 +2224,7 @@ SDKROOT = appletvos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Valet tvOS Test Host App.app/Valet tvOS Test Host App"; }; @@ -2151,6 +2237,7 @@ CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; @@ -2164,6 +2251,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Valet tvOS Test Host App.app/Valet tvOS Test Host App"; }; @@ -2294,6 +2382,7 @@ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "Valet watchOS Test Host App/Valet watchOS Test Host App.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; @@ -2323,6 +2412,7 @@ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "Valet watchOS Test Host App/Valet watchOS Test Host App.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; @@ -2553,7 +2643,7 @@ ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.squareup.Valet; PRODUCT_NAME = Valet; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 9.0; WATCHOS_DEPLOYMENT_TARGET = 2.0; }; @@ -2607,7 +2697,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.squareup.Valet; PRODUCT_NAME = Valet; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 9.0; VALIDATE_PRODUCT = YES; WATCHOS_DEPLOYMENT_TARGET = 2.0; @@ -2632,6 +2722,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Valet iOS Test Host App.app/Valet iOS Test Host App"; }; name = Debug; @@ -2649,6 +2740,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.squareup.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Valet iOS Test Host App.app/Valet iOS Test Host App"; }; name = Release; @@ -2676,6 +2768,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -2698,6 +2791,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.squareup.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/ValetTouchIDTest/ValetTouchIDTestViewController.swift b/ValetTouchIDTest/ValetTouchIDTestViewController.swift index 22e9f19d..a71fd06c 100644 --- a/ValetTouchIDTest/ValetTouchIDTestViewController.swift +++ b/ValetTouchIDTest/ValetTouchIDTestViewController.swift @@ -45,7 +45,13 @@ final class ValetTouchIDTestViewController : UIViewController @IBAction func setOrUpdateItem(sender: UIResponder) { let stringToSet = "I am here! " + NSUUID().uuidString - let setOrUpdatedItem = singlePromptSecureEnclaveValet.set(string: stringToSet, forKey: username) + let setOrUpdatedItem: Bool + do { + try singlePromptSecureEnclaveValet.setString(stringToSet, forKey: username) + setOrUpdatedItem = true + } catch { + setOrUpdatedItem = false + } updateTextView(messageComponents: #function, (setOrUpdatedItem ? "Success" : "Failure")) } @@ -53,15 +59,14 @@ final class ValetTouchIDTestViewController : UIViewController @IBAction func getItem(sender: UIResponder) { let resultString: String - switch singlePromptSecureEnclaveValet.string(forKey: username, withPrompt: "Use TouchID to retrieve password") { - case let .success(password): - resultString = password - - case .userCancelled: + do { + resultString = try singlePromptSecureEnclaveValet.string(forKey: username, withPrompt: "Use TouchID to retrieve password") + } catch KeychainError.userCancelled { resultString = "user cancelled TouchID" - - case .itemNotFound: + } catch KeychainError.itemNotFound { resultString = "object not found" + } catch { + resultString = "caught unknown error \(error)" } updateTextView(messageComponents: #function, resultString) @@ -70,14 +75,26 @@ final class ValetTouchIDTestViewController : UIViewController @objc(removeItem:) @IBAction func removeItem(sender: UIResponder) { - let removedItem = singlePromptSecureEnclaveValet.removeObject(forKey: username) + let removedItem: Bool + do { + try singlePromptSecureEnclaveValet.removeObject(forKey: username) + removedItem = true + } catch { + removedItem = false + } + updateTextView(messageComponents: #function, (removedItem ? "Success" : "Failure")) } @objc(containsItem:) @IBAction func containsItem(sender: UIResponder) { - let containsItem = singlePromptSecureEnclaveValet.containsObject(forKey: username) + let containsItem: Bool + do { + containsItem = try singlePromptSecureEnclaveValet.containsObject(forKey: username) + } catch { + containsItem = false + } updateTextView(messageComponents: #function, (containsItem ? "YES" : "NO")) }