// // KeychainTests.swift // OneTimePassword // // Copyright (c) 2013-2018 Matt Rubin and the OneTimePassword authors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest import OneTimePassword import Base32 let testToken = Token( name: "Name", issuer: "Issuer", // swiftlint:disable:next force_try generator: try! Generator( factor: .timer(period: 45), secret: MF_Base32Codec.data(fromBase32String: "AAAQEAYEAUDAOCAJBIFQYDIOB4"), algorithm: .sha256, digits: 8 ) ) class KeychainTests: XCTestCase { let keychain = Keychain.sharedInstance func testPersistentTokenWithIdentifier() { // Create a token let token = testToken // Save the token let savedToken: PersistentToken do { savedToken = try keychain.add(token) } catch { XCTFail("addToken(_:) failed with error: \(error)") return } // Restore the token do { let fetchedToken = try keychain.persistentToken(withIdentifier: savedToken.identifier) XCTAssertEqual(fetchedToken, savedToken, "Token should have been saved to keychain") } catch { XCTFail("persistentTokenWithIdentifier(_:) failed with error: \(error)") } // Modify the token let modifiedToken = Token( name: "New Name", issuer: "New Issuer", generator: token.generator.successor() ) do { let updatedToken = try keychain.update(savedToken, with: modifiedToken) XCTAssertEqual(updatedToken.identifier, savedToken.identifier) XCTAssertEqual(updatedToken.token, modifiedToken) } catch { XCTFail("updatePersistentToken(_:withToken:) failed with error: \(error)") } // Fetch the token again do { let fetchedToken = try keychain.persistentToken(withIdentifier: savedToken.identifier) XCTAssertEqual(fetchedToken?.token, modifiedToken) XCTAssertEqual(fetchedToken?.identifier, savedToken.identifier) } catch { XCTFail("persistentTokenWithIdentifier(_:) failed with error: \(error)") } // Remove the token do { try keychain.delete(savedToken) } catch { XCTFail("deletePersistentToken(_:) failed with error: \(error)") } // Attempt to restore the deleted token do { let fetchedToken = try keychain.persistentToken(withIdentifier: savedToken.identifier) XCTAssertNil(fetchedToken, "Token should have been removed from keychain") } catch { XCTFail("persistentTokenWithIdentifier(_:) failed with error: \(error)") } } // swiftlint:disable:next function_body_length func testDuplicateTokens() { let token1 = testToken, token2 = testToken // Add both tokens to the keychain let savedItem1: PersistentToken let savedItem2: PersistentToken do { savedItem1 = try keychain.add(token1) savedItem2 = try keychain.add(token2) XCTAssertEqual(savedItem1.token, token1) XCTAssertEqual(savedItem2.token, token2) } catch { XCTFail("addToken(_:) failed with error: \(error)") return } // Fetch both tokens from the keychain do { let fetchedItem1 = try keychain.persistentToken(withIdentifier: savedItem1.identifier) let fetchedItem2 = try keychain.persistentToken(withIdentifier: savedItem2.identifier) XCTAssertEqual(fetchedItem1, savedItem1, "Saved token not found in keychain") XCTAssertEqual(fetchedItem2, savedItem2, "Saved token not found in keychain") } catch { XCTFail("persistentTokenWithIdentifier(_:) failed with error: \(error)") } // Remove the first token from the keychain do { try keychain.delete(savedItem1) } catch { XCTFail("deletePersistentToken(_:) failed with error: \(error)") } do { let checkItem1 = try keychain.persistentToken(withIdentifier: savedItem1.identifier) let checkItem2 = try keychain.persistentToken(withIdentifier: savedItem2.identifier) XCTAssertNil(checkItem1, "Token should not be in keychain: \(token1)") XCTAssertNotNil(checkItem2, "Token should be in keychain: \(token2)") } catch { XCTFail("persistentTokenWithIdentifier(_:) failed with error: \(error)") } // Remove the second token from the keychain do { try keychain.delete(savedItem2) } catch { XCTFail("deletePersistentToken(_:) failed with error: \(error)") } do { let recheckItem1 = try keychain.persistentToken(withIdentifier: savedItem1.identifier) let recheckItem2 = try keychain.persistentToken(withIdentifier: savedItem2.identifier) XCTAssertNil(recheckItem1, "Token should not be in keychain: \(token1)") XCTAssertNil(recheckItem2, "Token should not be in keychain: \(token2)") } catch { XCTFail("persistentTokenWithIdentifier(_:) failed with error: \(error)") } // Try to remove both tokens from the keychain again do { try keychain.delete(savedItem1) // The deletion should throw and this line should never be reached. XCTFail("Removing again should fail: \(token1)") } catch { // An error thrown is the expected outcome } do { try keychain.delete(savedItem2) // The deletion should throw and this line should never be reached. XCTFail("Removing again should fail: \(token2)") } catch { // An error thrown is the expected outcome } } func testAllPersistentTokens() { let token1 = testToken, token2 = testToken, token3 = testToken do { let noTokens = try keychain.allPersistentTokens() XCTAssert(noTokens.isEmpty, "Expected no tokens in keychain: \(noTokens)") } catch { XCTFail("allPersistentTokens() failed with error: \(error)") } let persistentToken1: PersistentToken let persistentToken2: PersistentToken let persistentToken3: PersistentToken do { persistentToken1 = try keychain.add(token1) persistentToken2 = try keychain.add(token2) persistentToken3 = try keychain.add(token3) } catch { XCTFail("addToken(_:) failed with error: \(error)") return } do { let allTokens = try keychain.allPersistentTokens() XCTAssertEqual(allTokens, [persistentToken1, persistentToken2, persistentToken3], "Tokens not correctly recovered from keychain") } catch { XCTFail("allPersistentTokens() failed with error: \(error)") } do { try keychain.delete(persistentToken1) try keychain.delete(persistentToken2) try keychain.delete(persistentToken3) } catch { XCTFail("deletePersistentToken(_:) failed with error: \(error)") } do { let noTokens = try keychain.allPersistentTokens() XCTAssert(noTokens.isEmpty, "Expected no tokens in keychain: \(noTokens)") } catch { XCTFail("allPersistentTokens() failed with error: \(error)") } } func testMissingData() throws { let keychainAttributes: [String: AnyObject] = [ kSecValueData as String: testToken.generator.secret as NSData, ] let persistentRef = try addKeychainItem(withAttributes: keychainAttributes) XCTAssertThrowsError(try keychain.persistentToken(withIdentifier: persistentRef)) // TODO: Restore deserialization error handling in allPersistentTokens() // XCTAssertThrowsError(try keychain.allPersistentTokens()) XCTAssertNoThrow(try deleteKeychainItem(forPersistentRef: persistentRef), "Failed to delete the test token from the keychain. This may cause future test runs to fail.") } func testMissingSecret() throws { let data = try testToken.toURL().absoluteString.data(using: .utf8)! let keychainAttributes: [String: AnyObject] = [ kSecAttrGeneric as String: data as NSData, ] let persistentRef = try addKeychainItem(withAttributes: keychainAttributes) XCTAssertThrowsError(try keychain.persistentToken(withIdentifier: persistentRef)) // TODO: Restore deserialization error handling in allPersistentTokens() // XCTAssertThrowsError(try keychain.allPersistentTokens()) XCTAssertNoThrow(try deleteKeychainItem(forPersistentRef: persistentRef), "Failed to delete the test token from the keychain. This may cause future test runs to fail.") } func testBadData() throws { let badData = Data(" ".utf8) let keychainAttributes: [String: AnyObject] = [ kSecAttrGeneric as String: badData as NSData, kSecValueData as String: testToken.generator.secret as NSData, ] let persistentRef = try addKeychainItem(withAttributes: keychainAttributes) XCTAssertThrowsError(try keychain.persistentToken(withIdentifier: persistentRef)) // TODO: Restore deserialization error handling in allPersistentTokens() // XCTAssertThrowsError(try keychain.allPersistentTokens()) XCTAssertNoThrow(try deleteKeychainItem(forPersistentRef: persistentRef), "Failed to delete the test token from the keychain. This may cause future test runs to fail.") } func testBadURL() throws { let badData = Data("http://example.com".utf8) let keychainAttributes: [String: AnyObject] = [ kSecAttrGeneric as String: badData as NSData, kSecValueData as String: testToken.generator.secret as NSData, ] let persistentRef = try addKeychainItem(withAttributes: keychainAttributes) XCTAssertThrowsError(try keychain.persistentToken(withIdentifier: persistentRef)) // TODO: Restore deserialization error handling in allPersistentTokens() // XCTAssertThrowsError(try keychain.allPersistentTokens()) XCTAssertNoThrow(try deleteKeychainItem(forPersistentRef: persistentRef), "Failed to delete the test token from the keychain. This may cause future test runs to fail.") } } // MARK: Keychain helpers private func addKeychainItem(withAttributes attributes: [String: AnyObject]) throws -> Data { var mutableAttributes = attributes mutableAttributes[kSecClass as String] = kSecClassGenericPassword mutableAttributes[kSecReturnPersistentRef as String] = kCFBooleanTrue // Set a random string for the account name. // We never query by or display this value, but the keychain requires it to be unique. if mutableAttributes[kSecAttrAccount as String] == nil { mutableAttributes[kSecAttrAccount as String] = UUID().uuidString as NSString } var result: AnyObject? let resultCode: OSStatus = withUnsafeMutablePointer(to: &result) { SecItemAdd(mutableAttributes as CFDictionary, $0) } guard resultCode == errSecSuccess else { throw Keychain.Error.systemError(resultCode) } guard let persistentRef = result as? Data else { throw Keychain.Error.incorrectReturnType } return persistentRef } public func deleteKeychainItem(forPersistentRef persistentRef: Data) throws { let queryDict: [String: AnyObject] = [ kSecClass as String: kSecClassGenericPassword, kSecValuePersistentRef as String: persistentRef as NSData, ] let resultCode = SecItemDelete(queryDict as CFDictionary) guard resultCode == errSecSuccess else { throw Keychain.Error.systemError(resultCode) } }