mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-10 17:46:07 -06:00
[PM-21681] Build out Shared Keychain functionality (#1636)
This commit is contained in:
parent
e0aa268b98
commit
b609b053c2
22
AuthenticatorBridgeKit/MocksInfo.plist
Normal file
22
AuthenticatorBridgeKit/MocksInfo.plist
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -12,7 +12,7 @@ public protocol SharedCryptographyService: AnyObject {
|
||||
///
|
||||
/// - Parameter items: The encrypted array of items to be decrypted
|
||||
/// - Returns: the array of items with their data decrypted
|
||||
/// - Throws: AuthenticatorKeychainServiceError.keyNotFound if the Authenticator
|
||||
/// - Throws: SharedKeychainServiceError.keyNotFound if the Authenticator
|
||||
/// key is not in the shared repository.
|
||||
///
|
||||
func decryptAuthenticatorItems(
|
||||
@ -24,7 +24,7 @@ public protocol SharedCryptographyService: AnyObject {
|
||||
///
|
||||
/// - Parameter items: The decrypted array of items to be encrypted
|
||||
/// - Returns: the array of items with their data encrypted
|
||||
/// - Throws: AuthenticatorKeychainServiceError.keyNotFound if the Authenticator
|
||||
/// - Throws: SharedKeychainServiceError.keyNotFound if the Authenticator
|
||||
/// key is not in the shared repository.
|
||||
///
|
||||
func encryptAuthenticatorItems(
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import BitwardenKit
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
public class MockSharedKeychainRepository: SharedKeychainRepository {
|
||||
public var authenticatorKey: Data?
|
||||
public var errorToThrow: Error?
|
||||
public var accountAutoLogoutTime = [String: Date]()
|
||||
|
||||
public init() {}
|
||||
|
||||
/// Generates a `Data` object that looks like the key used for encrypting shared items.
|
||||
/// Useful for tests that want reasonably authentic-looking data.
|
||||
public func generateMockKeyData() -> Data {
|
||||
let key = SymmetricKey(size: .bits256)
|
||||
return key.withUnsafeBytes { Data(Array($0)) }
|
||||
}
|
||||
|
||||
public func deleteAuthenticatorKey() throws {
|
||||
if let errorToThrow { throw errorToThrow }
|
||||
|
||||
authenticatorKey = nil
|
||||
}
|
||||
|
||||
public func getAuthenticatorKey() async throws -> Data {
|
||||
if let errorToThrow { throw errorToThrow }
|
||||
|
||||
if let authenticatorKey {
|
||||
return authenticatorKey
|
||||
} else {
|
||||
throw SharedKeychainServiceError.keyNotFound(.authenticatorKey)
|
||||
}
|
||||
}
|
||||
|
||||
public func setAuthenticatorKey(_ value: Data) async throws {
|
||||
if let errorToThrow { throw errorToThrow }
|
||||
|
||||
authenticatorKey = value
|
||||
}
|
||||
|
||||
public func getAccountAutoLogoutTime(userId: String) async throws -> Date? {
|
||||
if let errorToThrow { throw errorToThrow }
|
||||
|
||||
return accountAutoLogoutTime[userId]
|
||||
}
|
||||
|
||||
public func setAccountAutoLogoutTime(_ value: Date?, userId: String) async throws {
|
||||
if let errorToThrow { throw errorToThrow }
|
||||
|
||||
accountAutoLogoutTime[userId] = value
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import Foundation
|
||||
|
||||
public class MockSharedKeychainService: SharedKeychainService {
|
||||
// MARK: Properties
|
||||
|
||||
public var addAttributes: CFDictionary?
|
||||
public var addResult: Result<Void, SharedKeychainServiceError> = .success(())
|
||||
public var deleteQueries = [CFDictionary]()
|
||||
public var deleteResult: Result<Void, SharedKeychainServiceError> = .success(())
|
||||
public var searchQuery: CFDictionary?
|
||||
public var searchResult: Result<AnyObject?, SharedKeychainServiceError> = .success(nil)
|
||||
|
||||
public init() {}
|
||||
|
||||
public func add(attributes: CFDictionary) throws {
|
||||
addAttributes = attributes
|
||||
try addResult.get()
|
||||
}
|
||||
|
||||
public func delete(query: CFDictionary) throws {
|
||||
deleteQueries.append(query)
|
||||
try deleteResult.get()
|
||||
}
|
||||
|
||||
public func search(query: CFDictionary) throws -> AnyObject? {
|
||||
searchQuery = query
|
||||
return try searchResult.get()
|
||||
}
|
||||
|
||||
public func setSearchResultData(_ data: Data) {
|
||||
let dictionary = [kSecValueData as String: data]
|
||||
searchResult = .success(dictionary as AnyObject)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import Foundation
|
||||
|
||||
public class MockSharedKeychainStorage: SharedKeychainStorage {
|
||||
public var storage = [SharedKeychainItem: any Codable]()
|
||||
|
||||
public init() {}
|
||||
|
||||
public func deleteValue(for item: SharedKeychainItem) async throws {
|
||||
storage[item] = nil
|
||||
}
|
||||
|
||||
public func getValue<T>(for item: SharedKeychainItem) async throws -> T where T: Codable {
|
||||
guard let stored = storage[item] as? T else {
|
||||
throw SharedKeychainServiceError.keyNotFound(item)
|
||||
}
|
||||
return stored
|
||||
}
|
||||
|
||||
public func setValue<T>(_ value: T, for item: SharedKeychainItem) async throws where T: Codable {
|
||||
storage[item] = value
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - SharedKeychainRepository
|
||||
|
||||
/// A repository for managing keychain items to be shared between Password Manager and Authenticator.
|
||||
/// This should be the entry point in retrieving items from the shared keychain.
|
||||
public protocol SharedKeychainRepository {
|
||||
/// Deletes the authenticator key.
|
||||
///
|
||||
func deleteAuthenticatorKey() async throws
|
||||
|
||||
/// Gets the authenticator key.
|
||||
///
|
||||
/// - Returns: Data representing the authenticator key.
|
||||
///
|
||||
func getAuthenticatorKey() async throws -> Data
|
||||
|
||||
/// Stores the access token for a user in the keychain.
|
||||
///
|
||||
/// - Parameter value: The authenticator key to store.
|
||||
///
|
||||
func setAuthenticatorKey(_ value: Data) async throws
|
||||
|
||||
/// Gets when a user account should automatically log out.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - userId: The user ID of the account
|
||||
/// - Returns: The time the user should be automatically logged out. If `nil`, then the user should not be.
|
||||
///
|
||||
func getAccountAutoLogoutTime(
|
||||
userId: String
|
||||
) async throws -> Date?
|
||||
|
||||
/// Sets when a user account should automatically log out.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: when the user should be automatically logged out
|
||||
/// - userId: The user ID of the account
|
||||
///
|
||||
func setAccountAutoLogoutTime(
|
||||
_ value: Date?,
|
||||
userId: String
|
||||
) async throws
|
||||
}
|
||||
|
||||
public class DefaultSharedKeychainRepository: SharedKeychainRepository {
|
||||
/// The shared keychain storage used by the repository.
|
||||
let storage: SharedKeychainStorage
|
||||
|
||||
/// Initialize a `DefaultSharedKeychainStorage`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - storage: The shared keychain storage used by the repository
|
||||
public init(storage: SharedKeychainStorage) {
|
||||
self.storage = storage
|
||||
}
|
||||
|
||||
public func deleteAuthenticatorKey() async throws {
|
||||
try await storage.deleteValue(for: .authenticatorKey)
|
||||
}
|
||||
|
||||
/// Gets the authenticator key.
|
||||
///
|
||||
/// - Returns: Data representing the authenticator key.
|
||||
///
|
||||
public func getAuthenticatorKey() async throws -> Data {
|
||||
try await storage.getValue(for: .authenticatorKey)
|
||||
}
|
||||
|
||||
/// Stores the access token for a user in the keychain.
|
||||
///
|
||||
/// - Parameter value: The authenticator key to store.
|
||||
///
|
||||
public func setAuthenticatorKey(_ value: Data) async throws {
|
||||
try await storage.setValue(value, for: .authenticatorKey)
|
||||
}
|
||||
|
||||
/// Gets when a user account should automatically log out.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - userId: The user ID of the account
|
||||
/// - Returns: The time the user should be automatically logged out. If `nil`, then the user should not be.
|
||||
///
|
||||
public func getAccountAutoLogoutTime(userId: String) async throws -> Date? {
|
||||
try await storage.getValue(for: .accountAutoLogout(userId: userId))
|
||||
}
|
||||
|
||||
/// Sets when a user account should automatically log out.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: when the user should be automatically logged out
|
||||
/// - userId: The user ID of the account
|
||||
///
|
||||
public func setAccountAutoLogoutTime(
|
||||
_ value: Date?,
|
||||
userId: String
|
||||
) async throws {
|
||||
try await storage.setValue(value, for: .accountAutoLogout(userId: userId))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import AuthenticatorBridgeKitMocks
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
final class SharedKeychainRepositoryTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var storage: MockSharedKeychainStorage!
|
||||
var subject: DefaultSharedKeychainRepository!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
|
||||
override func setUp() {
|
||||
storage = MockSharedKeychainStorage()
|
||||
subject = DefaultSharedKeychainRepository(
|
||||
storage: storage
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
storage = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// `deleteAuthenticatorKey()` deletes the authenticator key from storage.
|
||||
func test_deleteAuthenticatorKey_success() async throws {
|
||||
storage.storage[.authenticatorKey] = Data()
|
||||
try await subject.deleteAuthenticatorKey()
|
||||
XCTAssertNil(storage.storage[.authenticatorKey])
|
||||
}
|
||||
|
||||
/// `getAuthenticatorKey()` retrieves the authenticator key from storage.
|
||||
func test_getAuthenticatorKey_success() async throws {
|
||||
let key = SymmetricKey(size: .bits256)
|
||||
let data = key.withUnsafeBytes { Data(Array($0)) }
|
||||
storage.storage[.authenticatorKey] = data
|
||||
let authenticatorKey = try await subject.getAuthenticatorKey()
|
||||
XCTAssertEqual(authenticatorKey, data)
|
||||
}
|
||||
|
||||
/// `getAuthenticatorKey()` throws an error if the key is not in storage.
|
||||
func test_getAuthenticatorKey_nil() async throws {
|
||||
await assertAsyncThrows(error: SharedKeychainServiceError.keyNotFound(.authenticatorKey)) {
|
||||
_ = try await subject.getAuthenticatorKey()
|
||||
}
|
||||
}
|
||||
|
||||
/// `setAuthenticatorKey()` sets the authenticator key in storage.
|
||||
func test_setAuthenticatorKey_success() async throws {
|
||||
let key = SymmetricKey(size: .bits256)
|
||||
let data = key.withUnsafeBytes { Data(Array($0)) }
|
||||
try await subject.setAuthenticatorKey(data)
|
||||
XCTAssertEqual(storage.storage[.authenticatorKey] as? Data, data)
|
||||
}
|
||||
|
||||
/// `getAccountAutoLogoutTime()` retrieves the last active time from storage.
|
||||
func test_getPMAccountAutoLogoutTime_success() async throws {
|
||||
let date = Date(timeIntervalSince1970: 12345)
|
||||
storage.storage[.accountAutoLogout(userId: "1")] = date
|
||||
let lastActiveTime = try await subject.getAccountAutoLogoutTime(userId: "1")
|
||||
XCTAssertEqual(lastActiveTime, date)
|
||||
}
|
||||
|
||||
/// `setAccountAutoLogoutTime()` sets the last active time in storage.
|
||||
func test_setPMAccountAutoLogoutTime_success() async throws {
|
||||
let date = Date(timeIntervalSince1970: 12345)
|
||||
try await subject.setAccountAutoLogoutTime(date, userId: "1")
|
||||
XCTAssertEqual(storage.storage[.accountAutoLogout(userId: "1")] as? Date, date)
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - AuthenticatorKeychainService
|
||||
// MARK: - SharedKeychainService
|
||||
|
||||
/// A Service to provide a wrapper around the device keychain shared via App Group between
|
||||
/// the Authenticator and the main Bitwarden app.
|
||||
///
|
||||
public protocol AuthenticatorKeychainService: AnyObject {
|
||||
public protocol SharedKeychainService: AnyObject {
|
||||
/// Adds a set of attributes.
|
||||
///
|
||||
/// - Parameter attributes: Attributes to add.
|
||||
@ -26,10 +26,10 @@ public protocol AuthenticatorKeychainService: AnyObject {
|
||||
func search(query: CFDictionary) throws -> AnyObject?
|
||||
}
|
||||
|
||||
// MARK: - AuthenticatorKeychainServiceError
|
||||
// MARK: - SharedKeychainServiceError
|
||||
|
||||
/// Enum with possible error cases that can be thrown from `AuthenticatorKeychainService`.
|
||||
public enum AuthenticatorKeychainServiceError: Error, Equatable, CustomNSError {
|
||||
/// Enum with possible error cases that can be thrown from `SharedKeychainService`.
|
||||
public enum SharedKeychainServiceError: Error, Equatable, CustomNSError {
|
||||
/// When a `KeychainService` is unable to locate an auth key for a given storage key.
|
||||
///
|
||||
/// - Parameter KeychainItem: The potential storage key for the auth key.
|
||||
@ -4,82 +4,96 @@ import Foundation
|
||||
|
||||
/// Enumeration of support Keychain Items that can be placed in the `SharedKeychainRepository`
|
||||
///
|
||||
public enum SharedKeychainItem: Equatable {
|
||||
public enum SharedKeychainItem: Equatable, Hashable {
|
||||
/// The keychain item for the authenticator encryption key.
|
||||
case authenticatorKey
|
||||
|
||||
/// A date at which a PM account automatically logs out.
|
||||
case accountAutoLogout(userId: String)
|
||||
|
||||
/// The storage key for this keychain item.
|
||||
///
|
||||
var unformattedKey: String {
|
||||
public var unformattedKey: String {
|
||||
switch self {
|
||||
case .authenticatorKey:
|
||||
"authenticatorKey"
|
||||
case let .accountAutoLogout(userId: userId):
|
||||
"accountAutoLogout_\(userId)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SharedKeychainRepository
|
||||
|
||||
/// A repository for managing keychain items to be shared between the main Bitwarden app and the Authenticator app.
|
||||
/// A storage layer for managing keychain items that are shared between Password Manager
|
||||
/// and Authenticator. In particular, it is able to construct the appropriate queries to
|
||||
/// talk with a `SharedKeychainService`.
|
||||
///
|
||||
public protocol SharedKeychainRepository: AnyObject {
|
||||
/// Attempts to delete the authenticator key from the keychain.
|
||||
public protocol SharedKeychainStorage {
|
||||
/// Deletes the value in the keychain for the given item.
|
||||
///
|
||||
func deleteAuthenticatorKey() throws
|
||||
/// - Parameters:
|
||||
/// - value: The value (Data) to be stored into the keychain
|
||||
/// - item: The item for which to store the value in the keychain.
|
||||
///
|
||||
func deleteValue(for item: SharedKeychainItem) async throws
|
||||
|
||||
/// Gets the authenticator key.
|
||||
/// Retrieve the value for the specific item from the Keychain Service.
|
||||
///
|
||||
/// - Returns: Data representing the authenticator key.
|
||||
/// - Parameter item: the keychain item for which to retrieve a value.
|
||||
/// - Returns: The value (Data) stored in the keychain for the given item.
|
||||
///
|
||||
func getAuthenticatorKey() async throws -> Data
|
||||
func getValue<T: Codable>(for item: SharedKeychainItem) async throws -> T
|
||||
|
||||
/// Stores the access token for a user in the keychain.
|
||||
/// Store a given value into the keychain for the given item.
|
||||
///
|
||||
/// - Parameter value: The authenticator key to store.
|
||||
/// - Parameters:
|
||||
/// - value: The value (Data) to be stored into the keychain
|
||||
/// - item: The item for which to store the value in the keychain.
|
||||
///
|
||||
func setAuthenticatorKey(_ value: Data) async throws
|
||||
func setValue<T: Codable>(_ value: T, for item: SharedKeychainItem) async throws
|
||||
}
|
||||
|
||||
// MARK: - DefaultKeychainRepository
|
||||
|
||||
/// A concrete implementation of the `SharedKeychainRepository` protocol.
|
||||
///
|
||||
public class DefaultSharedKeychainRepository: SharedKeychainRepository {
|
||||
public class DefaultSharedKeychainStorage: SharedKeychainStorage {
|
||||
// MARK: Properties
|
||||
|
||||
/// The keychain service used by the repository
|
||||
///
|
||||
private let keychainService: SharedKeychainService
|
||||
|
||||
/// An identifier for the shared access group used by the application.
|
||||
///
|
||||
/// Example: "group.com.8bit.bitwarden"
|
||||
///
|
||||
private let sharedAppGroupIdentifier: String
|
||||
|
||||
/// The keychain service used by the repository
|
||||
///
|
||||
private let keychainService: AuthenticatorKeychainService
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initialize a `DefaultSharedKeychainRepository`.
|
||||
/// Initialize a `DefaultSharedKeychainStorage`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - sharedAppGroupIdentifier: An identifier for the shared access group used by the application.
|
||||
/// - keychainService: The keychain service used by the repository
|
||||
/// - sharedAppGroupIdentifier: An identifier for the shared access group used by the application.
|
||||
public init(
|
||||
sharedAppGroupIdentifier: String,
|
||||
keychainService: AuthenticatorKeychainService
|
||||
keychainService: SharedKeychainService,
|
||||
sharedAppGroupIdentifier: String
|
||||
) {
|
||||
self.sharedAppGroupIdentifier = sharedAppGroupIdentifier
|
||||
self.keychainService = keychainService
|
||||
self.sharedAppGroupIdentifier = sharedAppGroupIdentifier
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Retrieve the value for the specific item from the Keychain Service.
|
||||
///
|
||||
/// - Parameter item: the keychain item for which to retrieve a value.
|
||||
/// - Returns: The value (Data) stored in the keychain for the given item.
|
||||
///
|
||||
private func getSharedValue(for item: SharedKeychainItem) async throws -> Data {
|
||||
public func deleteValue(for item: SharedKeychainItem) async throws {
|
||||
try keychainService.delete(
|
||||
query: [
|
||||
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
||||
kSecAttrAccessGroup: sharedAppGroupIdentifier,
|
||||
kSecAttrAccount: item.unformattedKey,
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
] as CFDictionary
|
||||
)
|
||||
}
|
||||
|
||||
public func getValue<T: Codable>(for item: SharedKeychainItem) async throws -> T {
|
||||
let foundItem = try keychainService.search(
|
||||
query: [
|
||||
kSecMatchLimit: kSecMatchLimitOne,
|
||||
@ -93,20 +107,14 @@ public class DefaultSharedKeychainRepository: SharedKeychainRepository {
|
||||
)
|
||||
|
||||
guard let resultDictionary = foundItem as? [String: Any],
|
||||
let data = resultDictionary[kSecValueData as String] as? Data else {
|
||||
throw AuthenticatorKeychainServiceError.keyNotFound(item)
|
||||
let data = resultDictionary[kSecValueData as String] as? T else {
|
||||
throw SharedKeychainServiceError.keyNotFound(item)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/// Store a given value into the keychain for the given item.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: The value (Data) to be stored into the keychain
|
||||
/// - item: The item for which to store the value in the keychain.
|
||||
///
|
||||
private func setSharedValue(_ value: Data, for item: SharedKeychainItem) async throws {
|
||||
public func setValue<T: Codable>(_ value: T, for item: SharedKeychainItem) async throws {
|
||||
let query = [
|
||||
kSecValueData: value,
|
||||
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
||||
@ -122,34 +130,3 @@ public class DefaultSharedKeychainRepository: SharedKeychainRepository {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public extension DefaultSharedKeychainRepository {
|
||||
/// Attempts to delete the authenticator key from the keychain.
|
||||
///
|
||||
func deleteAuthenticatorKey() throws {
|
||||
try keychainService.delete(
|
||||
query: [
|
||||
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
||||
kSecAttrAccessGroup: sharedAppGroupIdentifier,
|
||||
kSecAttrAccount: SharedKeychainItem.authenticatorKey.unformattedKey,
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
] as CFDictionary
|
||||
)
|
||||
}
|
||||
|
||||
/// Gets the authenticator key.
|
||||
///
|
||||
/// - Returns: Data representing the authenticator key.
|
||||
///
|
||||
func getAuthenticatorKey() async throws -> Data {
|
||||
try await getSharedValue(for: .authenticatorKey)
|
||||
}
|
||||
|
||||
/// Stores the access token for a user in the keychain.
|
||||
///
|
||||
/// - Parameter value: The authenticator key to store.
|
||||
///
|
||||
func setAuthenticatorKey(_ value: Data) async throws {
|
||||
try await setSharedValue(value, for: .authenticatorKey)
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,23 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import AuthenticatorBridgeKitMocks
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import AuthenticatorBridgeKit
|
||||
|
||||
final class SharedKeychainRepositoryTests: AuthenticatorBridgeKitTestCase {
|
||||
final class SharedKeychainStorageTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
let accessGroup = "group.com.example.bitwarden"
|
||||
var keychainService: MockAuthenticatorKeychainService!
|
||||
var subject: DefaultSharedKeychainRepository!
|
||||
var keychainService: MockSharedKeychainService!
|
||||
var subject: SharedKeychainStorage!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
|
||||
override func setUp() {
|
||||
keychainService = MockAuthenticatorKeychainService()
|
||||
subject = DefaultSharedKeychainRepository(
|
||||
sharedAppGroupIdentifier: accessGroup,
|
||||
keychainService: keychainService
|
||||
keychainService = MockSharedKeychainService()
|
||||
subject = DefaultSharedKeychainStorage(
|
||||
keychainService: keychainService,
|
||||
sharedAppGroupIdentifier: accessGroup
|
||||
)
|
||||
}
|
||||
|
||||
@ -28,10 +28,10 @@ final class SharedKeychainRepositoryTests: AuthenticatorBridgeKitTestCase {
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// Verify that `deleteAuthenticatorKey()` issues a delete with the correct search attributes specified.
|
||||
/// Verify that `deleteValue(for:)` issues a delete with the correct search attributes specified.
|
||||
///
|
||||
func test_deleteAuthenticatorKey_success() async throws {
|
||||
try subject.deleteAuthenticatorKey()
|
||||
func test_deleteValue_success() async throws {
|
||||
try await subject.deleteValue(for: .authenticatorKey)
|
||||
|
||||
let queries = try XCTUnwrap(keychainService.deleteQueries as? [[CFString: Any]])
|
||||
XCTAssertEqual(queries.count, 1)
|
||||
@ -46,16 +46,16 @@ final class SharedKeychainRepositoryTests: AuthenticatorBridgeKitTestCase {
|
||||
String(kSecClassGenericPassword))
|
||||
}
|
||||
|
||||
/// Verify that `getAuthenticatorKey()` returns a value successfully when one is set. Additionally, verify the
|
||||
/// Verify that `getValue(for:)` returns a value successfully when one is set. Additionally, verify the
|
||||
/// search attributes are specified correctly.
|
||||
///
|
||||
func test_getAuthenticatorKey_success() async throws {
|
||||
func test_getValue_success() async throws {
|
||||
let key = SymmetricKey(size: .bits256)
|
||||
let data = key.withUnsafeBytes { Data(Array($0)) }
|
||||
|
||||
keychainService.setSearchResultData(data)
|
||||
|
||||
let returnData = try await subject.getAuthenticatorKey()
|
||||
let returnData: Data = try await subject.getValue(for: .authenticatorKey)
|
||||
XCTAssertEqual(returnData, data)
|
||||
|
||||
let query = try XCTUnwrap(keychainService.searchQuery as? [CFString: Any])
|
||||
@ -73,45 +73,47 @@ final class SharedKeychainRepositoryTests: AuthenticatorBridgeKitTestCase {
|
||||
/// Verify that `getAuthenticatorKey()` fails with a `keyNotFound` error when an unexpected
|
||||
/// result is returned instead of the key data from the keychain
|
||||
///
|
||||
func test_getAuthenticatorKey_badResult() async throws {
|
||||
let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
||||
func test_getValue_badResult() async throws {
|
||||
let key = SharedKeychainItem.accountAutoLogout(userId: "1")
|
||||
let error = SharedKeychainServiceError.keyNotFound(key)
|
||||
keychainService.searchResult = .success([kSecValueData as String: NSObject()] as AnyObject)
|
||||
|
||||
await assertAsyncThrows(error: error) {
|
||||
_ = try await subject.getAuthenticatorKey()
|
||||
let _: Data = try await subject.getValue(for: key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify that `getAuthenticatorKey()` fails with a `keyNotFound` error when a nil
|
||||
/// Verify that `getValue(for:)` fails with a `keyNotFound` error when a nil
|
||||
/// result is returned instead of the key data from the keychain
|
||||
///
|
||||
func test_getAuthenticatorKey_nilResult() async throws {
|
||||
let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
||||
func test_getValue_nilResult() async throws {
|
||||
let key = SharedKeychainItem.accountAutoLogout(userId: "1")
|
||||
let error = SharedKeychainServiceError.keyNotFound(key)
|
||||
keychainService.searchResult = .success(nil)
|
||||
|
||||
await assertAsyncThrows(error: error) {
|
||||
_ = try await subject.getAuthenticatorKey()
|
||||
let _: Data = try await subject.getValue(for: key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify that `getAuthenticatorKey()` fails with an error when the Authenticator key is not
|
||||
/// Verify that `getValue(for:)` fails with an error when the Authenticator key is not
|
||||
/// present in the keychain
|
||||
///
|
||||
func test_getAuthenticatorKey_keyNotFound() async throws {
|
||||
let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
||||
let error = SharedKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
||||
keychainService.searchResult = .failure(error)
|
||||
|
||||
await assertAsyncThrows(error: error) {
|
||||
_ = try await subject.getAuthenticatorKey()
|
||||
let _: Data = try await subject.getValue(for: .authenticatorKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify that `setAuthenticatorKey(_:)` sets a value with the correct search attributes specified.
|
||||
/// Verify that `setValue(_:for:)` sets a value with the correct search attributes specified.
|
||||
///
|
||||
func test_setAuthenticatorKey_success() async throws {
|
||||
let key = SymmetricKey(size: .bits256)
|
||||
let data = key.withUnsafeBytes { Data(Array($0)) }
|
||||
try await subject.setAuthenticatorKey(data)
|
||||
try await subject.setValue(data, for: .authenticatorKey)
|
||||
|
||||
let attributes = try XCTUnwrap(keychainService.addAttributes as? [CFString: Any])
|
||||
try XCTAssertEqual(XCTUnwrap(attributes[kSecAttrAccessGroup] as? String), accessGroup)
|
||||
@ -1,3 +1,5 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import AuthenticatorBridgeKitMocks
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import Foundation
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import AuthenticatorBridgeKitMocks
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import Foundation
|
||||
@ -170,7 +172,7 @@ final class AuthenticatorBridgeItemServiceTests: AuthenticatorBridgeKitTestCase
|
||||
/// Verify that `isSyncOn` returns true when the key is present in the keychain.
|
||||
///
|
||||
func test_isSyncOn_true() async throws {
|
||||
let key = keychainRepository.generateKeyData()
|
||||
let key = keychainRepository.generateMockKeyData()
|
||||
try await keychainRepository.setAuthenticatorKey(key)
|
||||
let sync = await subject.isSyncOn()
|
||||
XCTAssertTrue(sync)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import AuthenticatorBridgeKitMocks
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import XCTest
|
||||
@ -16,7 +18,7 @@ final class SharedCryptographyServiceTests: AuthenticatorBridgeKitTestCase {
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
sharedKeychainRepository = MockSharedKeychainRepository()
|
||||
sharedKeychainRepository.authenticatorKey = sharedKeychainRepository.generateKeyData()
|
||||
sharedKeychainRepository.authenticatorKey = sharedKeychainRepository.generateMockKeyData()
|
||||
subject = DefaultAuthenticatorCryptographyService(
|
||||
sharedKeychainRepository: sharedKeychainRepository
|
||||
)
|
||||
@ -56,7 +58,7 @@ final class SharedCryptographyServiceTests: AuthenticatorBridgeKitTestCase {
|
||||
///
|
||||
func test_decryptAuthenticatorItems_throwsKeyMissingError() async throws {
|
||||
let encryptedItems = try await subject.encryptAuthenticatorItems(items)
|
||||
let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
||||
let error = SharedKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
||||
|
||||
try sharedKeychainRepository.deleteAuthenticatorKey()
|
||||
await assertAsyncThrows(error: error) {
|
||||
@ -110,7 +112,7 @@ final class SharedCryptographyServiceTests: AuthenticatorBridgeKitTestCase {
|
||||
/// when the `SharedKeyRepository` authenticator key is missing.
|
||||
///
|
||||
func test_encryptAuthenticatorItems_throwsKeyMissingError() async throws {
|
||||
let error = AuthenticatorKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
||||
let error = SharedKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
|
||||
|
||||
try sharedKeychainRepository.deleteAuthenticatorKey()
|
||||
await assertAsyncThrows(error: error) {
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
@testable import AuthenticatorBridgeKit
|
||||
|
||||
class MockAuthenticatorKeychainService {
|
||||
// MARK: Properties
|
||||
|
||||
var addAttributes: CFDictionary?
|
||||
var addResult: Result<Void, AuthenticatorKeychainServiceError> = .success(())
|
||||
var deleteQueries = [CFDictionary]()
|
||||
var deleteResult: Result<Void, AuthenticatorKeychainServiceError> = .success(())
|
||||
var searchQuery: CFDictionary?
|
||||
var searchResult: Result<AnyObject?, AuthenticatorKeychainServiceError> = .success(nil)
|
||||
}
|
||||
|
||||
// MARK: KeychainService
|
||||
|
||||
extension MockAuthenticatorKeychainService: AuthenticatorKeychainService {
|
||||
func add(attributes: CFDictionary) throws {
|
||||
addAttributes = attributes
|
||||
try addResult.get()
|
||||
}
|
||||
|
||||
func delete(query: CFDictionary) throws {
|
||||
deleteQueries.append(query)
|
||||
try deleteResult.get()
|
||||
}
|
||||
|
||||
func search(query: CFDictionary) throws -> AnyObject? {
|
||||
searchQuery = query
|
||||
return try searchResult.get()
|
||||
}
|
||||
}
|
||||
|
||||
extension MockAuthenticatorKeychainService {
|
||||
func setSearchResultData(_ data: Data) {
|
||||
let dictionary = [kSecValueData as String: data]
|
||||
searchResult = .success(dictionary as AnyObject)
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
@testable import AuthenticatorBridgeKit
|
||||
|
||||
class MockSharedKeychainRepository {
|
||||
var authenticatorKey: Data?
|
||||
}
|
||||
|
||||
extension MockSharedKeychainRepository: SharedKeychainRepository {
|
||||
func generateKeyData() -> Data {
|
||||
let key = SymmetricKey(size: .bits256)
|
||||
return key.withUnsafeBytes { Data(Array($0)) }
|
||||
}
|
||||
|
||||
func deleteAuthenticatorKey() throws {
|
||||
authenticatorKey = nil
|
||||
}
|
||||
|
||||
func getAuthenticatorKey() async throws -> Data {
|
||||
if let authenticatorKey {
|
||||
return authenticatorKey
|
||||
} else {
|
||||
throw AuthenticatorKeychainServiceError.keyNotFound(.authenticatorKey)
|
||||
}
|
||||
}
|
||||
|
||||
func setAuthenticatorKey(_ value: Data) async throws {
|
||||
authenticatorKey = value
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import TestHelpers
|
||||
import XCTest
|
||||
|
||||
open class BitwardenTestCase: BaseBitwardenTestCase {
|
||||
@MainActor
|
||||
override open class func setUp() {
|
||||
TestDataHelpers.defaultBundle = Bundle(for: Self.self)
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - KeychainService
|
||||
@ -112,6 +113,6 @@ class DefaultKeychainService: KeychainService {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AuthenticatorKeychainService
|
||||
// MARK: - SharedKeychainService
|
||||
|
||||
extension DefaultKeychainService: AuthenticatorKeychainService {}
|
||||
extension DefaultKeychainService: SharedKeychainService {}
|
||||
|
||||
@ -226,9 +226,13 @@ public class ServiceContainer: Services {
|
||||
authenticatorItemDataStore: dataStore
|
||||
)
|
||||
|
||||
let sharedKeychainStorage = DefaultSharedKeychainStorage(
|
||||
keychainService: keychainService,
|
||||
sharedAppGroupIdentifier: Bundle.main.sharedAppGroupIdentifier
|
||||
)
|
||||
|
||||
let sharedKeychainRepository = DefaultSharedKeychainRepository(
|
||||
sharedAppGroupIdentifier: Bundle.main.sharedAppGroupIdentifier,
|
||||
keychainService: keychainService
|
||||
storage: sharedKeychainStorage
|
||||
)
|
||||
|
||||
let sharedCryptographyService = DefaultAuthenticatorCryptographyService(
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
/// An enumeration of session timeout values to choose from.
|
||||
///
|
||||
public enum SessionTimeoutValue: RawRepresentable, Equatable, Hashable, Sendable {
|
||||
public enum SessionTimeoutValue: Codable, RawRepresentable, Equatable, Hashable, Sendable {
|
||||
/// Time out immediately.
|
||||
case immediately
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - KeychainService
|
||||
@ -132,6 +133,6 @@ class DefaultKeychainService: KeychainService {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AuthenticatorKeychainService
|
||||
// MARK: - SharedKeychainService
|
||||
|
||||
extension DefaultKeychainService: AuthenticatorKeychainService {}
|
||||
extension DefaultKeychainService: SharedKeychainService {}
|
||||
|
||||
@ -239,7 +239,7 @@ actor DefaultAuthenticatorSyncService: NSObject, AuthenticatorSyncService {
|
||||
return
|
||||
}
|
||||
}
|
||||
try sharedKeychainRepository.deleteAuthenticatorKey()
|
||||
try await sharedKeychainRepository.deleteAuthenticatorKey()
|
||||
}
|
||||
|
||||
/// Determine if the given userId has sync turned on and an unlocked vault. This method serves as the
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import AuthenticatorBridgeKit
|
||||
import AuthenticatorBridgeKitMocks
|
||||
import BitwardenKitMocks
|
||||
import BitwardenSdk
|
||||
import Combine
|
||||
@ -92,7 +93,7 @@ final class AuthenticatorSyncServiceTests: BitwardenTestCase { // swiftlint:disa
|
||||
func test_createAuthenticatorKeyIfNeeded_keyAlreadyExists() async throws {
|
||||
setupInitialState()
|
||||
await subject.start()
|
||||
let key = sharedKeychainRepository.generateKeyData()
|
||||
let key = sharedKeychainRepository.generateMockKeyData()
|
||||
try await sharedKeychainRepository.setAuthenticatorKey(key)
|
||||
|
||||
stateService.syncToAuthenticatorSubject.send(("1", true))
|
||||
|
||||
@ -777,9 +777,13 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
|
||||
storeType: .persisted
|
||||
)
|
||||
|
||||
let sharedKeychainStorage = DefaultSharedKeychainStorage(
|
||||
keychainService: keychainService,
|
||||
sharedAppGroupIdentifier: Bundle.main.sharedAppGroupIdentifier
|
||||
)
|
||||
|
||||
let sharedKeychainRepository = DefaultSharedKeychainRepository(
|
||||
sharedAppGroupIdentifier: Bundle.main.sharedAppGroupIdentifier,
|
||||
keychainService: keychainService
|
||||
storage: sharedKeychainStorage
|
||||
)
|
||||
|
||||
let sharedCryptographyService = DefaultAuthenticatorCryptographyService(
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
@testable import AuthenticatorBridgeKit
|
||||
|
||||
class MockSharedKeychainRepository {
|
||||
var authenticatorKey: Data?
|
||||
var errorToThrow: Error?
|
||||
}
|
||||
|
||||
extension MockSharedKeychainRepository: SharedKeychainRepository {
|
||||
func generateKeyData() -> Data {
|
||||
let key = SymmetricKey(size: .bits256)
|
||||
return key.withUnsafeBytes { Data(Array($0)) }
|
||||
}
|
||||
|
||||
func deleteAuthenticatorKey() throws {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
|
||||
authenticatorKey = nil
|
||||
}
|
||||
|
||||
func getAuthenticatorKey() async throws -> Data {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
|
||||
if let authenticatorKey {
|
||||
return authenticatorKey
|
||||
} else {
|
||||
throw AuthenticatorKeychainServiceError.keyNotFound(.authenticatorKey)
|
||||
}
|
||||
}
|
||||
|
||||
func setAuthenticatorKey(_ value: Data) async throws {
|
||||
guard errorToThrow == nil else { throw errorToThrow! }
|
||||
|
||||
authenticatorKey = value
|
||||
}
|
||||
}
|
||||
@ -79,7 +79,11 @@ targets:
|
||||
sources:
|
||||
- path: AuthenticatorBridgeKit
|
||||
excludes:
|
||||
- "**/Fixtures/*"
|
||||
- "**/Mocks/*"
|
||||
- "**/Tests/*"
|
||||
- "**/*Tests.*"
|
||||
- "**/TestHelpers/*"
|
||||
dependencies:
|
||||
- target: BitwardenKit
|
||||
AuthenticatorBridgeKitTests:
|
||||
@ -92,11 +96,31 @@ targets:
|
||||
- path: AuthenticatorBridgeKit
|
||||
includes:
|
||||
- "**/Tests/*"
|
||||
- "**/*Tests.*"
|
||||
- "**/TestHelpers/*"
|
||||
dependencies:
|
||||
- target: AuthenticatorBridgeKit
|
||||
- target: AuthenticatorBridgeKitMocks
|
||||
- target: BitwardenKit
|
||||
- target: BitwardenKitMocks
|
||||
- target: TestHelpers
|
||||
randomExecutionOrder: true
|
||||
AuthenticatorBridgeKitMocks:
|
||||
type: framework
|
||||
platform: iOS
|
||||
settings:
|
||||
base:
|
||||
ENABLE_TESTING_SEARCH_PATHS: YES
|
||||
INFOPLIST_FILE: AuthenticatorBridgeKit/MocksInfo.plist
|
||||
sources:
|
||||
- path: AuthenticatorBridgeKit
|
||||
includes:
|
||||
- "**/Fixtures/*"
|
||||
- "**/Mocks/*"
|
||||
dependencies:
|
||||
- target: AuthenticatorBridgeKit
|
||||
- target: BitwardenKit
|
||||
- target: TestHelpers
|
||||
BitwardenKit:
|
||||
type: framework
|
||||
platform: iOS
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user