102 lines
3.7 KiB
Swift
102 lines
3.7 KiB
Swift
import CoreData
|
|
|
|
enum PersistenceControllerError: Error {
|
|
case encryptionKeyUnavailable
|
|
case persistentStoreMissing
|
|
case rekeyingUnavailable
|
|
}
|
|
|
|
final class PersistenceController {
|
|
static let shared: PersistenceController = {
|
|
do {
|
|
return try PersistenceController()
|
|
} catch {
|
|
fatalError("Failed to initialize PersistenceController: \(error)")
|
|
}
|
|
}()
|
|
|
|
let container: NSPersistentContainer
|
|
private let keyManager: DatabaseEncryptionKeyManager
|
|
|
|
var viewContext: NSManagedObjectContext {
|
|
container.viewContext
|
|
}
|
|
|
|
init(
|
|
inMemory: Bool = false,
|
|
keyManager: DatabaseEncryptionKeyManager = .shared,
|
|
fileManager: FileManager = .default
|
|
) throws {
|
|
self.keyManager = keyManager
|
|
|
|
let modelName = "YobbleDataModel"
|
|
guard let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd"),
|
|
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL) else {
|
|
fatalError("Unable to load Core Data model \(modelName)")
|
|
}
|
|
|
|
container = NSPersistentContainer(name: modelName, managedObjectModel: managedObjectModel)
|
|
|
|
let description = container.persistentStoreDescriptions.first ?? NSPersistentStoreDescription()
|
|
description.type = NSSQLiteStoreType
|
|
|
|
if inMemory {
|
|
description.url = URL(fileURLWithPath: "/dev/null")
|
|
} else {
|
|
description.url = try Self.makeStoreURL(fileManager: fileManager, fileName: "\(modelName).sqlite")
|
|
}
|
|
|
|
description.shouldInferMappingModelAutomatically = true
|
|
description.shouldMigrateStoreAutomatically = true
|
|
|
|
let key: String
|
|
do {
|
|
key = try keyManager.currentKey()
|
|
} catch {
|
|
throw PersistenceControllerError.encryptionKeyUnavailable
|
|
}
|
|
|
|
let pragmas: [String: String] = [
|
|
"journal_mode": "WAL",
|
|
"cipher_page_size": "4096",
|
|
"key": key
|
|
]
|
|
description.setOption(pragmas as NSDictionary, forKey: NSSQLitePragmasOption)
|
|
description.setOption(FileProtectionType.completeUntilFirstUserAuthentication as NSObject, forKey: NSPersistentStoreFileProtectionKey)
|
|
|
|
container.persistentStoreDescriptions = [description]
|
|
|
|
container.loadPersistentStores { _, error in
|
|
if let error {
|
|
fatalError("Unresolved error \(error)")
|
|
}
|
|
}
|
|
|
|
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
|
container.viewContext.automaticallyMergesChangesFromParent = true
|
|
}
|
|
|
|
func newBackgroundContext() -> NSManagedObjectContext {
|
|
container.newBackgroundContext()
|
|
}
|
|
|
|
/// Placeholder for a future rekey flow once a password-based key is available.
|
|
///
|
|
/// On iOS 16 with SQLCipher you typically re-encrypt by running `PRAGMA rekey` via a raw
|
|
/// SQLite handle and then persisting the new key to the key manager. This helper keeps
|
|
/// the signature in place for when that flow is implemented.
|
|
func rekeyStore(to newKey: String) throws {
|
|
throw PersistenceControllerError.rekeyingUnavailable
|
|
}
|
|
|
|
private static func makeStoreURL(fileManager: FileManager, fileName: String) throws -> URL {
|
|
guard let baseURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
|
|
fatalError("Unable to resolve Application Support directory")
|
|
}
|
|
if !fileManager.fileExists(atPath: baseURL.path) {
|
|
try fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true)
|
|
}
|
|
return baseURL.appendingPathComponent(fileName)
|
|
}
|
|
}
|