Skip to content

Commit

Permalink
[BITAU-134] [BITAU-121] Create Shared CoreData Store (#937)
Browse files Browse the repository at this point in the history
  • Loading branch information
brant-livefront authored Sep 19, 2024
1 parent 235f340 commit fe37cb6
Show file tree
Hide file tree
Showing 16 changed files with 1,003 additions and 0 deletions.
135 changes: 135 additions & 0 deletions AuthenticatorBridgeKit/AuthenticatorBridgeDataStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import CoreData

// MARK: - AuthenticatorStoreType

/// A type of data store.
///
public enum AuthenticatorBridgeStoreType {
/// The data store is stored only in memory and isn't persisted to the device. This is used for
/// unit testing.
case memory

/// The data store is persisted to the device.
case persisted
}

// MARK: - AuthenticatorDataStore

/// A data store that manages persisting data across app launches in Core Data.
///
public class AuthenticatorBridgeDataStore {
// MARK: Properties

/// A managed object context which executes on a background queue.
private(set) lazy var backgroundContext: NSManagedObjectContext = {
let context = persistentContainer.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return context
}()

/// The service used by the application to report non-fatal errors.
let errorReporter: ErrorReporter

/// The CoreData model name.
private let modelName = "Bitwarden-Authenticator"

/// The Core Data persistent container.
public let persistentContainer: NSPersistentContainer

// MARK: Initialization

/// Initialize a `AuthenticatorBridgeDataStore`.
///
/// - Parameters:
/// - errorReporter: The service used by the application to report non-fatal errors.
/// - groupIdentifier: The app group identifier for the shared resource.
/// - storeType: The type of store to create.
///
public init(
errorReporter: ErrorReporter,
groupIdentifier: String,
storeType: AuthenticatorBridgeStoreType = .persisted
) {
self.errorReporter = errorReporter

#if SWIFT_PACKAGE
let bundle = Bundle.module
#else
let bundle = Bundle(for: type(of: self))
#endif

let modelURL = bundle.url(forResource: modelName, withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL)!
persistentContainer = NSPersistentContainer(
name: modelName,
managedObjectModel: managedObjectModel
)
let storeDescription: NSPersistentStoreDescription
switch storeType {
case .memory:
storeDescription = NSPersistentStoreDescription(url: URL(fileURLWithPath: "/dev/null"))
case .persisted:
let storeURL = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier)!
.appendingPathComponent("\(modelName).sqlite")
storeDescription = NSPersistentStoreDescription(url: storeURL)
}
persistentContainer.persistentStoreDescriptions = [storeDescription]

persistentContainer.loadPersistentStores { _, error in
if let error {
errorReporter.log(error: error)
}
}

persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
}

// MARK: Methods

/// Executes a batch delete request and merges the changes into the background and view contexts.
///
/// - Parameter request: The batch delete request to perform.
///
public func executeBatchDelete(_ request: NSBatchDeleteRequest) async throws {
try await backgroundContext.perform {
try self.backgroundContext.executeAndMergeChanges(
batchDeleteRequest: request,
additionalContexts: [self.persistentContainer.viewContext]
)
}
}

/// Executes a batch insert request and merges the changes into the background and view contexts.
///
/// - Parameter request: The batch insert request to perform.
///
public func executeBatchInsert(_ request: NSBatchInsertRequest) async throws {
try await backgroundContext.perform {
try self.backgroundContext.executeAndMergeChanges(
batchInsertRequest: request,
additionalContexts: [self.persistentContainer.viewContext]
)
}
}

/// Executes a batch delete and batch insert request and merges the changes into the background
/// and view contexts.
///
/// - Parameters:
/// - deleteRequest: The batch delete request to perform.
/// - insertRequest: The batch insert request to perform.
///
public func executeBatchReplace(
deleteRequest: NSBatchDeleteRequest,
insertRequest: NSBatchInsertRequest
) async throws {
try await backgroundContext.perform {
try self.backgroundContext.executeAndMergeChanges(
batchDeleteRequest: deleteRequest,
batchInsertRequest: insertRequest,
additionalContexts: [self.persistentContainer.viewContext]
)
}
}
}
80 changes: 80 additions & 0 deletions AuthenticatorBridgeKit/AuthenticatorBridgeItemData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import CoreData
import Foundation

/// A data model for persisting authenticator items into the shared CoreData store.
///
public class AuthenticatorBridgeItemData: NSManagedObject, CodableModelData {
public typealias Model = AuthenticatorBridgeItemDataModel

// MARK: Properties

/// The item's ID
@NSManaged public var id: String

/// The data model encoded as encrypted JSON data
@NSManaged public var modelData: Data?

/// The ID of the user who owns the item
@NSManaged public var userId: String

// MARK: Initialization

/// Initialize an `AuthenticatorBridgeItemData` object for insertion into the managed object context
///
/// - Parameters:
/// - context: The managed object context to insert the initialized item
/// - userId: The ID of the user who owns the item
/// - authenticatorItem: the `AuthenticatorBridgeItemDataModel` used to create the item
convenience init(
context: NSManagedObjectContext,
userId: String,
authenticatorItem: AuthenticatorBridgeItemDataModel
) throws {
self.init(context: context)
id = authenticatorItem.id
model = authenticatorItem
self.userId = userId
}
}

// MARK: - ManagedUserObject

extension AuthenticatorBridgeItemData: ManagedUserObject {
/// Create an NSPredicate based on both the userId and id properties.
///
/// - Parameters:
/// - userId: The userId to match in the predicate
/// - id: The id to match in the predicate
/// - Returns: The NSPredicate for searching/filtering by userId and id
///
static func userIdAndIdPredicate(userId: String, id: String) -> NSPredicate {
NSPredicate(
format: "%K == %@ AND %K == %@",
#keyPath(AuthenticatorBridgeItemData.userId),
userId,
#keyPath(AuthenticatorBridgeItemData.id),
id
)
}

/// Create an NSPredicate based on the userId property.
///
/// - Parameter userId: The userId to match in the predicate
/// - Returns: The NSPredicate for searching/filtering by userId
///
static func userIdPredicate(userId: String) -> NSPredicate {
NSPredicate(format: "%K == %@", #keyPath(AuthenticatorBridgeItemData.userId), userId)
}

/// Updates the object with the properties from the `value` struct and the given `userId`
///
/// - Parameters:
/// - value: the `AuthenticatorBridgeItemDataModel` to use in updating the object
/// - userId: userId to update this object with.
///
func update(with value: AuthenticatorBridgeItemDataModel, userId: String) throws {
id = value.id
model = value
self.userId = userId
}
}
39 changes: 39 additions & 0 deletions AuthenticatorBridgeKit/AuthenticatorBridgeItemDataModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation

/// A struct for storing information about items that are shared between the Bitwarden and Authenticator apps.
///
public struct AuthenticatorBridgeItemDataModel: Codable, Equatable {
// MARK: Properties

/// Bool indicating if this item is a favorite.
public let favorite: Bool

/// The unique id of the item.
public let id: String

/// The name of the item.
public let name: String

/// The TOTP key used to generate codes.
public let totpKey: String?

/// The username of the Bitwarden account that owns this iteam.
public let username: String?

/// Initialize an `AuthenticatorBridgeItemDataModel` with the values provided.
///
/// - Parameters:
/// - favorite: Bool indicating if this item is a favorite.
/// - id: The unique id of the item.
/// - name: The name of the item.
/// - totpKey: The TOTP key used to generate codes.
/// - username: The username of the Bitwarden account that owns this iteam.
///
public init(favorite: Bool, id: String, name: String, totpKey: String?, username: String?) {
self.favorite = favorite
self.id = id
self.name = name
self.totpKey = totpKey
self.username = username
}
}
115 changes: 115 additions & 0 deletions AuthenticatorBridgeKit/AuthenticatorBridgeItemService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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 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 -> [AuthenticatorBridgeItemDataModel]

/// 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: [AuthenticatorBridgeItemDataModel],
forUserId userId: String) async throws

/// 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: [AuthenticatorBridgeItemDataModel],
forUserId userId: String) async throws
}

/// A concrete implementation of the `AuthenticatorBridgeItemService` protocol.
///
public class DefaultAuthenticatorBridgeItemService: AuthenticatorBridgeItemService {
// MARK: Properties

/// The CoreData store for working with shared data.
let dataStore: AuthenticatorBridgeDataStore

/// The keychain repository for working with the shared key.
let sharedKeychainRepository: SharedKeychainRepository

// MARK: Initialization

/// Initialize a `DefaultAuthenticatorBridgeItemService`
///
/// - Parameters:
/// - dataStore: The CoreData store for working with shared data
/// - sharedKeychainRepository: The keychain repository for working with the shared key.
///
init(dataStore: AuthenticatorBridgeDataStore, sharedKeychainRepository: SharedKeychainRepository) {
self.dataStore = dataStore
self.sharedKeychainRepository = sharedKeychainRepository
}

// MARK: Methods

/// 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 -> [AuthenticatorBridgeItemDataModel] {
let fetchRequest = AuthenticatorBridgeItemData.fetchByUserIdRequest(userId: userId)
let result = try dataStore.backgroundContext.fetch(fetchRequest)

return result.compactMap { data in
data.model
}
}

/// 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: [AuthenticatorBridgeItemDataModel],
forUserId userId: String) async throws {
try await dataStore.executeBatchInsert(
AuthenticatorBridgeItemData.batchInsertRequest(objects: items, userId: userId)
)
}

/// 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: [AuthenticatorBridgeItemDataModel],
forUserId userId: String) async throws {
let deleteRequest = AuthenticatorBridgeItemData.deleteByUserIdRequest(userId: userId)
let insertRequest = try AuthenticatorBridgeItemData.batchInsertRequest(objects: items, userId: userId)
try await dataStore.executeBatchReplace(
deleteRequest: deleteRequest,
insertRequest: insertRequest
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23G93" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="AuthenticatorBridgeItemData" representedClassName="AuthenticatorBridgeKit.AuthenticatorBridgeItemData" syncable="YES">
<attribute name="id" attributeType="String"/>
<attribute name="modelData" attributeType="Binary"/>
<attribute name="userId" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="userId"/>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>
Loading

0 comments on commit fe37cb6

Please sign in to comment.