diff --git a/README.md b/README.md index 221d1e99..4e5055e8 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ However, because the Keychain is effectively disk storage, there is no guarantee ### Migrating Existing Keychain Values into Valet -Already using the Keychain and no longer want to maintain your own Keychain code? We feel you. That’s why we wrote `migrateObjects(matching query: [String : AnyHashable], removeOnCompletion: Bool)`. This method allows you to migrate all your existing Keychain entries to a Valet instance in one line. Just pass in a Dictionary with the `kSecClass`, `kSecAttrService`, and any other `kSecAttr*` attributes you use – we’ll migrate the data for you. +Already using the Keychain and no longer want to maintain your own Keychain code? We feel you. That’s why we wrote `migrateObjects(matching query: [String : AnyHashable], removeOnCompletion: Bool)`. This method allows you to migrate all your existing Keychain entries to a Valet instance in one line. Just pass in a Dictionary with the `kSecClass`, `kSecAttrService`, and any other `kSecAttr*` attributes you use – we’ll migrate the data for you. If you need more control over how your data is migrated, use `migrateObjects(matching query: [String : AnyHashable], compactMap: (MigratableKeyValuePair) throws -> MigratableKeyValuePair?)` to filter or remap key:value pairs as part of your migration. ### Integrating Valet into a macOS application diff --git a/Sources/Valet/Internal/Keychain.swift b/Sources/Valet/Internal/Keychain.swift index 1ba9cfb1..d2511868 100644 --- a/Sources/Valet/Internal/Keychain.swift +++ b/Sources/Valet/Internal/Keychain.swift @@ -174,55 +174,55 @@ internal final class Keychain { } // MARK: Migration - - internal static func migrateObjects(matching query: [String : AnyHashable], into destinationAttributes: [String : AnyHashable], removeOnCompletion: Bool) throws { + + internal static func migrateObjects(matching query: [String : AnyHashable], into destinationAttributes: [String : AnyHashable], compactMap: (MigratableKeyValuePair) throws -> MigratableKeyValuePair?) 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 { // Migration requires kSecMatchLimit to be set to kSecMatchLimitAll. throw MigrationError.invalidQuery } - + guard query[kSecReturnData as String] as? Bool != true else { // kSecReturnData is not supported in a migration query. throw MigrationError.invalidQuery } - + guard query[kSecReturnAttributes as String] as? Bool != false else { // Migration requires kSecReturnAttributes to be set to kCFBooleanTrue. throw MigrationError.invalidQuery } - + guard query[kSecReturnRef as String] as? Bool != true else { // kSecReturnRef is not supported in a migration query. throw MigrationError.invalidQuery } - + guard query[kSecReturnPersistentRef as String] as? Bool != false else { // Migration requires kSecReturnPersistentRef to be set to kCFBooleanTrue. throw MigrationError.invalidQuery } - + guard query[kSecClass as String] as? String as CFString? == kSecClassGenericPassword else { // Migration requires kSecClass to be set to kSecClassGenericPassword to avoid data loss. throw MigrationError.invalidQuery } - + guard query[kSecAttrAccessControl as String] == nil else { // 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 secItemQuery[kSecMatchLimit as String] = kSecMatchLimitAll secItemQuery[kSecReturnAttributes as String] = true secItemQuery[kSecReturnData as String] = false secItemQuery[kSecReturnRef as String] = false secItemQuery[kSecReturnPersistentRef as String] = true - + let collection: Any = try SecItem.copy(matching: secItemQuery) let retrievedItemsToMigrate: [[String: AnyHashable]] if let singleMatch = collection as? [String : AnyHashable] { @@ -234,7 +234,7 @@ internal final class Keychain { } 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 { @@ -242,7 +242,7 @@ internal final class Keychain { throw KeychainError.couldNotAccessKeychain } - + let retrieveDataQuery: [String : AnyHashable] = [ kSecValuePersistentRef as String : retrievedPersistentRef, kSecReturnData as String : true @@ -253,7 +253,7 @@ internal final class Keychain { guard !data.isEmpty else { throw MigrationError.dataToMigrateInvalid } - + var retrievedItemToMigrateWithData = retrievedItem retrievedItemToMigrateWithData[kSecValueData as String] = data retrievedItemsToMigrateWithData.append(retrievedItemToMigrateWithData) @@ -265,73 +265,94 @@ internal final class Keychain { throw error } } - + // Sanity check that we are capable of migrating the data. - var keysToMigrate = Set() + var keyValuePairsToMigrate = [String: Data]() for keychainEntry in retrievedItemsToMigrateWithData { - guard let key = keychainEntry[kSecAttrAccount as String] as? String, key != Keychain.canaryKey else { + guard let key = keychainEntry[kSecAttrAccount as String] else { + throw MigrationError.keyToMigrateInvalid + } + + guard key as? String != Keychain.canaryKey else { // We don't care about this key. Move along. continue } - - guard !key.isEmpty else { + + guard let data = keychainEntry[kSecValueData as String] as? Data else { + // This state should be impossible, per Apple's documentation for `kSecValueData`. + throw MigrationError.dataToMigrateInvalid + } + + guard let migratablePair = try compactMap(MigratableKeyValuePair(key: key, value: data)) else { + // We don't care about this key. Move along. + continue + } + + guard !migratablePair.key.isEmpty else { throw MigrationError.keyToMigrateInvalid } - - guard !keysToMigrate.contains(key) else { + + guard keyValuePairsToMigrate[migratablePair.key] == nil else { throw MigrationError.duplicateKeyToMigrate } - - guard let data = keychainEntry[kSecValueData as String] as? Data, !data.isEmpty else { + + guard !migratablePair.value.isEmpty else { throw MigrationError.dataToMigrateInvalid } - if Keychain.performCopy(forKey: key, options: destinationAttributes) == errSecItemNotFound { - keysToMigrate.insert(key) + if Keychain.performCopy(forKey: migratablePair.key, options: destinationAttributes) == errSecItemNotFound { + keyValuePairsToMigrate[migratablePair.key] = migratablePair.value } else { throw MigrationError.keyToMigrateAlreadyExistsInValet } } - - // All looks good. Time to actually migrate. - var alreadyMigratedKeys = [String]() - func revertMigration() { - // Something has gone wrong. Remove all migrated items. - for alreadyMigratedKey in alreadyMigratedKeys { - try? Keychain.removeObject(forKey: alreadyMigratedKey, options: destinationAttributes) - } - } - - for keychainEntry in retrievedItemsToMigrateWithData { - guard let key = keychainEntry[kSecAttrAccount as String] as? String else { - revertMigration() - throw MigrationError.keyToMigrateInvalid - } - guard let value = keychainEntry[kSecValueData as String] as? Data else { - revertMigration() - throw MigrationError.dataToMigrateInvalid - } + // Capture the keys in the destination prior to migration beginning. + let keysInKeychainPreMigration = Set(try Keychain.allKeys(options: destinationAttributes)) + // All looks good. Time to actually migrate. + for keyValuePair in keyValuePairsToMigrate { do { - try Keychain.setObject(value, forKey: key, options: destinationAttributes) - alreadyMigratedKeys.append(key) + try Keychain.setObject(keyValuePair.value, forKey: keyValuePair.key, options: destinationAttributes) } catch { - revertMigration() + revertMigration(into: destinationAttributes, keysInKeychainPreMigration: keysInKeychainPreMigration) throw error } } - + } + + internal static func migrateObjects(matching query: [String : AnyHashable], into destinationAttributes: [String : AnyHashable], removeOnCompletion: Bool) throws { + // Capture the keys in the destination prior to migration beginning. + let keysInKeychainPreMigration = Set(try Keychain.allKeys(options: destinationAttributes)) + + // Attempt migration. + try migrateObjects(matching: query, into: destinationAttributes) { keychainKeyValuePair in + guard let key = keychainKeyValuePair.key as? String else { + throw MigrationError.keyToMigrateInvalid + } + return MigratableKeyValuePair(key: key, value: keychainKeyValuePair.value) + } + // Remove data if requested. if removeOnCompletion { do { try Keychain.removeAllObjects(matching: query) } catch { - revertMigration() + revertMigration(into: destinationAttributes, keysInKeychainPreMigration: keysInKeychainPreMigration) + throw MigrationError.removalFailed } // We're done! } } + + internal static func revertMigration(into destinationAttributes: [String : AnyHashable], keysInKeychainPreMigration: Set) { + if let allKeysPostPotentiallyPartialMigration = try? Keychain.allKeys(options: destinationAttributes) { + let migratedKeys = allKeysPostPotentiallyPartialMigration.subtracting(keysInKeychainPreMigration) + migratedKeys.forEach { migratedKey in + try? Keychain.removeObject(forKey: migratedKey, options: destinationAttributes) + } + } + } } diff --git a/Sources/Valet/MigratableKeyValuePair.swift b/Sources/Valet/MigratableKeyValuePair.swift new file mode 100644 index 00000000..736d4422 --- /dev/null +++ b/Sources/Valet/MigratableKeyValuePair.swift @@ -0,0 +1,134 @@ +// +// MigratableKeyValuePair.swift +// Valet +// +// Created by Dan Federman on 5/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 + +/// A struct that represented a key:value pair that can be migrated. +public struct MigratableKeyValuePair: Hashable { + + // MARK: Initialization + + /// Creates a migratable key:value pair with the provided inputs. + /// - Parameters: + /// - key: The key in the key:value pair. + /// - value: The value in the key:value pair. + public init(key: Key, value: Data) { + self.key = key + self.value = value + } + + /// Creates a migratable key:value pair with the provided inputs. + /// - Parameters: + /// - key: The key in the key:value pair. + /// - value: The desired value in the key:value pair, represented as a String. + public init(key: Key, value: String) { + self.key = key + self.value = Data(value.utf8) + } + + // MARK: Public + + /// The key in the key:value pair. + public let key: Key + /// The value in the key:value pair. + public let value: Data +} + +// MARK: - Objective-C Compatibility + +@objc(VALMigratableKeyValuePairInput) +public final class ObjectiveCCompatibilityMigratableKeyValuePairInput: NSObject { + + // MARK: Initialization + + internal init(key: Any, value: Data) { + self.key = key + self.value = value + } + + // MARK: Public + + /// The key in the key:value pair. + @objc + public let key: Any + /// The value in the key:value pair. + @objc + public let value: Data +} + +@objc(VALMigratableKeyValuePairOutput) +public class ObjectiveCCompatibilityMigratableKeyValuePairOutput: NSObject { + + // MARK: Initialization + + /// Creates a migratable key:value pair with the provided inputs. + /// - Parameters: + /// - key: The key in the key:value pair. + /// - value: The value in the key:value pair. + @objc + public init(key: String, value: Data) { + self.key = key + self.value = value + preventMigration = false + } + + /// Creates a migratable key:value pair with the provided inputs. + /// - Parameters: + /// - key: The key in the key:value pair. + /// - stringValue: The desired value in the key:value pair, represented as a String. + @objc + public init(key: String, stringValue: String) { + self.key = key + self.value = Data(stringValue.utf8) + preventMigration = false + } + + // MARK: Public Static Methods + + /// A sentinal `ObjectiveCCompatibilityMigratableKeyValuePairOutput` that conveys that the migration should be prevented. + @available(swift, obsoleted: 1.0) + @objc + public static func preventMigration() -> ObjectiveCCompatibilityMigratableKeyValuePairOutput { + ObjectiveCCompatibilityPreventMigrationOutput() + } + + // MARK: Public + + /// The key in the key:value pair. + @objc + public let key: String + /// The value in the key:value pair. + @objc + public let value: Data + + // MARK: Internal + + internal fileprivate(set) var preventMigration: Bool + +} + +private final class ObjectiveCCompatibilityPreventMigrationOutput: ObjectiveCCompatibilityMigratableKeyValuePairOutput { + + init() { + super.init(key: "", stringValue: "") + preventMigration = true + } + +} diff --git a/Sources/Valet/Valet.swift b/Sources/Valet/Valet.swift index addc6978..5084903f 100644 --- a/Sources/Valet/Valet.swift +++ b/Sources/Valet/Valet.swift @@ -338,6 +338,18 @@ public final class Valet: NSObject { } } + /// Migrates objects matching the input query into the receiving Valet instance. + /// - Parameters: + /// - query: The query with which to retrieve existing keychain data via a call to SecItemCopyMatching. + /// - compactMap: A closure that transforms a key:value pair from the raw pair currently in the keychain into a key:value pair we'll insert into the destination Valet. Returning `nil` from this closure will cause that key:value pair not to be migrated. `throw`ing from this closure will prevent migration. + /// - Throws: An error of type `KeychainError` or `MigrationError`. Will rethrow any error thrown by `compactMap`. + /// - Note: The keychain is not modified if an error is thrown. + public func migrateObjects(matching query: [String : AnyHashable], compactMap: (MigratableKeyValuePair) throws -> MigratableKeyValuePair?) throws { + try execute(in: lock) { + try Keychain.migrateObjects(matching: query, into: baseKeychainQuery, compactMap: compactMap) + } + } + /// Migrates objects matching the input query into the receiving Valet instance. /// - Parameters: /// - query: The query with which to retrieve existing keychain data via a call to SecItemCopyMatching. @@ -350,7 +362,19 @@ public final class Valet: NSObject { try Keychain.migrateObjects(matching: query, into: baseKeychainQuery, removeOnCompletion: removeOnCompletion) } } - + + /// Migrates objects matching the input query into the receiving Valet instance. + /// - Parameters: + /// - valet: The Valet used to retrieve the existing keychain data that should be migrated. + /// - compactMap: A closure that transforms a key:value pair from the raw pair currently in the keychain into a key:value pair we'll insert into the destination Valet. Returning `nil` from this closure will cause that key:value pair not to be migrated. `throw`ing from this closure will prevent migration. + /// - Throws: An error of type `KeychainError` or `MigrationError`. Will rethrow any error thrown by `compactMap`. + /// - Note: The keychain is not modified if an error is thrown. + public func migrateObjects(from valet: Valet, compactMap: (MigratableKeyValuePair) throws -> MigratableKeyValuePair?) throws { + try execute(in: lock) { + try Keychain.migrateObjects(matching: valet.baseKeychainQuery, into: baseKeychainQuery, compactMap: compactMap) + } + } + /// 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. @@ -620,6 +644,8 @@ extension Valet { } #endif + // MARK: Public Methods + /// - 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. @@ -632,6 +658,51 @@ extension Valet { return containsObject } + /// Migrates objects matching the input query into the receiving Valet instance. + /// - Parameters: + /// - query: The query with which to retrieve existing keychain data via a call to SecItemCopyMatching. + /// - compactMap: A closure that transforms a key:value pair from the raw pair currently in the keychain into a key:value pair we'll insert into the destination Valet. Returning `nil` from this closure will cause that key:value pair not to be migrated. Returning `VALMigratableKeyValuePairOutput.preventMigration` will prevent migrating any key:value pairs. + /// - error: An error of type `KeychainError` or `MigrationError`. + @available(swift, obsoleted: 1.0) + @objc(migrateObjectsMatching:compactMap:error:) + public func 🚫swift_migrateObjects(matching query: [String : AnyHashable], compactMap: (ObjectiveCCompatibilityMigratableKeyValuePairInput) -> ObjectiveCCompatibilityMigratableKeyValuePairOutput?) throws { + try objc_compatibility_migrateObjects(matching: query, compactMap: compactMap) + } + + /// Migrates objects matching the input query into the receiving Valet instance. + /// - Parameters: + /// - valet: The Valet used to retrieve the existing keychain data that should be migrated. + /// - compactMap: A closure that transforms a key:value pair from the raw pair currently in the keychain into a key:value pair we'll insert into the destination Valet. Returning `nil` from this closure will cause that key:value pair not to be migrated. Returning `VALMigratableKeyValuePairOutput.preventMigration` will prevent migrating any key:value pairs. + /// - error: An error of type `KeychainError` or `MigrationError`. + @available(swift, obsoleted: 1.0) + @objc(migrateObjectsFrom:compactMap:error:) + public func 🚫swift_migrateObjects(from valet: Valet, compactMap: (ObjectiveCCompatibilityMigratableKeyValuePairInput) -> ObjectiveCCompatibilityMigratableKeyValuePairOutput?) throws { + try objc_compatibility_migrateObjects(matching: valet.baseKeychainQuery, compactMap: compactMap) + } + + // MARK: Private Methods + + private func objc_compatibility_migrateObjects(matching query: [String : AnyHashable], compactMap: (ObjectiveCCompatibilityMigratableKeyValuePairInput) -> ObjectiveCCompatibilityMigratableKeyValuePairOutput?) throws { + try execute(in: lock) { + struct PreventedMigrationSentinel: Error {} + do { + try Keychain.migrateObjects(matching: query, into: baseKeychainQuery) { input in + guard let output = compactMap(ObjectiveCCompatibilityMigratableKeyValuePairInput(key: input.key, value: input.value)) else { + return nil + } + guard !output.preventMigration else { + throw PreventedMigrationSentinel() + } + return MigratableKeyValuePair(key: output.key, value: output.value) + } + } catch is PreventedMigrationSentinel { + // Do nothing. This error shouldn't be surfaced to Objective-C. + } catch { + throw error + } + } + } + } // MARK: - Testing diff --git a/Tests/ValetIntegrationTests/KeychainIntegrationTests.swift b/Tests/ValetIntegrationTests/KeychainIntegrationTests.swift new file mode 100644 index 00000000..4ecaf3f1 --- /dev/null +++ b/Tests/ValetIntegrationTests/KeychainIntegrationTests.swift @@ -0,0 +1,60 @@ +// +// KeychainIntegrationTests.swift +// Valet iOS +// +// Created by Dan Federman on 5/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 KeychainIntegrationTests: XCTestCase { + + func test_revertMigration_removesAllMigratedKeys() throws { + guard testEnvironmentIsSigned() else { + return + } + + let migrationValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) + try migrationValet.removeAllObjects() + + let anotherValet = Valet.valet(with: Identifier(nonEmpty: #function)!, accessibility: .whenUnlocked) + try anotherValet.removeAllObjects() + + let keyValuePairsToMigrate = [ + "yo": "dawg", + "we": "heard", + "you": "like", + "migrating": "to", + "other": "valets" + ] + + for (key, value) in keyValuePairsToMigrate { + try migrationValet.setString(value, forKey: key) + } + + try anotherValet.setString("password", forKey: "accountName") + try anotherValet.migrateObjects(from: migrationValet, removeOnCompletion: false) + Keychain.revertMigration(into: anotherValet.baseKeychainQuery, keysInKeychainPreMigration: Set(["accountName"])) + + XCTAssertEqual(try anotherValet.allKeys().count, 1) + XCTAssertEqual(try anotherValet.string(forKey: "accountName"), "password") + } + +} diff --git a/Tests/ValetIntegrationTests/ValetIntegrationTests.swift b/Tests/ValetIntegrationTests/ValetIntegrationTests.swift index 7f2e8bb3..8ad1bebd 100644 --- a/Tests/ValetIntegrationTests/ValetIntegrationTests.swift +++ b/Tests/ValetIntegrationTests/ValetIntegrationTests.swift @@ -746,7 +746,7 @@ class ValetIntegrationTests: XCTestCase } } - func test_migrateObjectsMatching_withAccountNameAsData_doesNotRaiseException() throws + func test_migrateObjectsMatching_withAccountNameAsData_raisesException() throws { let identifier = "Keychain_With_Account_Name_As_NSData" @@ -770,7 +770,153 @@ class ValetIntegrationTests: XCTestCase XCTAssertEqual(error as? MigrationError, .keyToMigrateInvalid) } } - + + func test_migrateObjectsMatchingCompactMap_successfullyMigratesTransformedKey() throws { + guard testEnvironmentIsSigned() else { + return + } + + let migrationValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) + try migrationValet.removeAllObjects() + + let anotherValet = Valet.valet(with: Identifier(nonEmpty: #function)!, accessibility: .whenUnlocked) + try anotherValet.removeAllObjects() + + try migrationValet.setString("password", forKey: #function) + + try anotherValet.migrateObjects(matching: [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: migrationValet.service.description, + ]) { pair in + MigratableKeyValuePair(key: "transformed_key", value: pair.value) + } + + XCTAssertEqual(try? anotherValet.string(forKey: "transformed_key"), "password") + } + + func test_migrateObjectsMatchingCompactMap_successfullyMigratesTransformedValue() throws { + guard testEnvironmentIsSigned() else { + return + } + + let migrationValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) + try migrationValet.removeAllObjects() + + let anotherValet = Valet.valet(with: Identifier(nonEmpty: #function)!, accessibility: .whenUnlocked) + try anotherValet.removeAllObjects() + + try migrationValet.setString("password", forKey: #function) + + try anotherValet.migrateObjects(matching: [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: migrationValet.service.description, + ]) { _ in + MigratableKeyValuePair(key: #function, value: "12345") + } + + XCTAssertEqual(try? anotherValet.string(forKey: #function), "12345") + } + + func test_migrateObjectsMatchingCompactMap_returningNilDoesNotMigratePair() throws { + guard testEnvironmentIsSigned() else { + return + } + + let migrationValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) + try migrationValet.removeAllObjects() + + let anotherValet = Valet.valet(with: Identifier(nonEmpty: #function)!, accessibility: .whenUnlocked) + try anotherValet.removeAllObjects() + + let key1 = #function + "1" + let key2 = #function + "2" + try migrationValet.setString("password1", forKey: key1) + try migrationValet.setString("password2", forKey: key2) + + try anotherValet.migrateObjects(matching: [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: migrationValet.service.description, + ]) { input in + guard let key = input.key as? String else { + return nil + } + if key == key1 { + return nil + } else { + return MigratableKeyValuePair(key: key, value: input.value) + } + } + + XCTAssertFalse(try anotherValet.containsObject(forKey: key1)) + XCTAssertEqual(try? anotherValet.string(forKey: key2), "password2") + } + + func test_migrateObjectsMatchingCompactMap_throwingErrorPreventsAllMigration() throws { + guard testEnvironmentIsSigned() else { + return + } + + let migrationValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) + try migrationValet.removeAllObjects() + + let anotherValet = Valet.valet(with: Identifier(nonEmpty: #function)!, accessibility: .whenUnlocked) + try anotherValet.removeAllObjects() + + let key1 = #function + "1" + let key2 = #function + "2" + try migrationValet.setString("password1", forKey: key1) + try migrationValet.setString("password2", forKey: key2) + + try? anotherValet.migrateObjects(matching: [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: migrationValet.service.description, + ]) { input in + guard let key = input.key as? String else { + return nil + } + if key == key1 { + return MigratableKeyValuePair(key: key, value: input.value) + } else { + struct FakeError: Error {} + throw FakeError() + } + } + + XCTAssertFalse(try anotherValet.containsObject(forKey: key1)) + XCTAssertFalse(try anotherValet.containsObject(forKey: key2)) + } + + func test_migrateObjectsMatchingCompactMap_thrownErrorFromCompactMapIsRethrown() throws { + guard testEnvironmentIsSigned() else { + return + } + + let migrationValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) + try migrationValet.removeAllObjects() + + let anotherValet = Valet.valet(with: Identifier(nonEmpty: #function)!, accessibility: .whenUnlocked) + try anotherValet.removeAllObjects() + + try migrationValet.setString("password", forKey: #function) + + struct FakeError: Error {} + var caughtExpectedError = false + do { + try anotherValet.migrateObjects(matching: [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: migrationValet.service.description, + ]) { _ in + throw FakeError() + } + } catch is FakeError { + caughtExpectedError = true + } catch { + // Nothing to do + } + + XCTAssertTrue(caughtExpectedError) + } + // MARK: Migration - Valet func test_migrateObjectsFromValet_migratesSingleKeyValuePairSuccessfully() throws @@ -901,4 +1047,135 @@ class ValetIntegrationTests: XCTestCase } } + func test_migrateObjectsFromValetCompactMap_successfullyMigratesTransformedKey() throws { + guard testEnvironmentIsSigned() else { + return + } + + let migrationValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) + try migrationValet.removeAllObjects() + + let anotherValet = Valet.valet(with: Identifier(nonEmpty: #function)!, accessibility: .whenUnlocked) + try anotherValet.removeAllObjects() + + try migrationValet.setString("password", forKey: #function) + + try anotherValet.migrateObjects(from: migrationValet) { pair in + MigratableKeyValuePair(key: "transformed_key", value: pair.value) + } + + XCTAssertEqual(try? anotherValet.string(forKey: "transformed_key"), "password") + } + + func test_migrateObjectsFromValetCompactMap_successfullyMigratesTransformedValue() throws { + guard testEnvironmentIsSigned() else { + return + } + + let migrationValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) + try migrationValet.removeAllObjects() + + let anotherValet = Valet.valet(with: Identifier(nonEmpty: #function)!, accessibility: .whenUnlocked) + try anotherValet.removeAllObjects() + + try migrationValet.setString("password", forKey: #function) + + try anotherValet.migrateObjects(from: migrationValet) { _ in + MigratableKeyValuePair(key: #function, value: "12345") + } + + XCTAssertEqual(try? anotherValet.string(forKey: #function), "12345") + } + + func test_migrateObjectsFromValetCompactMap_returningNilDoesNotMigratePair() throws { + guard testEnvironmentIsSigned() else { + return + } + + let migrationValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) + try migrationValet.removeAllObjects() + + let anotherValet = Valet.valet(with: Identifier(nonEmpty: #function)!, accessibility: .whenUnlocked) + try anotherValet.removeAllObjects() + + let key1 = #function + "1" + let key2 = #function + "2" + try migrationValet.setString("password1", forKey: key1) + try migrationValet.setString("password2", forKey: key2) + + try anotherValet.migrateObjects(from: migrationValet) { input in + guard let key = input.key as? String else { + return nil + } + if key == key1 { + return nil + } else { + return MigratableKeyValuePair(key: key, value: input.value) + } + } + + XCTAssertFalse(try anotherValet.containsObject(forKey: key1)) + XCTAssertEqual(try? anotherValet.string(forKey: key2), "password2") + } + + func test_migrateObjectsFromValetCompactMap_throwingErrorPreventsAllMigration() throws { + guard testEnvironmentIsSigned() else { + return + } + + let migrationValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) + try migrationValet.removeAllObjects() + + let anotherValet = Valet.valet(with: Identifier(nonEmpty: #function)!, accessibility: .whenUnlocked) + try anotherValet.removeAllObjects() + + let key1 = #function + "1" + let key2 = #function + "2" + try migrationValet.setString("password1", forKey: key1) + try migrationValet.setString("password2", forKey: key2) + + try? anotherValet.migrateObjects(from: migrationValet) { input in + guard let key = input.key as? String else { + return nil + } + if key == key1 { + return MigratableKeyValuePair(key: key, value: input.value) + } else { + struct FakeError: Error {} + throw FakeError() + } + } + + XCTAssertFalse(try anotherValet.containsObject(forKey: key1)) + XCTAssertFalse(try anotherValet.containsObject(forKey: key2)) + } + + func test_migrateObjectsFromValetCompactMap_thrownErrorFromCompactMapIsRethrown() throws { + guard testEnvironmentIsSigned() else { + return + } + + let migrationValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) + try migrationValet.removeAllObjects() + + let anotherValet = Valet.valet(with: Identifier(nonEmpty: #function)!, accessibility: .whenUnlocked) + try anotherValet.removeAllObjects() + + try migrationValet.setString("password", forKey: #function) + + struct FakeError: Error {} + var caughtExpectedError = false + do { + try anotherValet.migrateObjects(from: migrationValet) { _ in + throw FakeError() + } + } catch is FakeError { + caughtExpectedError = true + } catch { + // Nothing to do + } + + XCTAssertTrue(caughtExpectedError) + } + } diff --git a/Tests/ValetObjectiveCBridgeTests/VALValetTests.m b/Tests/ValetObjectiveCBridgeTests/VALValetTests.m index 4014d04c..dc3a1d3c 100644 --- a/Tests/ValetObjectiveCBridgeTests/VALValetTests.m +++ b/Tests/ValetObjectiveCBridgeTests/VALValetTests.m @@ -64,6 +64,11 @@ - (NSString *)sharedAppGroupIdentifier; return @"valet.test"; } +- (BOOL)testEnvironmentIsSigned; +{ + return NSBundle.mainBundle.bundleIdentifier != nil && ![NSBundle.mainBundle.bundleIdentifier isEqualToString:@"com.apple.dt.xctest.tool"]; +} + - (void)test_valetWithIdentifier_accessibility_returnsCorrectValet_VALAccessibilityWhenUnlocked; { VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; @@ -247,6 +252,314 @@ - (void)test_iCloudValetWithSharedAppGroupIdentifier_accessibility_returnsNilWhe XCTAssertNil(valet); } +- (void)test_containsObjectForKey_returnsTrueWhenObjectExistsInKeychain; +{ + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + [valet setString:@"password" forKey:NSStringFromSelector(_cmd) error:nil]; + XCTAssertTrue([valet containsObjectForKey:NSStringFromSelector(_cmd)]); + + // Clean up. + [valet removeObjectForKey:NSStringFromSelector(_cmd) error:nil]; +} + +- (void)test_containsObjectForKey_returnsFalseWhenObjectDoesNotExistInKeychain; +{ + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + XCTAssertFalse([valet containsObjectForKey:NSStringFromSelector(_cmd)]); + + // Clean up. + [valet removeObjectForKey:NSStringFromSelector(_cmd) error:nil]; +} + +- (void)test_migrateObjectsMatching_compactMap_error_successfullyMigratesSingleValue; +{ + if (![self testEnvironmentIsSigned]) { + return; + } + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + + [valet setString:@"password" forKey:NSStringFromSelector(_cmd) error:nil]; + + VALValet *const otherValet = [VALValet valetWithIdentifier:NSStringFromSelector(_cmd) accessibility:VALAccessibilityWhenUnlocked]; + [otherValet migrateObjectsMatching:@{ + (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrService : @"VAL_VALValet_initWithIdentifier:accessibility:_identifier_AccessibleWhenUnlocked", + } compactMap:^VALMigratableKeyValuePairOutput * _Nullable(VALMigratableKeyValuePairInput * _Nonnull input) { + return [[VALMigratableKeyValuePairOutput alloc] initWithKey:input.key value:input.value]; + } error:nil]; + XCTAssertEqualObjects([otherValet stringForKey:NSStringFromSelector(_cmd) error:nil], @"password"); + + // Clean up. + [valet removeAllObjectsAndReturnError:nil]; + [otherValet removeAllObjectsAndReturnError:nil]; +} + +- (void)test_migrateObjectsFrom_compactMap_error_successfullyMigratesSingleValue; +{ + if (![self testEnvironmentIsSigned]) { + return; + } + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + + [valet setString:@"password" forKey:NSStringFromSelector(_cmd) error:nil]; + + VALValet *const otherValet = [VALValet valetWithIdentifier:NSStringFromSelector(_cmd) accessibility:VALAccessibilityWhenUnlocked]; + [otherValet migrateObjectsFrom:valet compactMap:^VALMigratableKeyValuePairOutput * _Nullable(VALMigratableKeyValuePairInput * _Nonnull input) { + return [[VALMigratableKeyValuePairOutput alloc] initWithKey:input.key value:input.value]; + } error:nil]; + XCTAssertEqualObjects([otherValet stringForKey:NSStringFromSelector(_cmd) error:nil], @"password"); + + // Clean up. + [valet removeAllObjectsAndReturnError:nil]; + [otherValet removeAllObjectsAndReturnError:nil]; +} + +- (void)test_migrateObjectsMatching_compactMap_error_successfullyMigratesTransformedKey; +{ + if (![self testEnvironmentIsSigned]) { + return; + } + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + + [valet setString:@"password" forKey:NSStringFromSelector(_cmd) error:nil]; + + VALValet *const otherValet = [VALValet valetWithIdentifier:NSStringFromSelector(_cmd) accessibility:VALAccessibilityWhenUnlocked]; + [otherValet migrateObjectsMatching:@{ + (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrService : @"VAL_VALValet_initWithIdentifier:accessibility:_identifier_AccessibleWhenUnlocked", + } compactMap:^VALMigratableKeyValuePairOutput * _Nullable(VALMigratableKeyValuePairInput * _Nonnull input) { + return [[VALMigratableKeyValuePairOutput alloc] initWithKey:@"transformed_key" value:input.value]; + } error:nil]; + XCTAssertEqualObjects([otherValet stringForKey:@"transformed_key" error:nil], @"password"); + + // Clean up. + [valet removeAllObjectsAndReturnError:nil]; + [otherValet removeAllObjectsAndReturnError:nil]; +} + +- (void)test_migrateObjectsMatching_compactMap_error_successfullyMigratesTransformedValue; +{ + if (![self testEnvironmentIsSigned]) { + return; + } + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + + [valet setString:@"password" forKey:NSStringFromSelector(_cmd) error:nil]; + + VALValet *const otherValet = [VALValet valetWithIdentifier:NSStringFromSelector(_cmd) accessibility:VALAccessibilityWhenUnlocked]; + [otherValet migrateObjectsMatching:@{ + (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrService : @"VAL_VALValet_initWithIdentifier:accessibility:_identifier_AccessibleWhenUnlocked", + } compactMap:^VALMigratableKeyValuePairOutput * _Nullable(VALMigratableKeyValuePairInput * _Nonnull input) { + return [[VALMigratableKeyValuePairOutput alloc] initWithKey:input.key value:[@"12345" dataUsingEncoding:NSUTF8StringEncoding]]; + } error:nil]; + XCTAssertEqualObjects([otherValet stringForKey:NSStringFromSelector(_cmd) error:nil], @"12345"); + + // Clean up. + [valet removeAllObjectsAndReturnError:nil]; + [otherValet removeAllObjectsAndReturnError:nil]; +} + +- (void)test_migrateObjectsFrom_compactMap_error_successfullyMigratesTransformedKey; +{ + if (![self testEnvironmentIsSigned]) { + return; + } + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + + [valet setString:@"password" forKey:NSStringFromSelector(_cmd) error:nil]; + + VALValet *const otherValet = [VALValet valetWithIdentifier:NSStringFromSelector(_cmd) accessibility:VALAccessibilityWhenUnlocked]; + [otherValet migrateObjectsFrom:valet compactMap:^VALMigratableKeyValuePairOutput * _Nullable(VALMigratableKeyValuePairInput * _Nonnull input) { + return [[VALMigratableKeyValuePairOutput alloc] initWithKey:@"transformed_key" value:input.value]; + } error:nil]; + XCTAssertEqualObjects([otherValet stringForKey:@"transformed_key" error:nil], @"password"); + + // Clean up. + [valet removeAllObjectsAndReturnError:nil]; + [otherValet removeAllObjectsAndReturnError:nil]; +} + +- (void)test_migrateObjectsFrom_compactMap_error_successfullyMigratesTransformedValue; +{ + if (![self testEnvironmentIsSigned]) { + return; + } + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + + [valet setString:@"password" forKey:NSStringFromSelector(_cmd) error:nil]; + + VALValet *const otherValet = [VALValet valetWithIdentifier:NSStringFromSelector(_cmd) accessibility:VALAccessibilityWhenUnlocked]; + [otherValet migrateObjectsFrom:valet compactMap:^VALMigratableKeyValuePairOutput * _Nullable(VALMigratableKeyValuePairInput * _Nonnull input) { + return [[VALMigratableKeyValuePairOutput alloc] initWithKey:input.key value:[@"12345" dataUsingEncoding:NSUTF8StringEncoding]]; + } error:nil]; + XCTAssertEqualObjects([otherValet stringForKey:NSStringFromSelector(_cmd) error:nil], @"12345"); + + // Clean up. + [valet removeAllObjectsAndReturnError:nil]; + [otherValet removeAllObjectsAndReturnError:nil]; +} + +- (void)test_migrateObjectsMatching_compactMap_error_returningNilDoesNotMigratePair; +{ + if (![self testEnvironmentIsSigned]) { + return; + } + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + + NSString *const key1 = [NSString stringWithFormat:@"%@-1", NSStringFromSelector(_cmd)]; + NSString *const key2 = [NSString stringWithFormat:@"%@-2", NSStringFromSelector(_cmd)]; + [valet setString:@"password1" forKey:key1 error:nil]; + [valet setString:@"password2" forKey:key2 error:nil]; + + VALValet *const otherValet = [VALValet valetWithIdentifier:NSStringFromSelector(_cmd) accessibility:VALAccessibilityWhenUnlocked]; + [otherValet migrateObjectsMatching:@{ + (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrService : @"VAL_VALValet_initWithIdentifier:accessibility:_identifier_AccessibleWhenUnlocked", + } compactMap:^VALMigratableKeyValuePairOutput * _Nullable(VALMigratableKeyValuePairInput * _Nonnull input) { + if ([input.key isEqualToString:key1]) { + return nil; + } else { + return [[VALMigratableKeyValuePairOutput alloc] initWithKey:input.key value:input.value]; + } + } error:nil]; + XCTAssertNil([otherValet stringForKey:key1 error:nil]); + XCTAssertEqualObjects([otherValet stringForKey:key2 error:nil], @"password2"); + + // Clean up. + [valet removeAllObjectsAndReturnError:nil]; + [otherValet removeAllObjectsAndReturnError:nil]; +} + +- (void)test_migrateObjectsFrom_compactMap_error_returningNilDoesNotMigratePair; +{ + if (![self testEnvironmentIsSigned]) { + return; + } + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + + NSString *const key1 = [NSString stringWithFormat:@"%@-1", NSStringFromSelector(_cmd)]; + NSString *const key2 = [NSString stringWithFormat:@"%@-2", NSStringFromSelector(_cmd)]; + [valet setString:@"password1" forKey:key1 error:nil]; + [valet setString:@"password2" forKey:key2 error:nil]; + + VALValet *const otherValet = [VALValet valetWithIdentifier:NSStringFromSelector(_cmd) accessibility:VALAccessibilityWhenUnlocked]; + [otherValet migrateObjectsFrom:valet compactMap:^VALMigratableKeyValuePairOutput * _Nullable(VALMigratableKeyValuePairInput * _Nonnull input) { + if ([input.key isEqualToString:key1]) { + return nil; + } else { + return [[VALMigratableKeyValuePairOutput alloc] initWithKey:input.key value:input.value]; + } + } error:nil]; + XCTAssertNil([otherValet stringForKey:key1 error:nil]); + XCTAssertEqualObjects([otherValet stringForKey:key2 error:nil], @"password2"); + + // Clean up. + [valet removeAllObjectsAndReturnError:nil]; + [otherValet removeAllObjectsAndReturnError:nil]; +} + +- (void)test_migrateObjectsMatching_compactMap_error_preventingMigrationPreventsAllMigration; +{ + if (![self testEnvironmentIsSigned]) { + return; + } + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + + NSString *const key1 = [NSString stringWithFormat:@"%@-1", NSStringFromSelector(_cmd)]; + NSString *const key2 = [NSString stringWithFormat:@"%@-2", NSStringFromSelector(_cmd)]; + [valet setString:@"password1" forKey:key1 error:nil]; + [valet setString:@"password2" forKey:key2 error:nil]; + + VALValet *const otherValet = [VALValet valetWithIdentifier:NSStringFromSelector(_cmd) accessibility:VALAccessibilityWhenUnlocked]; + [otherValet migrateObjectsMatching:@{ + (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrService : @"VAL_VALValet_initWithIdentifier:accessibility:_identifier_AccessibleWhenUnlocked", + } compactMap:^VALMigratableKeyValuePairOutput * _Nullable(VALMigratableKeyValuePairInput * _Nonnull input) { + if ([input.key isEqualToString:key1]) { + return [[VALMigratableKeyValuePairOutput alloc] initWithKey:input.key value:input.value];; + } else { + return VALMigratableKeyValuePairOutput.preventMigration; + } + } error:nil]; + XCTAssertNil([otherValet stringForKey:key1 error:nil]); + XCTAssertNil([otherValet stringForKey:key2 error:nil]); + + // Clean up. + [valet removeAllObjectsAndReturnError:nil]; + [otherValet removeAllObjectsAndReturnError:nil]; +} + +- (void)test_migrateObjectsFrom_compactMap_error_preventingMigrationPreventsAllMigration; +{ + if (![self testEnvironmentIsSigned]) { + return; + } + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + + NSString *const key1 = [NSString stringWithFormat:@"%@-1", NSStringFromSelector(_cmd)]; + NSString *const key2 = [NSString stringWithFormat:@"%@-2", NSStringFromSelector(_cmd)]; + [valet setString:@"password1" forKey:key1 error:nil]; + [valet setString:@"password2" forKey:key2 error:nil]; + + VALValet *const otherValet = [VALValet valetWithIdentifier:NSStringFromSelector(_cmd) accessibility:VALAccessibilityWhenUnlocked]; + [otherValet migrateObjectsFrom:valet compactMap:^VALMigratableKeyValuePairOutput * _Nullable(VALMigratableKeyValuePairInput * _Nonnull input) { + if ([input.key isEqualToString:key1]) { + return [[VALMigratableKeyValuePairOutput alloc] initWithKey:input.key value:input.value];; + } else { + return VALMigratableKeyValuePairOutput.preventMigration; + } + } error:nil]; + XCTAssertNil([otherValet stringForKey:key1 error:nil]); + XCTAssertNil([otherValet stringForKey:key2 error:nil]); + + // Clean up. + [valet removeAllObjectsAndReturnError:nil]; +} + +- (void)test_migrateObjectsMatching_compactMap_error_preventingMigrationReturnsNoError; +{ + if (![self testEnvironmentIsSigned]) { + return; + } + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + + [valet setString:@"password" forKey:NSStringFromSelector(_cmd) error:nil]; + + VALValet *const otherValet = [VALValet valetWithIdentifier:NSStringFromSelector(_cmd) accessibility:VALAccessibilityWhenUnlocked]; + NSError *error = nil; + [otherValet migrateObjectsMatching:@{ + (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrService : @"VAL_VALValet_initWithIdentifier:accessibility:_identifier_AccessibleWhenUnlocked", + } compactMap:^VALMigratableKeyValuePairOutput * _Nullable(VALMigratableKeyValuePairInput * _Nonnull input) { + return VALMigratableKeyValuePairOutput.preventMigration; + } error:&error]; + XCTAssertNil(error); + + // Clean up. + [valet removeAllObjectsAndReturnError:nil]; +} + +- (void)test_migrateObjectsFrom_compactMap_error_preventingMigrationReturnsNoError; +{ + if (![self testEnvironmentIsSigned]) { + return; + } + VALValet *const valet = [VALValet valetWithIdentifier:self.identifier accessibility:VALAccessibilityWhenUnlocked]; + + [valet setString:@"password" forKey:NSStringFromSelector(_cmd) error:nil]; + + VALValet *const otherValet = [VALValet valetWithIdentifier:NSStringFromSelector(_cmd) accessibility:VALAccessibilityWhenUnlocked]; + NSError *error = nil; + [otherValet migrateObjectsFrom:valet compactMap:^VALMigratableKeyValuePairOutput * _Nullable(VALMigratableKeyValuePairInput * _Nonnull input) { + return VALMigratableKeyValuePairOutput.preventMigration; + } error:&error]; + XCTAssertNil(error); + + // Clean up. + [valet removeAllObjectsAndReturnError:nil]; +} + // MARK: Mac Tests #if TARGET_OS_OSX diff --git a/Valet.xcodeproj/project.pbxproj b/Valet.xcodeproj/project.pbxproj index fcfa7094..850aa989 100644 --- a/Valet.xcodeproj/project.pbxproj +++ b/Valet.xcodeproj/project.pbxproj @@ -190,6 +190,13 @@ 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 */; }; + 32DC88852475111D005A9BFA /* MigratableKeyValuePair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DC88842475111D005A9BFA /* MigratableKeyValuePair.swift */; }; + 32DC88862475111D005A9BFA /* MigratableKeyValuePair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DC88842475111D005A9BFA /* MigratableKeyValuePair.swift */; }; + 32DC88872475111D005A9BFA /* MigratableKeyValuePair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DC88842475111D005A9BFA /* MigratableKeyValuePair.swift */; }; + 32DC88882475111D005A9BFA /* MigratableKeyValuePair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DC88842475111D005A9BFA /* MigratableKeyValuePair.swift */; }; + 32DC88932475BB9F005A9BFA /* KeychainIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DC88912475BA5F005A9BFA /* KeychainIntegrationTests.swift */; }; + 32DC88942475BBA1005A9BFA /* KeychainIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DC88912475BA5F005A9BFA /* KeychainIntegrationTests.swift */; }; + 32DC88952475BBA1005A9BFA /* KeychainIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DC88912475BA5F005A9BFA /* KeychainIntegrationTests.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 */; }; @@ -473,6 +480,8 @@ 32C1ED1B224C85660063E91D /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/Main.storyboard; sourceTree = ""; }; 32C1ED1C224C85820063E91D /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/Main.storyboard; sourceTree = ""; }; 32C1ED1D224C85890063E91D /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/ValetSecureElementTestMain.storyboard; sourceTree = ""; }; + 32DC88842475111D005A9BFA /* MigratableKeyValuePair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratableKeyValuePair.swift; sourceTree = ""; }; + 32DC88912475BA5F005A9BFA /* KeychainIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainIntegrationTests.swift; sourceTree = ""; }; 371150A51E2962D8004A45D4 /* Valet iOS Test Host App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Valet iOS Test Host App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 371150A71E2962D8004A45D4 /* ValetIOSTestHostAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetIOSTestHostAppDelegate.swift; sourceTree = ""; }; 371150A91E2962D8004A45D4 /* ValetIOSTestHostViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetIOSTestHostViewController.swift; sourceTree = ""; }; @@ -607,6 +616,7 @@ 1612FCFE22A9C95400FC1142 /* MacTests.swift */, 1612FCFF22A9C95400FC1142 /* SecItemTests.swift */, 1612FD0022A9C95400FC1142 /* CloudIntegrationTests.swift */, + 32DC88912475BA5F005A9BFA /* KeychainIntegrationTests.swift */, 1612FD0122A9C95400FC1142 /* BackwardsCompatibilityTests */, 1612FD0622A9C95400FC1142 /* SinglePromptSecureEnclaveIntegrationTests.swift */, 1612FD0722A9C95400FC1142 /* ValetIntegrationTests.swift */, @@ -644,6 +654,7 @@ 1612FD6A22A9CAAB00FC1142 /* Valet */ = { isa = PBXGroup; children = ( + 32DC88842475111D005A9BFA /* MigratableKeyValuePair.swift */, 1612FD6B22A9CAAB00FC1142 /* MigrationError.swift */, 1612FD6D22A9CAAB00FC1142 /* SecureEnclave.swift */, 1612FD6E22A9CAAB00FC1142 /* CloudAccessibility.swift */, @@ -1565,6 +1576,7 @@ 1612FD8F22A9CAAB00FC1142 /* Valet.swift in Sources */, 1612FD9322A9CAAB00FC1142 /* SecItem.swift in Sources */, 32E7115B2336B03800018E15 /* SinglePromptSecureEnclaveValet.swift in Sources */, + 32DC88872475111D005A9BFA /* MigratableKeyValuePair.swift in Sources */, 1612FDBF22A9CAAB00FC1142 /* Identifier.swift in Sources */, 1612FD8B22A9CAAB00FC1142 /* CloudAccessibility.swift in Sources */, 1612FD9B22A9CAAB00FC1142 /* Keychain.swift in Sources */, @@ -1584,6 +1596,7 @@ files = ( 1612FD1322A9C95500FC1142 /* SecItemTests.swift in Sources */, 1612FD3122A9C95500FC1142 /* SecureEnclaveTests.swift in Sources */, + 32DC88952475BBA1005A9BFA /* KeychainIntegrationTests.swift in Sources */, 167E251223D6328E00889121 /* ConfigurationTests.swift in Sources */, 167E24F123D4B89800889121 /* SinglePromptSecureEnclaveIntegrationTests.swift in Sources */, 169E9A6C23D181DC001B69F5 /* VALValetTests.m in Sources */, @@ -1610,6 +1623,7 @@ 1612FD9022A9CAAB00FC1142 /* Valet.swift in Sources */, 1612FD9422A9CAAB00FC1142 /* SecItem.swift in Sources */, 32E7115C2336B03800018E15 /* SinglePromptSecureEnclaveValet.swift in Sources */, + 32DC88882475111D005A9BFA /* MigratableKeyValuePair.swift in Sources */, 1612FDC022A9CAAB00FC1142 /* Identifier.swift in Sources */, 1612FD8C22A9CAAB00FC1142 /* CloudAccessibility.swift in Sources */, 1612FD9C22A9CAAB00FC1142 /* Keychain.swift in Sources */, @@ -1654,6 +1668,7 @@ 1612FDB922A9CAAB00FC1142 /* SecureEnclaveValet.swift in Sources */, 1612FD8D22A9CAAB00FC1142 /* Valet.swift in Sources */, 1612FD9122A9CAAB00FC1142 /* SecItem.swift in Sources */, + 32DC88852475111D005A9BFA /* MigratableKeyValuePair.swift in Sources */, 1612FDBD22A9CAAB00FC1142 /* Identifier.swift in Sources */, 1612FDB122A9CAAB00FC1142 /* SinglePromptSecureEnclaveValet.swift in Sources */, 1612FD8922A9CAAB00FC1142 /* CloudAccessibility.swift in Sources */, @@ -1676,6 +1691,7 @@ 1612FD8E22A9CAAB00FC1142 /* Valet.swift in Sources */, 1612FD9222A9CAAB00FC1142 /* SecItem.swift in Sources */, 1612FDBE22A9CAAB00FC1142 /* Identifier.swift in Sources */, + 32DC88862475111D005A9BFA /* MigratableKeyValuePair.swift in Sources */, 1612FDB222A9CAAB00FC1142 /* SinglePromptSecureEnclaveValet.swift in Sources */, 1612FD8A22A9CAAB00FC1142 /* CloudAccessibility.swift in Sources */, 1612FD9A22A9CAAB00FC1142 /* Keychain.swift in Sources */, @@ -1716,6 +1732,7 @@ 1612FD2C22A9C95500FC1142 /* SinglePromptSecureEnclaveTests.swift in Sources */, 169E9A6723D181DC001B69F5 /* VALSinglePromptSecureEnclaveValetTests.m in Sources */, 1612FD2622A9C95500FC1142 /* ValetIntegrationTests.swift in Sources */, + 32DC88932475BB9F005A9BFA /* KeychainIntegrationTests.swift in Sources */, 1612FD2022A9C95500FC1142 /* ValetBackwardsCompatibilityTests.swift in Sources */, 169E9A6D23D181DC001B69F5 /* VALSecureEnclaveValetTests.m in Sources */, 167E251023D6328E00889121 /* ConfigurationTests.swift in Sources */, @@ -1745,6 +1762,7 @@ 167E250323D624EF00889121 /* MigrationErrorTests.swift in Sources */, 1612FD2D22A9C95500FC1142 /* SinglePromptSecureEnclaveTests.swift in Sources */, 169E9A6823D181DC001B69F5 /* VALSinglePromptSecureEnclaveValetTests.m in Sources */, + 32DC88942475BBA1005A9BFA /* KeychainIntegrationTests.swift in Sources */, 169E9A6E23D181DC001B69F5 /* VALSecureEnclaveValetTests.m in Sources */, 169E9A6B23D181DC001B69F5 /* VALValetTests.m in Sources */, 1612FD2722A9C95500FC1142 /* ValetIntegrationTests.swift in Sources */,