Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persist authenticator items to a database #18

Merged
merged 24 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2c1ef68
Create TokenData object to work towards storing a token to disk
KatherineInCode Apr 5, 2024
ce5697d
Create a token data store
KatherineInCode Apr 5, 2024
50505e3
Create AuthenticatorItemData
KatherineInCode Apr 5, 2024
a708252
Create AuthenticatorItemDataStore
KatherineInCode Apr 5, 2024
4657ba6
Create AuthenticatorItemService
KatherineInCode Apr 6, 2024
5b48135
Delete token data and data store
KatherineInCode Apr 6, 2024
0afba80
Create CryptographyService
KatherineInCode Apr 6, 2024
bdfd0ef
Create an AuthenticatorItemRepository
KatherineInCode Apr 6, 2024
c1a379b
Add authenticator item repository as a service
KatherineInCode Apr 6, 2024
bf7a92b
Wire Item List up to use authenticator items
KatherineInCode Apr 8, 2024
1d9b39f
Add totp field to authenticator item
KatherineInCode Apr 8, 2024
af2514b
Put TOTP code calculation into the TOTP service
KatherineInCode Apr 8, 2024
2a061e4
Fix tests
KatherineInCode Apr 9, 2024
40292a7
Use TOTP service instead of token repo for TOTP calculation
KatherineInCode Apr 9, 2024
a284577
Rename token to authenticator item in a lot of cases
KatherineInCode Apr 9, 2024
eafbc05
Move files
KatherineInCode Apr 9, 2024
dc14eba
Migrate more from token to authenticator item
KatherineInCode Apr 9, 2024
65515df
Make item list item view work
KatherineInCode Apr 9, 2024
3569e72
Hook edit up
KatherineInCode Apr 9, 2024
ebbf13e
Fix warnings and errors
KatherineInCode Apr 9, 2024
f687542
Make persisting to core data work
KatherineInCode Apr 9, 2024
19ac744
Remove token and token repository
KatherineInCode Apr 9, 2024
418ce57
Remove some references to tokens
KatherineInCode Apr 9, 2024
d263081
Address feedback
KatherineInCode Apr 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.bitwarden.authenticator</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.bitwarden.authenticator</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation

extension Bundle {
/// Return's the app's action extension identifier.
var appExtensionIdentifier: String {
"\(bundleIdentifier!).find-login-action-extension"
}

/// Returns the app's name.
var appName: String {
infoDictionary?["CFBundleName"] as? String ?? ""
}

/// Returns the app's version string (e.g. "2023.8.0").
var appVersion: String {
infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
}

/// Returns the app's build number (e.g. "123").
var buildNumber: String {
infoDictionary?["CFBundleVersion"] as? String ?? ""
}

/// Return's the app's app identifier.
var appIdentifier: String {
infoDictionary?["BitwardenAppIdentifier"] as? String
?? bundleIdentifier
?? "com.x8bit.bitwarden"
}

/// Return's the app's app group identifier.
var groupIdentifier: String {
"group." + appIdentifier
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import CoreData

extension NSManagedObjectContext {
/// Executes the batch delete request and/or batch insert request and merges any changes into
/// the current context plus any additional contexts.
///
/// - Parameters:
/// - batchDeleteRequest: The batch delete request to execute.
/// - batchInsertRequest: The batch insert request to execute.
/// - additionalContexts: Any additional contexts other than the current to merge the changes into.
///
func executeAndMergeChanges(
batchDeleteRequest: NSBatchDeleteRequest? = nil,
batchInsertRequest: NSBatchInsertRequest? = nil,
additionalContexts: [NSManagedObjectContext] = []
) throws {
var changes: [AnyHashable: Any] = [:]

if let batchDeleteRequest {
batchDeleteRequest.resultType = .resultTypeObjectIDs
if let deleteResult = try execute(batchDeleteRequest) as? NSBatchDeleteResult {
changes[NSDeletedObjectsKey] = deleteResult.result as? [NSManagedObjectID] ?? []
}
}

if let batchInsertRequest {
batchInsertRequest.resultType = .objectIDs
if let insertResult = try execute(batchInsertRequest) as? NSBatchInsertResult {
changes[NSInsertedObjectsKey] = insertResult.result as? [NSManagedObjectID] ?? []
}
}

NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self] + additionalContexts)
}

/// Performs the closure on the context's queue and saves the context if there are any changes.
///
/// - Parameter closure: The closure to perform.
///
func performAndSave(closure: @escaping () throws -> Void) async throws {
try await perform {
try closure()
try self.saveIfChanged()
}
}

/// Saves the context if there are changes.
func saveIfChanged() throws {
guard hasChanges else { return }
try save()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import CoreData
import OSLog

/// A protocol for a `NSManagedObject` which persists a data model as JSON encoded data. The model
/// can be set via the `model` property which encodes the model to the data property, which should
/// be a `@NSManaged` property of the `NSManagedObject`. When the managed object is populated from
/// the database, the `model` property can be read to decode the data.
///
protocol CodableModelData: AnyObject, NSManagedObject {
associatedtype Model: Codable

/// A `@NSManaged` property of the manage object for storing the encoded model as data.
var modelData: Data? { get set }
}

extension CodableModelData {
/// Encodes or decodes the model to/from the data instance.
var model: Model? {
get {
guard let modelData else { return nil }
do {
return try JSONDecoder().decode(Model.self, from: modelData)
} catch {
Logger.application.error("Error decoding \(String(describing: Model.self)): \(error)")
return nil
}
}
set {
guard let newValue else {
modelData = nil
return
}
do {
modelData = try JSONEncoder().encode(newValue)
} catch {
Logger.application.error("Error encoding \(String(describing: Model.self)): \(error)")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import CoreData

/// A protocol for an `NSManagedObject` data model that adds some convenience methods for working
/// with Core Data.
///
protocol ManagedObject: AnyObject {
/// The name of the entity of the managed object, as defined in the data model.
static var entityName: String { get }
}

extension ManagedObject where Self: NSManagedObject {
static var entityName: String {
String(describing: self)
}

/// Returns a `NSBatchInsertRequest` for batch inserting an array of objects.
///
/// - Parameters:
/// - objects: The objects (or objects that can be converted to managed objects) to insert.
/// - handler: A handler that is called for each object to set the properties on the
/// `NSManagedObject` to insert.
/// - Returns: A `NSBatchInsertRequest` for batch inserting an array of objects.
///
static func batchInsertRequest<T>(
objects: [T],
handler: @escaping (Self, T) throws -> Void
) throws -> NSBatchInsertRequest {
var index = 0
var errorToThrow: Error?
let insertRequest = NSBatchInsertRequest(entityName: entityName) { (managedObject: NSManagedObject) -> Bool in
guard index < objects.count else { return true }
defer { index += 1 }

if let managedObject = (managedObject as? Self) {
do {
try handler(managedObject, objects[index])
} catch {
// The error can't be thrown directly in this closure, so capture it, return
// from the closure, and then throw it.
errorToThrow = error
return true
}
}

return false
}

if let errorToThrow {
throw errorToThrow
}

return insertRequest
}

/// Returns a `NSFetchRequest` for fetching instances of the managed object.
///
/// - Parameter predicate: An optional predicate to apply to the fetch request.
/// - Returns: A `NSFetchRequest` used to fetch instances of the managed object.
///
static func fetchRequest(predicate: NSPredicate? = nil) -> NSFetchRequest<Self> {
let fetchRequest = NSFetchRequest<Self>(entityName: entityName)
fetchRequest.predicate = predicate
return fetchRequest
}

/// Returns a `NSFetchRequest` for fetching a generic `NSFetchRequestResult` instances of the
/// managed object.
///
/// - Parameter predicate: An optional predicate to apply to the fetch request.
/// - Returns: A `NSFetchRequest` used to fetch generic `NSFetchRequestResult` instances of the
/// managed object.
///
static func fetchResultRequest(predicate: NSPredicate? = nil) -> NSFetchRequest<NSFetchRequestResult> {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
fetchRequest.predicate = predicate
return fetchRequest
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import CoreData
import XCTest

@testable import AuthenticatorShared

class ManagedObjectTests: AuthenticatorTestCase {
// MARK: Tests

/// `fetchRequest()` returns a `NSFetchRequest` for the entity.
func test_fetchRequest() {
let fetchRequest = TestManagedObject.fetchRequest()
XCTAssertEqual(fetchRequest.entityName, "TestManagedObject")
}

/// `fetchResultRequest()` returns a `NSFetchRequest` for the entity.
func test_fetchResultRequest() {
let fetchRequest = TestManagedObject.fetchResultRequest()
XCTAssertEqual(fetchRequest.entityName, "TestManagedObject")
}
}

private class TestManagedObject: NSManagedObject, ManagedObject {
static var entityName = "TestManagedObject"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import CoreData

/// A protocol for a `ManagedObject` data model associated with a user that adds some convenience
/// methods for building `NSPersistentStoreRequest` for common CRUD operations.
///
protocol ManagedUserObject: ManagedObject {
/// The value type (struct) associated with the managed object that is persisted in the database.
associatedtype ValueType

/// Returns a `NSPredicate` used for filtering by a user's ID.
///
/// - Parameter userId: The user ID associated with the managed object.
///
static func userIdPredicate(userId: String) -> NSPredicate

/// Returns a `NSPredicate` used for filtering by a user and managed object ID.
///
/// - Parameter userId: The user ID associated with the managed object.
///
static func userIdAndIdPredicate(userId: String, id: String) -> NSPredicate

/// Updates the managed object from its associated value type object and user ID.
///
/// - Parameters:
/// - value: The value type object used to update the managed object.
/// - userId: The user ID associated with the object.
///
func update(with value: ValueType, userId: String) throws
}

extension ManagedUserObject where Self: NSManagedObject {
/// A `NSBatchInsertRequest` that inserts objects for the specified user.
///
/// - Parameters:
/// - objects: The list of objects to insert.
/// - userId: The user associated with the objects to insert.
/// - Returns: A `NSBatchInsertRequest` that inserts the objects for the user.
///
static func batchInsertRequest(objects: [ValueType], userId: String) throws -> NSBatchInsertRequest {
try batchInsertRequest(objects: objects) { object, value in
try object.update(with: value, userId: userId)
}
}

/// A `NSBatchDeleteRequest` that deletes all objects for the specified user.
///
/// - Parameter userId: The user associated with the objects to delete.
/// - Returns: A `NSBatchDeleteRequest` that deletes all objects for the user.
///
static func deleteByUserIdRequest(userId: String) -> NSBatchDeleteRequest {
let fetchRequest = fetchResultRequest(predicate: userIdPredicate(userId: userId))
return NSBatchDeleteRequest(fetchRequest: fetchRequest)
}

/// A `NSFetchRequest` that fetches objects for the specified user matching an ID.
///
/// - Parameters:
/// - id: The ID of the object to fetch.
/// - userId: The user associated with the object to fetch.
/// - Returns: A `NSFetchRequest` that fetches all objects for the user.
///
static func fetchByIdRequest(id: String, userId: String) -> NSFetchRequest<Self> {
fetchRequest(predicate: userIdAndIdPredicate(userId: userId, id: id))
}

/// A `NSFetchRequest` that fetches all objects for the specified user.
///
/// - Parameter userId: The user associated with the objects to delete.
/// - Returns: A `NSFetchRequest` that fetches all objects for the user.
///
static func fetchByUserIdRequest(userId: String) -> NSFetchRequest<Self> {
fetchRequest(predicate: userIdPredicate(userId: userId))
}
}
Loading