import BitwardenKit import Combine import CoreData import Foundation // MARK: - AuthenticatorBridgeItemService /// A service that provides a number of convenience methods for working with the shared /// `AuthenticatorBridgeItemData` objects. /// public protocol AuthenticatorBridgeItemService { /// Removes all items and deletes the authenticator key. /// func deleteAll() async throws /// Removes all items that are owned by the specific userId /// /// - Parameter userId: the id of the user for which to delete all items. /// func deleteAllForUserId(_ userId: String) async throws /// Fetches all items that are owned by the specific userId /// /// - Parameter userId: the id of the user for which to fetch items. /// func fetchAllForUserId(_ userId: String) async throws -> [AuthenticatorBridgeItemDataView] /// Fetches the temporary item stored by the Authenticator app and removes all temporary items from the store. /// If there are no temporary items in the store, this method will return `nil`. /// /// - Returns: The temporary item from the store, or `nil` if none was found. /// func fetchTemporaryItem() async throws -> AuthenticatorBridgeItemDataView? /// Inserts the list of items into the store for the given userId. /// /// - Parameters: /// - items: The list of `AuthenticatorBridgeItemDataModel` to be inserted into the store. /// - userId: the id of the user for which to insert the items. /// func insertItems(_ items: [AuthenticatorBridgeItemDataView], forUserId userId: String) async throws /// Inserts a temporary item into the store. This method is for an item that originate in the Authenticator app that /// need to move to the BWPM app (e.g. the user chooses Move to BW, or manually creates an item and selects /// Save in BW). When the item originates from the Authenticator, we don't yet know what account it will be stored /// in, so we save it to a temporary account and retrieve it for processing in the BWPM app. /// /// The expectation is that only *one* temporary item will be stored at a time. Each time this method is called, it /// will replace the one temporary item. When `fetchTemporaryItem()`is called, it will retrieve only /// the last item stored. /// /// - Parameters: /// - item: The temporary `AuthenticatorBridgeItemDataModel` to be inserted into the store. /// func insertTemporaryItem(_ item: AuthenticatorBridgeItemDataView) async throws /// Returns `true` if sync has been enabled for one or more accounts in the Bitwarden PM app, `false` /// if there are no accounts with sync currently turned on. /// /// - Returns: `true` if there is one or more accounts with sync turned on; `false` otherwise. /// func isSyncOn() async -> Bool /// Deletes all existing items for a given user and inserts new items for the list of items provided. /// /// - Parameters: /// - items: The new items to be inserted into the store /// - userId: The userId of the items to be removed and then replaces with items. /// func replaceAllItems(with items: [AuthenticatorBridgeItemDataView], forUserId userId: String) async throws /// A Publisher that returns all of the items in the shared store. /// /// - Returns: Publisher that will publish the initial list of all items and any future data changes. /// func sharedItemsPublisher() async throws -> AnyPublisher<[AuthenticatorBridgeItemDataView], any Error> } /// A concrete implementation of the `AuthenticatorBridgeItemService` protocol. /// public class DefaultAuthenticatorBridgeItemService: AuthenticatorBridgeItemService { // MARK: Private Properties /// A constant to use as the userId for storing a temporary item. private static let temporaryUserId = "000000000000" // MARK: Properties /// Cryptography service for encrypting/decrypting items. let cryptoService: SharedCryptographyService /// The CoreData store for working with shared data. let dataStore: AuthenticatorBridgeDataStore /// The keychain repository for working with the shared key. let sharedKeychainRepository: SharedKeychainRepository /// A service that manages account timeout between apps. let sharedTimeoutService: SharedTimeoutService // MARK: Initialization /// Initialize a `DefaultAuthenticatorBridgeItemService` /// /// - Parameters: /// - cryptoService: Cryptography service for encrypting/decrypting items. /// - dataStore: The CoreData store for working with shared data /// - sharedKeychainRepository: The keychain repository for working with the shared key. /// - sharedTimeoutService: The shared timeout service for managing session timeouts. /// public init(cryptoService: SharedCryptographyService, dataStore: AuthenticatorBridgeDataStore, sharedKeychainRepository: SharedKeychainRepository, sharedTimeoutService: SharedTimeoutService) { self.cryptoService = cryptoService self.dataStore = dataStore self.sharedKeychainRepository = sharedKeychainRepository self.sharedTimeoutService = sharedTimeoutService } // MARK: Methods public func deleteAll() async throws { let fetchRequest = NSFetchRequest(entityName: AuthenticatorBridgeItemData.entityName) let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) try await dataStore.executeBatchDelete(deleteRequest) try await sharedKeychainRepository.deleteAuthenticatorKey() } /// Removes all items that are owned by the specific userId /// /// - Parameter userId: the id of the user for which to delete all items. /// public func deleteAllForUserId(_ userId: String) async throws { try await dataStore.executeBatchDelete(AuthenticatorBridgeItemData.deleteByUserIdRequest(userId: userId)) } /// Fetches all items that are owned by the specific userId /// /// - Parameter userId: the id of the user for which to fetch items. /// public func fetchAllForUserId(_ userId: String) async throws -> [AuthenticatorBridgeItemDataView] { let fetchRequest = AuthenticatorBridgeItemData.fetchByUserIdRequest(userId: userId) let result = try dataStore.backgroundContext.fetch(fetchRequest) let encryptedItems = result.compactMap { data in data.model } return try await cryptoService.decryptAuthenticatorItems(encryptedItems) } public func fetchTemporaryItem() async throws -> AuthenticatorBridgeItemDataView? { let decryptedItems = try await fetchAllForUserId( DefaultAuthenticatorBridgeItemService.temporaryUserId, ) try await deleteAllForUserId( DefaultAuthenticatorBridgeItemService.temporaryUserId, ) return decryptedItems.first } public func isSyncOn() async -> Bool { let key = try? await sharedKeychainRepository.getAuthenticatorKey() return key != nil } /// Inserts the list of items into the store for the given userId. /// /// - Parameters: /// - items: The list of `AuthenticatorBridgeItemDataModel` to be inserted into the store. /// - userId: the id of the user for which to insert the items. /// public func insertItems(_ items: [AuthenticatorBridgeItemDataView], forUserId userId: String) async throws { let encryptedItems = try await cryptoService.encryptAuthenticatorItems(items) try await dataStore.executeBatchInsert( AuthenticatorBridgeItemData.batchInsertRequest(objects: encryptedItems, userId: userId), ) } public func insertTemporaryItem(_ item: AuthenticatorBridgeItemDataView) async throws { try await replaceAllItems( with: [item], forUserId: DefaultAuthenticatorBridgeItemService.temporaryUserId, ) } /// Deletes all existing items for a given user and inserts new items for the list of items provided. /// /// - Parameters: /// - items: The new items to be inserted into the store /// - userId: The userId of the items to be removed and then replaces with items. /// public func replaceAllItems(with items: [AuthenticatorBridgeItemDataView], forUserId userId: String) async throws { let encryptedItems = try await cryptoService.encryptAuthenticatorItems(items) let deleteRequest = AuthenticatorBridgeItemData.deleteByUserIdRequest(userId: userId) let insertRequest = try AuthenticatorBridgeItemData.batchInsertRequest( objects: encryptedItems, userId: userId, ) try await dataStore.executeBatchReplace( deleteRequest: deleteRequest, insertRequest: insertRequest, ) } public func sharedItemsPublisher() async throws -> AnyPublisher<[AuthenticatorBridgeItemDataView], any Error> { try await checkForLogout() let fetchRequest = AuthenticatorBridgeItemData.fetchRequest( predicate: NSPredicate( format: "userId != %@", DefaultAuthenticatorBridgeItemService.temporaryUserId, ), ) fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \AuthenticatorBridgeItemData.userId, ascending: true)] return FetchedResultsPublisher( context: dataStore.persistentContainer.viewContext, request: fetchRequest, ) .map { dataItems in dataItems.compactMap(\.model) } .asyncTryMap { itemModel in try await self.cryptoService.decryptAuthenticatorItems(itemModel) } .eraseToAnyPublisher() } // MARK: Private Functions /// Iterates through all of the users with shared items and determines if they've passed their /// logout timeout. If so, then their shared items are deleted. /// private func checkForLogout() async throws { let fetchRequest = NSFetchRequest(entityName: AuthenticatorBridgeItemData.entityName) fetchRequest.propertiesToFetch = ["userId"] fetchRequest.returnsDistinctResults = true fetchRequest.resultType = .dictionaryResultType let results = try dataStore.persistentContainer.viewContext.fetch(fetchRequest) let userIds = results.compactMap { ($0 as? [String: Any])?["userId"] as? String } try await userIds.asyncForEach { userId in if try await sharedTimeoutService.hasPassedTimeout(userId: userId) { try await deleteAllForUserId(userId) } } } }